同样是调
GPT或者Claude,网页聊天框里它只能回你一段话;放进Codex或者Claude Code里,它能自己翻文件、改代码、跑测试,折腾二十分钟把活干完。差别不在模型,在模型外面那圈代码——一段
while循环。
本文收录于微信公众号:愤怒的代码 AngryCode
0x00、Agent 和聊天框,差的就是一个循环
先看一个说法:AI Agent 就是一段会重复执行的代码。
它干的事可以用一句话说完:把当前状况发给大模型,执行模型选中的工具,把结果塞回去,再发一遍,一直转到任务做完。
Think(想)→ Act(做)→ Observe(看)→ 重复,直到目标达成。
这段代码有个正式名字,叫 Agent Loop,也叫 agentic loop。它跑一圈叫一个 step,或者叫一个 turn。
删掉循环,同一个模型就退化成只会一问一答的聊天框;加上循环,它就成了能追着一个目标跑几十步的 Agent。模型没变,工具没变,变的只是外面这圈壳。
0x01、模型思考,循环执行
大模型输出的不是动作,是描述动作的一段文本:想调用 search_flights,参数是出发地、目的地、日期。它没有手,没有网络,没有文件系统。真正去调接口、真正去读文件、真正把结果拿回来的,是外面这圈循环。
模型出主意,循环干活。所以超时、重试、并发、限流这些脏活,全归循环管,跟模型无关。
那为什么非要有循环,不一次问完拉倒?两个理由:
第一,一次调用干不完多步任务。
你让它“找明天深圳飞北京最便宜的 3 个直飞航班”,它没法凭空回答。得查实时航班、读返回结果、筛掉中转、再按价格排一遍。每一步都依赖上一步的结果,而模型一次只能想一步。循环让它想一步、看一步、再想一步。
第二,模型是无状态的。
它不记得上一次调用发生过什么。每次请求,它都是从零开始读你递过去的那段文本,读完就忘。
那它怎么知道自己已经干到第几步了?
靠循环每轮把完整历史重发一遍:最初的目标、模型每次选中的工具、每个工具返回的结果,一条不落。这份不断变长的历史,就是 Agent 的短期记忆。
把模型想成一个每天换班的人:接班时,他对上一班干过什么毫无印象,必须把交接班记录从头读一遍,才知道活干到哪了。那本记录就是 messages,每轮从头读一遍的动作就是循环重发历史。记录越写越厚,读起来越慢——后面要讲的上下文溢出,根子就埋在这。
0x02、Think-Act-Observe
把任意一轮拆开,里面永远是这三个环节。
| 环节 | 谁在做 | 干了什么 |
|---|---|---|
| Think(想) | 模型 | 读目标、指令、工具清单、全部历史,决定“调哪个工具、传什么参”,或者“我干完了,这是答案” |
| Act(做) | 循环 | 真的执行那个工具:搜网页、查库、调接口、跑命令 |
| Observe(看) | 循环 | 把返回值记成一条观察记录(observation),追加进历史,下一轮整段再喂回去 |
三个环节走完,历史长了一截,模型比上一轮多知道一点,于是下一轮能做出更准的判断。几十轮下来,Agent 一点点逼近目标。
这套流程有个更出名的名字:ReAct(Reason and Act,推理与行动)。ReAct 是这套循环最流行的实现范式,但循环本身比任何范式都大——所有 Agent 骨架,拆到底都是这三个环节。
有个细节影响很大,却常被略过:observation 的写法,直接决定下一轮能不能做对决策。 同一个工具,把原始 JSON 整坨塞回去,和提炼成“航班 AI-123,6500 元,直飞”,模型下一轮的表现不一样。现在不少 Agent 的瓶颈不在模型,在工具返回值的可读性。
0x03、查询航班过程
用户给的目标:“查明天深圳飞北京的直飞,给我最便宜的 3 个。”
- 循环把目标、指令、历史发给模型。
- 模型想一下:得先查航班。于是输出一个工具调用:
search_flights(深圳, 北京, 明天)。 - 循环把这条调用执行掉。
- 工具返回一堆航班,循环把它记成 observation,追加进历史。
- 模型读这批结果,发现里面混着经停航班,于是决定:再调一次,按直飞过滤。
- 循环执行,记结果。
- 模型再读,发现过滤后只剩 2 个直飞,凑不够 3 个,于是换一个晚些的时段再查。
- 循环执行,记结果。
- 模型确认目标达成,输出最终答案:3 个航班号、时间、价格。
整个过程里,没有任何一步是提前写死的。循环不知道自己要转几圈,也不知道会用到哪几个工具——这是模型边跑边决定的。循环只负责把这三个环节转下去。写死流程的那叫工作流,不叫 Agent。
0x04、Agent Loop 代码实现
把上面这套东西落成代码,只要 25 行左右。call_llm 换成任何一家模型都行,call_tool 换成自己的工具集:
async def agent_loop(user_message, tools, max_steps=10):
messages = [{"role": "user", "content": user_message}]
step = 0
while True:
# Guard:安全阀,撞上限就退出
if step >= max_steps:
return "Reached step limit without completing the task."
step += 1
# Think:模型思考,问模型下一步干什么
response = await call_llm(messages, tools)
# Done:模型说干完了,收工
if response.is_done:
return response.final_answer
# Act + Observe:跑模型挑选的工具,把结果记录到历史中
for tool_call in response.tool_calls:
result = await call_tool(tool_call.name, tool_call.arguments) # 执行工具
# 把工具执行结果添加到历史记录中
messages.append({
"role": "tool",
"name": tool_call.name,
"content": str(result),
})
逐段看:
while True:循环本体,Agent 的运行时。max_steps:硬性上限,防止它转到天荒地老。这是整套代码里最重要的一道保险。call_llm(messages, tools):把全量历史和工具清单一起递过去。await的意义在于这段时间不占着进程——同一个进程可以同时服务别的会话。if response.is_done:模型主动喊停,直接返回答案,跳出循环。for tool_call in response.tool_calls:执行工具,把结果 append 进messages,下一轮跟着历史一起回去。
最后这点是整段代码的重心:messages 是这个函数里唯一活着的东西。它从用户那句话开始,每轮长一截。长期记忆、并行调用、重试、日志,都是在它外面加的装饰,内核就这 25 行。
0x05、并行工具调用
上面演示的是一轮一个工具,真实场景往往不是这样。
现代模型能在一轮里吐出多个工具调用。要查三个城市的天气,它不必查一个等一次,可以一次发三个。循环收到后并行执行,三份结果一起记回历史。
这就是代码里写 for tool_call in response.tool_calls 的原因——它是个列表,可能只有一条,也可能有五条。
循环的形状一点没变:模型决策、工具执行、观察回流。变的只是单轮能携带的动作数。把互相独立的工具并行跑,是给 Agent 提速最直接的手段,而且不需要改动循环结构。
同一套循环也被用在动作根本不是接口调用的地方——比如点屏幕、敲键盘的 computer-use Agent。工具换成了“点这里”“输这段”,三个环节不变。
0x06、循环结束
一个停不下来的循环就是灾难。停止条件只有两类,两类都得有。
自然停止:模型说完了。 某轮它读完历史,判断目标达成,直接给最终答案而不是再点工具。循环看到这个信号就退出。这是我们想要的那种结束。
安全停止:步数上限。 如果模型永远不说“我干完了”,它就会一直点工具。所以循环自己数着轮次,撞到 max_steps 就强制退出。
区别很关键:自然停止的判断来自模型,安全停止的判断来自你的代码。 只靠前者,等于把刹车交给一个可能判断失误的司机。
生产环境通常还会再叠几层:时间预算(跑太久就停)、成本预算(token 或钱花超了就停)、人工中断(让人随时能喊停)。
如果 Agent 三天两头撞上步数上限,问题一般不在循环,在指令和工具——它没能靠自己找到终点。上限是安全网,不该当成正常出口。
0x07、四种翻车方式
循环本身很简单,但它的失败方式很固定。认清这四种,比背下循环结构有用得多。
| 失败模式 | 现象 | 修法 |
|---|---|---|
| 无限循环 | 一直点工具,永远不给最终答案,时间和钱一起烧 | 硬性步数上限,这道保险不能省 |
| 卡在同一个动作 | 同样的工具、同样的参数反复调,结果一样,毫无进展 | 检测重复调用,回灌一句“这条你已经试过了,换个思路”;多数时候改指令也能解决 |
| 上下文溢出 | 历史只增不减,几十轮后超出 context window(模型一次能读的最大文本量) | 摘要早期轮次,或丢掉过期的 observation,让最有用的信息留在窗口里 |
| 过早停止 | 任务没做完就交答案,比无限循环更隐蔽 | 把“什么算完成”写清楚,越具体越好 |
这四条里,第三条最容易被低估。它不报错,只是从某天开始,Agent 突然忘了十分钟前自己干过什么——因为那些轮次已经被摘要掉了。
生产级 Agent 的工程量,基本都花在这四种失败上。 顺风顺水跑几轮就完事的路径,是所有情况里最简单的一条。
0x08、总结
同一个模型,在聊天框里只能一问一答,在编辑器里能自己干二十分钟的活。模型没变,工具没变,变的只是外面那圈壳。
那圈壳是 25 行代码:一个 while、一次模型调用、一个停止判断、一段把工具结果塞回历史的代码。
模型提供智能,循环提供记忆和方向。智能可以靠换模型升级,记忆和方向只能靠写对这段循环。这是理解 Agent 的入场券:Agent 不是更聪明的模型,是一个能反复使用同一个模型的装置。
本文收录于微信公众号:愤怒的代码 AngryCode