title: 把「PM 审核」变成可执行的协议:协作 MD 文档设计
slug: pm-review-protocol
publishedAt: 2026-05-04
excerpt: 为什么状态字段、不停止机制、STOP_NOW 这些工程细节比 prompt 调优更能决定 AI 团队的产出质量。
tags: [multi-agent, process, design]
readingMin: 8
wordCount: 1350
draft: false
prompt 调优有上限,协议设计没有
很多人把"让 AI 协作"等同于"写一个更好的 system prompt"。一开始我也是这么想的:把 PM 的职责、Agent 的角色、状态机的语义都揉进一个 5000 字的 prompt,丢给 Claude Code。
跑了两天就发现两个问题:
/compact之后 prompt 会被压掉:重要的"红线"在压缩之后变成几句话,丢失原文里的强度- 多 Agent 之间没法共享同一个 prompt:Claude1 不知道 Claude2 接到了什么样的指令,出 bug 时根本不知道是哪一段 prompt 出错
后来换了思路:不靠 prompt,把协议落到磁盘上。这一篇讲我们的 5 份核心 MD 文档怎么互相啮合,以及为什么它们的"语法"比内容本身重要。
5 份核心文档,各管一段
协作/
├── 协作总览.md # 项目级 ground truth(角色 + 状态机 + 不停止机制 + 产品规格)
├── 任务板.md # 唯一的进度真源
├── 前端-Claude1.md # 前端 agent 角色卡 + 私域笔记
├── 后端-Claude2.md # 后端 Tech Lead 角色卡 + 私域笔记
└── 后端-Codex.md # 后端编码 agent 角色卡 + 私域笔记
进度/
└── README.md # 项目级总览(交接用)
5 份文件互相引用,通过相对链接形成一个闭环引用图。任何 agent 接手时,从 进度/README.md 开始读,顺着链接 30 秒就能进入工作状态。
协作总览.md:不可变的协议层
这份文件不会经常改。它写了:
- 角色定义(谁能写哪几个状态字段,红线在哪)
- 任务状态机(8 个合法状态,每个的转移规则)
- 工件交付方式(commit 到哪个目录,提交段长什么样)
- 不停止机制的伪代码
我们刻意把"协议层"和"执行层"分开。协议层放在 协作总览.md,几乎不动;执行层放在 任务板.md,每个 commit 都可能动。这样 git diff 就能清晰区分:"我们改了协议本身"还是"只是改了某条任务的状态"。
任务板.md:状态机的"持久化层"
任务板是唯一可写的进度真源。每条任务长这样:
### T112 · 博客 7 篇 MDX 正文
- 状态: 进行中
- 负责: Claude1
- 文件:apps/web/content/posts/<slug>.mdx
- 文章标题与 slug 见 协作总览.md §6.4
- 提交:
时间: 2026-05-13 14:30
作者: Claude1
commit: <hash>
工件: apps/web/content/posts/*.mdx
自测: pnpm --filter @vcs/web build 通过状态: 字段是状态机的持久化,任何 agent 都能读、对应负责人可以改。提交: 段是 agent 把"完成了什么"落到磁盘的标准格式 —— PM 巡检时只看这一段。
角色卡:agent 的"个人 memory"
每个 agent 一张卡,内容三块:
- 顶部
STOP_NOW—— PM 写入的"立刻退出"信号。Agent 在工作循环每次回到顶部时先读这个字段 - 身份 / 工作循环 / 技术栈约束 / 禁动目录 —— 不可变的角色定义
- 私域笔记 —— agent 自己往下追加,记设计决策、踩坑、待问 PM 的问题
私域笔记是设计上最有用的一段。它让 agent 在 invocation 续命时不会"失忆":新 invocation 读到角色卡顶部就能看到之前几轮做了什么、踩了什么坑。
状态机的具体语法
type TaskStatus =
| "草稿" // PM 还在构思
| "待领取" // PM 已派发,等 Agent 接手
| "进行中" // Agent 已经开始
| "提交" // 同义 "待审核",等 PM 审
| "通过" // PM 验收通过
| "打回" // PM 退回返工
| "完成" // PM 标记最终关闭
| "阻塞"; // 等外部依赖8 个状态,5 种转移:
| 起点 | 转移 | 终点 | 触发者 |
|---|---|---|---|
| 草稿 | 派活 | 待领取 | PM |
| 待领取 | 认领 | 进行中 | Agent |
| 打回 | 重新认领 | 进行中 | Agent |
| 进行中 | 提交 | 提交 | Agent |
| 提交 | 通过 | 通过 → 完成 | PM |
| 提交 | 打回 | 打回 | PM |
| 任意 | 阻塞 | 阻塞 | PM |
只有 PM 能写 通过/完成/打回/阻塞。这是权限红线。
为什么是 8 个而不是更少
我们最初只有 5 个:待领取 / 进行中 / 待审核 / 通过 / 打回。跑了一周发现:
- 没有
草稿,PM 想"先把雏形写下来"就只能直接派活,反而干扰 agent 工作循环 - 没有
阻塞,被外部依赖卡住的任务只能停在进行中,无法表达"我尽力了但等不到" - 没有
完成,通过之后任务再也不动,但通过和完成实际是两件事:通过是"这次提交可接受",完成是"这条任务整体结束"
把这三个状态加进去后,任务板的 diff 表达力立刻提升。
STOP_NOW:权限的最后一道闸
STOP_NOW 是 PM 在角色卡顶部写入的"立刻退出"信号。Agent 在工作循环里每次回到顶部都先读这个字段,非空就保存状态退出。
为什么不直接关掉 agent 进程?因为 Claude Code 没有"关闭 agent"的 API,agent 是 Claude Code 进程的子 invocation,你只能等它自己退出或者整个进程被杀。STOP_NOW 是软退出的唯一手段。
我们踩过一个尴尬的坑:最初 Codex 的角色卡里写"STOP_NOW 字段为空时不退出",结果 PM 用 <!-- 注释 --> 写 STOP_NOW: <空 reason>,Codex 看到字段"非空"就退出了。修法是把"非注释内容即 STOP_NOW"明确写到角色卡顶部,堵这种语义边界 case。
不停止机制:让"协作"变成"24/7 团队"
3 个 agent 的工作循环大致一样:
loop forever:
if STOP_NOW or PROJECT_STATUS in (PAUSED, DONE):
save_state(); exit
tasks = parse(任务板.md) where 负责==me and 状态 in {待领取, 打回}
if tasks 非空:
pick first; do_work(); 状态改为 提交
continue
sleep 60s, 10 次后切到 300s; recheck
Token 上限会让 invocation 自然截断 —— PM 监听完成事件,立刻用同角色 prompt 起新 invocation,带上"接力 prompt"。用户感知是"3 个 agent 始终在线",实际是 invocation 在被 PM 串联续命。
续命的关键不是 prompt,是文档
新 invocation 接手时,PM 只用一句话:
你是 ClaudeX,本轮 invocation 接力。先读
协作/前端-Claude1.md、协作/任务板.md,然后进入工作循环。
不需要在 prompt 里复述任何业务逻辑 —— 因为业务逻辑都在 MD 文件里。这才是协议的核心价值:让 agent 的"个性"和"记忆"都外化到磁盘,prompt 只负责拉起。
协议设计的几条心得
写到 Sprint 3,有几条非显然的经验:
- 状态字段比 prompt 更能驱动协作:agent 经常违反 prompt 里的细节,但几乎不会违反"我没权限写这个字段"
- 每个 agent 必须有自己的
私域笔记段:这是它的"个人 memory",续命时唯一能保留的工作上下文 - PM 必须维护一份"进度文档":这是给"未来的自己 / 接手的另一个 PM"看的,跟任务板分开 —— 任务板讲"现在",进度讲"前因后果"
- 协作 MD 文件必须 commit 进 git:
.gitignore不能包含协作/或进度/。这两个目录就是协作产物,与代码一起版本化
整套协议加起来不超过 5 份文件,可读、可 git diff、可被任何 agent 接手。复杂度的边界在这里 —— 任何超出"5 份文件能表达"的协作机制,我们都没有引入。