导学阶段-最小 Agent Loop
本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。
初学 Agent 时容易把注意力放在工具的定义上:schema 怎么写、参数怎么传、返回值怎么拼。这些确实重要,但把它们串起来跑起来的,是一个三十行上下的循环。模型每轮给出一个决定,程序执行这个决定,把结果交回去,再问一次。Agent 的骨架就是这样。
五步转一圈
下面这段循环依赖前文定义好的 client 与 registry,registry.to_schemas() 返回工具定义,registry.invoke() 按名字执行工具。
1 | def run_agent(question: str, max_iters: int = 6) -> str: |
循环的动作只有五步:调模型、读 tool_calls、执行工具、把结果追加回 messages、进入下一轮。全程没有一步要求模型”输出 JSON 再解析”。
一次响应里返回多个 tool_calls 时,循环逐个执行、逐个追加,再统一进入下一轮。并行发起省下的是一轮往返,执行本身仍然是串行的,所以顺序和耗时都由代码控制。
退出条件只有一个,就是 tool_calls 为空。这个判断不能换成检查 content 里有没有”完成”或者”答案”。协议保证模型不再调用工具时该字段为空,自然语言的措辞没有这种保证,模型完全可能在一句寒暄里带上”完成”两个字。
每轮都要重传全部历史
模型不保存会话。第一轮请求里的 messages 是 [system, user];模型要求调用工具后,列表变成 [system, user, assistant(tool_calls), tool];第二轮再把整个列表发过去。到第十轮,前面十轮的每一条都还在请求体里。
这份历史只增不减,是上下文膨胀的直接来源。对话越长,每轮 token 越多,成本和延迟一起上涨,直到撞上窗口上限。压缩、截断、记忆抽取这些手段,处理的都是这个列表。
温度也在同一个请求里定下来。调试阶段把它压到 0,同一个问题才能跑出可复现的路径;否则两次结果不同,很难判断差别来自代码还是采样。
两条消息,漏一条就出问题
assistant 消息必须带着 tool_calls 原样追加。漏掉它,下一轮模型看不到自己刚刚决定调什么,于是重新决策:要么重复同一个调用,要么换个工具从头试。表现出来就是原地打转,一轮一轮没有进展。
tool 消息必须带 tool_call_id,与 assistant.tool_calls 中的 id 一一对应。模型靠这个字段判断哪条结果属于哪个调用。一次返回多个工具调用时,如果靠顺序去对,只要执行顺序有变就会错位,模型拿到的是张冠李戴的结果。
刹车
循环需要一个上限。没有上限,下面三种情形都会让它一直转下去:模型反复调用一个持续失败的工具;工具抛出异常而循环没有接住,整个程序退出;模型意识不到信息已经足够,继续发起调用。
| 刹车 | 实现方式 |
|---|---|
| 最大轮次 | for 循环加上限,课程示例取 6 |
| 工具异常兜底 | try/except 把错误信息转成字符串,作为 tool 结果塞回去 |
| 重复调用检测 | 记录工具名与参数,连续相同达到阈值就停止 |
异常兜底值得展开一句。工具报错时把错误文本交还给模型,模型往往能读到原因并改参数,比如路径写错就换一个路径。把异常抛到循环外面,模型连补救的机会都没有。
跟”让模型输出 JSON”比
在 tool_calls 出现之前,让模型调用工具的常见做法是在 system prompt 里约定输出格式,再用正则或 JSON 解析器读取。课程里演示过这个写法,也演示了它的失败方式。
| 维度 | prompt 约定 JSON | tool_calls |
|---|---|---|
| 格式来源 | system prompt 里的约定 | 协议字段 |
| 解析失败 | 模型多说一句就可能失败 | 极少 |
| 并行调用 | 自己写解析 | 协议原生支持 |
| 生态 | 各自为政 | OpenAI、Anthropic、Google 通用 |
课程示例里那四个任务正好覆盖四种情况:问时间、算 17 × 23、抓取网页字符长度再除以 7、问 2099 年世界杯冠军。前两个一轮结束,第三个要串两个工具、跑两轮,第四个没有合适工具。第四个任务最能看出循环的质量:理想表现是承认不知道,常见失败是硬挑一个工具调用,把无关结果拼成答案。
这对搭 Agent 意味着什么
循环里刻意没有放的东西同样值得列出来:上下文压缩、跨会话记忆、多 Agent 协作、终端界面。它们各自单独成章,全塞进来只会让三十行变成三百行,而骨架反而看不清。
把循环写对,门槛不在代码量,而在几个容易被省略的细节:退出只看 tool_calls、assistant 与 tool 两条消息都要追加、上限和异常兜底不能省。这些细节决定了 Agent 是稳定地跑完,还是跑几轮后开始重复自己。
真正的复杂度在循环之外,工具集怎么组织、上下文怎么压缩、过程怎么展示,都要在骨架之上继续搭。
循环定型之后,下一个问题在协议边界上:工具从哪来、怎么打包、怎么把私有知识喂进去。下一篇看 Tool Calling 的三个扩展面。


