导学阶段-Agent 与 Chatbot 的区别
本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。
让模型做一件它做不到的事:抓取某个网页,报出内容有多少字节,再把这个数字除以 7。只调用一次模型的程序,回答通常落在两种结果里。一种老实承认访问不了网络,请对方把内容贴过来。另一种给出一个很像样的数字,比如 1256 字节,除以 7 约等于 179.43,而这个字节数是编的。
同一个模型,外面加一个循环和两个工具,这道题就能走完:抓网页拿回真实的 1256 字节,调计算器做除法,再把两步结果组织成一句回答。模型没有变强,结构变了。
一条直线与一个循环
Chatbot 的结构是一条直线。消息传进去,回答取回来,函数返回。
1 | resp = client.chat.completions.create( |
Agent 在这条直线上套了一个循环,退出条件只有一个:模型某次返回里没有 tool_calls。
1 | messages = [{"role": "user", "content": question}] |
循环体里没有复杂逻辑,只有三件事:调一次模型,看它要不要工具,把工具的执行结果写回消息列表。能力来自重复:模型每一次被调用,都能看到上一次执行的结果,于是可以接着往下走。
循环把 messages 变成工作记录
Chatbot 的 messages 在开头构造一次,之后不再变化。Agent 的 messages 每轮都在变长,模型的工具请求和工具的执行结果都会追加进去。第二轮调用时,模型看到的已经不只是一句问题,还有”我上一步决定抓哪个网址””那个网址返回了多少字节”这些中间状态。
这也是 Agent 的上下文开销比 Chatbot 高的原因。一个任务调用三五次模型是常态,每次都要重新传输整段历史,越往后越贵。
那道题的实际走向大致如下:
1 | 第 1 轮:模型要求调 fetch_url("https://example.com") |
每一轮都是同一次模型调用,只是输入的 messages 不同。
控制权在外层程序手里
一种常见误解是换一个更强的模型,它就能自己把活干完。模型本身没有”继续做下一步”的机制。它每次被调用只做一件事:根据当前的 messages 给出下一条消息,内容是一段文字,或者一组工具调用请求。要不要再来一轮、工具失败后怎么处理、什么条件下停下来,全部由外层程序决定。
业界给这层程序起过一个形象的名字:Harness,马具。模型是马,Harness 是缰绳和鞍。马再快,没有鞍具也载不了人。
1 | Chatbot: 用户 -> [LLM] -> 用户 |
这解释了另一件事:Agent 的很多问题不在模型侧。同一套提示词,换一批工具、改一下轮次上限、调整错误信息的回填格式,行为会明显不同。后面会讲到的 ReAct、Plan-and-Solve、Reflection,都是在循环体内部做文章,循环本身不动。
多轮对话也跨不过这条分界。带历史记录的 Chatbot 会把之前的对话一起传给模型,看起来消息很多,但每一轮仍然是一次调用、一次回答:用户说一句,程序调一次模型,返回,结束。驱动循环的是人。Agent 的循环在程序内部,模型和工具可以来回走好几趟,中间不需要人插话。程序会不会自己决定再调一次模型,才是判断标准,消息条数不算。
2023 年那条分界线
用提示词约定格式、再从文本里用正则抠出工具名,这条路 2022 年的 ReAct 论文就走过。它的脆弱点在于没有结构化字段:模型多写一句解释、把 Action 换行、参数里带个逗号,解析就会错位。工程上只能靠大量容错分支兜住。
2023 年 6 月,OpenAI 推出 Function Calling,把”要调哪个工具、传什么参数”从自然语言搬进了 assistant 消息里的 tool_calls 字段。解析器不再需要猜测模型的意思。写 Agent 的门槛从”能写几百行容错解析”降到”能写一个循环”,Cursor、Claude Code 这类工具也是在这之后集中出现的。
对搭建 Agent 意味着什么
循环必须有边界。模型可能连续要求调用工具而不给出最终回答,也可能反复调同一个失败的工具。轮次上限、未知工具的兜底、工具异常的捕获,这些不是可选项,缺一个就可能让程序卡死或跑飞。
工具的描述和结果的格式,直接决定任务能不能完成。模型只能看到两样东西:schema 里的 description,以及回填进 messages 的结果字符串。描述含糊,模型就选错工具;结果格式混乱,模型就误判下一步。
下一节把这份消息结构逐字段拆开:tool_calls 里到底有什么,tool 消息凭什么对得上对应的调用。


