Skip to content

04 · 手写 Agent 循环

不用框架,100 行看清本质

📚 系列导航:这是《Agent 开发进阶》的第 4 篇(共 24 篇)。上一篇你做完了选型 ADR,这一篇兑现"先裸写懂原理":不用任何框架,手写一个完整的 agent 循环。下一篇:05 · 工具设计

很多人用框架跑通了 agent,却说不清"模型到底是怎么决定调工具的"。不怪你,框架把最关键的那个循环藏起来了。剥掉所有包装,agent 的本质小得惊人:Agent = LLM + 循环 + 工具。亲手写一遍,以后看框架文档就像看说明书,不再是天书。这一篇只干一件事:带你逐段写出并跑通一个最小但完整的 agent 循环。

看完这一篇,你会拿到:

  • 一段能跑双工具任务的最小循环代码
  • 三个必处理边界的方案:死循环、工具报错、token 超限
  • 一份终端可见的完整"思考-行动-观察"轨迹
  • 一个受用终身的调试习惯:打印每轮完整消息

01 最小公式:消息数组是唯一事实源

骨架就四件:消息数组存历史,循环反复调模型——模型要么给最终答案,要么要求调工具;你执行工具、把结果追加回数组,进下一轮。最容易被误解的是消息数组:模型是无状态的,每轮调用它都"失忆",全靠你把完整历史重新递给它。打个比方,消息数组就像会议纪要——与会的模型每次进会议室都是新人,靠读纪要恢复现场,工具结果就是新增的纪要行。落地做法:把消息数组当唯一事实源,想让模型知道什么,就 append 什么。

手写 Agent 循环:LLM+循环+工具

💡 一句话总结: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 顶到上限、循环报错中断——回填前先想预算。