--- name: writing-for-agents description: 用于编写、修改或测试 agent 指令,包括 skill、`AGENTS.md`、`CLAUDE.md` 和上下文指针。 --- 为 agent 编写任何文档时的参考:skill、`AGENTS.md` 或 `CLAUDE.md`,以及被上下文指针引用的文档。形式不同,写法相同:同一组方法能让文档的执行结果更稳定,也就是让 agent 每次都走相同的*过程*,而不是每次产出相同的结果。 - 编写 skill 时,阅读 [SKILL-MECHANICS.md](SKILL-MECHANICS.md),了解 frontmatter、触发方式和路由 skill。 - 需要验证一句指引是否真的改变了 agent 的行为,或者编写完一个约束型 skill(agent 容易以“就这一次”为由绕过的那种)需要验证时,阅读 [TESTING.md](TESTING.md)。 ## 上下文指针 **上下文指针**(context pointer)是留在 agent 上下文中的一条引用,指向上下文之外的材料,并写明在什么条件下去读取它。skill 的 description 是一种上下文指针;`AGENTS.md` 中提到某份文档的一行文字也是。 决定 agent 何时读取、读取是否可靠的,是指针的*措辞*,而不是它指向的内容。如果必须阅读的材料挂在措辞很弱的指针后面,agent 有时读、有时不读,执行结果就会不稳定。先把指针措辞改得更具体;仍然无效时,再把材料直接写进正文。 指针做两件事:说明材料是什么,并列出应当触发读取的**分支**(分支是文档要处理的一种不同情况,不同的运行会走不同的路径)。常驻上下文的指针每一轮都会占用 token,所以要比正文更精简: - **先导词放在前面**:指针正是先导词发挥触发作用的位置。 - **一个分支一个触发词。** 为同一分支换一种说法的同义词,等于把一个分支写了两遍;合并它们,只保留真正不同的分支。 - **删除正文已经说明的身份信息。** - **只写何时使用,不概括流程。** description 一旦概括了步骤,agent 会照着摘要执行,跳过正文。 ## 两种成本 每增加一份文档或一个指针,都会消耗以下两种成本之一: - **上下文成本**:常驻材料占用 agent 的上下文窗口。`AGENTS.md` 中的一行、skill 的 description,以及任何每轮都在上下文中的内容,无论是否被触发,都会消耗 token 和注意力。 - **认知成本**:人需要记住有哪些文档、什么时候用哪个,人本身就成了索引。这不是必须最小化的成本,而是保留人类控制权的代价:在需要人类判断的地方承担它,在不需要的地方去掉它。 只通过指针读取的材料,只消耗指针那一行的上下文成本;没有任何指针指向的材料,完全依赖认知成本。 ## 信息层级 文档由两类内容组成:**步骤**(agent 按顺序执行的动作)和**参考**(按需查阅的定义、规则和事实)。两者可以自由组合:全是步骤(例如操作手册)、全是参考(例如评审规则、本 skill),或两者兼有。 核心决策是把每一块内容放在**信息层级**的哪一级。层级按 agent 需要它的紧迫程度排列: 1. **文件内的步骤**:最高一级,agent 按顺序执行的内容。 2. **文件内的参考**:按需查阅。通常是一组平铺并列的内容(例如一次评审的所有规则放在同一级),这是正常的组织方式。 3. **拆分出去的参考**:放到独立文件中,由上下文指针引用,指针被触发时才加载。范围可以是同目录的文件,也可以是任何文档都能引用的外部参考。 拆分得太少,顶层会臃肿;拆分得太多,又会把 agent 真正需要的内容藏起来。所有决策都在这两者之间权衡。 **渐进披露**是沿层级往下移动内容(移出主文件,放到指针后面),让顶层保持清晰。它首先是保护层级的方式,其次才是节省 token。分支是最清楚的判断标准:所有分支都需要的内容写在正文,只有部分分支会用到的内容放到指针后面。 文档包含步骤时,本应拆出去的参考留在文件里会淹没步骤,agent 能否注意到步骤就变得不确定。这会直接影响执行结果的稳定性,而不只是可读性问题。 **就近放置**是文件内的配套原则:层级决定一块内容放多深,就近放置决定它旁边放什么。把一个概念的定义、规则和注意事项放在同一个标题下,读到其中一部分时就能顺带读到相关内容。检验标准:文档读起来应该像专门写给 agent 的说明书。相关内容集中放置时是这样,分散放置时不是。(这与重复不同:重复是同一个含义出现在两处,分散是一个含义被拆到多处。) **篇幅膨胀**是这里的失败模式:文档就是太长,即使每一行都有效且不重复。多出来的内容会分散注意力,每多一行,就多一行需要保持相关。解决方法是利用层级:把参考放到指针后面,按分支或顺序拆分,让每条执行路径只携带它需要的内容。 ## 步骤与完成条件 每个步骤都以一个**完成条件**结束,也就是告诉 agent 这一步已经完成的标准。完成条件有两个属性,都会影响执行效果: - **清晰度**:agent 能否分辨完成和未完成?模糊的标准(例如“达成理解”)容易导致**过早完成**:步骤没有真正做完就结束,注意力转向*结束*本身。 - 后续还能看到的步骤会把 agent 往前拉,清晰的完成条件则能让它停在当前步骤。 - 按顺序处理:**先把完成条件写得更具体**(局部修改,成本低)。 - 只有完成条件本质上无法写得更具体,*并且*确实观察到 agent 提前进入后续步骤时,才通过拆分顺序把后续步骤藏起来。 - 只有跨越真正的上下文边界时,隐藏才有效(交接或派发子代理);在当前上下文中调用 skill 仍会让后续步骤可见,起不到隐藏作用。 - **要求程度**:完成条件要求多少工作。“每个被修改的模型都有说明”能促使 agent 完成彻底的工作,“列出变更清单”则不能。要求程度决定了 agent 在工作中会做多少查证(这些工作隐含在措辞中,而不是写成单独步骤)。它不局限于步骤:“每条规则都已应用”约束一组平铺的参考,与“每一步都已完成”约束一串步骤的作用相同。这就是纯参考文档也能要求完整覆盖的方式。 最有效的完成条件既可检查,又要求完整覆盖。 ## 何时拆分文档 把一份文档拆成两份会消耗两种成本之一,所以只在拆分划算时进行: - **按顺序拆分**:一串步骤中,如果后续步骤诱使 agent 提前结束当前步骤,就拆开。让后续步骤不可见,能促使 agent 在当前任务上做更多查证。反过来也要注意:把按顺序拆开的文档合并,会让每一步都看到后续步骤,容易导致过早完成。 - **按触发方式拆分**:skill 特有,见 [SKILL-MECHANICS.md](SKILL-MECHANICS.md)。 ## 先导词 **先导词**(leading word)是模型在预训练中已经熟悉的紧凑概念,agent 执行文档时会用它来思考(例如 _课程_、_深模块_、_接缝_)。它以词语的形式反复出现,不必每次展开成句子,并在多次出现中逐渐形成定义。它用最少的 token 表达一整类行为,因为它调用了模型已有的知识。 自造新词也可以,只要定义清楚。但自造词没有模型已有的知识可以借用:预训练中的常用词无需额外说明,自造词则需要用定义的 token 来弥补。优先使用现成的词。 先导词在两个地方起作用: - **在正文中影响执行**:这个词每次出现,agent 都会执行相同的行为;在平铺的参考中,它把注意力集中到一类需要查找的内容上。 - **在指针中影响触发**:同一个词出现在你的提示、文档和代码库中时,agent 会把这套共享语言与材料联系起来,更可靠地读取它。 用中文写作时,先导词首次出现时附上英文原词,例如“接缝(seam)”,这样能同时利用中英文两种语境中的已有知识。优先采用中文开发者熟悉的经典译名,例如《软件设计的哲学》中的“深模块”、《修改代码的艺术》中的“接缝”。目标读者不熟悉的译名或隐喻,改用直白的说法,必要时在首次出现时括注来源。 主动寻找可以用先导词压缩的地方,例如三处分别展开的同一组描述,或一个指针用一整句话去描述一个概念。每一处都可以压缩成一个词: - “在修改文件、代码或外部状态之前,让用户对要做什么明确表示同意”:写成 _实施前确认_ - “一个你信得过的反馈回路”:写成 _变红_,把模糊的检查点变成二元的可观察状态(反馈回路在这个 bug 上会失败,或者不会) 这样做有两个好处:token 更少,也给 agent 提供了更明确的思考抓手。默认假设每份文档都有可以用先导词替代的重复表述,并把它们找出来。 **否定表述**是与先导词相对的失败模式:用禁令引导行为,会把被禁止的行为带入上下文,反而让它更容易被想到。就像“不要去想大象”会让人满脑子都是大象;否定是一个较弱的修饰,被强烈激活的概念会压过它,禁令有一部分会被理解成去做那件事的指令。 改为**正向表述**:直接说明目标行为(例如“注释写成一行”),不提被禁止的行为。禁令只用于无法正向表述的硬性护栏;即使如此,也要配上正向目标,让注意力集中在应该做什么上。 ## 按失败类型选择写法 动笔修改之前,先判断观察到的失败属于哪一类。对一类失败有效的写法,用在另一类上会适得其反: | 观察到的失败 | 有效写法 | 适得其反的写法 | | ------------------------------------------ | ------------------------------------------------------------ | -------------------------------- | | 知道规则,压力下仍然违反 | 硬性护栏加借口与事实对照表 | 软性建议(“尽量”“可以考虑”) | | 照做了,但产出的形状不对(冗长、结论靠后) | 写出产出由哪几部分组成、按什么顺序排列 | 禁令清单(“不要复述”“不要铺垫”) | | 漏掉某个必需元素 | 在 agent 要填写的模板中加一个必填栏位 | 在模板旁边加一句文字提醒 | | 行为应该随情况变化 | 以可观察的条件开头写成条件句(“派发提示列出了多个文件时……”) | 无条件规则再加例外说明 | 产出形状的问题尤其要用正向的组成说明:agent 同时受到另一个目标牵引时(例如“让提示自成一体”),会和禁令讨价还价;组成说明没有可以讨价还价的余地,产出要么符合,要么不符合。 无论选哪种写法: - **不加保留条款。** 在有效的写法后面追加“除非确有必要”,会重新打开讨价还价的空间,结果从稳定变得不稳定。真正的例外,单独写成一条以可观察条件开头的条件句。 - **例外说明管不住范围。** “这条限制不适用于代码块”仍会压制代码块。某部分产出必须豁免时,调整结构,让规则根本作用不到它。 ## 精简与维护 - **每个含义只有一个唯一权威来源**:修改行为只需要改一处。**重复**(同一个含义出现在多处)会增加维护成本和 token,还会让这个含义在层级中显得比实际更重要。(这与先导词正好相反:先导词是有意重复一个*词*,而不是重复一个*含义*。) - **环境本身也是权威来源**,例如 `package.json` 脚本、配置文件、目录结构、`--help` 输出。复述环境信息的文档相当于一份**缓存**:一次查询结果的副本,只有查询成本很高时才值得维护。只缓存 agent 查不到的内容:未写下的约定、选择背后的理由、配置文件无法体现的问题。查一个文件或运行一条命令就能得到的信息,留给环境本身,因为那里不会过时。 - **逐行检查相关性**:这一行是否仍然与文档要完成的任务相关?失去相关性的原因通常有两种:它从未影响任务(纯铺垫,或本应拆分出去的分支内容),或者它描述的行为或环境已经变化,内容已经过时。文档越短,越容易保持相关。没有精简习惯时,最终会出现**过时内容堆积**:添加内容感觉安全、删除内容感觉危险,过时内容一层层堆积,直到必须翻过好几层才能找到仍然有效的内容。 - **逐句查找无效指令**:模型默认就会遵守的指令,占用了成本却没有改变任何行为。判断标准是:与默认行为相比,它是否改变了行为?这个标准针对模型,而不是读者。两个人对某句话是否有效有分歧,本质上是对模型默认行为有分歧,应该通过实际运行文档来判断,而不是争论。一句话不合格就删除整句,不要只删几个字。这个标准也适用于先导词:如果一个词弱到无法改变默认行为(例如 agent 本来就很认真时写 _要认真_),它就是无效指令。解决方法是换一个更具体、更强的词(例如 _逐条核对_),而不是换一种写作技巧。