跳转到内容

第 9 节 · 最小 Agent Loop 的 5 个核心点

一句话回答

最小 Agent Loop 主体不超过 50 行,5 个步骤反复转:调 LLM → 看 tool_calls → 跑工具 → 拼回 messages → 回到第一步。

今天最后一节,把第 7 / 8 节的零件拼成一个"能干真活"的最小 Agent。

NOTE

到 2026 年,OpenAI Agents SDK、Claude Agent SDK、Claude Code、Codex 等产品都会帮你封装这层循环。但越是使用框架,越要知道底层发生了什么:模型做决策,程序执行工具,消息历史把两者串起来

核心循环长这样

Agent Loop 五个核心点 + 中央停止条件

python
def run_agent(question: str, max_iters: int = 6) -> str:
    messages = [
        {"role": "system", "content": "你是助手。需要时调用工具。"},
        {"role": "user", "content": question},
    ]

    for turn in range(max_iters):
        # ① 调 LLM
        msg = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=registry.to_schemas(),
            tool_choice="auto",
        ).choices[0].message

        # ② 检查 tool_calls
        if not msg.tool_calls:
            return msg.content                  # ③ 没有 → 结束

        # ④ 执行每个工具,结果以 role=tool 追加
        messages.append(serialize_assistant(msg))
        for tc in msg.tool_calls:
            args = json.loads(tc.function.arguments)
            result = registry.invoke(tc.function.name, args)
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result,
            })
        # ⑤ 进入下一轮

    return "⚠️ 达到最大轮次"

就这 30 行。这是大多数工具型 Agent 的最小骨架。真实 Coding Agent 会在外面再包上工具权限、沙箱、上下文压缩、进度 UI、tracing 和评测——复杂的不是循环本身,是循环周围的工程护栏

5 个核心点逐个拆

核心点 1:每轮都"全量"传 messages

python
client.chat.completions.create(
    messages=messages,    # ← 注意是完整列表
    ...,
)

LLM 是无状态的(Day1 第 2 节讲过)。每一轮你都要把之前所有消息都传回去:

 1 messages: [system, user]
 2 messages: [system, user, assistant(tool_calls), tool]
 3 messages: [system, user, assistant(tool_calls), tool, assistant(tool_calls), tool]
...

这就是为什么 Day4 必须做上下文管理——不然 messages 列表会无限膨胀。

核心点 2:用 tool_calls 当循环的"红绿灯"

这是最小循环里最可靠的退出信号:

python
if not msg.tool_calls:
    return msg.content

不要看 content 里有没有"完成"两个字,优先看 tool_calls 是不是空。通常模型决定不再调工具时,tool_calls 会是空;生产环境还会叠加最大轮次、超时、预算、人工中止等外部刹车。

核心点 3:assistant 消息必须原样追加

python
messages.append({
    "role": "assistant",
    "content": msg.content,                # 通常是 None
    "tool_calls": [
        {
            "id": tc.id,
            "type": "function",
            "function": {
                "name": tc.function.name,
                "arguments": tc.function.arguments,
            },
        }
        for tc in msg.tool_calls
    ],
})

漏了这条,下一轮模型就看不到自己刚刚"决定调什么"——会原地踏步重新决策。这是新手写 Agent Loop 时最常见的错误之一。

核心点 4:tool 消息必须配 tool_call_id

python
{"role": "tool", "tool_call_id": tc.id, "content": result}

tool_call_id 必须和 assistant.tool_calls[*].id 严格一对一,让模型知道这个结果是哪个调用的。一次有多个工具时尤其关键。

核心点 5:永远要有"刹车"

Agent 没刹车就会跑飞

python
for turn in range(max_iters):     # ← 上限是必须的
    ...
return "⚠️ 达到最大轮次"

为什么?三种典型死循环:

  1. 模型卡 bug:返回 tool_call → 工具失败 → 模型再调一遍 → 再失败 → ……
  2. 工具报错没兜底:工具崩了把 exception 抛出来,循环挂了
  3. 模型反复调 tool:自己也意识不到信息已经够了

几道常见刹车:

刹车长什么样
最大轮次for turn in range(max_iters):
超时单次 LLM 调用和工具调用都设置 timeout
Token 预算每轮检查累计上下文,必要时压缩或终止
工具异常兜底try/except 把错误转成字符串塞回 messages
同一工具同样参数连续调用 N 次就停进阶:基于历史去重
人工中止危险动作或长任务允许用户停下

今天先实现最大轮次和工具异常兜底;其他刹车会在后续上下文管理、工具集和集成章节里逐步加上。

2026 生产 Agent 的护栏清单

真实生产环境中,护栏远不止"不死循环"。2026 年的最佳实践还包括:

  • 工具白名单:只允许调用已注册的工具,拒绝模型"编造"的工具名
  • 危险操作确认:写文件、删文件、执行 shell 等行动型工具需要二次确认或 human-in-the-loop
  • 输入/输出 guardrails:调用前检查敏感参数,调用后过滤密钥、PII 或过长结果
  • 可观测日志:每轮循环记录 tool_calls、参数、结果、耗时,方便排查和审计
  • Trajectory evals:不只评最终答案,也评"有没有选对工具、有没有乱重试、有没有越权"

跟 demo_06 的"hand-rolled 版"对比

第 6 节那种"让模型输出 JSON 再 parse"的写法(demo_06 也演示了),跟今天的 tool_calls 写法对比:

维度hand-rolled JSONOpenAI tool_calls
模型输出格式约定写在 system prompt 里协议保证
解析失败经常(模型多说一句就崩)几乎不会
多工具并行自己手写解析协议原生支持
跨厂商迁移每家自己约定概念相同,字段名需要适配

但 hand-rolled 那种写法不是没用——Day3 ReAct 范式会用它来解释"思考-行动-观察"的历史写法,也让你看见为什么 Function Calling 会成为拐点。

2026 年的框架把循环藏到哪里了?

如果你用 OpenAI Agents SDK、Claude Agent SDK 或类似框架,经常看不到上面的 for turn in range(max_iters)。框架会帮你做:

框架能力底层对应本节哪一步
function tool / hosted MCP toolregistry.to_schemas()
built-in agent loop调 LLM → 执行工具 → 塞回结果
guardrails / approvals工具调用前后的权限与安全检查
tracing记录每轮模型调用、工具调用和结果
evals评估整条 trajectory,而不只看最后一句话
sessions / memory保存或压缩 messages

所以,框架不是替代 Agent Loop,而是把这个循环包装得更安全、更可观测、更适合生产。

还有什么不在循环里?

今天我们刻意做的事:

不做的事何时做
上下文压缩(messages 太长怎么办)Day4
Memory(跨会话持久化)Day4
多 Agent 协作 / 思考范式Day3
Coding 专用工具集(read/write/edit/grep)Day5
REPL / 流式输出 / 漂亮的终端 UIDay6
Skills 系统Day7

今天只做循环。这是 Agent 的骨架,先有骨架,剩下 6 天再往上长肉。

动手试试

运行 demo_09_mini_agent.py

bash
cd labs/02-tools-and-agent-loop
python demo_09_mini_agent.py

它会跑 4 个测试任务:

  1. 现在几点? —— 单工具一轮就完
  2. 17 × 23 = ? —— 单工具
  3. 抓 example.com 长度除以 7 —— 串联两个工具,两轮
  4. 2099 年世界杯冠军是谁? —— 没合适工具,模型应该承认不知道

观察 4 号任务里 Agent 的反应:理想情况是承认不知道;常见错误是"硬调"某个工具。这个失败 case 是今天 Checkpoint 必须交的内容之一。

小结

概念一句话理解
Agent Loop 5 步调 LLM → 看 tool_calls → 跑工具 → 拼回 messages → 回到第一步
退出条件最小判据:tool_calls 为空;生产还要叠加轮次、超时、预算和人工中止
必须追加的两条assistant(含 tool_calls) + tool(配 tool_call_id)
必须的刹车最大轮次 + 工具异常兜底;生产还要权限、日志、评测
今天不做上下文管理 / 记忆 / 范式 / Coding 工具集 / UI

理论部分到此结束。下面进 lab:part1 自己实现一个 ToolRegistry,part2 把循环搭起来 + 接 3 个工具。

Released under the MIT License.