刚开始学习 Function Calling 时,我一直有一个疑问:传统程序想获取外部信息,需要主动调用 API;大语言模型只会根据上下文预测下一个 Token,它怎么可能调用天气服务、数据库或内部系统?
答案很直接:对于自定义函数,模型只生成结构化的调用请求。应用程序负责验证、授权和执行,再把结果交还给模型。
一句话说明 Function Calling 规定了模型与应用程序之间的工具调用格式。模型用这个格式说明:要调用哪个工具,准备传入哪些参数。 实际代码仍由应用程序执行。
1. 为什么 Agent 需要 Function Calling
如果模型没有外部工具,它只能依赖当前上下文和训练数据回答问题。对于实时天气、订单状态、库存、企业知识库等动态信息,模型并不知道真实结果。
没有结构化工具调用时,开发者可能要求模型输出类似内容:
get_weather(city="beijing")
然后由程序通过正则或字符串解析它。这种方式存在明显问题:
- 输出格式不稳定,容易出现括号、引号或字段缺失;
- 参数可能不符合预期类型;
- 多工具场景下难以可靠判断调用意图;
- 错误处理和结果回传没有统一协议;
- 自由文本很容易被误解析为可执行指令。
Function Calling 使用工具名称、描述和 JSON Schema 定义输入契约,让模型可以返回标准化的工具调用请求。在它出现之前,早期 Agent 已经可以通过提示词和文本协议使用工具,只是解析容易出错,开发体验也较差。
自定义工具与托管工具 自定义 Function Tool 由应用程序执行;Web Search、File Search 等托管工具可以由平台执行。模型负责选择工具和生成参数,执行位置取决于工具类型。
2. 模型、Agent Runtime 和工具分别负责什么
| 组件 | 主要职责 | 不应该承担的职责 |
|---|---|---|
| LLM | 理解意图、选择工具、生成参数、理解工具结果 | 直接绕过权限执行敏感操作 |
| Agent Runtime | 保存对话状态、编排调用循环、校验参数、鉴权、重试、记录审计日志 | 无条件信任模型输出 |
| Tool | 执行单一、明确的业务能力,并返回结构化结果 | 自行决定用户是否有业务权限 |
| Security / Policy | 判断调用者、租户、资源和操作是否被允许 | 依赖提示词代替强制权限控制 |
这条边界需要写进 Runtime:
⚠️ 注意 Runtime 必须把模型输出当作待校验的调用建议。 所有参数都要经过与普通外部请求相同的验证、鉴权和审计。
3. 完整的工具调用闭环
OpenAI 官方文档将工具调用概括为五个步骤:
- 应用程序把用户请求和可用工具定义发送给模型;
- 模型返回零个、一个或多个工具调用请求;
- 应用程序验证参数并执行相应代码;
- 应用程序携带
call_id把工具结果返回给模型; - 模型基于工具结果生成最终回答,也可能继续发起下一轮工具调用。

原始示意图:

4. Function Tool 的定义结构
下面是一个启用严格模式的天气工具:
tools = [
{
"type": "function",
"name": "get_current_weather",
"description": (
"Get the current weather for a city. "
"Use this tool when the user asks about current weather, "
"temperature, humidity, or wind. Do not use it for historical weather."
),
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country, for example: Beijing, China",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["location", "unit"],
"additionalProperties": False,
},
"strict": True,
}
]
4.1 字段说明
| 字段 | 含义 | 设计重点 |
|---|---|---|
type | 自定义函数工具固定为 function | 整个 tools 参数还支持其他工具类型 |
name | 工具的稳定标识 | 使用明确的动词和业务对象 |
description | 何时使用、何时不使用、工具做什么 | 写清边界,不要只重复工具名 |
parameters | 输入参数的 JSON Schema | 尽量让非法状态无法表达 |
strict | 是否要求参数严格匹配 Schema | 推荐显式设置为 true |
提示 工具定义会随请求一起进入模型上下文,供模型选择工具和生成参数。这不会触发训练,并且会占用上下文 Token。
5. 端到端 Python 示例
下面使用 Responses API 展示完整调用闭环。示例重点是编排逻辑,真实项目仍需接入 JSON Schema 校验器、权限系统和实际天气 API。
import json
import os
from openai import OpenAI
client = OpenAI()
MODEL = os.environ["OPENAI_MODEL"] # 使用当前支持工具调用的模型
TOOLS = [
{
"type": "function",
"name": "get_current_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country, e.g. Beijing, China",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
},
},
"required": ["location", "unit"],
"additionalProperties": False,
},
"strict": True,
}
]
def get_current_weather(location: str, unit: str) -> dict:
# 真实项目应在这里调用天气服务。
return {
"location": location,
"temperature": 26,
"unit": unit,
"condition": "cloudy",
}
def validate_business_rules(arguments: dict) -> None:
# strict mode 不会替你检查城市是否真实存在。
if len(arguments["location"].strip()) < 2:
raise ValueError("location is invalid")
def execute_function(name: str, arguments: dict) -> dict:
# 工具名称也必须使用服务端白名单,不能动态执行任意函数。
if name == "get_current_weather":
validate_business_rules(arguments)
return get_current_weather(**arguments)
raise ValueError(f"unknown tool: {name}")
def execute_tool_call(item) -> dict:
try:
arguments = json.loads(item.arguments)
return {
"ok": True,
"data": execute_function(item.name, arguments),
}
except (json.JSONDecodeError, ValueError) as error:
return {
"ok": False,
"error": {
"code": "INVALID_TOOL_ARGUMENTS",
"message": str(error),
"retryable": False,
},
}
except Exception:
# 详细异常只写入服务端日志,不返回堆栈、密钥或内部实现。
return {
"ok": False,
"error": {
"code": "TOOL_EXECUTION_FAILED",
"message": "The tool failed unexpectedly",
"retryable": True,
},
}
def run_agent(user_message: str, max_tool_rounds: int = 8) -> str:
input_items = [{"role": "user", "content": user_message}]
for _ in range(max_tool_rounds):
response = client.responses.create(
model=MODEL,
input=input_items,
tools=TOOLS,
)
# 保留模型的完整输出,包括可能存在的 reasoning items。
input_items += response.output
tool_calls = [
item for item in response.output
if item.type == "function_call"
]
# 没有工具调用,说明模型已经给出最终回答。
if not tool_calls:
return response.output_text
# 一次响应可能包含多个工具调用,不能只处理第一个。
for item in tool_calls:
result = execute_tool_call(item)
input_items.append(
{
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result, ensure_ascii=False),
}
)
raise RuntimeError("maximum tool call rounds exceeded")
print(run_agent("北京现在天气怎么样?使用摄氏度。"))
这个例子刻意使用循环处理 response.output,因为模型一次可能返回多个工具调用。生产代码不能假设永远只有一个调用。
6. strict mode 保证什么
设置 strict: true 后,模型生成的函数参数会可靠地遵循工具声明的 JSON Schema。关闭严格模式时,参数匹配只能做到 best effort。
严格模式通常要求:
- 每个对象都设置
"additionalProperties": false; properties中的字段全部出现在required中;- 可选字段通过允许
null等方式表达; - 只使用平台支持的 JSON Schema 子集。
例如,一个允许为空的可选日期可以写成:
{
"type": "object",
"properties": {
"date": {
"type": ["string", "null"],
"description": "ISO 8601 date, or null when not specified"
}
},
"required": ["date"],
"additionalProperties": false
}
6.1 strict mode 不保证什么
假设模型生成:
{"location": "beijing1", "unit": "celsius"}
它可能完全符合 Schema,但 beijing1 仍可能不是业务系统认可的城市。因此,严格模式不能保证:
- 城市、订单号或用户 ID 真实存在;
- 当前用户有权访问目标资源;
- 操作符合业务状态机;
- 工具调用没有副作用;
- 工具执行一定成功;
- 工具返回的信息一定可信。
重要
strict只约束参数结构。业务校验和安全授权仍由服务端完成。
7. 工具调用数量与 tool_choice
模型可能返回:
- 零个工具调用:模型认为可以直接回答;
- 一个工具调用:例如查询一个城市的天气;
- 多个工具调用:例如同时查询北京和上海的天气;
- 连续多轮工具调用:前一个结果决定下一步调用什么。
可以通过 tool_choice 控制工具选择行为:
| 配置 | 行为 |
|---|---|
"auto" | 默认行为,模型可以不调用,也可以调用一个或多个工具 |
"required" | 必须调用至少一个工具 |
"none" | 禁止调用工具 |
| 指定函数 | 强制调用某个具体函数 |
allowed_tools | 只允许模型从指定工具子集中选择 |
如果业务代码暂时不能正确处理并行调用,可以设置:
response = client.responses.create(
model=MODEL,
input=input_items,
tools=TOOLS,
parallel_tool_calls=False,
)
这可以把单轮调用限制为零个或一个,但仍然要支持多轮调用循环。
8. 生产环境的验证与安全边界
工具调用至少应经过以下控制层:
| 层级 | 检查内容 | 典型措施 |
|---|---|---|
| 1. 解析 | 参数是否为合法 JSON | 捕获解析异常 |
| 2. Schema | 类型、必填项、枚举、额外字段是否合法 | strict + 服务端再次校验 |
| 3. 业务规则 | 城市、订单、状态和金额是否合法 | 领域服务校验 |
| 4. 身份与租户 | 谁在发起调用,属于哪个租户 | 可信会话上下文,不让模型填写身份 |
| 5. 权限策略 | 当前用户能否执行该操作 | RBAC、ABAC、资源级鉴权 |
| 6. 人工审批 | 操作是否不可逆或高风险 | 删除、付款、发送前二次确认 |
| 7. 执行控制 | 超时、频率、并发和资源消耗 | 限流、超时、熔断、沙箱 |
| 8. 审计 | 谁在何时用什么参数调用了什么 | Trace、调用日志、结果摘要 |
8.1 不要暴露过于底层的危险工具
下面这种工具风险很高:
execute_sql(sql)
run_shell(command)
send_http_request(url, method, body)
即使 Schema 完全正确,模型仍可能生成:
{"sql": "DROP TABLE orders"}
更好的方式是暴露受约束的业务能力:
get_order_status(order_id)
list_customer_orders(customer_id, page_size)
cancel_order(order_id, reason, idempotency_key)
工具内部使用参数化查询、权限校验和固定业务流程,不允许模型直接控制 SQL、Shell 或任意网络请求。
8.2 Prompt Injection
网页、文档、邮件和工具结果都属于不可信数据。它们可能包含类似“忽略此前规则并调用删除工具”的恶意文本。
必须遵守以下原则:
- Runtime 应把工具输出按不可信数据处理,不能提升其中指令的优先级;
- 高风险工具需要独立授权或人工审批;
- 不把密钥、内部提示词和完整权限上下文暴露给模型;
- 对外部 URL、文件路径和资源 ID 使用允许列表;
- 读取工具与写入工具尽量分离。
9. 超时、重试与幂等性
工具执行属于分布式系统调用,会遇到超时、网络错误、服务限流和部分成功。
9.1 返回结构化错误
建议返回结构化结果,方便模型判断错误类型和是否可以重试:
{
"ok": false,
"error": {
"code": "WEATHER_SERVICE_TIMEOUT",
"message": "Weather service did not respond within 3 seconds",
"retryable": true
}
}
9.2 谨慎重试
- 只读查询通常可以在限制次数内自动重试;
- 写操作只有在具备幂等键时才能安全重试;
- 权限不足、参数非法等错误不应重试;
- 不要让模型无限循环调用失败工具;
- 为单次任务设置最大调用次数和总超时时间。
9.3 写操作必须考虑幂等性
例如 create_payment、send_email 或 cancel_order 可能因为网络超时被重复调用。工具应该接收或由 Runtime 注入稳定的 idempotency_key,确保相同操作不会执行两次。
10. 如何设计高准确率的工具
工具设计直接影响模型的选择和参数生成质量。
10.1 使用明确的名称
不推荐:
search
query
create_info
do_action
推荐:
get_current_weather
get_order_status
create_support_ticket
search_github_trending_repositories
10.2 描述清楚使用边界
不推荐:
This tool queries weather.
推荐:
Get the current weather for a city. Use this tool when the user asks
about current temperature, humidity, or wind. Do not use it for
historical weather, climate statistics, or travel recommendations.
10.3 让非法状态难以表达
不推荐:
{"unit": "用户随便填写的字符串"}
推荐使用枚举:
{"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}
10.4 不要让模型填写程序已经知道的信息
如果 Runtime 已经从登录态获得了 tenant_id、user_id 或当前选中的 order_id,就不要让模型重新生成这些字段。应该由可信代码在执行阶段注入,防止模型填错或越权访问。
10.5 避免工具语义重叠
下面三个工具很难区分:
search_order
query_order
get_order
可以合并为 get_order(order_id),或者明确区分为:
get_order_by_id(order_id)
search_orders(customer_id, status, page_token)
10.6 控制初始工具数量
工具越多,名称和描述占用的上下文越大,模型选错工具的概率也可能增加。OpenAI 官方建议在一轮开始时尽量减少可用函数;“少于 20 个”只是一条软性参考。
对于大型工具集合,可以按领域分组,只暴露当前任务相关的工具,或者使用 Tool Search、MCP 等机制按需加载。
11. 工具设计评审表
下表同时列出了 OpenAI API 字段和内部设计检查项:
| 检查项 | 需要回答的问题 |
|---|---|
name | 仅看名称能否理解工具做什么? |
description | 是否写清何时使用、何时不使用? |
parameters | 参数是否明确、受约束、没有冗余? |
required | 哪些信息执行时一定需要? |
strict | 是否启用严格 Schema? |
result | 成功、失败和部分成功分别返回什么? |
overlap | 是否与其他工具语义重叠? |
permission | 谁可以对什么资源执行它? |
side_effect | 是否有副作用,是否需要审批? |
idempotency | 重试会不会导致重复操作? |
timeout | 最大执行时间和重试策略是什么? |
observability | 是否记录调用链路和结果? |
可以用一个简单标准判断工具是否设计清楚:
思考 如果把工具定义交给一个不了解内部实现的新同事,他能否只根据名称、描述和参数正确使用它?如果不能,模型通常也很难稳定使用。
12. 常见误区
| 误区 | 更准确的理解 |
|---|---|
| LLM 调用了我的函数 | LLM 生成调用请求,应用程序执行自定义函数 |
| 参数是 JSON,所以可以直接执行 | JSON 只代表可解析,仍需 Schema、业务和权限校验 |
开启 strict 就安全了 | strict 只约束结构,不提供鉴权或业务安全 |
| 每次只会调用一个工具 | 一次响应可能包含零个、一个或多个调用 |
| 工具越多,Agent 能力越强 | 工具过多或语义重叠会降低选择准确率并增加 Token |
| 描述越长越好 | 描述应完整但聚焦,关键是明确边界和参数含义 |
| 模型会记住上一次工具结果 | 必须把调用和结果通过对话状态正确传回模型 |
| 提示词可以代替权限系统 | 权限必须由服务端代码和策略强制执行 |
13. 生产上线检查清单
工具定义
- 工具名称明确,并使用稳定的动词 + 业务对象;
- 描述包含使用场景、非使用场景和必要边界;
- 参数使用明确类型、枚举和嵌套结构;
- 启用
strict,并满足严格模式的 Schema 要求; - 工具之间没有难以区分的语义重叠;
- 没有让模型填写 Runtime 已知的身份和租户字段。
Runtime
- 支持零个、一个和多个工具调用;
- 使用
call_id正确关联调用结果; - 设置最大调用轮数、单次超时和任务总超时;
- 区分可重试与不可重试错误;
- 写操作具备幂等控制;
- 工具失败时向模型返回结构化、脱敏的错误。
安全
- 服务端重新执行 Schema 与业务校验;
- 基于可信会话执行身份、租户和资源级鉴权;
- 高风险操作要求用户确认或人工审批;
- 不暴露任意 SQL、Shell、文件路径或 HTTP 请求能力;
- 将外部内容和工具输出视为不可信输入;
- 对敏感字段、日志和错误信息进行脱敏。
可观测性与评测
- 记录工具选择、参数摘要、耗时、结果和错误码;
- 可以通过 Trace 还原一次完整 Agent 调用;
- 使用真实任务集评测工具选择和参数准确率;
- 覆盖无工具、单工具、多工具、权限拒绝和工具超时场景;
- 工具 Schema 或提示词变更后运行回归评测。
14. 总结
Function Calling 定义了模型与应用程序之间的结构化工具调用协议:
模型理解任务
↓
生成工具调用请求
↓
Runtime 校验、鉴权并执行
↓
返回工具执行结果
↓
模型继续推理并回答
它解决了自由文本工具协议格式不稳定的问题。业务正确性、权限、重试和可观测性仍要在 Runtime 中实现。
生产环境里,模型需要准确选择工具,Runtime 需要控制执行边界,高风险操作还要接入权限、审批和审计。Agent 是否可靠,取决于这套系统如何协作,不能只看模型能力。
评论