sourceurl: "https://mp.weixin.qq.com/s/60H9httJacoMWPgbHG6SJg" title: "Agent Loop 什么时候该停?DSH 和 Pi 给了两种答案" account: "架构师" publishedat: "2026-08-30 23:10:36" savedat: "2026-08-31 10:41:07" syncid: "art9c528d7f330f43c494c01599b5ee72cc" parsestatus: "ok"
# Agent Loop 什么时候该停?DSH 和 Pi 给了两种答案
架构师(JiaGouX)
我们都是架构师!架构未来,你来不来?
最近,把 Agent 接进工单系统,让它自己领取任务、自己执行、自己反馈。
比如工单要求升级一个 SDK。代码改完,测试也绿了,Agent 调 API 创建 PR,接口等了三十秒,回了一个 504。
页面上没有 PR。可服务端也许已经创建成功,只是响应没能回来。让 Agent 再试一次,明天可能多出一个一模一样的 PR;就此收工,又可能把真正的失败漏掉。
就算 PR 建好了,CI 还在排队。模型这一轮说“已经完成”,到底算不算完成?
这类小事一进生产环境, while (hasToolCalls) 就不够用了。工具已经返回,模型可能还要再看一次结果;用户在命令执行中途发来新指令;一个并行工具说可以收口,另一个工具却留下了需要模型解释的结果。Agent 回到 idle 后,长期 Goal(目标)还可能把它叫醒。
“结束”其实要在几个不同的地方分别确认。
昨天梳理 Claude Code 的 /loop 时,我把“这一轮继续跑”和“过一会儿再回来”分开了。再看 DeepSeek Harness 和 Pi 的实现,问题又往里挪了一层:一次运行内部,究竟由谁按下停止键?
Pi 的作者说过,代码是最好的文档。
所以,还是得把 DeepSeek Harness 和 Pi 的代码翻一翻。比起循环有多少行,我更想搞清楚:到底谁说了算,什么时候可以接着跑。
# 先把“停”分成几件事
还是拿这个 PR 任务来说,程序会在好几个地方停下来。把代码摊开,大致能看到五层。
模型流结束,只能说明这次请求收到了结束信号。可能是正常结束,也可能是撞到 token 上限、报错,或者被取消。
模型发起的这一批工具都返回了,说明工具调用结束。模型通常还得看一眼结果再决定下一步,所以工具停下来,不等于这一轮也停了。
一个 turn 真正结束,要等工具结果已经交回模型、这一轮没有待处理的回复, next-step 里也没有新消息。用户中途塞进来的 steering,或者工具补进来的上下文,都会把这一轮重新拉起来。
driver activity 结束,才说明当前 Agent 暂时没有下一条 turn,终于可以回到 idle。
长期 Goal 是否结束,要看持久状态有没有变成 complete 、 blocked 或 paused 。Agent 这会儿 idle 了,不代表目标已经完成;Goal Driver 下一次检查时,仍可能调用 followup() 把它叫回来。
平时一路顺利,把它们都写成一个 done 也看不出什么。可一旦遇到 504、取消、进程崩溃或 CI 排队,日志就说不清“到底停在哪一层”。
在 DeepSeek Harness 里,一个 step 大致对应 Pi 的一次 turn;driver activity 大致对应一次 invocation。DSH 还在 activity 外面再包了一层 Goal Round,Pi 的长期工作则通常留给宿主或扩展去安排。

# DSH:继续之前,先看队列里还有什么
DSH 的 driver 入口看起来很短:
while (await this.turn()) {
}
turn() 返回 true ,就再跑一轮;返回 false ,driver 回到 idle。中间抛了异常,也会在 driver 边界记下来,然后回到 idle。
运行时只维护三个 phase: idle 、 maintenance 、 running 。进入 running 后,当前 turn、step、 AbortController 和 wakeRequested 都挂在这里。维护期间虽然对外显示 idle,消息进来时也不会立刻再开一个 driver;系统先记下“需要唤醒”,维护结束再去看 inbox,消息不会凭空消失。
取消也有自己的落点。Agent 正在沙箱里跑 npm test ,用户发来“先停,改查线上日志”。旧 activity 如果已经 abort,这条消息会写进 next-turn ,不会硬塞到正在收尾的旧 turn 里。消息分到当前 step、下一 step 或下一 turn 后,后面的重入事件也不能再把它挪走。
whenIdle() 也不能只等一个 Promise。它会反复对照 activityDone :等旧 activity 的时候,如果新的 driver 已经接上来,就继续等;不然很容易在“旧的刚结束、新的已经启动”的那一瞬间误报 idle。
# 一个 turn 如何收口
DSH 在领取输入前先写 turn/start 。即使 pre-step 拒绝了、系统提示没拼好,或者插件把第一个 step 改成了空消息,最后也会补上一条唯一的 turn/end 。
turn 里有两个状态会影响下一步: turnEnds 记已经出现的结束原因, target 决定下一次 pre-step 去 next-turn 还是 next-step 取消息。普通用户 prompt 会开新 turn;工具上下文和 steering 则留在当前 turn 的下一 step。
从循环的角度看,一个 step 最后只会落到三种结果:
completed:模型没有工具调用,或工具批次明确要求收口;max-tokens:模型输出达到上限;null:工具执行完了,模型还需要再处理一次结果。
null 不是异常,只是告诉循环“工具结果还得交回模型再看一遍”。以开 PR 为例, create_pr 返回 URL 后,CI 还是 pending ,这个结果会回到模型;模型可以选择等一会儿,也可以顺手查构建日志。接口回了 200,turn 仍然不能就此收工。
DSH 会在解析工具之前先处理 max-tokens 。被截断的响应可能只剩半截 JSON;就算解析器勉强补全、schema 也通过了,参数的意思仍可能已经变了。要是写操作在这里被执行,副作用已经落地,回滚不一定来得及。DSH 让这一轮先以 max-tokens 结束,后面要不要恢复,交给更高层决定。
Pi 的取舍不一样: stopReason === "length" 且带工具调用时,它会给工具写一条“参数可能被截断”的错误结果,再交回模型,让模型下一 turn 重新发起调用。只读研究任务这样做,少一次人工介入;换成有写权限的 Agent,我会先确认幂等和审批边界,再决定要不要自动接着跑。
# stopping 不是一个布尔回调
DSH 在 turn 快要结束时触发 agent/turn-stopping 。插件要继续,不是返回一个 true ,而是往 next-step 写一条消息。hook 跑完,核心循环再读一次 inbox;没有消息就停,有消息就开新 step。
这样一来,多个插件一起动作时,不用再合并一堆布尔值。假如每个插件都返回 shouldContinue() ,任意一个 true 算不算数、后执行的插件能不能覆盖前面的结果、明明要继续却没有输入时模型该看什么,都得另外定规则。把消息写进队列,至少留下了“为什么还要继续”的记录,事件日志也能照着重放。
代价也很实在:写插件的人得先弄懂 inbox 的 target 和 turn 边界。想让 Agent 再跑一次,不是拨一下开关,而是构造 UserMessage ,写到对的队列里。小应用可能觉得这有点繁琐;要做恢复、审计,或者让多个插件一起工作时,这些记录往往能省下排查时间。
# 工具能提出收口,队列消息还在
DSH 允许工具结果带 concludesTurn 。同一批工具按 OR 处理:只要有一个已经提交的结果是 true ,这一批就标成 concluded。
这不会打断已经启动的并行工具,也不会清空 next-step 。结果仍按模型调用顺序提交; additionalContexts 和同时到来的 steering 也照样进下一 step。工具最多只能告诉循环“这次不用默认回访模型了”,已经排队的输入还是要处理。
Pi 用的是 AND:只有非空批次里的每个 finalized result 都是 terminate: true ,整批才提前终止。一个工具完成事务、另一个工具返回待分析报告时,Pi 不会让前者替后者做决定。
用 OR 还是 AND,关键看这个字段到底在表达什么。它要是表示“有一个工具发现全局终止条件”,OR 说得通;要是表示“每个工具的结果都不需要模型再处理”,AND 会稳一些。DSH 选 OR,灵活是灵活,但工具契约得把“谁可以 conclude”写清楚。
并行执行还得把顺序捋回来。DSH 让 A、B、C 同时跑,结果先放进各自的 slot,再由 commitReady() 按模型原来的顺序连续提交。C 先回来,也要等 A、B 的 slot 准备好。日志、回放和下一轮上下文就不会被网络时序搅乱。Pi 用 Promise.all 保留输入数组顺序,取舍差不多。
取消时,两边都会停止派发还没开始的调用。DSH 会等已经启动的调用收尾,并给“还没派发”的工具补上成对的 tool/call 和合成错误 tool/result 事件。用户主动取消和 scheduler 自己出错是两种情况,后者不会为了把数组凑齐就假造结果。
# 结束原因要留下来
DSH 的 TurnEndReason 至少把 completed 、 blocked 、 max-tokens 、 aborted 、 error 和 interrupted 分开记。
aborted 表示运行时收到取消,也有机会做清理; interrupted 则是进程突然消失后,恢复层给尚未闭合的 turn 补上的状态。做副作用审计或决定是否重试时,这两个不能混为一谈。
max-tokens 还会保留下来。step 1 撞了上限,插件在 stopping 阶段又塞进 steering,step 2、3 最后完成, turn/end 仍然记着 max-tokens 。只盯最后一个 step,监控就可能把一次截断看成成功,Goal Driver 也可能跟着自动续行。
Pi 主要记最后一次模型调用的 stopReason : stop 、 length 、 toolUse 、 error 、 aborted 。这个字段离 provider 更近;DSH 的 reason 则贴着业务 turn。生产系统通常两类都保留,谁也替不了谁。
# Goal 在外面,idle 不等于完成
一个 Goal Round 大概会这样走:
turn/end(completed)
→ agent/status: idle
→ Goal Driver 读取持久状态
→ followup(goal round)
→ turn/start
Goal Driver 要同时确认几件事:Agent 还有效,没有普通 prompt 正在抢占,Goal phase 还是 active ,activation 处于 armed,Round 也没超限。条件都满足,新的 goal 消息才会放进 next-turn 。
持久化的 phase 和进程里的 activation 是两套状态。phase 可以是 active 、 complete 、 blocked 、 paused ;activation 只有 armed 、 disarmed 。服务重启后,active Goal 默认是 disarmed,不会因为进程刚启动就把几十个历史任务一起叫醒。Round 超限、 max-tokens 、Agent error、flush failure 和 driver failure 都会让它 disarm。状态说不清时,先停一下,通常比盲目重试稳妥。
update_goal(complete) 也不会立刻掐断 turn。它先把持久状态写好,再通过 deferred context 让模型补一段收尾文字,最后 turn 才以 completed 结束。就算收尾时的模型调用失败,Goal 也不会重新变回 active;代价是多走一次模型请求。

# Pi:内层循环停了,外层还可以再问一句
Pi 的 runLoop() 没有把长期 Goal 放进核心。内层循环只管工具回访和 steering:
hasMoreToolCalls || pendingMessages.length > 0
一轮大致会经过 prepareNextTurn 、处理 pending steering、调用模型、执行工具、发出 turnend ,然后由 shouldStopAfterTurn 决定要不要停。内层队列空了,外层再调用 getFollowUpMessages() ;有 follow-up 就放回 pending,没有才发出 agentend 。
还是看开 PR 这件事:模型拿到 PR 地址,工具和 steering 都处理完,内层循环可以先停。宿主如果还想“顺便看一下 CI”,就通过 follow-up 再跑一轮;没有追加,才结束这次 invocation。
Pi 暴露的三个入口也很直接: getSteeringMessages() 、 getFollowUpMessages() 和 shouldStopAfterTurn() 。扩展不用理解持久 inbox,也不用额外维护一套状态投影。另一面是,多个扩展怎么合并 follow-up、进程恢复后怎样找回回调里的临时状态、Goal 和审批要不要共用一套结束语义,都得由宿主来补上。
这两套写法,边界放在不同地方。Pi core 提供一个小而可组合的执行内核,不替你规定长期任务协议;DSH 则把恢复、审计和 Goal 的状态也收进来了。
一轮 PR/CI 检查如何接着跑

# 日志要看完整一段
回到开头那个 504。只翻聊天记录/log,最多看到 Agent 说“创建 PR”;只看最后一条 turn/end ,也不知道请求有没有重试,工具调用到底有没有发出去。
DSH 每次准备模型请求,都会把 provider、model、reasoning effort、系统提示和工具 schema 组成 header,再和 session 里的 baseline 对比。配置变了就记 change ;Goal Round 或显式的新消息序列开始时记 series 。这样一来,工具回访、用户 follow-up 和 Goal Round 不会在日志里挤成同一种请求。
所以,排查这类问题时,我一般先想清楚要回答哪一个问题,抱着问题找答案,再去找事件。模型调用怎么结束,看 assistant message、stream finish 和 usage;turn 有没有闭合,找配对的 turn/start 和 turn/end ;Agent 是不是真的空闲,看 status 或 whenIdle() ;长期任务有没有结束,读 Goal phase。Pi 则对应看 stopReason 、 turnend 和 agentend 。
这些信号各自回答一层问题,最后一条消息替代不了它们。
# “谁还能让它继续”
把两套实现放回同一个 PR 场景,差别会比较直观:
| 场景 | DeepSeek Harness | Pi |
|---|---|---|
| --- | --- | --- |
| 普通工具返回 | null ,欠一次模型回访 | 内层循环继续 |
| 工具宣布收口 | 任一结果 concludesTurn 可标记 concluded,但队列消息仍优先 | 所有 finalized 结果都 terminate: true 才提前结束 |
| 用户中途 steering | 按 inbox target 放入当前或下一 turn | 进入 pending steering |
| 自然结束后的追加工作 | Goal Driver / follow-up 开新 turn | getFollowUpMessages() 驱动外层循环 |
| 进程突然消失 | 恢复层写 interrupted 并闭合开放 turn | 由宿主决定如何恢复 |
DSH 把继续权分散给模型 finish、工具结果、inbox、生命周期 hook 和外围持久状态机,再用事件日志把它们串起来。Pi 把继续权集中在当前循环和几个宿主回调里,应用可以更快接入,也要自己补长期协议。
我在比较 Agent Harness 时,不太会先数 Agent Loop 有多少行,也不会因为某个实现更薄,就先替它下结论。通常先看四个问题:
- 工具返回后,谁来决定模型还要不要再回一次?
- 用户的新输入落在哪个边界,取消之后还能不能追溯?
- Agent idle 以后,谁可以把它叫回来,预算和授权又在哪检查?
- 进程崩溃或请求超时,日志能不能说清哪些副作用已经发生?
如果产品只是一次 prompt 驱动的工具调用,Pi 这样的小核心通常更好维护。等系统开始处理跨轮 Goal、审批、多插件 steering、高风险写操作和跨进程恢复,DSH 这些看起来偏重的层,能把宿主迟早要补的状态和边界先放进来。
Agent Loop 真正棘手的地方,不是让模型再调一次工具。PR 可能已经创建,CI 还在排队,用户又发来新指令——就在这几分钟里,系统得说清楚哪一层停了、谁还能让它继续,以及下一步依据的是什么。
# 参考资料
- DeepSeek Harness architecture 文档
- DeepSeek Harness session 与 TurnEndReason
- DeepSeek Harness persistence 文档
- Pi agent loop 源码
- Pi Agent 类型与 shouldStopAfterTurn
- Addy Osmani:Loop Engineering
- 从 Claude Code 的 Loop 看 Agent 进入生产以后
- Pi 长时 Harness 与恢复设计台账
如喜欢本文,请点击右上角,把文章分享到朋友圈
如有想了解学习的技术点,请留言给若飞安排分享
因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享
·END·
相关阅读:
Loop Engineering,应该赞成还是反对?
Claude 做方案,Codex 写代码:多模型协作怎么交接才稳
架构排熵:Loop Engineering 的持续清理系统
Claude Code 27 条实用技巧,快速升级
我终于搞明白了:Claude Code 为什么会忽略指令了
Loop 工程实战:从任务循环到可维护闭环
CLAUDE.md 拆解:Agent 进仓库前的上下文入口
Claude、Codex、Mira 都在讲 Loop,架构师更该看什么
如何用 Claude Code 搭建自己的 AI 学习系统
Anthropic CEO 核心访谈:AI时代,企业、职场与治理
Loop详解:从ReAct到Loop Engineering,Agent到底在循环什么
Harness工程还没唱罢,Environment工程已然登场
设计Self-Harness架构:会自我改进的Harness
Fable 5 的信号:Agent 开始拼 Runtime
Anthropic工程师:我们日常如何使用Claude Code
版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
架构师
我们都是架构师!

关注架构师(JiaGouX),添加“星标”
获取每天技术干货,一起成为牛逼架构师
技术群请加若飞:1321113940 进架构师群
投稿、合作、版权等邮箱:admin@137x.com
原文链接:https://mp.weixin.qq.com/s/60H9httJacoMWPgbHG6SJg