深色模式
04 · 手写 Agent 循环
不用框架,100 行看清本质
📚 系列导航:这是《Agent 开发进阶》的第 4 篇(共 24 篇)。上一篇你做完了选型 ADR,这一篇兑现"先裸写懂原理":不用任何框架,手写一个完整的 agent 循环。下一篇:05 · 工具设计。
很多人用框架跑通了 agent,却说不清"模型到底是怎么决定调工具的"。不怪你,框架把最关键的那个循环藏起来了。剥掉所有包装,agent 的本质小得惊人:Agent = LLM + 循环 + 工具。亲手写一遍,以后看框架文档就像看说明书,不再是天书。这一篇只干一件事:带你逐段写出并跑通一个最小但完整的 agent 循环。
看完这一篇,你会拿到:
- 一段能跑双工具任务的最小循环代码
- 三个必处理边界的方案:死循环、工具报错、token 超限
- 一份终端可见的完整"思考-行动-观察"轨迹
- 一个受用终身的调试习惯:打印每轮完整消息
01 最小公式:消息数组是唯一事实源
骨架就四件:消息数组存历史,循环反复调模型——模型要么给最终答案,要么要求调工具;你执行工具、把结果追加回数组,进下一轮。最容易被误解的是消息数组:模型是无状态的,每轮调用它都"失忆",全靠你把完整历史重新递给它。打个比方,消息数组就像会议纪要——与会的模型每次进会议室都是新人,靠读纪要恢复现场,工具结果就是新增的纪要行。落地做法:把消息数组当唯一事实源,想让模型知道什么,就 append 什么。
💡 一句话总结:agent 没有魔法——循环维护一份消息数组,模型每轮读它、你每轮喂它。
02 逐段实现:15 行看全四个零件
伪代码如下(接口以官方文档为准),四个零件全在:消息数组、工具 schema、执行分发、终止条件。
python
messages = [{"role": "user", "content": "上海今天适合穿什么?"}]
tools = [weather_schema, outfit_schema] # schema 下一篇细讲
for turn in range(10): # 边界一:最大轮数熔断
reply = call_llm(messages, tools)
messages.append(reply)
if not reply.tool_calls: # 无工具调用 = 最终答案
print(reply.content)
break
for c in reply.tool_calls: # 执行分发
try:
result = TOOL_MAP[c.name](**c.args)
except Exception as e:
result = f"工具报错:{e}" # 边界二:报错回填
messages.append(tool_result(c.id, str(result)[:2000])) # 边界三:截断预期输出:终端先看到两次工具调用(查天气 → 算穿衣),最后打印一句穿衣建议。
💡 一句话总结:所有框架的"agent 引擎",核心就是这 15 行的豪华包装。
03 三个必处理边界:熔断、回填、预算
最大轮数熔断要写在第一行——我没设熔断时,一个 bug 让 agent 空转一夜烧了几美元。工具报错回填:报错别抛异常中断循环,把错误文本当 observation 还给模型,它下一轮会自己换参数;我早期直接抛异常,agent 每次都死在同一处。token 预算:工具返回先截断,历史过长先压缩(06 篇细讲),别等超限才处理。
💡 一句话总结:熔断保钱包、回填保循环、预算保上下文——三个边界写全才算完整循环。
04 观察习惯:打印每一轮
每轮循环把完整消息数组打出来看,你会亲眼看到模型"思考-行动-观察"的全过程。我第一次打印全量消息才真正理解"历史每轮都重发"意味着什么——调试直觉从这一刻建立。
💡 一句话总结:看得见每一轮,才调得动每一轮——打印消息数组是最便宜的可观测性。
05 动手:跑通双工具任务
任务:照第 02 节骨架实现循环,注册两个工具(get_weather 返回固定气温、suggest_outfit 按气温给建议),提问"上海今天适合穿什么"。
验收标准:终端能看到完整循环轨迹——两次工具调用的参数与返回、最终自然语言答案,且总轮数 ≤4。
两种结果都正常:一次跑通,把打印轨迹的习惯固化下来;模型没按预期调工具(比如跳过天气直接瞎猜),大概率是工具描述含糊——正是下一篇要治的病,带着这个 badcase 去。
06 小结
你现在应该能:手写完整 agent 循环、说清三个边界的处理、用打印轨迹定位任意一轮。
引擎有了,能力上限却不在引擎——在工具。下一篇 05 · 工具设计,把"写给模型看的 API"打磨到位。
📎 实战案例
新手没设最大轮数,一个 bug 让 agent 空转一夜烧了几美元。他手写 100 行循环、第一行就写熔断、把工具报错当 observation 回填后,第一次完整看到「思考-行动-观察」轨迹,调试效率翻倍——框架从此祛魅。
踩坑
- 我没设最大轮数,一个 bug 让 agent 空转一夜烧了几美元——熔断是第一行代码。
- 工具报错直接抛异常,agent 十次里十次死在同一处——错误要回填给模型自己纠错。
- 工具返回没截断,一条 5000 行的结果把单轮 token 顶到上限、循环报错中断——回填前先想预算。