🐸 青蛙编程网
首页 / AI / 智能体 Agent / 3. Agent 的循环

3. Agent 的循环:思考—行动—观察

这是什么:Agent 循环 = 「思考 → 行动 → 观察」反复转下去,直到模型不再要求调用工具。

为什么用:上一章的 Agent 只会调一个工具;真实任务常常要连做几步(先查天气,再决定穿什么),单次调用不够。

如何用:给第 2 章的代码套一个 while,加上步数上限和失败兜底,就得到一个能自己往下跑的 Agent。

3.1 循环的骨架

把第 2 章的代码包进一个 while,就是 Agent 循环:

while 还没完成 and 步数没超限:
    模型思考 → 要调工具?
        是 → 执行工具 → 结果写回上下文 → 继续循环
        否 → 输出最终答案,结束

差别只有一处:上一章是「调一次就再问一次」,现在是「一直转到模型不再要工具」。

3.2 手写一个循环

import json
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["LLM_API_KEY"],
    base_url=os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"),
)
CHAT_MODEL = os.environ.get("CHAT_MODEL", "gpt-4o-mini")

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当前天气。城市名用中文。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名,如 '北京'"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_tip",
            "description": "根据天气给出穿衣建议。",
            "parameters": {
                "type": "object",
                "properties": {
                    "weather": {"type": "string", "description": "天气描述"}
                },
                "required": ["weather"]
            }
        }
    },
]

def run_tool(name, args):
    """按名字分发到真正的函数"""
    if name == "get_weather":
        return get_weather(**args)
    if name == "get_tip":
        return get_tip(**args)
    raise ValueError(f"未知工具:{name}")

def get_weather(city: str) -> str:
    fake = {"北京": "晴,26℃", "上海": "多云,29℃"}
    return fake.get(city, f"没查到 {city} 的天气")

def get_tip(weather: str) -> str:
    if "雨" in weather:
        return "记得带伞"
    if "晴" in weather:
        return "穿薄外套就行"
    return "穿件外套吧"

def run_agent(question, max_steps=5):
    """Agent 主循环:思考 → 行动 → 观察,转到模型不再要工具为止"""
    messages = [{"role": "user", "content": question}]

    for step in range(max_steps):
        resp = client.chat.completions.create(
            model=CHAT_MODEL,
            messages=messages,
            tools=TOOLS,
        )
        msg = resp.choices[0].message

        # 模型不再要工具了 → 任务完成
        if not msg.tool_calls:
            return msg.content, messages

        # 模型可能要调多个工具,逐个执行
        messages.append(msg)
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            result = run_tool(call.function.name, args)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": result,
            })
            print(f"[第 {step + 1} 步] 调用 {call.function.name}({args}) → {result}")

    return f"超过 {max_steps} 步仍未完成,放弃。", messages

answer, _ = run_agent("北京天气怎么样?需要穿什么?")
print(answer)

你会看到(示意):

[第 1 步] 调用 get_weather({'city': '北京'}) → 北京:晴,26℃
[第 2 步] 调用 get_tip({'weather': '晴,26℃'}) → 穿薄外套就行
北京现在晴,26℃,穿薄外套就行。

3.3 停止条件:三种「够了」

循环最怕停不下来。三种收尾方式,必须至少有一种兜底:

停止条件 说明 风险
任务完成 模型不再请求工具 模型赖着不结束
步数上限 超过 max_steps 就停 任务还没做完就断了
连续失败 连续 N 次工具报错就停 掩盖了真实错误
failures = 0
for step in range(max_steps):
    ...
    try:
        result = run_tool(call.function.name, args)
        failures = 0          # 成功一次就清零
    except Exception as e:
        failures += 1
        result = f"工具执行失败:{e}"
        if failures >= 3:
            return "工具连续失败,任务中止。", messages

步数上限是最重要的兜底。没有它,一个卡住的循环能把你的 API 额度烧穿。

3.4 ReAct:让它「边想边做」

Agent 的循环本质上是 ReAct(Reasoning + Acting)思路:让模型交替地推理和行动,而不是一次想到底。

思考:我需要知道北京天气才能给建议
行动:调用 get_weather(city="北京")
观察:结果返回「晴,26℃」
思考:现在我知道该建议穿什么了
行动:调用 get_tip(weather="晴,26℃")
观察:结果返回「穿薄外套就行」
思考:信息齐了,可以回答了

好处是每一步都有「真实的外部信息」进账,模型不容易凭空编;坏处是上下文会越来越长(第 9 章解决)。

本节只是把 ReAct 的形状先摆出来。它有正式的名字、论文出处和完整实现方式——下一章专门讲。

3.5 并行调用:一次要多个工具

模型有时会在一次回复里要多个工具(比如「查北京和上海的天气」):

for call in msg.tool_calls:      # ← 注意是复数,循环执行
    args = json.loads(call.function.arguments)
    result = run_tool(call.function.name, args)
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,     # 每个都要对上各自的 id
        "content": result,
    })

每个工具调用都要单独回一条 role: "tool" 消息,且 tool_call_id 必须和对应的 call.id 一一匹配。少回一条,模型就会一直等。

3.6 常见翻车现场

报错 / 现象 原因 修法
一直转停不下来 没有步数上限 加 max_steps
工具结果对不上号 并行调用时 id 搞混 每个 call 单独回一条,用 call.id
模型重复调同一个工具 工具返回值没信息量,它看不懂 让返回值更明确(如带上「已经查过了」)
上下文越来越长 每步都全量追加 压缩历史(第 9 章)
工具报异常直接把 Agent 掀翻 没 try/except 包一层,异常信息回传给模型

3.7 动手改

  1. 给 run_agent 加一个「连续失败 3 次就停」的逻辑,故意让一个工具抛异常试试
  2. 把 max_steps 改成 1,看任务怎么被截断;再改成 10,看它会不会做多余的事
  3. 加一个「查完天气后顺便查空气质量」的工具,体会多步协作

3.8 小结

  • Agent 循环 = while 包住「思考 → 行动 → 观察」
  • 三种停止条件:任务完成、步数上限、连续失败,步数上限是必备兜底
  • ReAct:交替推理与行动,每步都有真实信息进账
  • 并行工具调用时,每个调用都要单独回一条 role: "tool",id 一一对应
  • 工具异常要 catch 住,把错误信息回传给模型而不是让程序崩

下一章:ReAct 模式,这套循环有个正式名字和完整形态 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。