全书导航
大模型之路:从图灵、感知机到 ChatGPT · 卷 4

第 35 章:Agent:当模型开始使用工具

本章问题:语言模型只能"说"——如何让它能"做"?


35.1 被困在对话框里的智能

LLM 有一个奇怪的悖论:你可以问它"如何写一封给老板的邮件要求加薪"——它会给你一封极其完美的、论据充分、语气恰好的邮件。但它不能帮你这封邮件。

它可以告诉你"用 Python 的 requests 库发送 POST 请求"——但它不能帮你这个 API。它被困在对话框里——所有的能力都止于"说"——止步于生成 tokens。

Agent 的目标就是打破这道墙:在对话模型外面加一个执行层——让模型可以调用外部工具和采取行动。


35.2 ReAct:思考——行动——观察——再思考

2022 年,Yao 等人提出了 ReAct(Reasoning + Acting) 模式——把"推理"和"行动"交织在一起,让 LLM 能在多步推理和工具调用之间交替。

ReAct 的循环是这样的:

[思考] 我需要知道今天的日期才能回答"距离下次奥运会还有多少天"。[行动] 调用 get_current_date() → 返回 "2025-07-15"[观察] 下次奥运会是 2028 年 7 月 14 日(洛杉矶)。今天是 2025-07-15。[思考] 我需要计算两个日期之间的天数。[行动] 2028-07-14 减去 2025-07-15 = 1095 天。[回答] 距离 2028 年洛杉矶奥运会还有约 1095 天(约 3 年)。

注意这个过程中的关键点:

  • 模型不是一次性输出答案——它在思考、行动和观察之间交替循环。
  • 每个行动的结果(工具返回的值)被追加到模型的上下文中——模型基于新的信息继续推理。
  • 如果某一步失败了(函数返回空或报错)——模型可以重新思考并尝试其他工具。

Agent 不是一个新的大模型。它是在现有模型上包装了一个运行循环——让模型用自然语言"思考",用函数调用"行动",用外部返回结果"观察"——直到任务完成。


35.3 函数调用——Agent 最基本的操作原语

Agent 使用工具的方式是函数调用(Function Calling)——不是模型实际执行函数,而是模型决定调用哪个函数、用什么参数。运行循环(你的代码)去调这个函数,再把返回值递给模型。

以 OpenAI 兼容的 API 为例,函数定义格式如下:

python
tools = [    {        "type": "function",        "function": {            "name": "get_weather",            "description": "获取指定城市的当前天气",            "parameters": {                "type": "object",                "properties": {                    "city": {                        "type": "string",                        "description": "城市名,如 'Beijing'"                    }                },                "required": ["city"]            }        }    }]

当用户问"北京今天多少度?"——模型不会直接生成答案(它不知道实时天气)。它会生成一个特殊结构的输出:

json
{  "role": "assistant",  "tool_calls": [    {      "function": {"name": "get_weather", "arguments": "{\"city\": \"Beijing\"}"}    }  ]}

你的运行循环拦截这个 tool_call——去真正调用 get_weather("Beijing")——拿到返回值 "北京今天晴,28°C"——把返回值当作用户消息追加到对话中。模型看到返回值后——再生成最终的回答:"北京今天天气晴,气温 28°C。"

关键设计:模型不执行函数——模型只负责决定调哪个函数和传什么参数。 你的代码是模型的"手"——模型说"我想看天气",你的代码去看天气,把结果告诉模型,模型再组织语言。这种分层设计让模型的对话能力与外部的代码执行保持了解耦——模型不会因为一行代码执行失败而崩溃。


35.4 为什么 Agent 经常失败——以及如何让它不那么容易失败

Agent 的主要失败模式有四种:

1. 循环。 模型在"思考——行动——观察"之间来回跳——每次的点子都差不多,但一直不停。Agent 可能在连续 5 步中反复调用同一个工具的不同参数——每次都得不到满意结果却不知退——因为模型不知道"放弃"。解决方法:设硬性的最大步数上限(通常 5-10 步);当连续两次行动的结果相同时,自动终止并给用户返回部分结果。

2. 幻觉工具。 模型凭空编造一个不存在的函数调用——或在没有提供函数时假装调用了函数并编造返回结果。像 GPT-4 和 Claude 3+ 这样的模型在函数调用场景中相对不容易产生这种幻觉——但它们仍偶尔会在参数类型上犯错。解决方案:在 system prompt 中明确强调"如果信息不足,请要求用户提供补充,而不要猜测参数。"

3. 上下文膨胀。 每一步行动的结果都追加到上下文中——几轮后上下文变得极长。模型开始忽略关键信息("lost in the middle"问题——模型更容易关注上下文开头和结尾的内容,中间部分容易被忽略)。解决:在每轮后提炼出关键信息点,丢弃冗余的工具输出。

4. 权限贪婪。 让 Agent 拥有太多工具权限——允许它发邮件、删除文件、在数据库执行任意 SQL——可能无意中被利用。近年已经出现过 Agent 被 prompt injection 攻击后泄露用户个人信息的案例。解决:最小权限原则——只给完成当前任务必需的工具。对于高危工具——要求人类确认后再执行。


35.5 最小代码:一个玩具 Agent 运行循环

python
import jsondef run_agent(model, messages, tools, max_steps=5):    """    一个最小 Agent 运行循环。    model: 支持 function calling 的 LLM(OpenAI 兼容接口)    messages: 对话历史    tools: 可用工具的定义列表    """    step = 0    while step < max_steps:        step += 1        # 调用模型——可能返回文本,也可能返回 tool_call        response = model.chat(messages=messages, tools=tools)        msg = response.choices[0].message        # 情况 1:模型直接回答了——Agent 完成        if msg.content and not msg.tool_calls:            return msg.content        # 情况 2:模型要调工具——执行工具调用        if msg.tool_calls:            messages.append(msg)  # 把 tool_call 消息加入对话            for tc in msg.tool_calls:                fn_name = tc.function.name                fn_args = json.loads(tc.function.arguments)                # ---- 实际执行工具(在你的代码中) ----                if fn_name == "get_weather":                    result = f"{fn_args['city']}今天晴,28°C"                elif fn_name == "calculator":                    result = str(eval(fn_args["expression"]))                else:                    result = f"错误:未知工具 {fn_name}"                # ------------------------------------                # 把工具返回值作为"tool"角色消息加入对话                messages.append({                    "role": "tool",                    "tool_call_id": tc.id,                    "content": result,                })    return "Agent 达到最大步数限制,任务未完成。"# ---- 运行示例 ----msgs = [{"role": "user", "content": "北京今天多少度?"}]answer = run_agent(    model=some_llm,    messages=msgs,    tools=[weather_tool],    max_steps=5,)print(answer)# 输出: "北京今天晴,气温 28°C。"

这个 30 行的循环就是每个 Agent 框架的核心。LangChain、AutoGPT、CrewAI、Semantic Kernel——它们在本质上都是在重复这个模式:提示模型 → 拦截 tool_call → 执行工具 → 追加结果 → 再提示模型 → …… → 最终回答


35.6 多 Agent 协作——当一个 Agent 不够

单体 Agent 的局限在于——一个模型一次只能处理一个推理链。当任务需要多个领域的专业知识并行推进时——例如同时分析财务报表、起草法务条款和整理技术文档——单 Agent 会反复切换上下文,效率低下且容易混乱。

多 Agent 系统让多个模型实例(或同一个模型的不同副本,配以不同的 system prompt 和工具集)分别扮演不同角色——如"研究员"(搜索并摘要信息)、"分析师"(基于研究结果得出结论)、"作者"(将结论撰写成报告)——并通过标准化消息传递(通常是"任务委派→结果返回")进行协作。

实际效果有分歧。多 Agent 在特定任务(如代码审查 + 测试生成、辩论式事实核查)中展现了比单 Agent 更好的质量。但也频繁出现 Over-engineering 的问题——很多所谓的多 Agent 流程本质上只是一个复杂的 prompt chain,引入多个 Agent 带来的额外延迟、成本和协调开销并不总是值得。当前业内的共识是:先用单 Agent 解决;在明确发现单 Agent 有架构性瓶颈后再考虑多 Agent。


35.7 本章地图

text
问题:语言模型只能"说"——如何让它能"做"?核心机制:函数调用(Function Calling)——模型决定调哪个函数、用什么参数,运行循环真正执行函数,结果返回给模型。ReAct 模式:思考 → 行动 → 观察 → 再思考——推理和工具调用交织循环。主要失败模式:循环(设步数上限)、幻觉工具(明确的 system prompt)、上下文膨胀(提炼关键信息)、权限贪婪(最小权限原则)。多 Agent:单体 Agent 有瓶颈时才考虑——多个模型实例各自扮演不同角色,通过消息传递协作。今天:Agent 是 LLM 从"对话引擎"走向"数字员工"的核心范式——ReAct + 函数调用是很多 Agent 框架共享的底层模式。

35.8 本章结语:从信息处理器到行动处理器

Agent 范式把语言模型从一个"回答者"变成了一个"行动者"。它不再只是被问到才回答——它可以主动决定"为了回答这个问题,我需要调用哪些工具,按什么顺序"。

ReAct + 函数调用是 2023 年之后许多 AI 应用的架构基础。从客服机器人的后台工单创建,到代码助手的终端命令执行,到数据分析师的 SQL 执行——很多"会做事的 AI",底层都离不开这个循环。

但 Agent 也让人们更清楚地看到当前 LLM 的一个根本局限:它没有真正的"世界模型"——不能真正操作物理世界,其"行动"受限于代码预先暴露的工具。它可以在已知工具中规划和执行——但不是真正意义上的"自主智能体"。

这引出了更下一层的问题:当模型需要同时理解文字、图像、视频、音频,当它需要对物理世界有更直接的感知——纯粹的语言建模就不够了。

下一章:多模态——模型如何学会"看"。

SECTION §02 · ENGAGE

Discussion

留言区 · GitHub-powered comments via Giscus