Skip to content

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 清单(工具声明) ​

python
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 ​

python
{"role": "assistant", "content": None,
 "tool_calls": [{"id": "call_1",
                "function": {"name": "get_weather",
                             "arguments": '{"city":"深圳"}'}}]}

注意两个细节,都是新手常踩的坑:

  1. content 为空——模型这一轮不输出文字,只输出「我要调工具」的意图。
  2. arguments 是「字符串化的 JSON」,不是真正的 JSON 对象。你必须 json.loads 解析它,而且解析可能失败(模型有时会生成不合法 JSON),失败时要能容错重试。

2.3 第三段:程序执行后,把结果以 role=tool 回传 ​

程序拿到 tool_calls,执行对应函数,然后把结果原样回传,必须带上 tool_call_id 对齐,再调一次模型,它才会基于结果续写出最终回答:

python
{"role": "tool", "tool_call_id": "call_1", "content": "深圳当前 26°C,多云"}

这三段构成一个完整的「调工具」往返。真正跑一个 Agent,就是把这个往返放进一个循环里反复执行——这正是第 3 节要讲的 ReAct。

2.4 怎么写一个「好」工具 ​

模型能不能正确调用工具,一半看模型能力,一半看你描述写得好不好。三条硬规约:

规约反例正例
description 写使用时机「调用天气 API」「用户询问气温/天气/冷暖时调用」
参数 description 举例city: 城市city: 城市中文名,如深圳
enum 收敛取值level: stringlevel: enum["low","medium","high"]

关键认知,务必记住:工具描述不是写给开发者看的注释,是写给模型看的调用手册。 每个字都会进上下文、都影响模型的选择。认真打磨 description、给参数举例、用 enum 收敛取值,这三件小事做对了,模型调用工具的准确率会显著提升——这是投入产出比最高的优化之一。


3. 从 Function Calling 到 Agent:几个关键设计 ​

3.1 ReAct:让思考可观测 ​

最主流的 Agent 工作范式是 ReAct(Reasoning + Acting,推理 + 行动),核心循环是:

text
Thought:我现在该做什么?
Action:调用 xxx 工具,参数 yyy
Observation:工具返回 zzz
Thought:根据结果,下一步……
(循环,直到)
Final Answer:最终回答

把「思考」显式写出来,最大的好处是可观测、可调试。 你(和后续 09 篇的评估系统)能看清 Agent 每一步在想什么、为什么调这个工具、哪一步想错了。没有这个显式的 Thought,模型的黑盒推理你就无从追踪——所以 ReAct 的价值,一半在「会推理」,一半在「推理可见」。

完整的 ReAct 循环,用一段紧凑但完整的代码呈现(这是后面所有 Agent 框架的底层缩影):

python
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 AgentLLM 自主决定下一步探索性任务步骤冗长、成本敏感
Hybrid骨架是工作流,某些节点跑 Agent95% 生产场景—

生产黄金法则:能用 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 三件套)。

Released under the MIT License.