Agent 主循环:一个 while 循环的全部秘密

Agent 主循环:一个 while 循环的全部秘密

phenix-fledgling Lv5

把所有营销词剥掉,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 进行许可。
评论