SKILL是如何工作的

SKILL 是如何工作的?

写给完全没搞懂「skill 到底是个啥」的人。

本文拿一份虚构的 skill 当标本,从「你说一句话」讲到「文件落到你桌面上」 来剖析skill。

开始之前,先把前提说清楚:

本文举的 agent 客户端是 Codex。换成别的 agent 客户端,机制大同小异。 凡是支持 skill(或者叫规则文件、指令文件的)客户端,主线都是同一条:清单常驻 → 语义命中 → 读正文 → 按正文干活。差别主要在文件名、目录位置、触发语法、命令细节这些"皮"上。所以文中讲的主角是「大模型 + agent 客户端」这个组合,Codex 只是那个被拿出来示众的例子。


0. 先说结论(不想看长文的看这里)

  • skill 不是一个程序。 它没有入口函数、没有按钮,你也没法「运行」它。它就是磁盘上的一份 Markdown 说明书。
  • skill 不是被「调用」的,是被「想起来」的。 你说话 → 大模型想起来有这本手册 → 大模型去把它读进来 → 然后按它写的步骤干活。
  • 「流程规范化」不是代码约束,是「指令遵从」。 真正管流程的是大模型,不是客户端。所以它是概率,不是合同。
  • 所以写 skill 有一半功夫不是在写流程,是在堵漏洞------堵大模型偷懒、堵它脑补、堵它压缩上下文之后失忆。

下面展开讲。


1. 先分清四个角色,这是理解一切的前提

很多人把这四个东西混成一坨,然后越看越晕。先把它们拆开:

角色 是什么 能干什么
LLM(大模型) 一个只会「读文字、写文字、做决定」的脑子 没有手:不能读文件、不能跑命令,还记不住上一次
Agent 客户端(比如 Codex) 脑子 + 手 + 一个循环 给大模型接上工具(读文件、跑命令、开子 agent),让它反复「想 → 做 → 看结果 → 再想」
Skill 磁盘上的一份说明书(SKILL.md) 它自己不会运行,只是「等着被翻开」
Scripts 说明书里夹着的计算器 确定性计算,不带判断

打个比方:

  • 大模型是个刚招来的聪明实习生------理解力很强,但只会读读写写,而且还带点失忆;
  • Codex 是给他配了工位、终端机,然后说一句「自己想办法干完」;
  • skill 是工位上那本《操作手册》;
  • 脚本是手册里夹着的计算器。

关键点:实习生不会自动去翻工位上那本手册。 得有人提醒他,或者他自己想起来。这一步,就是后面所有「玄学感」的来源。

顺便强调一下:换个客户端,就是换了个牌子的工位和终端机。手册还是那本手册,实习生还是那个实习生。


2. 拿一个(虚构的)需求当标本

假设有这么一个团队:客服每天收到用户发来的报错截图。

截图上的提示有时是「系统繁忙,请稍后重试」,有时是「当前订单状态不允许该操作」,还有时是一串没人看得懂的码,比如 E-1007。

客服想知道的是三件事:

  1. 这个码到底是什么意思?
  2. 用户在什么情况下会撞上它?
  3. 我这句回复该怎么写?

而这些码在代码里散得很开:几个枚举类、一堆常量类,还有些直接写在业务方法里的字符串字面量。前端展示的文案又是通过 i18n key 翻译出来的,肉眼根本对不上。

于是有了这个需求:

「给我出一份《错误码手册》,给客服和运营看的。代码一直在变,以后要能随时重新生成。」

这个活有几个特点,正好适合当标本:

  1. 它重复发生(代码一变,手册就过期);
  2. 它有对错(写错场景,客服就会照着错的信息回复用户,还不如不给);
  3. 它体量不小(多个模块、成百上千个码值);
  4. 它一半靠机器、一半靠判断(哪些码要收、码值本身是什么,机器干;"用户会怎么撞上它、该怎么回",只有读懂代码才知道)。

于是就有了下面这份 skill。它是本文虚构的 ,仓库名 demo-shop、模块名 order / pay / stock 也都是假的------只为讲清机制。

它的产出是一条固定路径的文件:

docs/错误码手册/错误码手册_YYYYMMDD.md


3. 先把原文摆出来,后面每一节都对照着它看

这份 skill 一共 4 个配置文件加 3 个脚本,其中 SKILL.md 是主角。建议先通读一遍,再往下看------不然后面讲「第 3 步在干什么」的时候,读者手上没有对照物。

markdown 复制代码
---
name: error-code-handbook
description: 当需要生成或刷新《错误码手册》(把代码里的错误码整理成给客服和运营看的说明文档)时使用;也适用于回答"这个错误码是什么意思""用户在什么情况下会撞上它""同一个码是不是被复用在不同模块"这类问题
---

# 错误码手册生成

## 这个技能解决什么问题

客服和运营每天要面对用户的报错截图,但错误码散落在枚举类、常量类和字符串字面量里,前端文案又是 i18n key 翻译出来的,肉眼对不上。

本技能把「重新生成这份手册」变成一条可复现的流水线。

**产出**:`docs/错误码手册/错误码手册_YYYYMMDD.md`

## 铁律

违反任意一条,产物即为无效,必须重做:

1. **以代码为准,不以旧手册为准。** 旧手册只能当"对照物"用来发现遗漏,**绝不能当事实来源**。(2026-10 实测:旧手册里的 `E-1042` 已被下线,`E-2001` 的含义已被改写。)
2. **每个码都要有证据链。** 码值 → 定义位置 → 抛出位置,三跳都要能指出具体文件和行。给不出的必须进「待确认」。
3. **动态拼接、透传上游、日志专用码一律标注真实来源**,不许编一个"看起来合理"的场景。(客服会照着它回用户。)
4. **复用码必须分行列出**:同一个码值在不同模块含义不同时,两行都要写,不许挑一个。
5. **脚本扫到的码值必须 100% 落地**:要么进手册表格,要么在「待确认」里说明为什么不算用户可见的码。`verify_report.md` 里的未覆盖清单必须清空。
6. **不许静默跳过。** 脚本报的每一条 warning 都要么修掉,要么在最终回复里明确说出来。

## 输入

| 输入 | 位置 | 说明 |
| --- | --- | --- |
| 扫描配置 | `sources.yaml` | 要扫哪些模块、哪些包,哪些码段要忽略,前端文案从哪个 i18n 文件取 |
| 代码仓库 | `~/work/demo-shop/*` | 由 `sources.yaml` 的 `repos_root` 指定 |

前置:`python3` 且能 `import yaml`(缺就 `pip3 install pyyaml`)。

新增/下线一个模块,**只改 `sources.yaml`**,不要改本文件。

## 工作流

在技能目录下执行(`cd ~/work/skills/error-code-handbook`)。

### 第 1 步:抽取(确定性,不许跳过)

```bash
python3 scripts/extract_codes.py --config sources.yaml --out .work
```

先读 `.work/extract_report.md`。它会告诉你:扫到多少个码值、每个模块分布多少、每个码的定义位置、哪些码没有找到抛出点、**以及静态定义里出现但疑似动态拼接的部分**。

报错(文件不存在、i18n 路径对不上)必须先修 `sources.yaml`,再往下走。

### 第 2 步:填含义(语义判断,本技能的主要工作)

`extract_codes.py` 已经产出 `.work/codes_draft.yaml`:**码值、定义位置、抛出位置**是脚本抽的,这部分不需要大模型重新算。

大模型要做的是把「人话」补上,规则见 [references/judgment-rules.md](references/judgment-rules.md):

1. **用户会怎么撞上它**:去读抛出点的上下文(校验分支、前置状态、外部调用失败回滚),写成客服看得懂的一句话。
2. **客服该怎么回**:能自助解决的写操作路径;需要人工介入的写升级路径;查不清的写「按标准话术回复」。
3. **复用码排查**:同一个码值出现在多个模块时,逐一确认是不是同一件事,分行列出。
4. **动态码**:形如 `"E-" + module + code` 这种拼接出来的码,去代码里把可能取值列全;列不全就标注为动态码,并说明依据。
5. **透传码**:直接把上游返回的码透出来的,来源写「上游透传」,含义以对方文档为准,不要替对方编场景。
6. **两个固定惯例**(沿用上一版手册的形态,不要自创):
   - 「日志专用码」不单独占一行,在备注里说明「仅落日志,不返回前端」。
   - 备注里的模块名和枚举名直接取自代码,不要自己起名。

**大模型的判定写进 `overrides.yaml`,不要改脚本,也不要手改 `codes.yaml`。** 每条都要带 `evidence`(文件:行):

| 位置 | 用途 |
| --- | --- |
| `codes.<码值>` | 码级判定,覆盖自动抽取的结论 |
| `by_module.<模块>\|<码值>` | 复用码:同一码值在不同模块的不同含义 |
| `default_notes[]` | 一批码来源相同且都不返回前端时,按组给统一备注 |
| `open_questions[]` | 需要在交付文档里提醒人工确认的疑点 |

规则性的整理(丢弃 `ignored_codes`、动态码归并、复用码分行、日志码并入)由脚本完成,不要手工做:

```bash
python3 scripts/curate_codes.py --config sources.yaml --overrides overrides.yaml --work .work
```

脚本会打印「仍未确定含义的码」,这些码已自动进待确认;确认后请补进 `overrides.yaml` 的 `codes`,不要留在自动兜底里。

### 第 3 步:渲染 + 校验

```bash
python3 scripts/render_doc.py --config sources.yaml --codes .work/codes.yaml --work .work
```

脚本会校验:码值真的存在、扫到的码 100% 覆盖、标注为动态/透传的行有原因说明、来源 id 合法。

- **退出码非 0 = 文档没生成**,按 `.work/verify_report.md` 修 `codes.yaml` 后重跑。
- 提醒(warning)要人工过一遍,尤其是"未覆盖码值"。

### 第 4 步:子 agent 独立复核(不许省)

自己写的含义自己验,等于没验。用 `spawn_agent`(`fork_context: false`)起一个**看不到结论**的子 agent,让它独立走一遍证据链。

给它的输入只能是:

- 仓库根目录 `~/work/demo-shop`
- 要复核的**码值清单**,以及每个码所属模块
- 要求的输出格式

**不要**把 `codes.yaml`、`overrides.yaml` 或大模型的结论给它。

复核范围(按性价比排序):

1. **所有标注为动态/透传的行** ------ 必须全查。这些最容易被草率处理。
2. **所有 `by_module` 覆盖过的行** ------ 复用码在这里最容易两边都写错。
3. **其余行抽查不少于 15%**,优先挑:带枚举备注的码、上游透传的码、`open_questions` 里提到的码。

让子 agent 按「码值 → 定义位置 → 抛出点 → 用户可见性」输出它的结论 + 证据文件:行 + 它的不确定项。然后:

- 结论和 `codes.yaml` 不一致 → 自己去核实,**以代码为准**;真错了就改 `overrides.yaml` 重跑第 3 步。
- 子 agent 说"查不到" → 若大模型能给出证据,保留自己的结论并在最终回复里说明分歧;若给不出,降级为「待确认」。
- 子 agent 出错 → 在最终回复里写清它的误判点,不要只报"复核通过"。

### 第 5 步:交付前自检

```bash
python3 scripts/render_doc.py --config sources.yaml --codes .work/codes.yaml --work .work --check-only
```

再逐项确认:

- [ ] `verify_report.md` 里「未覆盖码值」为空,或已在「待确认」中说明
- [ ] 与上一版手册(`错误码手册/` 目录里日期最新的那份)做过 diff,**每一处变化都能解释**(新增码 = 代码新增;消失码 = 代码下线)
- [ ] 复用码清单不为空(只要存在复用,就该有内容;空清单通常意味着没认真查)
- [ ] 动态码与透传码都有来源说明,没有编造的场景
- [ ] 待确认清单不是空的(只要代码里存在歧义,就应该有内容)

## 常见错误

| 现象 | 原因 | 处理 |
| --- | --- | --- |
| 某个模块的码值明显偏少 | 该模块的码定义在另一个包,没配进 `sources.yaml` | 把包加进 `scan_packages` |
| 出现一堆 `E-undefined` | 前端把 i18n key 当成了码值 | 检查 `sources.yaml` 的 `i18n_path`,key 与码值要分开抽 |
| 码值对上了,含义却是错的 | 校验只保证「码存在」,不保证「含义对」 | 第 2 步的证据链复核不能省 |
| 同一个码在两处含义不同,手册只有一行 | 复用码没分行 | 用 `by_module` 显式拆开 |
| 「用户会怎么撞上它」写成技术话术 | 直接抄了日志或异常栈 | 抄了客服也看不懂,要翻译成业务话 |
| 手改了 `.work/codes.yaml`,下次重跑全丢 | `.work/` 是中间产物 | 判定写进 `overrides.yaml`,重跑 `curate_codes.py` 即可复现 |
| `overrides.yaml` 里堆了几十条「查不到」 | 兜底规则没先跑或模块名对不上 | 先看 `curate_codes.py` 打印的未确定清单;能归组的写 `default_notes` |
| 复核说某个码其实是用户可见的 | 第 2 步直接判了「日志专用」 | 三种情况要特别当心:网关统一拦截返回的码、定时任务里的码、被 try/catch 吞掉又重抛的码 |

## 反合理化

| 想法 | 现实 |
| --- | --- |
| "旧手册里是这么写的" | 旧手册正是要修的东西。代码优先。 |
| "这个码看起来就是超时" | "看起来"不是证据。要么给出文件:行,要么进待确认。 |
| "码值对上了,校验也过了" | 校验只证明码存在。含义对不对是另一回事。 |
| "动态码太麻烦,随便写个通用解释" | 客服会照着通用解释回复用户,用户会当场发现不对。 |
| "未覆盖的码不重要" | 它们正是用户在截图里看到、手册里却查不到的那些。 |
| "待确认为空说明我很确定" | 代码里真实存在的歧义不会因为确定而消失。 |

## 资源

- [references/judgment-rules.md](references/judgment-rules.md) --- 判定细则:证据链怎么查、复用码怎么拆、动态码与透传码怎么标
- `sources.yaml` --- 扫描配置(模块、包、忽略码段、i18n 路径)
- `overrides.yaml` --- 人工判定(带 evidence),跨次生成长期有效
- `scripts/extract_codes.py` --- 抽取码值/定义位置/抛出位置,产出草稿
- `scripts/curate_codes.py` --- 草稿 + overrides → `codes.yaml`(丢弃/归并/分行/并入)
- `scripts/render_doc.py` --- 校验 + 渲染交付文档

## 迭代记录

| 日期 | 信号来源 | 修改点 | 验证方式 |
| --- | --- | --- | --- |
| 2026-10-08 | 无技能基线(2 个独立 agent):同一个码在两处给出不同含义;旧手册残留已下线的码;产出写到 /tmp、无日期无固定结构 | 建立 6 条铁律;抽取脚本落地码值/抛出点/i18n 解析;渲染脚本固化产出路径与章节 | 抽取 412 个码值、三跳证据齐全、校验 0 错误 0 提醒;与旧手册逐条比对后仅剩 9 个源码里已搜不到的码,已进「待确认」 |
| 2026-10-08 | 复盘首轮:判定逻辑写在一次性的 `.work/curate.py` 里,`.work/` 被 gitignore → 下次执行会退化成"重新判断一遍" | 判定沉淀为 `scripts/curate_codes.py`(规则)+ `overrides.yaml`(带 evidence 的判定);新增第 4 步「子 agent 独立复核」 | 全流程重跑,校验 0 错误 0 提醒;子 agent 复核结果见交付说明 |
| 2026-10-09 | 第 4 步子 agent 复核(2 个独立 agent,共查 63+51 条):查出 4 个码被误判为「日志专用」、2 个复用码被合成一行、1 处动态码的取值清单漏了分支 | 修正 `overrides.yaml`;`curate_codes.py` 改为「按模块覆盖先于归并生效」,避免复用码被并成一行 | 复核意见逐条回代码验证;修正后待确认 11→8,校验仍 0 错误 0 提醒 |

看完这份原文,应该能感觉到一件事:它写得非常"啰嗦"------铁律、常见错误、反合理化,这三节加起来占了一半篇幅。

这不是作者话多。看完第 8 节就知道,这三节才是这份 skill 里最值钱的部分。


4. 为什么长这样:两层结构,两个不同的读者

一份 SKILL.md 分两层,而且是给两个不同的消费者看的------这是整个设计里最关键的一点:

层 内容 格式要求 谁读
frontmatter 只有 name + description 强制,YAML,多一个字段都可能算违规 客户端(机器),靠它建目录、做注入
正文 随便写:散文、表格、清单都行 完全没有格式要求 大模型(自然语言)

对照上面那份原文:

  • 开头那三行 --- 之间就是 frontmatter。它只干两件事:告诉机器「我叫 error-code-handbook」,以及「什么时候该用我」。
  • # 往下全部是正文。里面的「铁律」「工作流」「常见错误」,机器一个都不认识,它们是写给大模型看的。

为什么要这么分?因为两种东西的性质完全不同:

  • 「这本手册叫什么、什么时候该用它」------必须机器能判定,所以强格式;
  • 「这个活具体怎么干」------各种 skill 差异极大,只有自然语言能表达,所以不设格式。

一句话:索引层给机器,内容层给模型。

顺便说一句 description 该怎么写。原文里那一条长这样:

当需要生成或刷新《错误码手册》......时使用;也适用于回答"这个错误码是什么意思""用户在什么情况下会撞上它""同一个码是不是被复用在不同模块"这类问题

前半句是触发条件 ,后半句是用户可能说出的原话 。这么写的目的是:当用户用自然语言问「这个 E-1007 是啥意思」时,大模型能把它和这本手册对上号。

description 只写"什么时候用",绝不写"怎么干"。 原因见第 6 节。


5. 大模型是怎么「知道有这本手册」的?

每次会话开始,客户端会扫描 skills 目录,把所有 skill 的 frontmatter 拼进大模型的上下文------只有 name 和 description 两行,不是全文。

塞全文太贵了:一本手册正文几千字,几十个 skill 就是几十万 token 的常量开销。所以客户端只给大模型一份「货架清单」:

复制代码
货架上有什么(常驻在大模型上下文里):
  error-code-handbook   ------ 生成/刷新错误码手册,或问某个码是什么意思时用
  writing-skills        ------ 创建或修改 skill 本身时用
  systematic-debugging  ------ 排查 bug 时用
  ......(本机挂着二十来个)

所以大模型开局就知道有这么一本手册、以及什么时候该翻它,但它不知道里面写了什么。

这一点很重要,因为它解释了后面的所有失败模式:大模型可能「没想起来」这本手册,也可能「想起来了但懒得上手翻」。


6. 怎么被触发?两条完全不同的路

#mermaid-svg-5M4TAkpUPJYMJccX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5M4TAkpUPJYMJccX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5M4TAkpUPJYMJccX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5M4TAkpUPJYMJccX .error-icon{fill:#552222;}#mermaid-svg-5M4TAkpUPJYMJccX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5M4TAkpUPJYMJccX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5M4TAkpUPJYMJccX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5M4TAkpUPJYMJccX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5M4TAkpUPJYMJccX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5M4TAkpUPJYMJccX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5M4TAkpUPJYMJccX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5M4TAkpUPJYMJccX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5M4TAkpUPJYMJccX .marker.cross{stroke:#333333;}#mermaid-svg-5M4TAkpUPJYMJccX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5M4TAkpUPJYMJccX p{margin:0;}#mermaid-svg-5M4TAkpUPJYMJccX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5M4TAkpUPJYMJccX .cluster-label text{fill:#333;}#mermaid-svg-5M4TAkpUPJYMJccX .cluster-label span{color:#333;}#mermaid-svg-5M4TAkpUPJYMJccX .cluster-label span p{background-color:transparent;}#mermaid-svg-5M4TAkpUPJYMJccX .label text,#mermaid-svg-5M4TAkpUPJYMJccX span{fill:#333;color:#333;}#mermaid-svg-5M4TAkpUPJYMJccX .node rect,#mermaid-svg-5M4TAkpUPJYMJccX .node circle,#mermaid-svg-5M4TAkpUPJYMJccX .node ellipse,#mermaid-svg-5M4TAkpUPJYMJccX .node polygon,#mermaid-svg-5M4TAkpUPJYMJccX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5M4TAkpUPJYMJccX .rough-node .label text,#mermaid-svg-5M4TAkpUPJYMJccX .node .label text,#mermaid-svg-5M4TAkpUPJYMJccX .image-shape .label,#mermaid-svg-5M4TAkpUPJYMJccX .icon-shape .label{text-anchor:middle;}#mermaid-svg-5M4TAkpUPJYMJccX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5M4TAkpUPJYMJccX .rough-node .label,#mermaid-svg-5M4TAkpUPJYMJccX .node .label,#mermaid-svg-5M4TAkpUPJYMJccX .image-shape .label,#mermaid-svg-5M4TAkpUPJYMJccX .icon-shape .label{text-align:center;}#mermaid-svg-5M4TAkpUPJYMJccX .node.clickable{cursor:pointer;}#mermaid-svg-5M4TAkpUPJYMJccX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5M4TAkpUPJYMJccX .arrowheadPath{fill:#333333;}#mermaid-svg-5M4TAkpUPJYMJccX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5M4TAkpUPJYMJccX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5M4TAkpUPJYMJccX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5M4TAkpUPJYMJccX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5M4TAkpUPJYMJccX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5M4TAkpUPJYMJccX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5M4TAkpUPJYMJccX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5M4TAkpUPJYMJccX .cluster text{fill:#333;}#mermaid-svg-5M4TAkpUPJYMJccX .cluster span{color:#333;}#mermaid-svg-5M4TAkpUPJYMJccX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5M4TAkpUPJYMJccX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5M4TAkpUPJYMJccX rect.text{fill:none;stroke-width:0;}#mermaid-svg-5M4TAkpUPJYMJccX .icon-shape,#mermaid-svg-5M4TAkpUPJYMJccX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5M4TAkpUPJYMJccX .icon-shape p,#mermaid-svg-5M4TAkpUPJYMJccX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5M4TAkpUPJYMJccX .icon-shape .label rect,#mermaid-svg-5M4TAkpUPJYMJccX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5M4TAkpUPJYMJccX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5M4TAkpUPJYMJccX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5M4TAkpUPJYMJccX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 你说了一句话
走哪条路?
路线一:你显式点名技能
路线二:你用自然语言描述需求
客户端做字符串解析

把 SKILL.md 全文直接塞进你这条消息
大模型手里已经有全文

连要不要用都不用判断
大模型拿你的话

跟货架上的 description 做语义比对
命中:大模型自己发起工具调用

去读 SKILL.md 全文
没命中:当普通对话回答

手册继续躺着
按手册执行流程

路线一:你显式点名

你打 $error-code-handbook,或者用 @ 引用那份技能文件。

这时候是客户端干的 :它做字符串解析,直接把 SKILL.md 全文塞进你那条消息里。

证据很硬:这条消息到达大模型手里的时候,里面已经带着技能名、路径和整篇正文了------大模型一个字都没读过文件,它自己就在上下文里了。

走这条路,大模型完全不参与「要不要用」的判断。所以「技能没生效」这种事不会发生在你身上。

路线二:你用自然语言

你说「帮我重新生成一版错误码手册」,或者「这个 E-1007 是什么意思」。

这时候是大模型干的 :货架上只有两行 description,得靠它做语义比对,判断「这句话是不是在说这本手册」;命中之后,它自己发起工具调用 把正文读进来,才能看到「第 3 步要跑 render_doc.py」这种细节。

这条路会出三种错,而且不止一种:

失败模式 长什么样
漏触发 description 写太抽象(比如就写「处理错误码」),大模型根本想不到
误触发 description 写太宽,不相干的活也往里套
触发了但不读正文 description 里把工作流总结了一遍,大模型照着摘要就干了,跳过正文

第三条最阴。就拿这份手册举例:如果 description 写成「先跑抽取脚本,再补含义,再渲染校验,最后让子 agent 复核」,那么大模型很可能直接照着这句话干活,跳过第 4 步的独立复核------因为那句话听起来已经是一份完整的流程了。

这也是规范里明令禁止在 description 里写工作流摘要的原因:description 是相亲简介,不是简历。

所以如果你希望某个技能被用上,最省事的办法就是直接点名,走那条不依赖大模型语义判断的路。


7. 触发之后,到底发生了什么?

实际流程是这样:
大模型 Agent 客户端 你 大模型 Agent 客户端 你 #mermaid-svg-lCuAsF8olNmzCOWU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lCuAsF8olNmzCOWU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lCuAsF8olNmzCOWU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lCuAsF8olNmzCOWU .error-icon{fill:#552222;}#mermaid-svg-lCuAsF8olNmzCOWU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lCuAsF8olNmzCOWU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lCuAsF8olNmzCOWU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lCuAsF8olNmzCOWU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lCuAsF8olNmzCOWU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lCuAsF8olNmzCOWU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lCuAsF8olNmzCOWU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lCuAsF8olNmzCOWU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lCuAsF8olNmzCOWU .marker.cross{stroke:#333333;}#mermaid-svg-lCuAsF8olNmzCOWU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lCuAsF8olNmzCOWU p{margin:0;}#mermaid-svg-lCuAsF8olNmzCOWU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lCuAsF8olNmzCOWU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lCuAsF8olNmzCOWU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lCuAsF8olNmzCOWU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lCuAsF8olNmzCOWU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lCuAsF8olNmzCOWU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lCuAsF8olNmzCOWU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lCuAsF8olNmzCOWU .sequenceNumber{fill:white;}#mermaid-svg-lCuAsF8olNmzCOWU #sequencenumber{fill:#333;}#mermaid-svg-lCuAsF8olNmzCOWU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lCuAsF8olNmzCOWU .messageText{fill:#333;stroke:none;}#mermaid-svg-lCuAsF8olNmzCOWU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lCuAsF8olNmzCOWU .labelText,#mermaid-svg-lCuAsF8olNmzCOWU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lCuAsF8olNmzCOWU .loopText,#mermaid-svg-lCuAsF8olNmzCOWU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lCuAsF8olNmzCOWU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lCuAsF8olNmzCOWU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lCuAsF8olNmzCOWU .noteText,#mermaid-svg-lCuAsF8olNmzCOWU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lCuAsF8olNmzCOWU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lCuAsF8olNmzCOWU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lCuAsF8olNmzCOWU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lCuAsF8olNmzCOWU .actorPopupMenu{position:absolute;}#mermaid-svg-lCuAsF8olNmzCOWU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lCuAsF8olNmzCOWU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lCuAsF8olNmzCOWU .actor-man circle,#mermaid-svg-lCuAsF8olNmzCOWU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lCuAsF8olNmzCOWU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 帮我重新生成一版错误码手册发送系统提示、技能清单、工具清单、全部历史,以及你这句话请求读取 SKILL.md真的去读文件把文件内容作为工具结果追加进上下文,整个重发一遍请求运行抽取脚本真的去跑脚本,拿到 JSON工具结果追加,再重发输出结论 / 最终回答

注意这里有三件事:

  1. 「传参」不是大模型构造的。 是客户端以「整个请求」为单位构造的:系统提示、技能清单、工具清单、全部对话历史、全部工具往返结果,一次全发。
  2. 每一轮都要重发全部历史。 这是理解「为什么 skill 要死磕 token 效率」的钥匙------也是为什么原文让脚本直接吐 JSON,而不是让大模型去读几千行源码。
  3. 上下文是公共资源、有上限、超了会被压缩。 所以别指望大模型一直记得会话早期读过的代码------文件才是外部记忆,上下文不是。

真实的五步流水线

这份 skill 的设计哲学一句话概括:不需要判断的事全赶进脚本,需要判断的事才留给大模型。

步骤 谁干 干什么 耗时量级
1. 抽取 脚本 扫枚举/常量/字面量拿码值、定位定义与抛出点、解析 i18n 拿前端文案,输出草稿 ~1 秒
2. 填含义 大模型 脚本只会抽码值,含义得读懂代码;这是本技能的主要工作量 几十次读文件 / 搜代码
3. 渲染 + 校验 脚本 校验四件事,任一不过就退出码非 0、不生成文档 ~1 秒
4. 独立复核 另一个 agent 起一个看不到结论的子 agent,让它自己走一遍证据链 几十次读文件
5. 交付自检 大模型 + 脚本 跟上一版手册做 diff,确认每处变化都能解释 分钟级

第 2 步为什么不能交给脚本?举三个例子(都是这份虚构手册里真会遇到的类型):

  • 复用码 :E-1007 在支付模块表示「余额不足」,在库存模块表示「库存不足」。脚本只能抽到"这个码出现了两次";到底是不是同一件事,得读两处抛出点的上下文才能确定。原文里的 by_module 和「铁律第 4 条」就是为它准备的。
  • 动态拼接码 :代码里写成 "E-" + module + "_" + code。脚本能扫到模板,但扫不出可能取值------得顺着变量的赋值链去找枚举定义,把取值列全。列不全就不能编,必须标成动态码并写清依据。
  • 透传码 :第三方网关失败时,代码把对方返回的码原样抛出来。这时候含义不在本仓库里,只能标「上游透传,以对方文档为准」。原文「铁律第 3 条」堵的就是这里:不许替对方编一个看起来合理的场景------因为客服会照着它回用户。

第 3 步的校验也不是走形式,它拦的是四类硬错误:

  1. 手册里写的每个码值,在代码里必须真的存在(写错码值直接拦下);
  2. 脚本扫到的每个码,必须 100% 有交代(防漏码);
  3. 标「动态」「透传」「不返回前端」的行必须写明原因(防偷懒);
  4. 引用的来源 id 必须合法。

第 4 步是最值得抄的一个设计:自己写的含义自己验,等于没验。

所以原文强制另起一个 agent,不给它结论 (fork_context: false,不继承上下文),只给它「码值清单 + 所属模块」,让它独立走一遍证据链。

上面那份迭代记录里写着:这一步抓出了 4 个被误判为「日志专用」的码、2 个被合成一行的复用码、1 处动态码漏了分支。


8. 所以「流程规范化」到底是怎么实现的?

到这里就能回答那个最核心的问题了。先给一个可能有点反直觉的答案:

客户端根本不解析 SKILL.md 的正文。

对 Codex(以及同类客户端)来说,那篇文档就是一坨不透明字符串 。它不知道「第 3 步」是什么意思,也不会检查大模型有没有先跑抽取脚本、后跑渲染脚本。如果大模型当时跳过抽取、直接手写手册,客户端不会拦它。

那流程到底是谁在管?是大模型这一侧在管。

客户端(运行时) 大模型
技能 扫目录、解析 frontmatter、把清单注入上下文 拿用户的话和 description 做语义比对,决定要不要用
工具 维护工具清单、执行工具调用、把结果回灌 决定什么时候调哪个工具、参数是什么
边界 硬约束:危险的删除命令会被直接拒绝 只能换个办法绕开
流程 完全不管 所有「第几步」「先跑什么后跑什么」,都是大模型读完散文之后自己编排的

看最后一行------这就是答案。客户端那层只是一个写死的循环:

把上下文发给模型 → 模型要么说话、要么发工具调用 → 执行工具、把结果追加 → 再发一次。

它没有任何关于业务流程的分支。所有的判断、顺序、纪律,全部来自大模型这一侧。

而大模型这一侧,靠的是「指令遵从」------不是程序约束,是自然语言指令的遵从。这带来三个必须接受的后果:

第一,它不保证。 同一份 skill,换个模型、或者上下文压力大的时候,执行结果会不一样。遵从是概率,不是契约。

第二,所以 skill 的写法才那么「啰嗦」。 回到原文第 3 节,那些「铁律」「不许静默跳过」「反合理化表」不是装饰,是在跟大模型的偷懒倾向对抗:

大模型脑子里的想法 手册里的现实
「旧手册里是这么写的」 旧手册正是要修的东西。代码优先。
「这个码看起来就是超时」 「看起来」不是证据。要么给出文件:行,要么进待确认。
「码值对上了,校验也过了」 校验只证明码存在。含义对不对是另一回事。
「动态码太麻烦,随便写个通用解释」 客服会照着通用解释回复用户,用户会当场发现不对。
「待确认为空说明我很确定」 代码里真实存在的歧义不会因为确定而消失。

这就是为什么原文里最长的三节是「铁律 / 常见错误 / 反合理化」------它们记录的全是踩过的坑。写 skill 有一半功夫,花在堵漏洞上。

第三,能机器化的部分,一律下沉成脚本。 这就是「自由度分级」:

活的类型 交给谁 为什么
确定性计算(扫码、定位、校验、渲染) 脚本 快、准、可重复,出错会报错
语义判断(这个码什么含义、用户怎么撞上) 大模型 脚本干不了
流程编排(第几步、什么时候停) 大模型的指令遵从 只能这样
硬边界(不许删库、不许越权) 客户端 这是写死的代码

9. 绕不开的话题:上下文被压缩了,手册会丢吗?

会。而且丢的正是手册正文------但「跑偏」的真实原因,通常不是你想的那个。

先把「什么在哪儿」分清楚,才知道谁会丢:

内容 存放位置 会被压缩吗
技能清单(name + description) 系统提示,每轮重新注入 不会。这是「货架」,始终在
技能正文(SKILL.md 全文) 对话历史里的一条消息 / 工具结果 会。它没有任何特权
读过的代码、跑过的命令返回 对话历史 会,而且这是损失最大的部分
磁盘上的文件 文件系统 完全不受影响

关键在第二行:技能正文一旦被读进来,它就只是"历史里的一段文字",没有 VIP 通道。真要腾空间,它和三个月前的一句闲聊待遇一样。

压缩到底做了什么

以 Codex 为例(其它客户端机制类似,细节可能有别),它的运行时里有一段专门写给大模型的压缩说明,原话大意是:

上下文耗尽时,对话会被自动摘要;但用户说过的每一句话你都能看到原文 。

也就是说时间不会用完,只是有时你看到的是摘要而不是完整历史。

遇到这种情况,假设压缩发生在你工作过程中,不要从头再来 ,对摘要里缺失的东西做合理假设。

翻译成机制:

  • 用户说过的每句话:保留原文;
  • 大模型的回答、探索过程、工具调用与返回:被摘要替代;
  • 磁盘上的文件:无关,不受影响。

极端情况下还有一段兜底提示词,大意是:

当前窗口已耗尽。不要在这个窗口里继续任务或给最终答案。

立刻写一份 checkpoint,把目标、决策、进度、学到的、下一步,以及每个还没解决的用户请求的 id 记下来。

写完之后,开启新窗口。

也就是说,极端情况下上下文能不能续上,取决于那份 checkpoint 写得全不全。

还有一句更值得警惕的话

上面那段提示词里有一句:「对摘要里缺失的东西做合理假设」。

对写代码来说这没问题。但对「错误码手册」这种要求逐条精确 的活,合理假设就是造错的源头 ------大模型可能「合理地」记得某个码是「余额不足」,然后交出一份自相矛盾、而且没人能看出问题的手册。而这份手册是给客服用的,客服照着它回用户,用户当场就发现不对。

这也正好解释了原文里那些看起来"过度设计"的规矩------它们不是讲究,是在对冲这条机制:

原文里的设计 对冲的是什么
判定结果外置成 overrides.yaml,每条带 evidence(文件:行) 不依赖大模型「记得自己判断过什么」
中间产物全部落盘(草稿 → 整理后 → 最终 md,每步是下一步的输入) 天然 checkpoint,忘了也能从磁盘捡回来
校验脚本从文件重新算一遍码值是否存在、是否全覆盖 不依赖大模型的记忆,错了就退出码非 0
子 agent 独立复核,且不给它结论 对冲「大模型自己不知道自己在脑补」
命令写成完整的一整行 bash 压缩后只要还能认出那行命令,就能重新捡起来继续

但诚实说,还有一个洞

「现在跑到第几步了」这件事没有落盘。

如果压缩发生在「第 3 步渲染完成」和「第 4 步独立复核」之间,大模型可能会忘记"还没做复核",直接交付 ------而这时候所有文件都齐了、校验也过了,看不出任何异常。

堵法也很简单:让 skill 每完成一步就往 .work/progress.md 追加一行,并且规定「开工第一条命令是先读它」。这样进度也变成磁盘状态------压缩只能让大模型失忆,不能让它丢失进度。

(顺便说,这个改进同样适用于开头那份原文。好东西不是一次写成的,都是被坑出来的。)


10. 给想写 skill 的人:6 条从坑里长出来的建议

  1. description 只写「什么时候用」,不要写「怎么干」。 写了工作流摘要,大模型可能照着摘要干、跳过正文------这是最隐蔽的失效方式。
  2. 把不需要判断的事全部下沉成脚本。 token 预算和大模型的注意力,都应该花在真正需要判断的地方。
  3. 重要的结论要落盘,别指望大模型的记忆。 带证据的判定写进配置文件,中间产物留在工作目录,每一步都能重跑。
  4. 校验要能「从文件重新算一遍」。 让校验脚本独立于大模型的记忆,错了就非 0 退出;不接受一句「检查过了」就当过关。
  5. 给自己配一个独立的复核者。 换个上下文、不给它结论,让它自己走一遍证据链。这一步抓出来的错,往往是你最自信的那些。
  6. 留一节「反合理化」和「常见错误」。 踩过的坑写下来就是下一次的护栏。那份虚构手册的迭代记录里写着:首轮因为没写清楚,同一个码在两处给出了两种不同含义------所以「铁律」才会越写越长。

11. 总结

你负责说一句话;

skill 负责把动作固定成流程;

脚本 负责不能出错的部分;

大模型 负责需要判断的部分;

agent 客户端 负责发请求、执行工具、以及拦住危险操作。

四者各管一段。缺了任何一环,这份手册要么做不出来,要么做出来不敢给客服看。

最后回到开头那条前提:文中所有机制都用 Codex 举例,但换成别的 agent 客户端,主线是一样的。 变的只是「货架目录叫什么名字」「点名技能用什么语法」「工具怎么配」这些细节;不变的是------说明书要靠大模型想起来并读懂,流程的纪律靠大模型的指令遵从,而可靠性只能靠脚本、校验和独立复核兜住。


附:本文里那些"机制细节"是怎么来的

为了避免"看起来像编的",交代一下出处:文中关于机制描述的部分(技能清单如何注入、触发路径有哪些、压缩时保留什么丢失什么、客户端不解析 skill 正文、预采样压缩的触发条件等)来自 Codex 运行时里可直接观察到的事实,以及作者本机日志、配置与二进制字符串的实测结果(Codex CLI 0.153.4,2026-09)。

这些是具体实现的行为,不是官方文档原文------写这篇文章时,官方文档域名返回了 403,没能引到页面。不同客户端、不同版本的行为细节可能有差异,如果官方文档有更明确的说明,以官方为准。

至于文中那份 error-code-handbook:它是为了讲清机制而虚构的,请当作一个"长得很像真的"的例子来看,别去找它的仓库。

相关推荐
全栈弄潮儿1 小时前
小项目实战 2:让 AI 帮你补齐接口设计和异常处理
aigc·openai·ai编程
VIP_CQCRE1 小时前
在 Visual Studio 中接入 Ace Data Cloud:让 LMLocal 直接调用 OpenAI 兼容模型
openai·ai编程·开发工具·visual studio·ace data cloud
ZzT2 小时前
Pro 200 额度砍半,OpenAI 给的理由是模型变聪明了
openai·ai编程
OpsEye2 小时前
企业如何统一管理多家大模型 API?
javascript·ai编程
xhy_07072 小时前
AI 编程工具怎么选?Cursor、Copilot、Claude Code、Trae、WES Code 理解代码库的三条技术路线
人工智能·机器学习·copilot·知识图谱·ai编程·wes code
Sunny_GMF3 小时前
所见即所得编辑器原理实战:源码与渲染永不失真的三层一致性设计(Markdown/CodeMirror 装饰)
microsoft·编辑器·ai编程·harmonyos
AINative软件工程3 小时前
LLM 应用的 Chaos Engineering 工程实践:给 AI 系统下毒,才能知道它有多抗造
后端·llm·ai编程
全栈弄潮儿11 小时前
《小项目实战 1:用 AI 从零搭一个 API 服务》
aigc·openai·ai编程
金字塔頂の蝸牛15 小时前
每周GitCode开源项目推荐
开源·软件工程·ai编程