这份文档解释”为什么这么写”,不是 SKILL.md 的中文版。
skills/intent-router/SKILL.md是英文单源,也是唯一可执行的版本。这里按它的章节顺序,逐节回答 三个问题:这段为什么存在 / 删掉会发生什么 / 它锁住的是哪条决策。刻意不提供指令正文的中文翻译。理由很实际:一份逐句翻译版会被人(和模型)误当成可执行版本,然后 与英文原文各自漂移,最终没人知道哪个是真的。要改行为,改英文那份;要理解行为,读这份。
SKILL.md 是一份给模型读的运行手册,不是给人读的说明书。它的每一行都在争夺同一份稀缺资源: 模型的注意力。所以全文遵守三条隐含预算:
references/。ask-protocol.md,探测面清单交给 probe-surfaces.md。理解这三条,下面每一节”为什么这么短”就都有答案了。
为什么存在:name 和 description 是各家 harness 唯一始终在上下文里的两个字段。正文是
按需加载的,description 不是——它决定了这个 skill 会不会被想起来。写不好 description,后面 500
行写得再对也不会被执行。
为什么是现在这个写法:
disable-model-invocation)会让 Claude Code 的 IDE 校验器告警;而且本 skill 本来就需要自动
触发,禁用模型调用与设计意图相反。删掉会发生什么:description 写含糊 → skill 沉默;写太宽 → 用户问”这函数干嘛的”它也跳出来。 两种都是产品级失败,而且第二种更难被发现。
为什么存在:这是全文唯一一处防止误触发的机制。没有它,一个自动触发的 skill 会变成骚扰。
为什么是”两个条件同时满足”:只要求”是做事型请求”会把已完全指定的任务也拦下来(用户明明说清楚 了还要被走一遍流程);只要求”存在未知项”会把提问类也算进来(任何问题都有未知项)。两条缺一不可。
no-ask 模式为什么单独写:用户说”别问我,直接做”时,最容易出的错是把 ASK 预算清零之后就开始
猜。这里明确:预算归零但 PROBE 照常,未决项标 inferred;若某个 inferred 项触及不可逆边界,
仍然 HALT。这是对设计原则第 7 条的落实——”别问我”是授权它少问,不是授权它赌。
删掉会发生什么:skill 在解释代码、回答问题时也启动,用户会直接卸载它。
为什么静默出口之后还要加一次前提检查:四项齐全只说明”没有要收敛的决策”,没说明”决策本身 没错”。请求可能写得很完整,却依赖一个已被决策记录否定的方案,或点名一个已经不存在的依赖——四项 齐全的静默检查本身抓不住这种情况。这次改动让 skill 在真正保持静默之前,先花至多两次查找确认 “没人反对”,而不是止于”没人问”;查到矛盾就转为运行,查不到就照旧零输出。
为什么 description 要删掉”四项齐全就别用我”:不删的话 harness 根本不会为四项齐全的请求 加载 skill,上面这条前提检查永远没有机会跑——闸门写得越窄,能走到闸门跟前的请求就越少。代价是 skill 在这类请求上被加载得更频繁,但只加载不代表会输出:静默检查和前提检查两道关都过了才真正 不产出。
为什么存在:后面 200 多行反复使用 decision-bearing、evidence、probe surface、irreversible 这些词。不定义就会被模型按常识理解,而常识理解恰好是错的——例如”evidence”在常识里可以是一段 论述,在这里必须是一个可验证的指针。
为什么放在这么靠前:它是后面所有判断的前提。术语定义放后面,等于让模型先执行再理解。
为什么只有 8 条:术语表是有成本的。只定义那些”按常识理解会出错”的词;request、objects
这类不言自明的不进表。
为什么新增了 issue 这个 unknown 项可选字段:unknown 原来只回答”这一项还不知道”,不回答
“为什么会不知道”。两种读法、自相矛盾、前提被否定,这三类缺陷不是”信息不够”,
是”请求本身有问题”,需要一个与语言无关的标记,让评测能判定分类对不对,也让下游一眼看出这是该
优先处理的那类未知项。缺省不写 issue 就是普通缺信息,对旧 spec 完全兼容。
为什么第一步是”原样记录用户的话”:request 字段要求截断但不改写。审阅者需要能把 spec 和用户
实际说的话对照。一旦允许转述,模型会在转述里就把歧义”解决”掉了——而那正是这个 skill 要暴露的
东西。
为什么 unknown 要分六类:这套分类压缩自 spec-kit /clarify 的 11 类,砍掉了实现细节层面的
类别,只留意图层面。理由:实现细节归执行者管,把它们拉进来会让提问预算浪费在”变量叫什么”上。
分类本身的作用不是归档,而是提示模型去想那些它容易漏掉的维度——尤其 failure_behavior 和
non_goals_constraints,这两类是模型最常整段忘记的。
为什么要标 decision-bearing 并明说”false 的不进 Resolve”:不加这个闸门,模型会把每个未知项都 当成要解决的,于是要么探测预算爆掉,要么开始问琐碎问题。这是提问数量的第一道闸。
为什么明说的约束现在可以被质疑,但只限两种情况:铁律本身没变——用户说过的永远不重新推导;
变的是新增两个例外:这条约束和另一条用户明说的约束冲突(conflict),或者被一个真的打开过的
来源否定(premise)。不加这两个例外,遇到自相矛盾或前提有误的请求,模型只能悄悄选一条约束
执行、或者假装没看见矛盾——两者都是猜。为什么不能扩大到”任何看起来可疑的约束”:那会退回到”拆分
用户没有区分的子情形”这个已经修过的过度提问缺陷,ambiguous/conflict/premise 三类因此被
写成封闭列表,不是一个开放的”觉得不对就问”的口子。
为什么铁律用引用块单独排版:它是整个引擎的灵魂。排版上让它跳出来,是为了让模型在长上下文里 仍能”看见”它。语义不可改动——改了这一句,这个项目就退化成又一个访谈式 skill。
为什么措辞是”能力描述”而不是工具名:SKILL.md 里不出现 Read / Grep / Bash 这类名字,而是写 “use whatever file-reading, search, or shell capability your environment provides”。原因有二: ① Agent Skills 开放规范明确”只用 name + description + 纯 markdown 即可全平台通用”,写死工具名会 让它在别家 harness 上指向不存在的东西;② 工具名会随 harness 版本变,能力不会。代价是措辞啰嗦一 点,换来的是 26 个 harness 的兼容性——这笔交易值得。
为什么探测面在正文只列 6 条:完整清单(含各语言生态、evidence 格式、degraded 判例)在
references/probe-surfaces.md。正文列的这 6 条是按命中率排序的,绝大多数请求在前 3 条就解决
了;把 40 行清单塞进正文只会稀释铁律的存在感。
为什么探测预算是每个 unknown 3 次:不设上限时模型会通读仓库(慢、贵、且注意力被稀释)。3 次 是经验值:一次定向搜索 + 一个文件 + 一次历史查询,足以解决绝大多数客观问题。超预算后的处理路径 被明确写死(能问人的转 ASK,不能的留在 unknown 里等 HALT 点名),否则模型会在这里自由发挥—— 自由发挥的结果通常是猜。
为什么 probed/inferred 必须带 evidence,而且由 schema 强制:一条没有来源的”仓库里说……”, 事后无法与凭空编造区分开。这是评测里唯一的一票否决项:只要出现一条指向不存在文件的 evidence, 整套评测判不达标。理由是信誉不对称——答错会被评审发现,编造的引用不会。
为什么 degraded 要单独讲、还强调”不得伪装成信息不足”:这是全项目唯一的原创运维洞察。把”探测 失败”报成”信息不足”,会让一个环境故障看起来像用户的问题;反过来则会让一个含糊请求触发运维告警。 两者合并的实际后果是:某个后端挂了一周,流量静默全走兜底,日志里什么都看不出来。
为什么证据有 record: 与 doc: 两个保留命名空间:出了代码领域,”这条答案的来源”往往不是仓库
里的一个文件,而是一条订单记录、一节政策文档。给它们保留命名空间,指针才能保持”一个 token、无空
格、指向你真的打开过的东西”;没有这个出口,模型只能退化成写推理句——那正是 evidence 机制要拦掉
的形态。
为什么非代码领域的探测面单独写在 domains.md:探测面调度是”客观答案在哪里”这个问题的领域化
表达。编码域查 repo,客服域查工单与政策,研究域查先前的报告与来源白名单。四类领域各写一份
“PROBE 能够到什么、什么值得问、什么不值得问”,正文只留一句”按领域读对应文件”——逐领域清单进正文
会把铁律撑散。
为什么”悬空引用”被单独定义、而预算耗尽仍保留历史查询:指向不存在文件的证据按幻觉判定,一票 否决;”上一轮已经查过的 git 历史”则是合法的探测记忆,预算外保留它。不这样分,模型要么为查同一条 log 反复烧预算而放弃查证,要么干脆开始编——两种都比”允许重查已开过的历史”更坏。
为什么”一次一问”要写死、还要禁止”预告后续问题”:预告等于批量提问加了层包装——用户仍然要一次性 在脑子里排序所有问题,而那正是这个 skill 要消除的成本。
为什么每问必须带”Why you, not me”:这一句是对铁律的自我审查。模型在写这句话时,如果发现 理由是”我没查”,它就该回去查。它对用户的价值是次要的,对模型的约束作用是主要的。
为什么必须给推荐项:不给默认值的问题,成本几乎等于没问——用户还是得自己权衡。带推荐的问题 用户可以一个字符回答。
为什么选项限定 2–3 个且互斥:4 个以上选项说明没探测干净;”其它”选项等于把问题原样退回。
为什么委托(”你决定”)要记 inferred 而不是 asked:asked 的语义是”有人真的做了决定”。
把委托记成 asked 会让指标虚高,而这个指标是本项目的核心卖点,必须干净。
为什么不可逆项不接受委托:用户说”你决定”时,往往没意识到这个决定改不回来。重述风险再问一次 (并计入预算)是唯一诚实的处理;再拒绝就 HALT。这条是设计原则第 7 条在对话层面的落点。
为什么规定”用用户的语言提问,但 key 恒英文”:value 用用户的语言是礼貌;key 用英文是因为 IntentSpec 要被下游程序消费,key 一旦本地化就没法机读了。
为什么谓词要写成公式:与 grill-me 的”46 问”和 dumb zone 对照,这里的主张是停止条件必须可 计算。写成散文(”当你觉得信息足够时”)等于没写——模型对”足够”的判断会随上下文长度漂移。
为什么谓词是”unknown 为空 ∧ 没有 inferred 且 irreversible 的约束”两项而不是一项:只要第一项 的话,一个”模型自己猜了一个不可逆决定但没标成 unknown”的 spec 会顺利 ROUTE。第二项专门堵这个洞。
为什么 ROUTE 时要明确”交接后停止,不要在本 skill 内开始实现”:模型的默认倾向是一路做下去。 不写死这一句,它会在产出 spec 之后顺手开始改代码——那样这个 skill 就从”路由的前一层”变成了”又一个 编码 agent”,失去全部定位。
为什么 ASK 态也要输出完整 spec 快照:让用户在回答问题之前就能看到已经探到了什么。很多时候 用户看完快照会发现模型探错了方向,这比回答完问题再发现便宜得多。
为什么有 silence check(静默出口):请求本身已完整时,走一遍 parse→probe→ask 的全套流程是纯 开销,还会把”没什么可问”包装成一份没人需要的 spec。静默出口让 skill 在”没有值得问的”输入上只花 至多两次查找做前提检查(见 §2),没查到矛盾就直接放行——不产出、不写盘、不打扰。它是”少烦人”这个卖点的最后一块:连路由这个动作本身, 在不需要时也不发生。
为什么 emit 前要做三项自检:一是有没有漏掉”inferred 且触不可逆边界”的约束(该 HALT 而没有 HALT),二是每条 evidence 是否合法(形态 + 指向真实存在的东西),三是三个计数器能不能算平。schema 拦得住结构错误,拦不住这三类语义错误;把它们写成产出前的 checklist,让最常见的三种坏 spec 死在 发送之前,而不是死在评测里。
为什么两类 HALT 互斥、且各有必填字段:互斥由 schema 的 oneOf 强制。underspecified 必须给
open_fields(否则用户不知道该补什么),degraded 必须给 error(否则运维不知道修什么)。
“halt 了但没说为什么”是最没用的输出。
为什么交接之后还要 Verify,而且只在同一对话内做:ROUTE 只保证”决定齐了”,不保证”照着决定
做对了”。交接后的实现如果就发生在同一个对话里,核对一次的机会成本几乎是零,却能补上”决定和交付
不一致”这个此前完全没人管的缺口;换了对话或换了 session,spec 已经存进 .intent/,核对的事就
交给读那份契约的人。为什么只写回复、不写 .intent/(D8):这不是新功能,是对既有 spec 的一次
核验,不该产生新的写盘副作用,契约本身也不需要多一个”已被核对”的状态。为什么清单标题固定写英文
Intent check:runner 判定”这一轮有没有做 Verify”必须和语言无关,这与 spec 键恒为英文是同一个
理由;标题下面的内容仍用用户的语言。
为什么字段顺序被固定、还要求与 schema 一致:审阅者是从上往下读的。顺序是
request → intent → objects → constraints → unknown → decision:先看到用户说了什么,再看到
结论。反过来排,人会先被结论锚定,然后倾向于认同它。
为什么给完整 YAML 骨架而不是字段列表:模型照着例子生成的准确率远高于照着描述生成。这段 60 行
的成本换来的是解析成功率——评测里 degraded_output(产出了但解析不了)是单独计数的一类失败。
为什么还要一条 quoting 硬规则、且示例本身要带”脏”标量:2026-09-22 首轮实测里解析失败出现了
5 次,全部是标量值含 :、{} 或引号却未加引号——而当时的示例里没有一个标量是”脏”的,模型照抄的
就是那个干净风格。所以规则和示例必须同时改:示例是主要杠杆,规则防止示例被后人”清理”回干净风格。
默认用单引号而非双引号,因为失败值本身常含双引号,单引号零转义。其中最危险的是”空格 + #“(如
reverted in #412):YAML 会把它当行内注释静默截断,围栏照样解析成功,换来一份内容被砍掉却骗
得过审阅的 spec。
为什么强调”围栏里只放 spec,散文放围栏外”:runner 取的是输出里最后一个 ```yaml 围栏。 围栏里混入解释文字会让解析失败。
为什么每次产出都写盘:契约落在文件里才能跨 session 存活——下一个 session、同事或规划器接手
时,读 .intent/<intent>.intent.yaml 就能从已定的契约开始,而不是从零开始猜。ASK 态也写:挂起的
问题本身也是一份可查的记录,”上轮问了什么、还没答什么”有据可查。同一请求的后一次产出覆盖旧快照,
文件里留下的始终是最新结论;不同请求撞了同一个 intent 名时换一个更具体的 intent,不去覆盖别
的工作的契约。只写这一个文件、不碰别的、也不替你提交——.intent/ 进不进版本控制由项目自己决定。
先写盘再回复,是因为有的环境只保留一轮里的最后一条消息,围栏必须在那条消息里。写不了盘(工作区
只读)时只在围栏后说一句,不因此 HALT——spec 本身已经完整。.intent/ 里的旧文件不能当证据:
它记录的是上一轮推断出的结论,引用它会把旧推断洗成已核实的事实,要探测就去探测它当初的来源。
为什么专门留一节给”问不出来的问题”:这类请求(”让它更现代一点”)会让访谈式 skill 陷入死循环 ——每一轮都在换着法问同一个无法回答的问题。承认这类问题的存在、并给出出口(去做原型),比假装能 解决它诚实得多。这条局限直接继承自 grill-me,它也点明了这一点。
为什么出口是”移交原型”而不是 HALT:HALT 也可以,但对用户不够有用。”你需要一个能看见的东西来 反应”是个可执行的建议。
为什么要有这一节,而且用否定句:前面所有章节都在说”该做什么”。模型的失败模式往往不是”没做 该做的”,而是”做了不该做的”。八条里每一条都对应一个真实可能发生的退化,其中第 8 条 (passivity)直接取自 grill-me 记录的失败模式:用户一路说”同意”,而 skill 机械推进不收敛。
为什么第 1 条要写”treat a violation as a bug, not a style preference”:不加这句,模型会把 铁律当成一条建议来权衡。
为什么只有一句话的索引而不是内容摘要:索引的作用是让模型知道”还有东西可以读、以及什么时候 去读”。写成摘要会诱使模型直接用摘要作答而不去读原文;写成一句话反而提高了实际加载率。
五个文件的边界:
| 文件 | 装什么 | 刻意不装什么 |
|---|---|---|
probe-surfaces.md |
各生态完整探测面、evidence 格式、预算与 degraded 判例 | 任何工具名 |
ask-protocol.md |
问题模板、好坏问题对照、委托、预算覆盖、语言规则 | 铁律正文(只引用,不重抄) |
intentspec.md |
字段逐条说明、跨字段不变量、五个示例导读、.intent/ 约定 |
schema 本体(只链接) |
domains.md |
非编程领域的探测面与典型 ASK、负向判据注册模式 | 具体业务提示词 |
harness-compat.md |
各 harness 目录、自动加载、调用语法、三态状态 | 其它一切 |
为什么 harness-compat.md 是唯一允许出现工具名与 harness 专有语法的文件:工具中立是为了让
正文在任何地方都能跑;但”在 Codex 上怎么调用它”这个问题本身就是 harness 专有的。把这类内容全部
隔离在一个文件里,正文才能保持干净——同时也让”这个文件会过期”这件事有了明确的边界。
有五条约束写在 references/intentspec.md 里并由评测断言:
unknowns_found = resolved_by_probe + asked + inferred + len(unknown)state == ROUTE ⟹ unknown == []state == ASK ⟹ asked < ask_budgetstate == ROUTE ⟹ 不存在同时 source: inferred 且 irreversible: true 的约束git:、user:delegated、
record:、doc:)第 1 条是真的表达不了(JSON Schema 不做算术)。第 2–4 条技术上可以写进 schema,但刻意没写:
分开之后,违反它们时评测报的是 invariants_ok 失败而不是 schema_valid 失败,诊断信息更精确
——一个是”逻辑算错了”,一个是”结构不对”,修法完全不同。第 4 条就是充分性谓词的另一半,重述成
检查项;第 5 条的”路径是否存在”由 runner 对着 fixture 清单核,schema 只强制单 token 形状。
另外一个容易误解的点:resolution.asked 的语义是”已被人回答掉的 unknown 数”,不是”已抛出的
问题数”。ASK 态输出时问题还没被回答,所以该 unknown 仍留在 unknown[] 里、asked 仍是 0——否则
第 1 条加法就平不了。schema/examples/ask.yaml 就是这个状态的实例(4 = 3 + 0 + 0 + 1)。