开发者以 tags.ts 为例,说明向 AI 编程代理传递的 CLAUDE.md、AGENTS.md 等本质上是「被阅读后才会生效的警告标签」——代码中的约束规则若无人执行就形同虚设。
这个博客仓库里有一个名为 tags.ts 的文件。第八行这样写道:
重要提示:添加新标签前必须更新本文件。构建验证会拒绝包含未批准标签的文章。
这是我写的。它是写给智能体的,每个在这里写文章的智能体都会读到它。
负责执行拒绝逻辑的模式定义在另一个文件里,只有一行:
tags: z.array(z.string()).optional(),
任意数组,任意字符串。这个模式出现了八次,每个内容集合各一次。没有任何地方做过验证,把文章标签和批准列表做过比对。
于是我数了数。两百七十七个文件带有标签。批准列表里有十八个条目。实际使用的标签有四十九种——其中三十二种不在列表里,共出现六十四次。
最值得注意的那个标签很有启发性。ai-development 声明了一个 replaces 数组,列出它存在所要吸收的那些不规范标签。列表里明确写了 'AI'。AI 作为活跃标签出现在四篇文章上。
文件名标出了这个错误本身。这个错误在仓库里出现了四次。构建全程都是绿的。
文件里的规则是一封请求书
那个文件是一张警告标签。它通过被阅读和被遵守来生效。
我们给编程智能体的东西,绝大部分都是警告标签。CLAUDE.md、AGENTS.md、.cursorrules、系统提示词、会话顶部粘贴的那段话。所有这些都是模型会读取并大多会尊重的文本。"大多"是关键词,而"大多"比听起来更糟糕。当一条规则被跳过但工作看起来仍然正常时,什么会告诉你?绿色的构建告诉我,我的标签在两百七十七个文件里都是干净的。
实际上你可以构建另外两种东西,我一直把它们归在同一个名字下面。
我把 jig 定义得片面了
三月,我借用了木匠术语,给了我一直反复想到的那个概念一个定义:
大师级木匠不会从零开始制作每一件作品。他们制作 jig——一种定制工具,把材料固定在正确的角度、引导切割沿精确的线路、使可重复的操作既快又准。jig 不是产品,它是加速制作产品这一真正工作的东西。
读一下那些子句。加速。又快又准。可重复。每一个都关乎速度。
那句话之后的所有内容都是关于扩散的——智能体让 jig 变得廉价,廉价的 jig 大量繁殖,你最终会得到一抽屉的一次性夹具,而不是去做实际的工作。我仍然认为这个观点是对的。十一周后,我描述了让智能体把一整套方法论提取为可复用模板的做法,也称之为 jig。同一个定义,同一个方向。
jig 有两项职责。那个定义只覆盖了其中一项。
两个方向,我只命名了一个
三月,我命名的每一个 jig 都是智能体为我建造的。重新格式化数据的脚本、预筛输入的工作流、搭建文档骨架的模板。智能体制作夹具,人类做工作。
另一个方向是我从未定义过的。我建造夹具,智能体在其中工作。
这两种类型一直都在我的仓库里。只有一种得到了名字。这说明了一切——我给那些节省我时间的夹具起了名字,却从未给那些夺走智能体选项的夹具命名。
让切割变得不可能的目录
写这篇文章之前,我去数了第二种,而没有相信自己的记忆。
那是一个零售店面,模型在其中组合页面布局。一个目录文件声明了该模型可以写入的每一个命名位置。二十八个区域,分布在十个界面上——首页、商品列表、商品详情、购物车、结账、搜索、账户、门店定位器,以及两种错误状态。每个条目带有两个布尔值:组合引擎是否可以将其作为目标,商家是否可以手动创作。
二十七个对引擎开放。二十七个对商家开放。它们不是同一组二十七个。
商品网格下方有一个区域对商家开放但对引擎关闭。商品页上有一个区域对引擎开放但对商家关闭。两个布尔值,两个例外,而每个例外存在的原因都是有人坐下来决定了这件东西模型不应该碰、而那件东西商家不应该碰。整个界面的治理模型就是配置文件里两个标志,在恰好两个地方不一致。
购物车有三个区域。结账按钮上方一个。折叠线下方一个,最多两项。以及一个空状态。
订单行项目、订单总计、促销入口、结账按钮本身——都不是区域。没有它们的地址。
那个目录不加速任何东西。通过它组合页面比让模型发出任何它想要的标记更慢。它所做的其实是移除一种切割。
那个系统里没有任何策略说模型不可以修改购物车总计。没有人写过那句话,也不需要人写。模型收到一个它可以放置内容的位置的模式,而那个模式里没有任何地址指向总计。它没有被禁止。只是没有地方放置那个指令。
这就是我自己的归档出错的地方,而且花了另一个人指着那段话指出问题,我才看到。
我这台笔记本上有两个钩子,我一直称之为 jig。一个强制并行智能体会话在独立的 git worktree 中工作,因为多个会话共享同一个仓库会互相切换对方的分支。这是一个针对 Bash、Write、Edit 和 MultiEdit 的 PreToolUse 钩子。它读取一个锁目录,检查是否有另一个活跃会话持有该仓库,并返回 permissionDecision: "deny",同时给出一条可以粘贴的命令来创建 worktree。
第二个保护读取。边缘超过 1400 像素的图片会被拒绝——它将副本缩放到 1000px、缓存起来、然后返回新路径。十分钟窗口内重新读取你已经读过的文件也会被拒绝,并附带一条注释让智能体从上下文引用。
那些不是 jig。它们是安全阀。
jig 是形状。区域目录不会检测到违规,因为违规没有表现形式——没有购物车总计的地址可以去尝试。安全阀正相反:它感知一个条件然后中断。它需要那件坏事是可以尝试的,才能捕获它。
关键特征被我当作脚注忽略了。两个钩子都让立即重试通过。在读取保护里这是一个明确的闩锁——拒绝一次,设置 pending_retry: True,在下一次调用时清除它,这样同一次读取就能成功。它的文档字符串明确说"设计为故障开放",并称自己"对反射性重读的摩擦,而非墙壁"。有一个关闭开关:touch ~/.claude/cache/read-guard/.guard-off。
每一个都是安全阀行为。夹具没有重置、没有故障开放、没有关闭开关,因为没有什么可以关闭。你不能通过重试绕过形状。
所以有三样东西,不是两样,阶梯是:智能体可以拒绝的规则、阻止它并可以重置的安全阀、以及一个没有地方放置那条坏指令的界面。
安全阀是廉价的那种。工作树钩子五百五十行 Python,读取保护二百九十三行,设置文件里几行 JSON。任何运行编程智能体的人今天下午都能加一个。哪天把自己的指令文件按三列分类一下——我的是两个安全阀、一个客户端规模的 jig,其余都是标签。
一个有效的 jig 会抹去自己的证据
这是我花了最长时间才看到的东西,而这就是第三列比看起来更难的原因。
安全阀会宣告自己。我的钩子每一次触发,拒绝消息都会带原因带内发送给智能体。那次触发就是记录。如果我把阈值放错了位置——比如说 1400 像素太激进了——我就会发现,因为我会看到智能体被阻止做某件合理的事。
jig 不产生任何东西。这是它的意义所在,也是它的问题所在。
回到店面解析器。它有 116 行。当它收到一个不在目录中的区域 ID 时,它会抛出:
throw new Error(`resolveZone: unknown zone instance "${opts.zoneId}"`);
响亮、即时、无法忽视。现在来看执行实际约束的那一行,在十二行之后:
if (engineRaw !== undefined && meta.engineComposable) {
如果引擎为一个不允许写入的区域生成了内容,那个条件为假。执行落到下一个块,然后落到静态回退。页面渲染成功。没有抛出异常,没有警告,商家看到一个完全正常的页面,里面是默认内容。
所以解析器对无害的失败响亮,而对真正的失败沉默。
我去找沉默的那种情况在哪里被记录。它旁边有一个检索日志,其条目类型有 surface、seed entities、session、brand 以及放置在每个区域里的产品的字段。每个字段描述的都是成功的东西。没有用于被丢弃的写入的字段、没有来源、没有原因。
Then I grepped for engineComposable across the entire codebase. It appears in three places: the catalog that declares it, a test file, and line 71 of the resolver. The one line that enforces the constraint is the same line that swallows the evidence, and nothing else in the system ever reads the flag.
然后我在整个代码库中 grep 了 engineComposable。它出现在三个地方:声明它的 catalog、一个测试文件,以及 resolver 的第 71 行。那行执行约束的代码正是吞噬证据的那一行,而系统中没有任何其他地方读取这个标志。
That's the second grade of invisibility. The third is worse, and it's the one that should actually worry me: a zone I never declared at all. The engine can't attempt it, so there's no branch to instrument, no discard to log, no line to grep for. The model simply never composes the thing I forgot to make a place for.
这是第二层级的不可见性。第三层更糟,而这才是真正应该让我担忧的:一个我根本没有声明的区域。引擎无法尝试它,所以没有分支可以插桩,没有 discard 可以记录,也没有一行代码可以 grep。模型只是从未组合出那个我忘记为它预留位置的东西。
How would I ever see that page? Not from an error, not from the log, not from the diff. The output that should have existed and doesn't leaves no trace anywhere in the system that excluded it.
我怎样才能看到那个页面?不是从错误,不是从日志,也不是从 diff。那个应该存在却不存在的输出,在整个排斥了它的系统中不留任何痕迹。
That's the asymmetry that makes jigs genuinely hard rather than just underrated. A fence in the wrong place and a fence in the right place produce identical telemetry: clean.
这就是让 jigs 真正困难而非只是被低估的不对称性。 fence 放错了地方和放对了地方产生完全相同的遥测数据:干净。
The fix isn't cleverer geometry. It's putting a valve next to the jig — one else branch at line 71 that records the discard, so the thing the fixture ate leaves a mark on the way down. The catalog stays geometry. The valve makes it observable. That's four lines, and it isn't written.
修复不是更精巧的几何形状。是在 jig 旁边放一个阀门——第 71 行的一个 else 分支,记录那个 discard,这样 fixture 吃掉的东西在经过时会留下痕迹。catalog 保持几何形状。阀门让它变得可观测。这是四行代码,而且它没有被写出来。
Build the Jig When You'll Make the Cut Twice
当你需要切两次时再构建 Jig
The old post buried the rule in a warning box. You built a jig only when the task was worth the setup. Agents dropping the cost of building one to near zero is what killed that discipline.
那篇旧文把规则埋在一个警告框里。只有当任务值得投入搭建成本时,你才构建 jig。智能体把这个成本降到接近于零,这杀死了那种纪律。
The rule survives intact. Nobody builds a jig for one cut. For one cut you clamp carefully, measure twice, go slow.
规则完整地保留了下来。没有人会为一次切割构建 jig。对于一次切割,你会小心夹紧、量两次、慢慢来。
The one I lean on hardest is ten shell commands that drive a real Chrome profile. Navigate, evaluate JavaScript in the page, screenshot, scrape to markdown, crawl, manage tabs. That's it. The whole thing is public.
我最依赖的是一个驱动真实 Chrome 配置文件的十个 shell 命令。导航、在页面中执行 JavaScript、截图、抓取为 markdown、爬取、管理标签页。就这样。全部是公开的。
I didn't build it for a project, and that's the part that matters. It's installed permanently, on PATH in every shell. Because it's already there, I start every integration reaching for a real interface — a CLI, an API — and driving a human interface stays the fallback it should be. That ordering only works if the fallback exists before I need it. Built per project, it doesn't. It's a detour I don't have time for at the moment I hit a system with no API, so I either skip the work or design the whole thing around mimicry from the start.
我不是为一个项目构建它的,而这才是重要的部分。它被永久安装,在每个 shell 的 PATH 中。因为它已经在那儿了,我每次做集成时都会先伸手去够真实的接口——一个 CLI、一个 API——而驱动人类界面始终是它应该保持的备选方案。这种排序只有在备选方案在我需要它之前就已存在时才能工作。每个项目构建一次,它就不存在了。这是我在遇到没有 API 的系统时没有时间去走的弯路,所以我要么跳过这项工作,要么从一开始就把整个设计围绕模拟来进行。
Which raises the obvious question: why not use the MCP servers that already do this?
这就引出了一个显而易见的问题:为什么不直接用已经做了这些事的 MCP servers?
My own config says to prefer the shell commands because of the token footprint. It doesn't say by how much, and the README for my own tool claims thirteen to eighteen thousand tokens with no method attached. So I measured: started each server over stdio, completed the handshake, called tools/list, and ran the result through a real tokenizer instead of dividing by four.
我自己的配置说因为 token 占用量而优先使用 shell 命令。它没有说具体多多少,而我自己的工具的 README 声称需要一万三到一万八千个 token,但没有附上方法。所以我测量了:通过 stdio 启动每个 server,完成握手,调用 tools/list,然后把结果通过一个真正的 tokenizer 运行,而不是除以四。
My own README is off by roughly three and a half times, in the direction that flatters my tool. It reads like a measurement and it was an assumption about characters per token — the real ratio here is 4.7, and somebody, probably me, used something near 1.3.
我自己的 README 偏差了大约 3.5 倍,偏向于美化我的工具的方向。它读起来像是一个测量结果,但实际上是一个关于每个 token 字符数的假设——这里的真实比率是 4.7,而有人(很可能是我)用了接近 1.3 的数字。
So the honest number is smaller than I'd been telling people, and the size isn't the argument anyway. The argument is when you pay.
所以诚实的数字比我一直告诉人们的要小,而且大小本来就不是论点所在。真正的论点是什么时候你为之付费。
Those bytes load at connection, in every session where the server is configured, whether or not the agent ever opens a page. The shell commands cost one sentence in my instructions file — a pointer to a README. Sessions that browse spend about 4,000 tokens reading it. Sessions that don't spend nothing.
这些字节在连接时加载,在每个配置了 server 的会话中,无论智能体是否真的打开过页面。shell 命令在我的指令文件中只花了一句话——一个指向 README 的指针。会浏览的会话花大约 4,000 个 token 来读取它。不浏览的会话什么都不花。
I have 3,711 session transcripts on this machine. Two hundred fifty of them made a real browse-* call. Sixty-eight made a real call to one of those MCP servers. Whatever the right denominator is, the shape is the same: a browser is a thing I need occasionally and pay for constantly.
我这台机器上有 3,711 份会话记录。其中 250 份做了真正的 browse-* 调用。68 份对一个 MCP server 做了真正的调用。无论正确的分母是什么,形状都是一样的:浏览器是我偶尔需要但始终为之付费的东西。
There's a second cost that doesn't show up in bytes. The agent already knows how to write JavaScript and read a DOM. A command that hands it a page and gets out of the way lets it use what it arrived with. A bespoke tool schema wrapping the same operations makes it learn a private vocabulary first, and that vocabulary is a fixture somebody else built for a cut I may not be making.
有一个不出现在字节数中的第二层成本。智能体已经知道如何写 JavaScript 和读取 DOM。一个把页面交给它然后让开的命令让它用自己带来的能力。一个包装了相同操作的定制 tool schema 让它先学习一个私有词汇,而那个词汇是别人为一次我可能不会做的切割而构建的 fixture。
None of which means don't use them. If you'll genuinely make the cut once, buy the assembled thing. That isn't a compromise; that's the rule working.
这些都不是说不要用它们。如果你真的只会做一次切割,买组装好的东西。这不是妥协;那是规则在起作用。
The Valve I Left on a Branch
我留在分支上的阀门
Here's the part that undercuts everything above.
以下是推翻上面一切的部分。
The operator card I load into every session cites one of my own scripts as a reference implementation — a mechanical check that verifies a short-form caption against the long-form post it was derived from, so a claim can't quietly change owners between the source and the summary. I wrote it because I'd watched exactly that failure happen and decided a rule wasn't enough.
我加载到每个会话中的 operator card 引用了我自己的一个脚本作为参考实现——一个机械检查,验证短格式 caption 与派生它的长格式文章是否一致,这样一个声明就不会在源和摘要之间悄悄易主。我写它是因为我目睹过恰好那种失败发生,然后认定一条规则是不够的。
It lives on a branch called caption-edits-0803. Five commits of gate work, none of them on main. The syndication/ directory on main has no gate script in it at all.
它在一个叫 caption-edits-0803 的分支上。五次 gate 工作的提交,没有一次在 main 上。main 上的 syndication/ 目录里根本没有 gate 脚本。
So the file that tells agents "this rule is enforced mechanically, here's the implementation" points at a check that has never run against anything I've published.
所以那个告诉智能体"这条规则是机械执行的,这是实现"的文件,指向的是一个从未在我的发布内容上运行过的检查。
That's the same failure as tags.ts, one level up. The first wrote a rule and never built the mechanism. This one built the mechanism and never installed it. Both read as enforcement from inside the instructions file, and both are labels.
这和 tags.ts 的失败是同一种,只是高一级。第一个写了规则却从未构建机制。这一个构建了机制却从未安装。两者从指令文件内部读起来都像是执行,而且两者都只是标签。
An uninstalled valve is indistinguishable from an installed one until the day it should have tripped. Which is the same silence as before, arriving by a different route.
一个未安装的阀门和一个已安装的在它本应触发的那天之前是无法区分的。这是和之前一样的沉默,只是通过不同的路径到达。
I ran through a vendor capability questionnaire last week. It has a row for no-code agent builders, and two words after the row: if available. The next row down asks how you use AI to speed up your own development — writing code, configuring, testing, deploying.
我上周填了一份供应商能力问卷。它有一行是关于无代码智能体构建器的,那行后面有两个词:if available。下一行问的是你如何用 AI 来加速你自己的开发——写代码、配置、测试、部署。
Two rows, opposite bets. One expects a finished product a buyer can score. The other only makes sense if the surface underneath is clean enough that a general-purpose agent can work it without a human translating.
两行,截然相反的赌注。一个期望一个买家可以评分的成品。另一个只有在底层表面足够干净、以至于通用智能体可以在没有人类翻译的情况下工作时才有意义。
The form isn't badly designed. A rubric compares things that come out the same in every hand, and turnkey is the only category that does. A valve at least demos — you can trip it on a stage and watch it stop something. But there's no cell for we made a class of mistake structurally impossible in this surface, because that isn't a feature. It's an absence, and the better it works the less there is to see.
这个表格设计得并不差。一个评分标准比较的是在每种情况下产出相同的东西,而 turnkey 是唯一能做到这一点的类别。一个阀门至少可以演示——你可以在一个阶段触发它,然后看着它阻止某事发生。但没有一格是关于"我们在这种表面上让一类错误变得结构上不可能",因为那不是一个功能。它是一种缺席,而且它工作得越好,可以看见的东西就越少。
What I Still Don't Have
我仍然没有的东西
I don't have an instrument for the third column. Nothing in that storefront can tell me what the engine tried to compose and wasn't allowed to, and nothing at all can tell me about the zone I never declared. That's the gap that matters, and it's four lines of else branch away from being half-closed.
我没有任何用于第三列的工具。那个 storefront 中没有任何东西能告诉我引擎尝试组合了什么但被拒绝的,而完全没有任何东西能告诉我关于我从未声明过的那个区域的事。这才是重要的差距,而且它距离半封闭状态只差四行 else 分支。
I also don't have a validator on the tag list. Read the canonical keys, refine the Zod schema from z.string() to an enum built from them, let the build fail. Small the entire time the file has been claiming it already exists.
我也还没有 tag 列表上的验证器。读取规范键,将 Zod schema 从 z.string() 精化为一个由它们构建的 enum,让构建失败。这个文件声称它已经存在了这么久,整个过程中它都是小的。
The drawer of snowflakes I warned about in March is still real. But that one's the cheerful failure — you trip over the clutter and clean it up. The other kind doesn't accumulate anywhere you can trip over it.
我在三月警告过的那个雪花抽屉仍然是真实存在的。但那是那种令人愉快的失败——你绊倒在杂物上然后清理掉它。另一种不会在任何你能绊倒的地方积累。
The fallback rendered. The page looked right. Nothing asked what was missing.
备选方案渲染了。页面看起来没问题。没有人问缺失了什么。