第 34 章:从 DAG 到 Agent Loop
"建个图不就行了"——行,直到下一个节点取决于一件你必须先跑完上一个节点才知道的事。
⏱️ 快速通道(5 分钟掌握核心)
- 事前画得出来的控制流用图;依赖观察的控制流用循环
- 选错的信号:所有节点都是生产环境先给了个意外之后才补上去的
- 生产系统最终都收敛成混合结构:确定性外壳 + 自适应内核 + 确定性退出
- 长驻 Daemon 不等于长驻 Loop——Loop 每个 Turn 重建一次
- 连续性来自持久化 Session + 显式历史快照,从来不是对象生命周期
10 分钟路径:34.1 → 34.4-34.5 → Kocoro OSS Lab
公开源码快照:实现细节取自 Kocoro 公开仓库
origin/main的4ec6772提交,复核日期为 2026-07-27。章节讲的是不变量;具体常量应以当前源码为准。
34.1 从一张停不下来的图说起
某个团队用 DAG 做竞品调研流水线。第一版五个节点,很漂亮:
fetch_sources → extract_claims → cross_check → synthesize → format
然后它上了生产。
有的来源返回付费墙而不是正文——加一个节点。有的 PDF 其实是扫描件——加一条 OCR 分支。某家供应商改了字段名——加一个归一化,带条件判断。两个来源互相矛盾,综合器很自信地取了平均,产出一个两边都没说过的结论——再加一个仲裁节点。半年后,这张图四十多个节点,其中大约三分之二是在处理上线第一天根本列不出来的状态。
有意思的不是节点数量,是节点是什么时候加的。每一个都是生产环境先给了个意外,才补上去的。这就是「控制流依赖于事前拿不到的观察」的签名。
反方向的错误更安静,也更贵。另一个团队把固定的夜间 ETL——五步、顺序固定、成功条件固定,两年没变过——包进 Agent Loop,理由是「更灵活」。于是每跑一次要花十二次模型调用,去重新发现一条从没变过的流水线;而模型状态一飘,某个原本必然执行的步骤就被跳过了。
两个团队做的是同一件事的两个方向:还没问「控制流事前可不可知」,就先选了执行模型。
34.2 这两样东西各自是干什么的
DAG 值得它那份复杂度,需要几个条件:你能在不运行任何东西的前提下命名每个节点;边画出来就不再变;每个节点的成功可以确定性地校验;以及——这条最容易被忘掉——你需要「图」这个形状本身。回放、审计、按阶段归集成本、可证明的并行安全,全都来自事前声明的拓扑。
最后这条被严重低估。如果合规审查必须在执行之前看到将要执行什么,循环给不了,图能给。第 13、14 章讲的就是这套机制,它们一样也没过时。
循环值回成本,只有一种情形:下一步动作取决于一个必须先动手才能拿到的观察。 你画不出「读这个文件」的出边,因为它通向哪里取决于文件里写了什么。你能列举下一步的种类,但列不出顺序,也列不出次数——把它塞进图里,就意味着为每种可能的观察加一条分支。上面那条调研流水线,正是这样长到四十个节点的。
所以循环拿策略换掉了列举:观察、决策、行动,直到某个终止条件为真。分支由模型在运行时给出,而不是由作者在设计时给出。交易内容就这些:你用每轮一次模型调用买到适应性,代价是失去事前知道执行形状的能力。
34.3 生产系统最终收敛成的形状
真实系统几乎没有纯粹属于某一边的。能扛过生产的形状是:确定性外壳包住自适应内核。
确定性外壳
→ 认证并识别来源
→ 解析路由,加载或创建 Session
→ 构建 Tool Registry,解析权限
→ 快照对话历史
┌──────────────────────────────────────────┐
│ 自适应内核 │
│ 观察 → 决策 → 行动 → 重复 │
└──────────────────────────────────────────┘
→ 确定性完成校验
→ 持久化终态,发出回复
框外的一切都事前可知,所以它应该是图、是函数、是一条直线——就是不该是模型调用。框内的一切都取决于模型刚刚看到了什么。
判据很直白:如果你能写出一个 if 把它判对,就别花一次模型调用。 每往框外挪一个确定性检查,你就在此后的每一次运行里、永久地少付一轮迭代。
34.4 长驻 Daemon 不是长驻 Loop
这里是大多数心智模型悄悄出错的地方,而它影响 Part 10 后面的全部内容。
一个跑了几周的 Daemon,并不会把一个 Loop 对象也留活几周。在公开的 Kocoro Runtime 里,RunAgent 是每请求入口,它为每个请求构造一个全新的 Loop,然后才调用 Run:
// internal/daemon/runner.go —— 每个请求构造一次
loop := agent.NewAgentLoop(deps.GW, reg, runCfg.ModelTier, deps.ShannonDir, ...)
...
result, usage, runErr = loop.Run(ctx, prompt, resolvedContent, history)
Loop 是 per-turn 对象,长驻的是 Daemon。把这两者混为一谈,你会在崩溃恢复、内存增长和状态隔离上推出错误结论——而这三件事你错不起。
那么,如果 Loop 每轮都重建,是什么让第 40 轮记得第 1 轮?两个机制,都是刻意设计的。
Session 在 Loop 运行之前写盘,而不只是结束之后。源码里的理由值得原样引用,因为它是「可持久 Agent」的全部基础:
Persist session to disk before
loop.Run()so there's a record even if the daemon crashes mid-execution. The final save after completion is still needed to capture the assistant's reply.
运行中途崩溃,你手上剩的是一条记录,不是一个洞。第 40 章的可恢复 Turn 就直接建在这个性质上。
历史以快照形式传入,不作为 Loop 状态持有。而且快照取在新用户消息追加之前——否则模型会收到同一条消息两次,一次作为 prompt,一次夹在 history 里。快照同时剥掉上一轮循环注入的 guardrail 提示,这样上一轮那句「你好像在重复」就不会泄漏进这一轮的对话。
第二个细节容易忽略,做错的代价很大。循环注入的系统消息只属于注入它的那一轮。把它们持久化进历史,此后每一轮都会继承一批二十轮前就不再适用的纠正。
所以,连续性是持久化 Session 加显式快照。从来不是对象生命周期。
34.5 快照证据
| 观察 | 4ec6772 源码位置 |
|---|---|
RunAgent 是每请求入口 | runner.go L1898 |
| 每个请求构造新的 Loop | runner.go L2732 |
历史传入 Run,不由它持有 | runner.go L3176 |
| Session 不只在执行后保存,也在执行前落盘 | runner.go L2576 |
| 委派是 Tool Call,不是图的边 | cloud_delegate.go L59 |
| 迭代上限是可配置的兜底 | config.go L421 |
以上描述的是一个有日期的实现快照,不是普遍契约。
34.6 什么时候该留住图
控制流固定的时候,用图,或者干脆用普通代码——夜间 ETL 不需要每晚重新发现自己。必须事前声明执行内容的时候,用图——受监管的流水线要的是运行前的形状,不是运行后的轨迹。需要可证明的并行安全的时候,用图——图能让你证明两条分支永远不碰同一个资源,而循环只能在运行时判断,这也正是第 44 章必须逐次调用做并发安全分类的原因。
还有一种:工作很便宜而模型调用不便宜。一个确定性地花一毫秒的步骤,换成一次花一秒多外加零点几分钱的模型调用,得先撑得起这三个数量级的延迟溢价。通常撑不起。
另外有个被低估的运维理由:「节点 7 失败」是一条你能处置的告警,「Agent 在第 23 轮放弃了」是一个研究课题。 如果失败必须能归因到阶段,图给你的东西是循环在结构上给不了的。
34.7 常见的坑
把迭代上限当控制流。 公开默认值是 40,从 25 提上来的,因为「重构 12 个文件」「批处理 20 个附件」这类真实任务经常需要更多。它是失控兜底,不是计划。如果你的循环靠撞上限来终止,那你没有终止条件,你有的是一个装扮成终止条件的超时。
把确定性校验放进循环。 Schema 校验、权限解析、路径归一化,一个都不需要模型,却全都按轮次付钱。
以为 Loop 对象跨轮携带状态。 它不会。在 Loop 上挂缓存指望它活到下一条消息,它每次都是空的。挂到 Session 上去。
让模型宣布完成。 「我已完成任务」是一个声明,不是后置条件。只要存在确定性检查——文件写了没、测试绿没、行数对不对——就用检查退出,别用那句话退出。
在循环里重建外壳。 Tool Registry、权限集合、工作目录按 Turn 解析。按迭代解析,你会重复付钱,还会让同一个 Turn 的各轮之间出现漂移。
Kocoro OSS Lab(10 分钟上手)
- 打开
runner.go,找出从RunAgent入口到loop.Run之间执行的每一行。那份清单就是确定性外壳——注意其中有多少完全不碰模型。 - 找到解释「为什么历史快照取在用户消息追加之前」的注释。写下:如果取在之后,会坏在哪里。
- 拿一条你自己的流水线,逐步标注:事前可知,还是依赖观察?如果没有一步依赖观察,你就不需要循环。
划重点
- 已知控制流用图,依赖观察的控制流用循环。 还没问清自己手上是哪一种就先选,是根上的错。
- 看节点是什么时候加的。 只在生产给了意外之后才出现的节点,就是你需要循环的信号。
- 确定性外壳,自适应内核。 一个
if能判对的事,不配拿到一次模型调用。 - Daemon 长驻,Loop 是 per-turn 的。 Part 10 后面的一切都取决于这条有没有搞对。
- 连续性是你持久化下来的状态,不是你留活的对象。 Session 在执行前写盘,历史以快照传入。