Agent 主循环:一个 while 循环的全部秘密
把所有营销词剥掉,Agent 的心脏是这样一段伪代码:
while True:
组装消息(系统提示 + 记忆 + 历史)
chunk = 调用大模型(messages, tools)
历史.append(chunk)
if 没有 tool_call:
return chunk.text # 回合结束
results = 执行工具(chunk.tool_calls)
历史.append(results) # 结果回填,进入下一轮
难的不是写出它,是绕开三个所有人都踩过的坑。
坑一:用 finish_reason 判断结束
直觉做法是看模型返回的 finish_reason 是不是 stop。错。部分模型在 finish_reason 为 stop 时仍然携带 tool_call,甚至有模型返回空字符串的 finish_reason。OpenCode 源码里专门有 hasToolCalls 的判断。正确做法只有一条:看这一轮有没有工具调用,有就继续,没有才算完。
坑二:孤儿 tool_call
模型说”我要调用工具 X”(assistant 消息带 tool_calls),如果进程在工具执行中被杀,会话历史里就留下一个没有结果的调用。下次恢复会话重放历史时,模型端 API 会直接拒绝这段历史——配对断裂。解法:恢复时扫描历史,为每个没有结果的 tool_call 补一条 [interrupted] 占位结果。我在自己的实现里真踩到了这个坑:重启进程杀掉了一个等待审批的回合,第二天 resume 就炸了。
坑三:流式里 text 和 tool_call 交错
SSE 流式输出时,文本增量和工具调用的分片是混在同一条流里的,tool_call 的参数甚至会被拆成多片(按 index 归并)。必须分别累积,流结束后再拼装出完整的工具调用列表。
两个结构性决策
真相源放哪? 答案是”逐条 append 到磁盘 + 内存工作副本”。每产生一条消息立刻写入 jsonl,内存里的列表只是当前回合的工作区。崩溃最多丢半个回合,恢复靠重放文件。Codex 的 rollout、grok 的 session 日志都是这个做法。
工具怎么执行? 按只读性分组:只读工具(读文件、搜索)并行跑,写和执行类(写文件、shell)串行跑,且每个派发前都过权限检查。并行提速,串行保安全,审批提示也不会互相打架。
防失控的两道闸
- max_turns 硬限:循环次数封顶(我设 40),无人值守时防止无限烧钱。
- doom loop 检测:连续 3 次一模一样的工具调用(同名同参)判定为卡死,强制转人工审批。
这些全部实现出来不到 200 行 Python。难的从来不是代码量,是知道这些判断为什么长这样。
- 标题: Agent 主循环:一个 while 循环的全部秘密
- 作者: phenix-fledgling
- 创建于 : 2026-07-28 12:00:00
- 更新于 : 2026-07-29 04:10:43
- 链接: https://blog.xugua.xyz//post/Agent 主循环:一个 while 循环的全部秘密.html
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。