第4章:Function Calling——让大模型从「聊天者」变成「执行者」

第4章:Function Calling——让大模型从「聊天者」变成「执行者」

系列导读:从本章开始,我们进入 LLM 的「行动能力」阶段。如果你还没看第1章,建议先了解 LLM 的本质,再去理解为什么它能「调用工具」是真正的事半功倍。


一、Function Calling 到底是什么?

LLM 本身是一个文本生成器,它生成了天气查询请求,但它不能自己去调用 API。

Function Calling(工具调用)就是解决这个问题的机制:让 LLM 输出结构化的工具调用指令(通常是 JSON 格式),你的代码负责解析并执行真实的调用。

核心原则:模型只判断「该做什么」,代码负责「真正把它做了」。

二、Function Calling 的两轮对话流程

用户提问 → LLM 判断需要调工具 → 输出 tool_call JSON →
代码解析 JSON → 真正调用 API → 获得结果 →
把结果塞回对话 → LLM 生成最终答案

这是最基础的流程,实际可能有多次工具调用循环。

具体例子

Round 1 - 用户输入:

“北京今天的天气怎么样?”

Round 1 - LLM 响应(不是答案,是工具调用指令):

{
  "tool_calls": [{
    "id": "call_abc123",
    "type": "function",
    "function": {
      "name": "get_weather",
      "arguments": "{\"city\": \"北京\", \"date\": \"2026-07-21\"}"
    }
  }]
}

代码层 - 解析并执行:

tool_call = response.tool_calls[0]
if tool_call.function.name == "get_weather":
    args = json.loads(tool_call.function.arguments)
    result = get_weather(args["city"], args["date"])

Round 2 - 把结果塞回对话:

User: 北京今天的天气怎么样?
Assistant: [tool_call: get_weather(city="北京", date="2026-07-21")]
Function: {"temperature": 32, "condition": "晴", "humidity": "65%"}
Assistant: 北京今天晴天,气温 32℃,湿度 65%,非常适合户外活动!

三、Schema 定义:关键工程细节

Function Calling 的核心是 schema 的定义。schema 告诉模型每个工具:

  • name:工具名称,越具体越好
  • description:工具的详细用途,模型靠这个决定要不要调它
  • parameters:参数结构,包含每个参数名、类型、描述、是否必填
  • required:必填参数列表

Schema 示例

{
  "name": "get_weather",
  "description": "获取指定城市和日期的天气信息,包括温度、天气状况、湿度。支持中国境内城市。",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "要查询天气的城市名称,例如:北京、上海、广州"
      },
      "date": {
        "type": "string",
        "description": "查询日期,格式为 YYYY-MM-DD"
      }
    },
    "required": ["city", "date"]
  }
}

description 是最关键的字段。模型完全靠 description 来判断该不该调这个工具。写 description 的技巧:

  1. 明确边界:不是什么都调,只在什么场景下调
  2. 给出例子:比如 “当用户问 xx 时调用”
  3. 避免触发词陷阱:description里别用模型的名字(如 “ChatGPT”),否则模型可能误触发

四、Function Calling 和「土办法」的区别

在没有 Function Calling 之前,开发者只能靠解析自然语言文本来判断模型要不要调工具:

# 土办法
response = model.chat("帮我查北京天气")
if "天气" in response:
    # 手动解析,极其脆弱
    city = extract_city(response)  # 容易出错

Function Calling 以后:

# Function Calling 方式
response = model.chat("帮我查北京天气", tools=[weather_tool_schema])
if response.tool_calls:
    # 模型输出的是结构化 JSON,解析安全得多
    tool_call = response.tool_calls[0]

关键区别:结构化输出 vs 自然语言解析。结构化输出准确率接近100%,自然语言解析经常抽风。

五、高级特性

1. 并行工具调用

模型可能在一次响应中输出多个 tool_calls,比如用户问"北京和上海的天气":

{
  "tool_calls": [
    {"function": {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}},
    {"function": {"name": "get_weather", "arguments": "{\"city\": \"上海\"}"}}
  ]
}

你的代码可以并行执行两个调用,然后再把结果一起塞回去。

2. 强制使用工具

有些场景要求模型必须调工具,不提供直接回答:

response = model.chat(query, tools=tools, tool_choice="required")
# 设置 tool_choice 为 "required" 强制模型输出 tool_calls

3. 多轮工具调用链

复杂任务可能需要多次调用:查天气 → 根据温度推荐衣服 → 查找推荐店铺的地址 → 查询交通路线。

每一轮把结果塞回 history,LLM 自动判断下一步该做什么。这就是 Agent 的基础。

六、常见坑

解决方案
模型选错工具 description 写清楚边界 + 用 tool_choice 限制
参数类型错误 schema 中严格定义 enum/number/string
函数结果太长塞爆 context 摘要压缩后返回
工具调用死循环 max_iterations 限制 + 超时熔断
结果格式不一致 在 function result 中也标准化输出格式

七、本章小结

  • Function Calling = 模型决策 + 代码执行,两者严格分离
  • Schema description 是核心,决定了模型是否选对工具、传对参数
  • 并行调用、强制调用、多轮链式调用是高级用法
  • 它是通往 Agent 的必经之路

思考题

  1. 为什么 Function Calling 要求模型输出的是 JSON schema 而不是自由文本?这背后的设计哲学是什么?
  2. 如果模型的 tool_calls 返回的参数含危险操作(如删除文件),你的拦截策略应该放在哪一层?