这是《LLM 应用开发 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Function Calling 让模型输出结构化工具调用请求,由应用执行工具并把结果返回给模型。模型不直接执行代码,真正执行者永远是应用服务。
14.1 基本流程
User
-> LLM
-> tool_call: get_order(order_id)
-> Application validates permission
-> Execute function
-> return tool result
-> LLM final answer
关键点:
- 模型决定“是否调用”和“参数是什么”;
- 应用决定“是否允许”和“如何执行”;
- 工具 schema 是接口契约;
- 工具结果必须可验证;
- 高危工具要审批;
- 调用过程要记录审计。
14.2 工具定义
{
"type": "function",
"function": {
"name": "get_order",
"description": "查询当前用户有权限查看的订单状态",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"pattern": "^[A-Z]{2}-[0-9]{6,12}$"
}
},
"required": ["order_id"],
"additionalProperties": false
}
}
}
description 要写清楚:
- 工具用途;
- 什么时候不该用;
- 参数格式;
- 返回内容;
- 副作用;
- 权限要求。
14.3 调用示例
import json
from openai import OpenAI
client = OpenAI()
tools = [{
"type": "function",
"function": {
"name": "get_order",
"description": "查询当前用户订单",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
},
}]
response = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[{"role": "user", "content": "帮我查订单 A-10001"}],
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
if message.tool_calls:
call = message.tool_calls[0]
args = json.loads(call.function.arguments)
14.4 返回工具结果
messages = [
{"role": "user", "content": "帮我查订单 A-10001"},
message,
{
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps({"status": "shipped", "eta": "2026-08-27"}, ensure_ascii=False),
},
]
final = client.chat.completions.create(
model="gpt-4.1-mini",
messages=messages,
)
工具返回建议:
- 结构化 JSON;
- 只返回必要字段;
- 敏感字段脱敏;
- 明确错误类型;
- 控制大小;
- 带上数据版本或更新时间。
14.5 参数校验
from pydantic import BaseModel, field_validator
class GetOrderArgs(BaseModel):
order_id: str
@field_validator("order_id")
@classmethod
def validate_order_id(cls, value: str):
if not value.startswith("A-"):
raise ValueError("invalid order id")
return value
校验层次:
| 层 | 示例 |
|---|---|
| Schema | 类型、必填、枚举 |
| 格式 | 正则、长度、时间 |
| 业务 | 订单是否存在 |
| 权限 | 是否属于当前用户 |
| 风控 | 金额、频率、状态 |
14.6 权限控制
def execute_tool(name, args, user):
policy = TOOL_POLICY[name]
if name not in user.allowed_tools:
raise PermissionError("tool not allowed")
if policy["scope"] == "own" and args.get("user_id") not in (None, user.id):
raise PermissionError("cross user access")
return policy["handler"](args, user)
权限设计:
- 用户只能查看自己的数据;
- 客服按工单归属授权;
- 管理员工具与普通工具分离;
- 写操作独立审批;
- 工具凭证使用最小权限;
- 服务间调用也认证。
14.7 错误处理
| 错误 | 返回给模型 | 用户看到 |
|---|---|---|
| 参数格式错 | invalid_argument |
换种说法提示 |
| 未登录 | unauthorized |
登录引导 |
| 无权限 | forbidden |
不暴露细节 |
| 数据不存在 | not_found |
查不到提示 |
| 依赖超时 | upstream_timeout |
稍后重试 |
| 频率超限 | rate_limited |
降级提示 |
不要把堆栈、SQL、内部主机名返回给模型。
14.8 工具选择策略
避免一次提供过多工具:
意图路由
-> order tools
-> logistics tools
-> account tools
优化方法:
- 工具按业务域分组;
- 每组数量控制在合理范围;
- 先路由再暴露工具;
- 相似工具描述要区分;
- 删除废弃工具;
- 用评测集测工具选择准确率;
- 对高风险工具强制确认。
14.9 审计日志
{
"trace_id": "t-1001",
"user_id": "u-1",
"tool": "get_order",
"arguments": {"order_id": "A-10001"},
"allowed": true,
"status": "success",
"latency_ms": 45,
"result_summary": "status=shipped"
}
记录参数前要脱敏。工具结果正文可只存摘要或采样。
14.10 常见问题
| 现象 | 处理 |
|---|---|
| 模型不调用工具 | 描述不清、工具太多 |
| 参数编造 | schema 和示例加强 |
| 循环调用 | 最大轮数和重复检测 |
| 调错相似工具 | 区分 description |
| 权限绕过 | 服务端强制校验 |
| 结果太大 | 截断、分页、摘要 |
| 副作用重复 | 幂等键 |
本章小结
Function Calling 的工程重点不是让模型“会调函数”,而是定义清晰工具契约、严格校验参数、在服务端执行权限控制、处理错误并记录审计。模型提出请求,系统拥有最终执行权。
思考题
- 模型输出的工具参数为什么要二次校验?
- 如何避免模型调用越权工具?
- 工具返回大结果时应如何处理?
- 相似工具太多会导致什么问题?
- 哪些工具必须设计成幂等?