Function Calling 与 Agent 范式
前面两篇,你让 LLM「会说话了」(02 提示词)「懂资料了」(03 RAG)。但它还做不了「事」——不会查数据库、不会调 API、不会操作系统。它就像一个博学的博士,却被关在玻璃房里:什么都懂,却什么都做不了。
这一篇回答一个核心问题:怎么让 LLM 从「会聊天的博士」升级成「能干活的实习生」。 答案的钥匙叫 Function Calling——而它往上再升一层,就是 Agent 范式。读完你会发现,真正考验功力的,不是「让模型会调工具」,而是「让它不乱调、不死循环、不烧钱」。
1. 从「会说话的博士」到「能干活的实习生」
1.1 玻璃房里的博士
LLM 原生状态下,能说不能做。看两个典型场景的对比:
| 用户请求 | 原生 LLM 的尴尬 | 加上 Function Calling 后 |
|---|---|---|
| 「深圳现在几度?」 | 「我不知道实时数据……」(或一本正经地编一个) | 调 get_weather(city=深圳) → 「深圳 26°C」 |
| 「帮我查下这个订单」 | 「我查不了你的数据库」 | 调 query_order(id) → 返回物流信息 |
Function Calling 就是在玻璃房墙上打了一排按钮。 模型知道每个按钮的名字和作用,需要时「按下」——注意,它不是真的去按,而是生成一段结构化 JSON,告诉外面的程序「我要调这个、参数是这些」。程序执行后,把结果递回玻璃房,模型再基于结果说话。
1.2 关键的「分工」:模型生成意图,程序执行动作
这里有一个极其重要的认知,很多人没搞清:
模型做的,从来只是「生成一段要调哪个工具、传什么参数的描述」,真正执行工具动作的,永远是程序。
模型不会真的去查天气、查数据库——它只负责「说」我要查;真的 HTTP 请求、真的数据库查询,是外面那段 Python 代码做的。理解了这一点,你就懂了为什么「工具调用鲁棒性」那么难做(见第 4 节):因为模型「说」出来的参数可能有错,错的参数得由程序去校验、去纠错。
1.3 往上升一层:Agent 的四个零件
把 Function Calling 再往上升一层,就是 Agent 范式:
Agent = 带了一排工具按钮、有记忆、会自己循环思考的实习生。
- 博士(LLM):负责「想下一步按哪个按钮、看结果怎么解读」——这是「推理」。
- 按钮(Tools):外部世界的接口(天气、数据库、HTTP、Shell……)——这是「手」。
- 工作台(Loop):循环驱动——「想一步 → 按按钮 → 看结果 → 再想」,直到交出答案——这是「推进力」。
- 便签本(Memory):短期工作记忆 + 长期经验——这是「记性」。
搞清这个分层,你就懂了业界那句常被引用却很少被解释的话:
Function Calling 是 Agent 的必要条件,但不是充分条件。
光有一排按钮不够——按钮只是「手」。你还需要 Loop 让它「持续做」、Memory 让它「记得住」、错误处理和止损让它「不翻车」。会调工具 ≠ 是个好 Agent,后面这四样,才是从「会调工具的聊天机器人」到「可靠 Agent」之间真正的落差。
2. Function Calling 协议:一次完整往返
Function Calling 在 OpenAI 兼容接口里,是一组固定的三元消息协议。把它完整拆开看,你就摸清了底层的「语言」——后面所有框架(05 篇的 LangChain、LangGraph)做的,都是把这套协议封装得更友好,协议本身不变。
2.1 第一段:请求时带上 tools 清单(工具声明)
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "用户询问气温、天气、冷暖时调用。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市中文名,如:深圳"},
},
"required": ["city"],
},
},
}]这段声明里最重要的,是 description。它会被拼进 prompt、算进 Token,而且是模型决定「调不调这个工具、传什么参数」的唯一依据。这一点在后面的「怎么写好工具」会展开。
2.2 第二段:模型决定调工具,返回 tool_calls
{"role": "assistant", "content": None,
"tool_calls": [{"id": "call_1",
"function": {"name": "get_weather",
"arguments": '{"city":"深圳"}'}}]}注意两个细节,都是新手常踩的坑:
content为空——模型这一轮不输出文字,只输出「我要调工具」的意图。arguments是「字符串化的 JSON」,不是真正的 JSON 对象。你必须json.loads解析它,而且解析可能失败(模型有时会生成不合法 JSON),失败时要能容错重试。
2.3 第三段:程序执行后,把结果以 role=tool 回传
程序拿到 tool_calls,执行对应函数,然后把结果原样回传,必须带上 tool_call_id 对齐,再调一次模型,它才会基于结果续写出最终回答:
{"role": "tool", "tool_call_id": "call_1", "content": "深圳当前 26°C,多云"}这三段构成一个完整的「调工具」往返。真正跑一个 Agent,就是把这个往返放进一个循环里反复执行——这正是第 3 节要讲的 ReAct。
2.4 怎么写一个「好」工具
模型能不能正确调用工具,一半看模型能力,一半看你描述写得好不好。三条硬规约:
| 规约 | 反例 | 正例 |
|---|---|---|
| description 写使用时机 | 「调用天气 API」 | 「用户询问气温/天气/冷暖时调用」 |
| 参数 description 举例 | city: 城市 | city: 城市中文名,如深圳 |
| enum 收敛取值 | level: string | level: enum["low","medium","high"] |
关键认知,务必记住:工具描述不是写给开发者看的注释,是写给模型看的调用手册。 每个字都会进上下文、都影响模型的选择。认真打磨 description、给参数举例、用 enum 收敛取值,这三件小事做对了,模型调用工具的准确率会显著提升——这是投入产出比最高的优化之一。
3. 从 Function Calling 到 Agent:几个关键设计
3.1 ReAct:让思考可观测
最主流的 Agent 工作范式是 ReAct(Reasoning + Acting,推理 + 行动),核心循环是:
Thought:我现在该做什么?
Action:调用 xxx 工具,参数 yyy
Observation:工具返回 zzz
Thought:根据结果,下一步……
(循环,直到)
Final Answer:最终回答把「思考」显式写出来,最大的好处是可观测、可调试。 你(和后续 09 篇的评估系统)能看清 Agent 每一步在想什么、为什么调这个工具、哪一步想错了。没有这个显式的 Thought,模型的黑盒推理你就无从追踪——所以 ReAct 的价值,一半在「会推理」,一半在「推理可见」。
完整的 ReAct 循环,用一段紧凑但完整的代码呈现(这是后面所有 Agent 框架的底层缩影):
import json
def run_agent(messages, tools, max_steps=10):
for step in range(max_steps):
resp = call_llm(messages, tools) # ① 调模型
if not resp.get("tool_calls"): # ② 没有工具调用 → 出最终答案
return resp["content"]
messages.append(resp) # ③ 把 assistant 消息加进历史
for tc in resp["tool_calls"]: # ④ 逐个执行工具
fn = TOOL_REGISTRY[tc["function"]["name"]]
try:
result = fn(**json.loads(tc["function"]["arguments"]))
except Exception as e:
result = f"错误:{e}" # ⑤ 工具出错要给模型反馈,而不是崩掉
messages.append({"role": "tool", "tool_call_id": tc["id"], "content": str(result)})
return "达到最大步数,仍未收敛" # ⑥ 熔断注意第 ⑤ 步:工具执行出错时,把错误信息以 role=tool 回传给模型,让它「知道自己错了、自己重试」,而不是让整个程序崩溃。 这是「工具调用鲁棒性」的核心——把纠错的主动权交还给模型。
3.2 三种工作模式怎么选
Function Calling 之上,按「控制权在谁手里」,有三种工作模式:
| 模式 | 本质 | 适合 | 不适合 |
|---|---|---|---|
| Workflow | 人工写死步骤,LLM 只填内容 | 流程固定 | 意图开放 |
| ReAct Agent | LLM 自主决定下一步 | 探索性任务 | 步骤冗长、成本敏感 |
| Hybrid | 骨架是工作流,某些节点跑 Agent | 95% 生产场景 | — |
生产黄金法则:能用 Workflow 就别用纯 Agent。 纯 Agent 演示很炫,但生产里成本、延迟、可控性都会崩——因为它每一步都要重新「想」,想错了还会绕远。绝大多数场景应该用 Hybrid:流程骨架确定(可预测、可控),只在需要「智能判断」的节点让 Agent 决策一下,且一层即止,不做深层递归(否则又回到「不可控」的老路)。
4. 生产级 Agent 的 5 道难题
把上面的循环搬到生产,这 5 个问题每一个都能搞出事故。它们是「会写 demo」和「能上生产」之间的真实鸿沟:
4.1 工具调用鲁棒性:模型会编造参数
模型可能给错参数类型、拼错参数名、甚至编一个不存在的参数。对策:收到参数后强校验,不通过就把错误以 role=tool 消息回传,让模型自己重试——而不是默默执行错误的、或直接崩溃。上一节代码的第 ⑤ 步就是这个含义。
4.2 幻觉工具调用:调了没注册的工具
模型可能返回一个你压根没在 tools 里注册的工具名(幻觉)。千万不要静默忽略——那会让模型以为工具成功了,继续用虚假结果续写,错上加错。必须显式回传错误:「工具 X 不存在,可用工具是……」,给模型一个自我纠正的机会。
4.3 循环爆炸:Agent 自己把自己搞挂
模型在「调工具 → 不满意 → 再调」里死循环。这是 Agent 最经典的事故,三道保险缺一不可:
- 硬性步数上限:
max_steps = 10,超了就中止。 - 循环检测:最近 3 轮调用完全相同 → 立即中止。
- Token 预算:超预算就按已有信息给降级回答。
关键认知:
max_steps不是性能调参,是熔断器。 超过 10~15 轮还不收敛的任务,99% 是设计问题(工具粒度错、Prompt 不清),不是「再多想几轮就能对」。别指望靠调大max_steps来「救」它——那只会让它烧更多钱、跑更偏。
4.4 成本失控:一次对话烧掉几块钱
Agent 调 15 次工具、塞 8 万 Token 上下文,账单惊人。控制点四个:
- 精简工具描述(每个字都算钱);
- 工具别超 20 个(工具清单本身每次请求都算 Input Token);
- 历史结果摘要化(别把每轮原始结果都堆进去);
- 模型分级路由(简单任务用便宜模型,复杂任务才上旗舰)。
4.5 Memory 压缩:长对话塞爆窗口
Agent 多轮循环后,messages 会膨胀直到塞爆窗口。三种策略,各自适用:
- 滑窗:只留最近 N 轮——简单,但丢历史;
- 摘要:旧轮次压缩成一条——保留大意,丢细节;
- 记忆库:历史入向量库,按需检索回填——最灵活,代价是要搭 RAG。
一个工程真相:几乎所有号称「Agent 有长期记忆」的系统,底层都是 RAG——把记忆当知识来检索。这也是为什么 03 篇 RAG 是 Agent 的必备前置。这个主题会在 08 篇 Memory 专门展开。
5. 最常踩的几个坑
| 现象 | 根因 | 正确姿势 |
|---|---|---|
| 并行调工具却没变快 | 后端 for 循环串行执行了 | 真正的并发执行多个 tool_calls |
| 模型乱调工具 | description 写太抽象 | 改成「使用时机 + 示例」 |
| Agent 无限循环 | 没设步数上限 | max_steps + 循环检测 + Token 预算 |
| 简单任务却烧钱 | 用纯 Agent 跑固定流程 | 改用 Workflow 或 Hybrid |
| 工具出错整个程序崩 | 没把错误回传给模型 | 异常信息以 role=tool 回传,让模型重试 |
6. 记住这一句
Function Calling = 给 LLM 装按钮;Agent = LLM + Tools + Memory + Loop;生产级 Agent 的 95% 功夫,不在让它会调工具,而在让它不乱调、不死循环、不烧钱。
三条硬规则:工具描述是 Prompt 不是注释、max_steps 是熔断器不是调参、能用 Workflow 就别用纯 Agent。
下一篇预告:这些「手搓」的协议拼接、循环、记忆管理,写起来又繁琐又易错——能不能交给框架,让你只写业务逻辑?这就到了 05 · 框架化开发(Python 三件套)。