Agent 架构 · 2025年4月9日 · 6 分钟

Function Calling 机制:LLM 如何学会用工具

Function Calling 的本质原理、工具描述优化、并行调用、错误处理与生产级实践。

在 AI Agent 的体系里,Function Calling 是最底层、最关键的能力之一。没有它,LLM 就是个"只会说话的聊天机器人";有了它,LLM 可以查数据库、调 API、发邮件、控制设备。

这篇文章彻底拆解 Function Calling 的原理、设计和生产级实践。

什么是 Function Calling

Function Calling 是让 LLM 输出结构化工具调用指令的能力。注意措辞——不是 LLM 真的"调用"了函数,而是 LLM 输出了一个结构化的指令(通常是一个 JSON),由外部代码来执行实际的调用。

用户说:"帮我查一下北京明天的天气"

LLM → 输出 Function Call:
  {
    "name": "get_weather",
    "arguments": {
      "city": "北京",
      "date": "2026-04-11"
    }
  }

你的代码 → 执行 get_weather("北京", "2026-04-11")
  → 拿到结果后回传给 LLM

LLM → 输出自然语言:
  "北京明天(4月11日)晴,12-24°C,适合出门。"

这个循环就是 Agent 的根基——理解意图 → 调用工具 → 处理结果 → 生成回复

底层原理

Function Calling 不是什么神奇的"Agent 能力",它的本质是两条技术路线的结合:

技术路线一:工具描述注入 System Prompt

当你注册函数时,LLM 提供商会把函数定义翻译成一段描述文本,拼入 System Prompt。

系统提示词(实际内容):
你现在是一个 AI 助手,你可以使用以下工具:
- get_weather(city, date): 查询某城市某日的天气
  参数: city (string, 必填,城市名), date (string, 可选,日期)
- send_email(to, subject, body): 发送邮件
  参数: to (string, 必填), subject (string, 必填), body (string, 必填)

LLM 在理解用户请求时,看到"天气"这个词就关联到 get_weather 工具,看到"发邮件"就关联到 send_email

不同厂商对这个注入的实现方式不同,但本质上都是在 prompt 中描述工具接口。这也是为什么 OpenAI 的 tools 参数本质上等价于在 System Prompt 里写一段工具描述。

技术路线二:结构化输出约束

除了描述,系统还会约束输出格式。LLM 的推理过程会偏向输出符合特定 schema 的 token 序列。

模型在生成 token 时:
  当模型预测到需要调用工具 →
  它生成的不是自然语言,而是:
  {"name": "get_we
    
  后面的 token 被约束为 JSON 结构:
  ather", "arguments": {"city": "北京", ...

这本质上是 guided generation / constrained decoding——模型在生成时,每一步的 token 概率分布被 bias 到符合 JSON schema 的方向。

两路线结合,才有了 Function Calling 的稳定性。单靠 prompt 描述(路线一)不约束输出格式,模型可能会生成格式错误的调用。单靠输出约束(路线二)不给上下文,模型不知道什么时候该调用哪个函数。

工具描述设计

描述的质量直接决定 LLM 能不能正确调用。一个优化前后的对比:

❌ 差的工具描述:
{
  "name": "search",
  "description": "搜索",
  "parameters": {
    "q": { "type": "string" }
  }
}

✅ 好的工具描述:
{
  "name": "search_documents",
  "description": "在知识库中搜索与用户问题相关的文档片段。当用户询问产品信息、技术文档、FAQ 相关内容时使用此工具。",
  "parameters": {
    "query": {
      "type": "string",
      "description": "搜索关键词,应该从用户问题中提取核心概念,通常 2-5 个词。例如用户问'支付接口超时怎么办',提取为'支付接口 超时'"
    },
    "top_k": {
      "type": "integer",
      "description": "返回结果数量,默认为 3",
      "default": 3
    }
  }
}

工具描述设计原则:

原则 1:名字要自解释

search 太笼统,search_documents 好一些,search_knowledge_base 更明确。名字是 LLM 匹配意图的第一线索。

原则 2:描述要说明"什么时候用"

不只是"这个工具做什么",还要说清楚"什么场景下该用它"。这相当于给 LLM 一个分类判断的依据。

原则 3:参数描述要给出提取指南

告诉 LLM 怎么从用户输入中提取参数。上面例子中直接示范了"支付接口超时"→"支付接口 超时"的转换,LLM 看到后提取准确率大幅提升。

并行调用

Function Calling 的一个"隐藏技能"是一次返回多个工具调用

用户说:"北京明天天气怎么样?顺便帮我订两个座位的餐厅"

LLM 一次返回:
  → get_weather("北京", "2026-04-11")
  → search_restaurant("北京", "晚餐")

为什么能一次返回?因为工具之间通常没有数据依赖。天气和餐厅互不依赖,没必要先查天气再查餐厅。让它们并行执行,整体延迟减半。

什么时候不适合并行?

当工具调用之间有数据依赖时:

用户说:"帮我找一下张三的客户信息,然后给他发一封邮件"

错误的做法(一次返回两个调用):
  → lookup_contact("张三")
  → send_email(to: ???)    // 还不知道 email 地址!

正确的做法(分两轮):
  第一轮:lookup_contact("张三") → 拿到 zhang@example.com
  第二轮:send_email("zhang@example.com", ...)

好一些的 Agent 框架会在工具描述里注明"本工具依赖 XX 工具的输出",让 LLM 知道调用顺序。但最可靠的方式还是 Agent Loop 中的多轮交互。

错误处理

Function Calling 的生产环境稳定性和 LLM 本身一样——不完美,需要防御性编程

错误类型 1:参数缺失

LLM 偶尔会漏掉必填参数。

调用 get_weather,但只传了 city,没传 date

防御:所有参数校验走 schema(JSON Schema 的 required 字段),校验失败时把错误信息返回给 LLM 让它补全。

系统返回:
{"error": "缺少必填参数: date。请提供日期,格式为 YYYY-MM-DD"}

LLM 看到后会重新生成完整的调用。这本质上是用 Agent Loop 的重试来处理 LLM 的不完美。

错误类型 2:幻觉工具名

LLM 有时会编造一个不存在的工具名。

{
  "name": "get_weather_data",   // 实际注册的是 "get_weather"
  "arguments": { ... }
}

防御:执行前做白名单校验。不在注册表中的工具直接报错。

错误类型 3:参数值幻觉

用户提到一个 ID,LLM 可能"猜"出一个看似合理但错误的值。

用户:"帮我查一下订单 12345"
LLM 可能调用 get_order("12345")
但这个订单其实不存在

防御:在工具执行后,把执行结果原样返回给 LLM。让 LLM 自己根据结果决定下一步——查到就说查到,查不到就说没有这个订单。

多工具调度

当工具数量增多(超过 10 个),LLM 的调用准确率会明显下降。这时需要做工具分层。

分层设计

┌─────────────────────────┐
│  第一层:路由器           │
│  分析用户意图,分派任务   │
│  ┌───────────────────┐  │
│  │ 工具组 A:数据查询  │  │
│  │  搜索 / 报表 / SQL │  │
│  ├───────────────────┤  │
│  │ 工具组 B:业务操作  │  │
│  │  下单 / 审批 / 发信 │  │
│  ├───────────────────┤  │
│  │ 工具组 C:系统管理  │  │
│  │  配置 / 日志 / 监控 │  │
│  └───────────────────┘  │
└─────────────────────────┘

第一层只输出意图分类和工具组选择,第二层在对应工具组内做具体的 Function Calling。这样可以避免把 50 个工具定义全部塞进 LLM 的上下文窗口。

工具数量的影响

工具数量 调用准确率 延迟
1-5 ~98%
5-15 ~93%
15-30 ~85%
30+ ~75% 高(上下文膨胀)

超过 15 个工具强烈建议分层。这不只是因为准确率下降,还因为每个工具的描述都会占用上下文窗口,30 个工具的描述加起来可能超过 3000 tokens。

Function Calling vs Tool Use vs MCP

这几个概念经常被混用,梳理一下关系:

Function Calling:LLM 输出结构化工具调用的能力
  └─ 底层机制,由 LLM 提供商实现(OpenAI / Anthropic / 等)

Tool Use:在 Agent 中调用工具的完整流程
  └─ 包含:定义工具 → LLM 选择工具 → 执行工具 → 反馈结果
  └─ 基于 Function Calling 能力构建

MCP:工具的标准化接口协议
  └─ 定义工具怎么"注册""被发现"
  └─ 跨平台跨语言的工具描述标准

三者的关系可以类比为:

Function Calling = CPU 指令集
Tool Use = 操作系统
MCP = USB 接口标准

Function Calling 让你能调用工具,Tool Use 是调用的流程和框架,MCP 让工具的描述和执行标准化。

不同厂商的 Function Calling 实现

OpenAI

// Chat Completion API
const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [...],
  tools: [
    {
      type: "function",
      function: {
        name: "get_weather",
        description: "...",
        parameters: { ... }
      }
    }
  ],
  tool_choice: "auto"  // auto / required / none
})

OpenAI 的 Function Calling 最成熟,支持最丰富的参数约束。tool_choice 参数可以控制 LLM 是否必须调用工具——设置为 required 时,每一次请求都返回工具调用,不输出自然语言。

Anthropic

const response = await anthropic.messages.create({
  model: "claude-sonnet-4-20250514",
  messages: [...],
  tools: [
    {
      name: "get_weather",
      description: "...",
      input_schema: { ... }
    }
  ]
})

Anthropic 叫 Tool Use 不叫 Function Calling。参数叫 input_schema 而不是 parameters。支持并行调用,但总体上工具描述的字段比 OpenAI 少一些。一个差异:Anthropic 默认会在输出工具调用前先有一段"思考文本",OpenAI 不会。

DeepSeek

DeepSeek 的 Function Calling 兼容 OpenAI 的格式,可以直接用 OpenAI SDK 调用。在实际项目(比如我的弼马温项目)中使用,稳定性在 V4 版本后明显提升,但偶尔会有参数缺失的情况。

生产级实践清单

设计阶段:
  □ 工具名符合命名规范(动词_名词)
  □ 工具描述包含"什么场景下使用"
  □ 参数描述包含提取指南
  □ 超过 15 个工具考虑分层

开发阶段:
  □ 所有参数有 JSON Schema 校验
  □ 并行调用做好数据依赖检查
  □ 2-3 个关键测试用例覆盖常见调用路径

测试阶段:
  □ 参数缺失测试
  □ 幻觉工具名测试
  □ 并行调用测试
  □ 长时间多轮会话测试

监控阶段:
  □ 记录所有 Function Call 的请求/响应
  □ 跟踪工具调用成功率
  □ 监控平均每次请求的工具调用次数
  □ 记录 LLM 返回了非法工具名的频率

总结

Function Calling 是 AI Agent 的"肢体"——没有它,LLM 只有大脑却无法行动。它看起来只是 LLM 的一个 API 参数,但实际上决定了你的 Agent 能力的上限、稳定性和扩展性。

好的 Function Calling 设计 = 清晰的工具语义 + 严格的参数校验 + 优雅的错误处理 + 适当的分层策略。这些做好了,Agent 的上限取决于你的工具集的广度,而不是 LLM 能力。

继续阅读

评论