函数调用(工具使用)原理

大模型只会"说话",不会"做事"。函数调用(Function Calling / Tool Calling)是让模型开口"请你帮我执行某函数"的协议:模型不真正运行代码,而是输出结构化的调用请求,由你的程序去执行并把结果回填给它。这是智能体获得行动能力的第一步。

核心流程:模型提议、程序执行

函数调用循环 一次工具调用的完整循环分四步,任何框架(LangChain、自研循环)都离不开它:

  1. 应用把工具清单随请求发给模型;
  2. 模型判断需要工具时,输出结构化的 tool_calls(含函数名与参数);
  3. 应用解析并真正执行对应函数;
  4. 应用把执行结果以消息形式回填,再次请求模型继续回答。
# 第 1 步:请求时带上工具清单
resp = client.chat.completions.create(
    model="deepseek-chat",      # 模型名示例,可换成 gpt-4o 等兼容型号
    messages=[{"role": "user", "content": "北京现在几度?"}],
    tools=tools,                # 工具清单,见下文
)
# 第 2 步:模型只"提议"调用,不真正联网
print(resp.choices[0].message.tool_calls)
# 输出:[ChatCompletionMessageToolCall(id='call_1', function=Function(name='get_weather', arguments='{"city": "北京"}'))]

tools 数组:把函数翻译给模型

模型看不懂代码,只能读 JSON Schema。每个工具用 name、description、parameters 描述自己,数组整体作为 tools 参数传入:

{
  "name": "get_weather",
  "description": "查询指定城市实时天气;城市用中文名",
  "parameters": {
    "type": "object",
    "properties": {
      "city": { "type": "string", "description": "城市名" }
    },
    "required": ["city"]
  }
}

请求与回填:结果也是普通消息

拿到 tool_calls 后,应用执行函数,然后把 assistant 的调用原样追加进历史,再把结果以 tool 角色消息按 tool_call_id 对应回填:

messages.append(resp.choices[0].message)       # 保留模型那次"提议"
messages.append({
    "role": "tool",
    "tool_call_id": "call_1",                  # 与调用一一对应
    "content": '{"city": "北京", "temp": 22}', # 函数真实返回
})
# 第 4 步:携带结果再次请求,模型据此组织最终回答

并行工具调用

一次响应中模型可输出多个 tool_calls,各自带独立 id;应用并行执行后按 id 逐个回填即可,能明显减少往返次数,但要注意工具间若有依赖则应串行。

与"让模型输出 JSON"的对比

让模型"按 JSON 格式回答"只是提示词约定,格式常飘、字段可能缺失;函数调用则是协议级保证:API 按你声明的 JSON Schema 校验参数,天然结构化、更稳定,还免去手工解析 JSON 的脆弱环节。

各家兼容性说明

OpenAI 定义了这一接口形态,DeepSeek、通义千问等国内模型普遍提供 OpenAI 兼容端点,把 base_url 换成厂商地址即可复用同一套代码;个别厂商字段名(如 tool_choice、并行数量上限)略有差异,接入时以官方文档为准。

小结

函数调用的四步是"声明工具 → 模型提议 → 程序执行 → 结果回填",tools 数组用 JSON Schema 描述函数能力。记住模型永远只提议、不执行,执行权始终在你手里。

笔记加载中…