先说清楚要解决的是什么问题,因为这决定了后面所有设计。

我同时推着几个项目,时间被会议切碎。拿到一个新任务,我能提供的原料通常只有会议记录和聊天记录 —— 而我没有时间把它们读完、想清楚、再设计

自然的想法是让 agent 去做。但这里有个矛盾:

我必须是那个做决策的人          ← 做错了是我的责任
但我付不起「理解全部上下文」的时间成本

传统上这两件事绑死:你要决策,就得先理解。

这套循环要做的,就是把它们解耦:

agent 承担理解成本   ——  读 68KB 的会议记录
压缩成决策形态       ——  N 个选择题,每个带推荐、代价、能不能拖
我在【不读原文】的情况下,做出和【读了原文】一样的决策

所以它的价值不是「省时间」,是**「在不理解全部的前提下,依然能做对决策」**。


一、整体架构

flowchart TD YOU[我] -->|扔一个主题| SESS[对话 session] SESS -->|反问三个问题| YOU SESS -->|建任务| Q[全局队列 queued] CRON[定时任务 每 N 分钟] -->|取队首| Q CRON ==启动独立进程==> MAIN[主 agent<br/>唯一有写权] MAIN ==按需派 只读==> RT[圆桌 agent<br/>0 到 4 个 各自独立] RT ==结论交回==> MAIN MAIN --> F[落盘 任务 决策 产出] YOU -->|中途插话| IN[INBOX.md] IN -.只在段边界读.-> MAIN F --> G1[闸1 脚本体检] G1 --> G2[闸2 终审 agent] G2 -->|PASS| DONE[done] G2 -->|FAIL| MAIN MAIN -.实时日志.-> LOG[stream-json]

三个关键分离:

① 对话 session 不干活。 它只负责入口和出口 —— 建任务、看结果、拍板。真正干活的是定时任务启动的另一个独立进程

这个分离是必须的。如果对话 session 自己干活,那就是普通的 agent 用法 —— 我得一直在场。

② 定时任务和对话 session 之间只能通过文件说话。 它们是两个进程,不共享内存。这不是设计选择,是硬约束 —— 而它恰好逼出了后面文件分层的设计。

③ 只有一个 agent 能写文件。 圆桌全部只读,结论交回主 agent 落盘。这条抄自 Cognition:额外的 agent 贡献判断,不贡献动作。


二、队列机制:目录即状态

任务是 markdown 文件,状态用目录位置表示:

tasks/
├── queued/     排队    序号小的先取
├── running/    在跑    WIP 上限
├── blocked/    挂起    卡在外部,等答案
├── done/       完成
└── dropped/    废弃    显式标原因,不删文件
取任务   mv queued/010__项目__标题.md   running/
挂起     mv running/xxx.md              blocked/
完成     mv running/xxx.md              done/

mv原子操作,没有中间态。整个生命周期就是文件在五个目录之间搬家:

stateDiagram-v2 [*] --> queued: 我扔进来 queued --> running: 调度器取队首 running --> blocked: 卡在外部 等答案 running --> blocked: 撞预算上限 blocked --> queued: 答案到了 放回队列 running --> queued: mtime 超时 判定 agent 已死 running --> done: 两道闸都过 running --> running: 闸没过 打回重做 running --> dropped: 显式废弃 写明原因 done --> [*] dropped --> [*]

注意 running --> queued 那条:agent 死了没人知道,只能靠文件 mtime 超时判定。 而撞预算走的是 blocked 而不是 queued —— 否则会被超时机制自动重跑,无限烧钱。

为什么不把状态写在文件里

一开始我在 frontmatter 里写 status: running,同时文件也在 running/ 目录。跑了几次就出问题了 —— agent 挂掉之后,目录说它在跑,而另一张记录表说没有任务在跑,谁都不知道该信哪个。

同一件事有两个记法,就一定会不一致。目录位置是唯一真相源,frontmatter 只放位置装不下的东西(比如「在等哪条决策」)。

命名:序号前缀,不是时间戳

010__项目名__任务标题.md
015__项目名__临时插进来的.md     ← 留空隙,插队只是改个文件名
020__项目名__另一个任务.md

时间戳只能表达「什么时候加的」,表达不了优先级。而队列的顺序就是优先级 —— 那是我扔进去的顺序,agent 不许重排。

分隔符用双下划线是踩出来的:第一版用单横杠 序号-项目-标题,结果接第二个项目时,项目名里带横杠,解析直接崩。文件名当结构化数据用,分隔符必须和内容不冲突。

双层队列:入队的是原料,不是任务

这一条我没在别处见过,但它是整套设计里最贴合真实场景的一处。

传统任务队列   你想清楚 → 写成任务 → 入队
                ⚠️ 它假设你已经知道任务是什么

我的场景       原料入队 → agent 消化 → 任务从原料里长出来
                ⭐ 因为我的痛点恰恰是「没时间想清楚任务是什么」

所以是两层,中间有一道人闸:

原料队列    我随便扔,零成本
    ↓ 消化
任务候选    agent 提出「我觉得这些是要做的事」
    ↓ 我确认       ← 这道闸不能省,否则它会自己给自己派活
任务队列    有序,agent 从上往下取

WIP 上限:瓶颈不是 agent,是我

同时在跑的任务有上限。这个上限不是技术限制:

1 个任务  ≈  若干条待我拍板的决策
3 个并行  =  三倍的决策 + 三次上下文切换

真实成本是上下文切换,不是「30 秒 × N」。 一旦决策队列的膨胀速度超过我清理的速度,我就会开始逃避看它 —— 那整套就崩了。


三、定时调度:出队逻辑

调度器是个 shell 脚本,cron 每隔几分钟调一次。它干七件事:

① WIP 检查       running/ 里 ≥ 上限 → 立刻退出          ⭐ 队列空时零成本
② 取队首         queued/ 里序号最小的那个
③ 解析项目名     从文件名拿
④ 组 prompt      指向项目配置 + 通用规矩 + 这个任务文件
⑤ ⭐ 先 mv 后启动  先移进 running/,再启动进程
                  → 下一个 tick 就取不到它了,天然幂等
⑥ nohup 后台跑   立刻返回
⑦ 后台守着       进程结束后看它是怎么结束的
flowchart TD T[cron 每 N 分钟触发] --> W{running 里<br/>够不够 WIP 上限} W -->|够了| E1[立刻退出<br/>零成本] W -->|没满| Q{queued 里<br/>有任务吗} Q -->|空| E2[立刻退出<br/>零成本] Q -->|有| P[取序号最小的] P --> M[⭐ 先 mv 到 running] M --> S[nohup 启动独立进程<br/>带预算上限 和 日志重定向] S --> R[立刻返回] S -.进程结束后.-> C{怎么结束的} C -->|正常| OK[任务已由 agent<br/>自己移到 done] C -->|撞预算| B[mv 到 blocked<br/>写明原因 不自动重跑]

第 ⑤ 步是关键。 先改状态再启动,所以 cron 跑多密都不会重复取同一个任务 —— 不需要锁,不需要额外的协调机制。

无人值守必须有的三个兜底

预算上限。 agent 按用量计费,而它自己不知道该停。一个卡住的任务可以反复试两小时。命令行有 --max-budget-usd,撞了就断。

撞预算不许自动重跑。 这一条是踩出来的:撞预算 → 进程死 → 任务留在 running/ → 一段时间后判定「agent 死了」→ 自动重跑 → 又撞 → 无限烧钱。所以撞预算是个特殊状态,任务进 blocked/ 并写明原因,等人决定。

死任务回收。 running/ 里的文件 mtime 超过 N 小时没变,判定它的 agent 已死,移回 queued/ 重跑。这个机制抄自一个断点续传的实现 —— 但要注意,它和上一条冲突,所以撞预算的必须走 blocked/ 而不是留在 running/

透明度:不用自己造

我一开始打算让 agent 主动往一个 progress 文件里写进度。这是个坏主意 —— 它靠 agent 自觉,而 context 一长就会漏。

后来发现命令行本来就给了:

claude -p "..." \
  --session-id "$(uuidgen)" \
  --output-format stream-json \
  --forward-subagent-text \
  --max-budget-usd 20 \
  >> logs/任务名.jsonl &
  • --output-format stream-json 实时流出每一步
  • --forward-subagent-text 连 subagent 的内容都带出来
  • --session-id 自己指定,跑之前就知道
  • 另有一个子命令能列出所有活着的 session,不需要 TTY

日志是 JSON,读起来费劲,所以我写了个几十行的脚本把它翻成人话。透明度这件事,大概率不用你自己造 —— 先翻文档。


四、多 agent 规划:这块我调研得最久,因为直觉全错

❌ 别按工种拆

我最初想的是「研究 agent / 设计 agent / 实现 agent」。有人试过:

  • Anthropic 做过按软件开发角色划分的实验(planner / implementer / tester / reviewer),结论是「subagent 花在协调上的 token 比花在干活上的还多」。他们给的替代原则是按上下文边界拆,不按工种拆
  • Cognition 更具体:把「做个小游戏」拆给子 agent,一个做出了某种风格的背景,另一个做的角色和整体美术对不上,负责合并的 agent 面对两个不兼容的产物无法收场。merge 这个问题他们坦承至今没解决,是直接绕开的。

判据:只有当上下文能被真正隔离时才拆。

「设计 / 实现 / 测试」拆不开,因为它们共享同一份上下文。拆了是伪隔离,只多出协调成本。

❌ 也别指望「多来几个 agent 就有多样性」

我想过一个看起来很聪明的做法:让 N 个 agent 各出一个方案再比较,总比一个 agent 自己出 A/B 强吧?

不强。同构输入下的多 agent 辩论在数学上是个鞅 —— 期望准确率不随轮次提升;3–10 个同构 agent 的各种聚合策略打不过单个 CoT agent。失败路径有三类:谄媚趋同、上下文脆化、共识坍缩(投票把已经生成出来的正确答案投掉了)。

换个 agent 但同模型、同上下文,约等于白花钱。

⭐ 多 agent 多在【输入】,不在【角色名】

这是我调研之后最重要的一条转变。

我这套里真正有效的那次,是三个「立场审」角色。但它们有效不是因为叫了三个名字:

角色 1  →  只拿到干系人 A 的表态记录
角色 2  →  只拿到干系人 B 的表态记录
角色 3  →  只拿到客户方的表态记录

三份输入互不重叠  →  这才叫异构  →  独立收敛才有信息量

如果它们共享输入,那「三个角色都指出了同一个问题」没有任何信息量 —— 那只是同一个推理被复制了三遍。

实测里它们各自抓到了不同的东西,又在一个真问题上撞到一起。这个「部分重叠」的模式才是真独立的特征:

完全一致   ⚠️ 可疑,大概率共享了上下文
完全不同   🟡 没有共识,信息量低
部分重叠   ⭐ 真独立

圆桌内部长这样:

sequenceDiagram autonumber participant M as 主 agent participant R as 角色库 participant A as 角色实例 A participant B as 角色实例 B M->>M: 出方案 v1 M->>M: ⭐ 判断 这方案最可能怎么失败 M->>R: 按失败模式【选】角色 不许临时编 R-->>M: 角色定义 含输入隔离规则 par 并行派出 各自独立 看不到彼此 M->>A: 角色定义 加 只给 A 该看到的输入 and M->>B: 角色定义 加 只给 B 该看到的输入 end A-->>M: 结论 只读 不写文件 B-->>M: 结论 M->>M: 汇总 Note over M: 能改的改进方案 v2<br/>有分歧的升级成待人拍板<br/>不裁决 裁决是人的

三条不能破:主 agent【选】角色不【编】角色(编出来的没有输入隔离规则)· 并行派出看不到彼此(顺序派会互相污染)· 圆桌只读,结论交回主 agent 落盘。

输入隔离 = 现实的保真度

于是「给这个角色看什么」不再是工程细节,而是角色定义的核心:

角色它在现实中是谁只给它绝不给
立场审一个具体的干系人方案 + 这一个人的表态记录会议原文 · 我自己的吐槽 · 方案的推理过程
技术审未来维护这段代码的人方案本身方案的推理过程(给了它会替作者辩护)
追问者会上不好糊弄的听众方案本身推理 · 背景材料(真听众也没看过)
终审刚来的验收员判据 + 产出 + 轨迹背景故事

原则一句话:每个角色只拿到它在现实中真能看到的东西。

给多了,它就在模拟一个不存在的、什么都知道的评审者 —— 那种评审没有预测力

还有个附带效果我是踩了才知道的:如果给评审 agent 看了我对某个人的抱怨,它会把那个人描绘得比实际更不讲理。 读了几周你的吐槽之后,agent 会在所有事情上站你这边 —— 而那时候它的判断已经不值钱了。

要不要派一个新 agent:四道闸

flowchart TD Q[想派一个新 agent] --> A{它贡献的是<br/>判断 还是 动作} A -->|动作| NO1[不派<br/>写操作单线程] A -->|判断| B{上下文能真正隔离吗} B -->|不能| NO2[不派<br/>伪隔离 只多协调成本] B -->|能| C{输入和主 agent<br/>有实质差异吗} C -->|没有| NO3[不派<br/>同构等于白花钱] C -->|有| D{输出契约明确吗} D -->|不明确| FIX[先定义契约] D -->|明确| YES[派]

我拿这四道闸回头验自己已有的角色:审查类的全部通过,而原本想加的「实现 agent」和「N 个方案生成 agent」在第一、二、三道闸就被拦下了。判据自洽,不是事后找理由。

最该警惕的一条

伯克利那份 MAST 研究标注了 1600+ 条多 agent 轨迹、覆盖 7 个框架:

41.8%   规格与系统设计问题   角色定义模糊 · 拆解不当 · 重复的 agent 角色 · 缺终止条件
36.9%   agent 间失配        交接丢上下文 · 输出冲突 · 格式不匹配

结论是:多数失败源于组织设计,而非模型能力。

配上成本 —— 多 agent 通常 3–10 倍 token,研究型场景约 15 倍。所以我给自己的规矩是:默认不加角色。 每加一个都得先回答:它的上下文和别人隔离吗?输出契约是什么?


五、关键文件设计:什么给 agent 看,什么给人看

这一块比我预期的重要得多。因为两个进程只能通过文件说话,文件的读者是谁,决定了它该长什么样

给 agent 看的                    给人看的
─────────────────────────       ─────────────────────────
PROTOCOL.md   通用规矩            DECISIONS.md  待我拍板的
PROJECT.md    项目配置 · 红线      看板 PNG      一眼扫的状态
HANDOFF.md    工作区              STATUS.md     项目当前状态
角色定义                          ⭐ 人机接口是【对话】,不是文件
flowchart LR subgraph AG[给 agent 看 · 人不读] P[PROTOCOL.md<br/>通用规矩] J[PROJECT.md<br/>项目配置 红线] H[HANDOFF.md<br/>工作区 死胡同] RL[角色定义] end subgraph HU[给人看 · agent 只写不依赖] D[DECISIONS.md<br/>待我拍板] ST[STATUS.md<br/>项目状态] PNG[看板 PNG<br/>一眼扫] end IN[INBOX.md<br/>我往里插话] -.段边界读.-> H AG --> HU

人机接口是【对话】,不是文件 —— 文件只是让两个进程能对上账。

HANDOFF.md:明确写「本文件不给人看」

这是 agent 之间的工作区 —— 完整推理、死胡同、失败尝试全留着。它的头部第一句就是:

⚠️ 本文件不给人看。不要为人的可读性做任何妥协。

为什么要写死这一句?因为不写的话,agent 会本能地把它整理成「给人看的样子」 —— 删掉死胡同、简化推理、加小标题。而那些恰恰是下一个 agent 最需要的东西。

死胡同尤其重要。有一条实测发现:append-only 的失败记录是个坏主意 —— agent 看到被删掉的旧方案,会跑去重建它。所以废弃的东西必须显式标记为废弃,而不是删掉

DECISIONS.md:唯一需要人看的那份

它的设计目标只有一个:让我 30 秒能拍一条板。

### D-003 · 一句话标题                    [待你拍板]

**推荐:A**

|       | 好在 | 代价 |
|-------|------|------|
| **A** | …    | …    |
| B     | …    | …    |

**推荐 A 的理由**   一句话
**反方意见**       圆桌说的
**不决定会怎样**   ⭐ 卡住哪一步 / 不卡,可以先跑别的

---
你的选择: ____
你的理由: ____        ← 必填

两处是刻意的:

  • 「不决定会怎样」 —— 告诉我这条能不能拖到明天。没这栏,每条我都得当紧急的看。
  • 「你的理由」必填 —— agent 能从中学我的口味;而且三个月后我自己会想知道当时为什么这么定。

一个花了我三次蒙圈才想明白的设计

前几轮跑下来,产出的决策队列里有 12 条,我看着一片空白 —— 完全不知道怎么拍

我一开始以为是我没时间理解项目。后来才发现是队列设计错了:

12 条里,10 条标着「需对外确认」
      ⚠️ 那些本来就不该由我拍,它们要拿去问别人

我把「所有未决项」都当成了「等你拍板」。 而我真正需要面对的只有 2 条。

修法很简单:看板只显示我自己能拍的,需要问别人的单独列一块,并且由 agent 汇成「按人分组的问题清单」。从 12 条变成 2 条。

这条推广开是:

写决策项之前先问 —— 接收方有没有判这条所需的上下文? 没有的话,要么翻译成他有的语言,要么根本不该进他的队列。

中途插话:一个 INBOX 文件

agent 跑起来之后,我想插一句「别做 X 了」怎么办?两个进程不能直接对话。

<项目>/INBOX.md      我往里写

agent 在【每个段边界】读一次
    有内容 → 读进来,按它调整,把原文挪进 HANDOFF 存档,⭐ 清空 INBOX
    没内容 → 继续

只在段边界读,不实时。 实时读会打断它的推理,而且它正在思考时读到新指令,行为不可预测。

这就是异步消息队列的最小实现 —— 一个文件。


六、凭什么算完成:两道闸

agent 说「我做完了」是不可信的,这是被反复点名的失败模式。有个描述很传神:verifier 跑一两个测试,看到通过,就宣布成功。

所以有两道闸:

闸1  脚本      量【能量化的】:产出图多高、产物目录在不在、任务有没有写判据
闸2  终审 agent  判【脚本判不了的】:比如「他看完能说出明天动哪件事」

能量化的绝不交给 agent。 让 agent 判自己的产出,它永远说「做完了」。

而终审 agent 的设计有个我一开始搞反的地方。我给它设了条约束:不给它看执行过程,只给判据和产出 —— 理由是看了过程会被「它挺努力的」影响。

查资料才发现结论相反:Meta 那篇 Agent-as-a-Judge 里,裁判看完整轨迹时与人类判断一致率是 90%;只看最终产物的传统做法只有 60–70%。

我把两件事混成了一件:

判「做完没有」    →  要看轨迹
判「写得好不好」  →  要干净上下文

同一个「审」字,底下是两种任务。


七、实证:三次被自己的系统打脸

上面的设计不是一次想出来的。这里给三个具体的翻车,它们是这套东西目前最硬的证据 —— 因为每一条都是我先拍脑袋、后被打脸。

打脸一:规矩写了,但没有执行者

我定过一条判据:产出的主干必须一屏能扫完。写了检查脚本,超了就 exit 1;规矩文档里也白纸黑字「体检不过不许标记完成」。

跑了几个任务后一查:三个产出分别是 6.9 屏、2.8 屏、7.1 屏,全部不合格,而且全都躺在「已完成」里

脚本确实 exit 1 了。但调用它的那段流程,没有任何一行代码去读那个退出码

规矩 ≠ 判据。判据必须有一个不讲情面的执行者。 写在文档里的约束对 agent 来说是建议;写在退出码里并且被消费的,才是边界。

打脸二:接上闸门后,判据被一行 CSS 绕过

线接上了,我以为这条结了。

然后我派了两个独立的审查 agent 去审一个方案 —— 它们看不到彼此。两个各自独立地指出同一件事:

字号 16px 调到 10px,或者加一行 body { zoom: .6 },或者单栏改三栏 —— 任何一条都能把 14000px 压进 2600px,而体检照样是绿的。

这是静默失败的教科书形态:检查通过了,产出是一张 9px 字、没人读得了的图。

我想要的是「可读」,但我能量化的只有「高度」,于是用高度代理了可读性。而只要一个指标成为目标,它就会被优化 —— 优化的方式往往不是你想要的。

修法不是找一个更聪明的指标(没有),而是把最省力的作弊路径显式禁掉,并让审查者专门查这一条:

压缩只准靠真删内容。禁止:调小字号 / zoom / transform:scale / 改多栏挤压 / 压行高

你堵不住所有作弊,但你可以让作弊比把事做对更费劲。

打脸三:审查 agent 拒绝回答我的问题

我给圆桌出了道题:「判据该怎么分档 —— 方案 A 还是方案 B」。

它没选。它绕过我的选项,直接指出:

你这个方案的前提不成立 —— 闸门根本没接上,判红的三个产物全在完成里。 在闸门接上之前,阈值定多少都不影响任何事。

它推翻的是我出题时的假设,不是我给的选项。而这恰恰是我最需要的那种反馈。

审查者最有价值的时候,是它拒绝回答你的问题。

顺带:接第二个项目时抓到的四个 bug

泛化验证的价值也在这里 —— 只跑一个项目,永远碰不到这些:

① 初始化脚本要求项目目录已存在   —— 而「接新项目」正是它该干的事
② 集中化改造时漏改了初始化脚本   —— 还在旧位置建目录
③ ⭐ 项目名解析用单横杠切分      —— 第二个项目名字里带横杠,直接崩
④ glob 模式还是旧分隔符          —— 改完命名规则后匹配不上

第 ③ 个最实质:文件名当结构化数据用时,分隔符必须和内容不冲突。


八、哪些是抄的,哪些是自己趟的

写这类文章最该说清的是每个结论的证据等级。

有出处的:按上下文拆不按工种拆(Anthropic)· 写操作单线程与合并失败(Cognition)· 同构多 agent 是个鞅(arXiv)· 裁判看轨迹一致率 90%(Meta Agent-as-a-Judge)· 41.8% 失败源于组织设计(MAST)· 人审 plan 不审代码(HumanLayer)· 用 IM 做决策队列会失败(LangChain 自用邮件 agent 的半年实践)· 多模型共识与人的判断不一致(Karpathy 的 llm-council)。

没找到前人的(也可能是我没找到):

  • 入队的是原料,不是任务 —— 所有现成的任务队列都假设你已经知道任务是什么
  • 「返工波及范围」判据 —— 卡住时继续还是停,判据是「假设错了会不会让已完成的部分返工」
  • 多 agent 多在输入,不在角色名
  • 派新 agent 的四道闸
  • 判据会被视觉压缩绕过,以及对应的反作弊条款
  • 审查者最有价值的时候,是它拒绝回答你的问题

还有一条,我最没底但最想留下来:

一条从来没被违反过的规矩,是废规矩。

因为它要么描述的是 agent 本来就会做的事,要么描述的是还没出现过的场景。而规矩文件一长,agent 遵守它的可靠性就下降 —— 这是有实测支撑的。

所以我留了个动作:跑够几轮之后,用审计日志统计每条规矩被违反过几次,一次都没有的删掉。


现在的状态,以及还没验的

已经验过的:

独立进程自己跑完      一次运行 $3.18 / 30 轮 / 325 秒,人没插手
圆桌不编              一次圆桌 42 处原话引用 / 0 处推测词 / 6 处标「无依据」,并把推荐改了
输入隔离有效          三个看不到彼此的角色独立收敛到同一个问题
圆桌能推翻发起人      证伪了我出题时的三个前提
泛化                  接第二个项目 = 项目配置 52 行,通用规矩一字不改

还没验的:

真改代码那一段        默认禁止,要开需要三个条件同时满足
打断频率              一周被打断几次?几次是真必要的?
                      ⚠️ 调研里明确说:全世界没有一份带实测数据的打断阈值
长期运行的腐化        规矩文件会不会越来越长、看板会不会没人看

最后那三条只能靠时间。等有数据了我再写一篇 —— 那时候才谈得上「这套东西到底有没有用」。