Skip to main content

第26章:Function Call——给 LLM 装上手

LLM 天生是"嘴强王者":它能谈天说地,却不知道今天几号;它能分析股市,却无法查到实时报价;它能写代码,却无法真正运行一段程序。语言模型的知识冻结在训练截止日期,它活在一个封闭的符号世界里。

要让 LLM 真正"有用",就必须给它装上手——让它能够调用外部工具、获取实时信息、执行真实动作。这就是 Function Call 的故事。


26.1 最朴素的尝试:提示词驱动工具调用

最初的想法

早期开发者很快发现,LLM 可以被"哄"着描述自己的意图。只要在提示词里告诉模型:"你有以下工具可以使用,当你需要时,请输出 ACTION: tool_name(args) 格式",模型往往真的会照做。

一个典型的早期 prompt 长这样:

你是一个助手,可以使用以下工具:
- search(query): 搜索互联网
- calculator(expr): 计算数学表达式
- weather(city): 查询天气

当你需要使用工具时,输出格式:ACTION: tool_name("参数")
当你得到工具结果时,输出格式:RESULT: ...

用户问题:今天北京天气怎么样?

模型可能输出:

我需要查询北京的天气。
ACTION: weather("北京")

应用层解析这段文本,调用真实的天气 API,再把结果塞回上下文,模型继续生成最终答案。

问题:一切都不稳定

这个方案在 demo 里看起来不错,但在生产环境中问题频出:

格式漂移:模型有时输出 ACTION: weather("北京"),有时输出 [TOOL: weather, args: 北京],有时干脆用中文写"我来调用天气工具查询北京"。同一个模型、同一个 prompt,不同轮次的输出格式可能完全不同。

解析噩梦:开发者不得不写脆弱的正则表达式来提取工具名和参数。参数里稍微有个引号嵌套,或者模型多输出了一个空格,解析就崩了。

生态碎片化:LangChain 用一套格式,AutoGPT 用另一套,ReAct 论文又有自己的 Thought/Action/Observation 三段式。每个框架都在重新发明轮子,工具提供商不知道该支持哪个标准,开发者在不同框架间迁移成本极高。

稳定性0,开发成本\text{稳定性} \approx 0, \quad \text{开发成本} \approx \infty

这不是可持续的路。


26.2 Function Call 的标准化(2023, OpenAI)

核心思想:结构化输出

2023 年 6 月,OpenAI 在 GPT-4 和 GPT-3.5-turbo 中引入了原生的 Function Call 支持。核心思路是:

不要让模型"描述"它想做什么,而是让模型"声明"一个结构化的调用意图,由应用层负责实际执行。

开发者用 JSON Schema 描述函数的签名和参数,模型则输出符合规范的结构化 JSON——而不是随意的自然语言。

函数描述:JSON Schema

以天气查询为例,开发者这样描述工具:

{
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位"
}
},
"required": ["city"]
}
}

这个描述同时服务于两个目的:一是告诉模型"这个工具能做什么、参数是什么类型";二是约束模型输出的 JSON 结构,使其可被机器可靠解析。

完整执行流程

Function Call 的执行是一个多轮交互过程:

┌─────────────────────────────────────────────────────┐
│ Step 1: 用户发送消息 │
│ 用户: "北京今天天气怎么样?" │
└──────────────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│ Step 2: 模型决定调用函数,输出 function_call JSON │
│ { │
│ "function_call": { │
│ "name": "get_weather", │
│ "arguments": "{\"city\": \"北京\"}" │
│ } │
│ } │
└──────────────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│ Step 3: 应用层执行函数,获取真实结果 │
│ result = weather_api.query("北京") │
│ → {"temp": 28, "condition": "晴", "humidity": 45} │
└──────────────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│ Step 4: 把结果放回上下文,模型生成最终回答 │
│ role: "function", name: "get_weather", │
│ content: '{"temp":28,"condition":"晴"}' │
│ │
│ 模型: "北京今天天气晴朗,气温28°C,湿度45%, │
│ 出门可以不带伞。" │
└─────────────────────────────────────────────────────┘

用代码表达这个流程:

import openai
import json

client = openai.OpenAI()

# 定义工具
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
},
"required": ["city"]
}
}
}]

messages = [{"role": "user", "content": "北京今天天气怎么样?"}]

# Step 1 & 2: 模型决定是否调用函数
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)

# Step 3: 检测是否有 function_call
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)

# 执行真实函数
result = get_weather_from_api(args["city"])

# Step 4: 把结果放回上下文
messages.append(response.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})

# 模型生成最终回答
final_response = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
print(final_response.choices[0].message.content)

:::info 关键设计原则 模型只输出调用意图,不直接执行函数。实际执行由应用层完成。这个设计很重要:它保留了开发者对"什么时候真正执行动作"的完全控制权,也让沙盒隔离、权限检查、审计日志等安全措施有了插入点。 :::


26.3 Function Call 能做什么

有了可靠的工具调用机制,LLM 的能力边界被大幅拓展。

实时信息获取

LLM 的训练数据有截止日期,但通过 Function Call 可以突破这一限制:

场景工具示例
实时新闻search_news(query, date_from)
股票行情get_stock_price(symbol)
天气预报get_weather(city, days)
航班状态check_flight(flight_no)

数据库查询

将自然语言转换为结构化查询,让非技术用户也能"对话式"访问数据:

# 工具定义
{
"name": "query_database",
"description": "查询销售数据库",
"parameters": {
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "要执行的 SQL 查询语句"
}
}
}
}

# 用户说:"上个月销售额最高的产品是什么?"
# 模型输出:
# {"name": "query_database", "arguments": {"sql": "SELECT product_name, SUM(amount) as total FROM sales WHERE month = '2026-05' GROUP BY product_name ORDER BY total DESC LIMIT 1"}}

执行真实动作

这是 Function Call 最强大也最需要谨慎的用途——让 LLM 真正改变世界的状态:

发送邮件: send_email(to, subject, body)
创建日历: create_event(title, time, attendees)
操作文件: write_file(path, content)
调用 API: post_to_slack(channel, message)
管理资源: create_k8s_deployment(config)

:::warning 动作不可逆 一旦邮件发出、文件删除、订单提交,就很难撤回。建议对高风险动作加入人工确认步骤(Human-in-the-loop),不要让模型全自动执行不可逆操作。 :::

代码执行

结合沙盒环境,LLM 可以生成代码、执行并验证结果,形成"写-运行-观察-修正"的自主循环:

# 工具:在沙盒中执行 Python 代码
{
"name": "execute_python",
"description": "在安全沙盒中执行 Python 代码并返回输出",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "要执行的 Python 代码"}
}
}
}

模型可以用这个工具来:验证算法正确性、生成并检查图表、处理用户上传的数据文件。


26.4 Function Call 的局限

Function Call 解决了格式不稳定的问题,但随着应用规模增大,新的问题浮现了。

问题一:工具管理成本爆炸

假设你在构建一个企业助手,需要集成 100 个内部工具(HR 系统、财务系统、代码仓库、项目管理……)。每个工具的 JSON Schema 平均 500 token,100 个工具就是 50,000 token 的系统提示。

这带来双重代价:

  • 上下文窗口消耗:即使模型支持 128K context,50K token 的工具定义就占去近 40%
  • 成本爆炸:每次调用都要发送全量工具定义,API 费用随工具数量线性增长
每次调用成本Ntools×avg_schema_tokens\text{每次调用成本} \propto N_{\text{tools}} \times \text{avg\_schema\_tokens}

问题二:各家格式不兼容

OpenAI、Anthropic、Google 各自设计了自己的 Function Call 格式:

提供商字段名格式特点
OpenAItools / tool_callsarguments 为 JSON 字符串
Anthropictools / tool_useinput 为直接 JSON 对象
Google Geminitools / functionCallargs 为直接 JSON 对象

开发者想切换模型提供商?对不起,请重写所有工具集成代码。工具提供商想支持多个平台?请为每个平台维护一份适配器。

问题三:无法动态发现工具

Function Call 要求所有工具在请求发出前全部定义好并附在请求中。模型无法在对话中途"发现"新工具,也无法说"我需要一个还不存在的工具"。

这导致一个根本矛盾:工具集合必须是静态的、提前已知的——但真实世界的工具生态是动态的、分布式的。

问题四:两个独立生态难以协同

工具提供商(Stripe、GitHub、Salesforce……)和 LLM 提供商(OpenAI、Anthropic……)是完全独立的生态系统。

工具提供商 LLM 提供商
────────── ──────────
Stripe API OpenAI GPT-4o
GitHub API ✗ Anthropic Claude
Salesforce CRM Google Gemini
内部企业系统 开源 Llama

没有标准接口,必须为每一对组合写集成代码

每次有新的 LLM 或新的工具,整个集成矩阵就要重新扩充。NN 个 LLM 和 MM 个工具提供商,理论上需要 O(N×M)O(N \times M) 个适配器。

:::tip 类比 这就像早期互联网,每个网站都要为每种浏览器写一套代码。直到 W3C 标准出现,这个问题才得到解决。Function Call 的世界,还在等待它的"W3C 时刻"。 :::


本章小结

概念要点
早期提示词工具调用格式不稳定,解析脆弱,生态碎片化
Function Call 标准化JSON Schema 描述工具,模型输出结构化调用意图
执行流程用户 → 模型决策 → 应用执行 → 结果回填 → 最终回答
核心价值突破知识截止日期,连接实时数据和真实动作
主要局限工具上下文膨胀、格式不兼容、无法动态发现、生态割裂

Function Call 给 LLM 装上了手,但这双手的力量受限于"必须事先告诉模型每一种工具的存在"。现实世界的工具是动态的、分布式的、由无数第三方维护的——我们需要一个协议,让工具和模型能够在运行时相互发现、相互理解。

这就引出了下一章的主角:MCP(Model Context Protocol)——Anthropic 在 2024 年推出的开放标准,试图为 LLM 工具生态建立统一的"USB 接口"。