agency-agents 排查:body seems very short,50 词这条线是怎么来的
body seems very short:50 词这条线是怎么来的
一、现象
你给 agency-agents 加了一个新 agent,或者改了一个老的,按仓库说明跑一遍 lint:
./scripts/lint-agents.sh
输出里蹦出这么一行:
WARN <你的 agent 文件>: body seems very short (< 50 words)
第一反应通常是「不可能」。文件明明写了好几屏,标题、职责、规则一样不少,怎么会不到 50 词。
先把结论摆在前面:这条 WARN 判定的不是你写了多少字,而是脚本切出来的那段正文里有多少个以空白分隔的词。切正文这一步和数词那一步,任意一步出问题,结果都是同一句话。所以看到它,不能直接去补内容,得先分清是「切歪了」还是「真的短」。
另外一句必须先说清楚:这是 WARN,不是 ERROR。lint-agents.sh 只有在 error 计数大于 0 时才打印 FAILED 并以退出码 1 结束;warning 再多,最后一行也是 PASSED。所以这条警告不会拦住合并,它是提示你去看一眼,不是判你死刑。
二、怎么确认是这个问题
判定动作 1:数一数这一轮 WARN 有几条、都是哪几条
这是最省事、也最能一击定性的一步。lint-agents.sh 对同一个文件是按顺序往下走的:前面几道是 ERROR 级(CRLF、frontmatter 分隔符、必填字段),任意一道不过就直接返回;走到后面才是三道只报 WARN 的检查,而这三道一共能吐出四种警告文案:
| 检查 | 级别 | 警告内容 |
|---|---|---|
| 推荐章节存在 | WARN | 缺 Identity / Core Mission / Critical Rules 各报一条 |
| 正文有实质内容 | WARN | body seems very short (< 50 words) |
| 有标题映射到 SOUL.md | WARN | no section headers map to SOUL.md in convert.sh |
| 有标题映射到 AGENTS.md | WARN | no section headers map to AGENTS.md in convert.sh |
关键在于:这四种警告吃的是同一份正文。脚本先把正文切出来,再拿这一份去分别做推荐章节匹配、词数统计、## 标题分流。
于是有个很好用的判据:
- 如果
body seems very short单独出现,其它 WARN 都没有——说明正文切出来了,推荐章节也匹配上了,标题也分流到两边了,那就是正文真的短。 - 如果它和另外几条 WARN 一起出现,尤其是「推荐章节全缺 + SOUL.md 没有 + AGENTS.md 也没有」这种一次报六条的场面——那基本可以断定正文压根没被切出来,脚本手里拿到的是个空串。空串当然词数为 0,当然匹配不到任何章节,当然没有任何
##标题。
这一条判据的价值在于:它把「你写少了」和「文件结构坏了」这两个完全不同的问题分开了。 后者去补内容是白补,补多少都还是 0 词。
判定动作 2:只 lint 这一个文件
脚本的用法是 ./scripts/lint-agents.sh [file ...],不给参数才扫全部 agent 目录。定位单文件时直接把路径递给它:
./scripts/lint-agents.sh engineering/engineering-ai-engineer.md
输出会短到一眼能看完,不用在几百行结果里翻自己那一个。(以上为按脚本自述的用法组合的示例,未逐项实跑,以仓库最新脚本与其头部 USAGE 注释为准。)
判定动作 3:用脚本自己的方式把正文切一遍
这是最直接的验尸。lint-agents.sh 取正文用的是这么一段 awk:
awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' <你的 agent 文件>
意思是:从头扫,遇到一行恰好等于 --- 就把计数加一并跳过这行;只有当计数已经到 2 之后,才开始往外打印。换句话说,正文 = 第二个 --- 之后的全部内容。
把上面这行单独跑一遍,如果打印出来的是空的,问题就锁死在分隔符上,跟你写了多少字毫无关系。再把它接到词数统计上,就能复现脚本第 4 步拿到的那个数:
awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' <你的 agent 文件> | wc -w
判定动作 4:盯死那个 ^---$
正则是 ^---$,锚了行首行尾,含义非常严格:
- 三个短横,不多不少。写成
----不算。 - 这一行不能有别的东西。行尾多一个空格就不算。
- 缩进了也不算。
frontmatter 的结束分隔符只要在这几点上出岔子,计数就永远停在 1,正文就永远是空的。你从别处复制粘贴一段 YAML、或者编辑器帮你「智能」处理了一下缩进,都可能踩到。
判定动作 5:先排掉行尾符
Windows 侧写 agent 文件最容易撞上 CRLF。仓库的标准是 LF,这一点写在 .gitattributes 里,lint-agents.sh 的第 0 步就专门拦 CRLF,且是 ERROR 级、直接返回、后面几道检查根本不跑。
脚本注释把动机写得很明白:行尾多一个 \r 会让后面的 frontmatter 检查报出令人困惑的「missing frontmatter ---」——文件开头明明就是 ---,报错却说没有。为了不让人对着这句话发懵,作者干脆把 CRLF 提前拦成一条自解释的 ERROR。
所以反过来推:你既然看到了 body seems very short 这条 WARN,就说明第 0 步没拦你,行尾符不是这次的原因。CRLF 的文件走不到第 4 步。
顺带提一句:lint-agents.sh 是 bash 脚本,Windows 下需要在 Git Bash 或 WSL 里跑。
三、源码语义给出的处置
对上号之后,按情形处理:
情形 A:正文被切成了空串(多条 WARN 结伴)。 去修第二个 ---,让它严格独占一行、无尾随空格、无缩进。修完之后那几条 WARN 应该整批消失,而不是消失一条留五条——只消失一条说明你修的不是这个。
情形 B:正文确实不足 50 词。 这时候补内容,但别只顾着凑词数。同一份正文还要过另外两道 WARN:推荐章节 Identity / Core Mission / Critical Rules(大小写不敏感匹配),以及 ## 标题必须两边都有——命中 identity、learning + memory、communication、style、critical rule、rules you must follow 这几类的标题会被分流到 SOUL.md,其余的去 AGENTS.md,两边任意一边为空都各报一条 WARN。也就是说,你给章节起的标题不只是排版,它决定了这段内容将来被转换到哪个产物文件里。补内容的时候顺手把这三件事一起满足,比来回修三轮划算。
情形 C:正文是中文写的。 第 4 步统计词数用的是 wc -w,按 wc -w 的通用语义,它以空白分隔计词。中文段落里没有空格,整段可能只被算作很少的几个「词」。这一条我们没有实跑验证,是按命令语义做的推断,但如果你写的是中文 agent 提示词、又确认正文切出来没问题、结构也齐全,那这条 WARN 大概率就是这么来的——它此时是个误伤,而不是内容问题。因为它只是 WARN、不影响退出码,可以当噪声接受;要更严谨就回去看脚本当前版本第 4 步的实现有没有变。
四、处置后怎么验证
- 重跑单文件 lint,看那条 WARN 是否消失:
./scripts/lint-agents.sh <你改的文件>。 - 看汇总行。脚本末尾会打印一行
Results: N error(s), M warning(s) in K files.,然后根据 error 数打印 PASSED 或 FAILED。确认 warning 计数确实降了,别只看最后一个词。 - 看退出码。error 为 0 时退出码 0,大于 0 时打印 FAILED 并退出 1。如果你只修了 WARN,退出码本来就一直是 0,这一步只是确认你没在修改过程中把 frontmatter 的必填字段(
name、description、color)碰坏——那三个缺任意一个都是 ERROR。 - 再切一次正文。用第三个判定动作里那条 awk 加
wc -w,确认数出来的词数确实过了 50。这一步比读 lint 输出更硬,因为它复现的就是脚本自己的算法。 - 全量扫一遍。不给参数直接跑,确认你没顺手把别的文件改出新警告。
五、什么情况说明不是这个原因
这一节比前面几节重要,因为「看见 WARN 就去补字数」是最常见的浪费。
一、你 lint 的根本不是 agent 文件。 仓库里不是每个顶层目录都放 agent:integrations/ 是转换脚本写出的每工具产物,strategy/ 放 playbook 与 runbook、没有 agent frontmatter,examples/ 和 scripts/ 同理。这些文件没有 frontmatter,会在检查 frontmatter 分隔符那一步就被判 ERROR(missing frontmatter opening --- 或 empty or malformed frontmatter)并直接返回,压根到不了词数那一步。如果你手动把这类路径喂给 lint 又困惑于报错,问题在于你选错了文件。仓库里带 frontmatter 的 agent markdown 共 255 个,分布在 17 个 division;文件总数比这个大,差额就是上面这些非 agent 文件,别把两个数混着看。
二、你看到的是 ERROR 不是 WARN。 missing frontmatter opening ---、empty or malformed frontmatter、missing frontmatter field 'color'、CRLF 那条——这些都是 ERROR 级,会让脚本以 1 退出。它们跟正文长度没关系,补一万字也不会好。先把 ERROR 清干净,WARN 的事后面再说。
三、你的 CI 红是别的脚本挂的。 仓库里守着一致性的脚本不止 lint 一个:check-divisions.sh 管 division 列表与磁盘目录、convert.sh 与 lint-agents.sh 里的 AGENT_DIRS、以及 workflow 路径过滤器是否一致;check-tools.sh 管 tools.json 与安装、转换两侧的工具集合是否对得上,条目缺 id/label/kebab/format/installKind/dest 任意一项就失败;check-runbooks.sh 管 runbook 里引用的 slug 能不能解析到真实文件。这些挂了跟你的正文长度无关。
四、你其实撞的是原创性检查。 check-agent-originality.sh 是另一个脚本,用实体中性化后的 8 词 shingle 重叠率给候选 agent 打分,默认 ORIGINALITY_FAIL=40(达到或超过这个百分比视为重复,退出码 1)、ORIGINALITY_WARN=20(达到或超过则警告、不失败),两个阈值都可用环境变量覆盖;它依赖 python3,缺了直接退出码 2。这个脚本管的是「太像别人」,lint 第 4 步管的是「太短」,一头一尾,别串台。
顺便说一个方向相反的判断依据。花名册的原始导出里,255 个 agent 的正文词数最少的那个约 192 词,中位数约 1819 词,最长的约 4638 词——50 这条线离真实写法非常远。这意味着它的定位是空壳哨兵:拦的是只有 frontmatter 没有正文、或者只写了一句话就提交的文件,而不是用来衡量你的提示词写得够不够。lint 这一整套检查管的是结构与重复度,它不评估提示词好不好用,通过了不代表这个 agent 有用,警告了也不代表内容差。
最后留一个源码里的小细节作参照。同一个脚本里,推荐章节那道检查特意用了 herestring 而不是管道向 grep 喂数据,注释解释了原因:grep -q 命中第一处就退出、不会读完输入,管道上游的 echo 会吃到 SIGPIPE,在 set -o pipefail 下那个 141 会变成整条管道的状态,跟「没匹配到」无法区分,结果是正文越大越容易莫名其妙冒出一条 WARN。这个坑修掉了,但它提醒我们:一条 WARN 出现,第一步是确认脚本拿到的输入是不是你以为的那份,而不是立刻去改内容。上面所有判定动作,本质上都是在做这一件事。
延伸阅读
本文依据 agency-agents 官方仓库(github.com/msitarzewski/agency-agents)的 README、
tools.json、divisions.json、scripts/ 下的安装与校验脚本整理,核对日 2026-08-09。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何 agent。
目录、脚本与安装路径随上游更新而变动,请以仓库最新内容与 ./scripts/install.sh --help 的实际输出为准。