Skip to content

Function Calling

🎯 学习目标

  • 说清楚 Function Calling 与「模型直接联网」的本质区别
  • 能编写符合 JSON Schema 的工具定义并在 API 中注册
  • 掌握 tool_calls 消息结构与 tool result 回传格式
  • 设计可维护的工具描述,减少参数幻觉与误调用

引言

Function Calling(也称 Tool Use)是让 LLM 决定要不要调用外部函数生成结构化参数,但真正执行发生在你的后端。这一节我们从零搭一条「查天气」链路,理解为什么「模型不直接执行代码」反而是更安全、更可控的设计。

章节正文

第 1 步:建立心智模型:谁决策、谁执行

想象用户问:「北京明天需要带伞吗?」模型本身没有实时天气数据。Function Calling 的流程是:

  1. 你在请求里注册 get_weather 工具及其参数 Schema
  2. 模型阅读用户问题,输出 tool_calls(工具名 + JSON 参数)
  3. 你的服务器调用真实天气 API
  4. 你把执行结果作为 tool 角色消息塞回对话
  5. 模型基于 Observation 生成自然语言回答

关键点:LLM 只负责选工具和填参数,不负责执行。 这样你可以做权限校验、限流、审计,避免模型「假装查过数据库」。

第 2 步:编写工具定义(JSON Schema)

工具定义的质量直接决定调用成功率。描述要回答三个问题:干什么、何时用、参数含义是什么。

javascript
const tools = [
  {
    type: 'function',
    function: {
      name: 'get_weather',
      description: '查询指定城市未来 24 小时天气预报。用户问天气、穿衣、是否带伞时使用。',
      parameters: {
        type: 'object',
        properties: {
          city: { type: 'string', description: '城市名,如 Beijing' },
          unit: { type: 'string', enum: ['celsius', 'fahrenheit'], description: '温度单位' },
        },
        required: ['city'],
      },
    },
  },
]

实践建议:description 写「触发条件」而不只是功能名;枚举值用 enum 约束;必填字段写进 required,减少模型编造可选参数。

第 3 步:发起带工具的 Chat 请求

以 OpenAI 兼容 API 为例,把 toolstool_choice 一并传入:

javascript
const response = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: '北京明天要带伞吗?' }],
  tools,
  tool_choice: 'auto', // auto | required | none | { type:'function', function:{ name:'...' } }
})

const message = response.choices[0].message
if (message.tool_calls?.length) {
  for (const call of message.tool_calls) {
    const args = JSON.parse(call.function.arguments)
    console.log(call.function.name, args) // get_weather { city: 'Beijing', ... }
  }
}

finish_reason === 'tool_calls' 时,不要直接把 assistant 消息展示给用户——先执行工具,再发起第二轮请求。

第 4 步:执行工具并回传 tool result

执行完成后,消息历史应包含:user → assistant(含 tool_calls)→ tool(每个 call 一条)。

javascript
async function runGetWeather({ city, unit = 'celsius' }) {
  // 调用真实 API 或 mock
  return { city, forecast: '小雨', temp: 18, unit }
}

const toolResults = []
for (const call of message.tool_calls) {
  const args = JSON.parse(call.function.arguments)
  const result = await runGetWeather(args)
  toolResults.push({
    role: 'tool',
    tool_call_id: call.id,
    content: JSON.stringify(result),
  })
}

const final = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [
    { role: 'user', content: '北京明天要带伞吗?' },
    message,
    ...toolResults,
  ],
})
console.log(final.choices[0].message.content)

tool_call_id 必须与 assistant 里的 call 一一对应,否则部分 API 会报错。

第 5 步:并行调用、错误处理与 tool_choice 策略

一次 assistant 消息可包含多个 tool_calls,后端应 Promise.all 并行执行(注意下游 API 限流)。

工具失败时,不要把异常吞掉——把结构化错误回传给模型,让它向用户解释或换策略:

javascript
content: JSON.stringify({ error: true, message: '城市未找到', code: 'CITY_NOT_FOUND' })

tool_choice 策略:

  • auto:模型自行决定(默认)
  • required:强制走工具(实时数据场景)
  • none:禁用工具(纯对话或安全模式)
  • 指定函数名:固定走某一工具(审批流中的固定步骤)

动手练习

  1. 为「查公司通讯录」设计一个 search_employee 工具 Schema,要求支持按姓名或工号查询,并写清何时不应调用该工具。
  2. 实现上述天气示例的完整两轮对话,用 mock 数据代替真实 API。
  3. 故意传入非法 city 参数,观察模型在收到 error tool result 后的回复是否更诚实。
  4. 对比 tool_choice 为 auto 与 required 在同一问题上的行为差异,记录各 3 个测试用例。
  5. 为工具执行添加 5 秒超时,超时后回传 timeout 错误,检查最终用户可见文案是否友好。

常见问题

Q:Function Calling 和 MCP 是什么关系?

Function Calling 是模型 API 层的协议:模型如何表达「我要调某个函数」。MCP 是工具服务层的开放标准:如何把文件、数据库、API 封装成统一可发现的 Tools/Resources。常见做法是 MCP Server 暴露能力,客户端转成模型所需的 tools 数组。

Q:模型返回的参数 JSON 解析失败怎么办?

先做 JSON.parse 容错;失败时将原始 arguments 与 schema 校验错误一并作为 tool result 或 system 提示回传,让模型重试一次。生产环境建议对关键参数做服务端校验,不要盲信模型输出。

Q:一个对话可以连续调用多轮工具吗?

可以。每轮执行完 tool result 后再次请求模型,直到 finish_reasonstop 且没有新的 tool_calls。多轮调用就是 Agent Loop 的基础,下一节 ReAct 会系统讲解循环与终止条件。

本节小结

Function Calling 的核心是「模型决策 + 后端执行 + 结果回传」。把 Schema 写清楚、执行放在受信环境、错误结构化回传,是从 Demo 走向可用工具链的第一步。