给 agent 章节起标题其实是在写路由:agency-agents 的 SOUL.md / AGENTS.md 分流机制

2026-08-09

写 agent markdown 的时候,绝大多数人对二级标题的态度是「排版」——把职责分几段,起个看着顺眼的名字,## Core Mission## 工作流程## 你必须遵守的规则,随手就写了。

在 agency-agents 这个仓库里,这个随手的决定其实是在写路由规则。你给章节起的标题,会直接决定这一段正文被转换到哪个产物文件里。更麻烦的是:起错了不会报错,它会安安静静地落到另一个文件里。

这篇就把这条链路拆开讲:路由发生在哪一环、规则的原文是什么、默认落到哪、什么时候仓库才会提醒你,以及你自己写 agent 时该怎么定标题。

一、路由发生在 convert 那一步,不是 install

先把链路摆清楚,不然容易找错地方。agency-agents 的安装是两步流水线:

源 agent markdown → scripts/convert.sh 渲染成各工具格式 → integrations/scripts/install.sh 拷贝到目标目录

install.sh 头部注释的原意是:它从 integrations/ 读取已经转换好的文件,拷到每个工具对应的配置目录;如果 integrations/ 缺失或者过期,先跑 convert.sh。默认情况下集成文件缺失时它会自动去调 convert,可以用 --no-convert 关掉。

所以标题分流是 convert.sh 的事。install.sh 只负责搬运,它看到的已经是分好家的文件。你改标题之后如果只重跑 install 而 integrations/ 是旧的,看到的自然还是旧结构。

二、只有一种格式会按标题拆文件

仓库 tools.json 里登记了 16 种工具,每种工具有一个 format。这 16 种里,只有 OpenClaw 的 openclaw-workspace 会按标题把正文拆开,它的 installKindper-agent,用户级落点是:

.openclaw/agency-agents/{slug}/SOUL.md
.openclaw/agency-agents/{slug}/AGENTS.md
.openclaw/agency-agents/{slug}/IDENTITY.md

一个 agent 一个目录,正文按 ## 标题关键词拆成 SOUL 和 AGENTS 两份。

对照一下其它几种格式就明白差别有多大:

  • identity(Claude Code、GitHub Copilot 共用)——顾名思义,原样输出,不做格式改写,正文是一整块。
  • codex-toml(Codex)——每个 agent 一个 TOML,整个正文塞进 developer_instructions 一个字段。
  • cursor-mdc(Cursor)——.mdc 文件,frontmatter 之后是完整的 ${body}
  • opencode-md(opencode)——.md 加 YAML frontmatter,多一个 mode: subagent,正文同样是完整的 ${body}

也就是说,你写的 ## 标题在这十几种格式里只是正文里的一行 ##,模型读到什么样就是什么样;只有到了 OpenClaw 这里,标题从「内容」变成了「分拣依据」。

tools.json_note 里对 format 的契约解释也印证了这点:同一个 format 名保证渲染出字节级相同的输出,两个工具能共用一个 format 的前提就是渲染结果完全一样。所以 identityopenclaw-workspace 是两套完全不同的产物形态,不能互相类推。

三、分流规则的原文

规则写在 scripts/lint-agents.shclassify_header_target() 里(第 40–53 行),它把每个 ## 级标题分流到两个目标:

if [[ "$header_lower" =~ identity ]] ||
   [[ "$header_lower" =~ learning.*memory ]] ||
   [[ "$header_lower" =~ communication ]] ||
   [[ "$header_lower" =~ style ]] ||
   [[ "$header_lower" =~ critical.rule ]] ||
   [[ "$header_lower" =~ rules.you.must.follow ]]; then
  printf 'soul'
else
  printf 'agents'
fi

读这段有三个要点,都是从代码字面就能确定的:

第一,这是子串匹配,不是整串相等。 条件写的是 =~,所以标题里带 emoji、带 “Your”、带别的修饰词都不影响命中。仓库样例 engineering/engineering-ai-engineer.md 里的 ## 🧠 Your Identity & Memory 就是靠里面的 identity 命中的。

第二,learning.*memory 是有顺序的。 正则要求 learning 在前、memory 在后。上面那个样例标题里虽然有 memory,但没有 learning,它命中的其实是 identity 这一条。如果你把标题写成「Memory & Learning」,这一条正则就不成立了——能不能进 SOUL 就得看有没有别的关键词兜底。

第三,style 是个很宽的词。 只要标题里出现 style 就进 SOUL,## Coding Style## Output Style 这类偏工作方式的章节也会被划过去。这一点值得在命名时留个心眼。

convert.sh 里对 OpenClaw 的处理是同一套逻辑:SOUL 的关键词是 identity、learning & memory、communication、style、critical rules、rules you must follow;其余全部进 AGENTS.md,源码注释举的例子是 mission、deliverables、workflow 等。默认桶是 agents

四、最坑的一条:不命中不会报错

回头看那个 else 分支——没命中任何关键词的标题,一律 printf 'agents'。没有异常、没有提示、没有「未分类」的第三个桶。

这意味着一件事:你精心写的「这个 agent 说话该是什么调性」,如果标题起成了 ## 表达方式 或者 ## 语气约定,它不会进 SOUL.md,会静静躺在 AGENTS.md 里。文件是生成了,内容一个字没丢,只是分到了另一半。

lint 唯一会提醒你的情况是某一边一个标题都没有。它要求两个目标各自至少有一个标题,否则各报一条 WARN:

no section headers map to SOUL.md in convert.sh
... to AGENTS.md in convert.sh

注意这是 WARN 不是 ERROR,不会让检查失败。而且只要你有一个标题命中了 SOUL,哪怕另外五个本该进 SOUL 的段落全落到了 AGENTS,lint 也一声不吭——它检查的是「两边都非空」,不是「每一段都分对了」。

五、判断依据:这一段该去哪边

规则本身很短,难的是自己写的时候怎么定标题。可以用一个很朴素的问法来分:这一段是在定义「它是谁」,还是在定义「它干什么」?

  • 定义它是谁——身份、记忆、说话方式、不可违背的红线——属于 SOUL。
  • 定义它干什么——使命、交付物、流程步骤、检查清单——属于 AGENTS。

按这个划分,对照上面六条正则,常用标题的落点是:

你想写的内容建议的标题写法命中的关键词落点
角色设定、背景## Identity / ## Your Identity & MemoryidentitySOUL.md
经验积累与复用## Learning & Memory(顺序别反)learning.*memorySOUL.md
汇报口径、交互方式## CommunicationcommunicationSOUL.md
语气、行文风格## Communication Stylecommunication / styleSOUL.md
绝不能做的事## Critical Rulescritical.ruleSOUL.md
强约束清单## Rules You Must Followrules.you.must.followSOUL.md
核心职责## Core MissionAGENTS.md(默认桶)
产出物定义## DeliverablesAGENTS.md
工作流程## WorkflowAGENTS.md

两个实操建议:

用英文关键词命名标题。 正则匹配的是英文小写子串,纯中文标题不可能命中任何一条,会全部掉进 AGENTS.md。如果你想写中文,稳妥做法是英文关键词打头、中文补充说明跟在后面。

别为了凑分流硬造标题。 lint 只要求两边各有一个标题,硬塞一个空章节去满足它,除了让文件更长没有别的收益——而且正文词数低于 50 会另外触发一条「实质内容不足」的 WARN。

六、动手自查:跑单文件 lint

写完一个 agent,最直接的验证动作是对着这一个文件跑 lint:

./scripts/lint-agents.sh path/to/your-agent.md

不给文件参数则扫描所有 agent 目录。这个脚本的检查项如下,和标题相关的是最后一道,但前面几道更容易先把你拦住,尤其在 Windows 上:

检查级别说明
拒绝 CRLF 行尾ERROR仓库标准是 LF
frontmatter 分隔符存在ERROR取首尾两个 --- 之间的内容
必填字段齐全ERRORname / description / color
推荐章节存在WARNIdentity / Core Mission / Critical Rules,大小写不敏感
正文有实质内容WARN正文词数低于 50
标题能映射到两个目标文件WARN就是本文这一条

CRLF 那道检查值得单独说一句。源码注释解释了为什么要专门拦它:行尾多一个 \r,会让后面的 frontmatter 检查报出令人困惑的「missing frontmatter ---」——明明文件开头就是 ---。在 Windows 上用编辑器新建文件、或者 Git 配置带自动换行转换的时候,很容易踩到,先把这一条排除再去研究标题分流,能省不少时间。

顺带提一句同族的另一个反直觉设计:Cursor 的转换模板里 alwaysApply: falseglobs: "" 是写死的,转换出来的规则默认不会自动生效,需要在 Cursor 里按需引用。这和标题分流是两回事,但都属于「产物看着装好了、行为跟预期不一样」的同一类坑。

七、这套检查的边界

最后把话说清楚,免得把 lint 当成质量保证:

  • lint 只管结构,不管提示词好不好用。 它校验的是 frontmatter、章节、词数、映射,不评估你写的人设有没有效果。
  • 推荐章节缺失只是 WARN,不会让检查失败,别看到黄字就以为构建挂了。
  • 重复内容是另一个脚本 scripts/check-agent-originality.sh 在管,它把候选 agent 与整个花名册做实体中性化后的 8 词 shingle 重叠率打分,默认 ORIGINALITY_FAIL=40ORIGINALITY_WARN=20(这两个是默认值,可用环境变量覆盖)。源码注释给出的库内校准结论是:现有 agent 之间最差相似度约 1.5%、中位数 0%,所以双位数就是强异常。这是该库内部的校准值,不是对任意 agent 库的普适结论。
  • 别把 SOUL 那半边的内容当成资历。仓库样例的 ## 🧠 Your Identity & Memory 段里写着 Experience: You've built and deployed ML systems at scale 这类句子,它是写给模型的第二人称人设设定,不是任何真人的履历,读的时候别当成能力证明。这也正是这一段被路由进 SOUL.md 的原因——它定义的是「它以什么身份说话」,不是某人做过什么。

回到标题这件事本身:它之所以值得单独写一篇,是因为它是整个仓库里「看起来最像排版、实际上最像配置」的一处。改一个 ## 后面的词,产物文件的归属就变了,而且大多数时候没有任何东西会告诉你变了。知道有这么一层路由,比记住那六个关键词更重要。

延伸阅读


本文依据 agency-agents 官方仓库(github.com/msitarzewski/agency-agents)的 README、 tools.jsondivisions.jsonscripts/ 下的安装与校验脚本整理,核对日 2026-08-09。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何 agent。 目录、脚本与安装路径随上游更新而变动,请以仓库最新内容与 ./scripts/install.sh --help 的实际输出为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。