🧠 图解记忆:模型不直接查数据库或调 API——它先决定要不要做,再做就生成结构化调用,应用负责执行并把结果告诉模型。
一句话定义
Function Calling = 让 LLM 输出结构化的 JSON 来描述"调用哪个函数、带什么参数",从而打通大模型与现实世界的操作接口。
完整工作流(面试手撕级)
用户:"帮我查北京今天的天气"
↓
[Step 1] 应用层声明可用函数(tool/function schema)
{
"name": "get_weather",
"description": "查询指定城市的实时天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
↓
[Step 2] 模型收到消息 + tool definitions,判断:
- 是否需要调用工具? → ✅ 需要
- 调用哪个函数? → get_weather
- 参数是什么? → city="北京", unit="celsius"
- 输出:
{
"message": null,
"tool_calls": [
{"id": "call_123", "type": "function",
"function": {"name": "get_weather",
"arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}"}}
]
}
↓
[Step 3] 应用层接收 tool_calls,执行函数调用
result = get_weather(city="北京", unit="celsius")
# 返回: {"temperature": 22, "condition": "晴", "humidity": 45}
↓
[Step 4] 应用将结果回传给模型
assistant_message = model.chat(messages + [{"role": "tool", "content": result_json}])
↓
[Step 5] 模型根据工具结果生成最终自然语言回复
"北京今天晴,气温22°C,湿度45%,体感舒适哦!"为什么重要?(三大理由,面试必答)
| 理由 | 说明 |
|---|---|
| 1. 打破封闭边界 | 模型只能"说"不能"做"——Function Calling 给它接入搜索、数据库、API 的手脚 |
| 2. 结构化交互 | 相比让模型从自由文本中"提取"参数,JSON 结构化输出可靠性大幅优于 Prompt 工程 |
| 3. 动态扩展能力 | 不用重新训练/微调模型就能增加新功能:加一个函数定义 = 多一项能力 |
两种调用模式对比
| 维度 | Autonomous(自动模式) | Chained(链式模式) |
|---|---|---|
| 工作方式 | 模型一次性调用多个工具后直接出答案 | 每调用一个工具后等待结果,再来一轮 |
| 适合场景 | 多个独立查询并行执行 | 需要前一步结果来决定下一步(顺序依赖) |
| 延迟 | 低(并行) | 较高(串行等待) |
| 实现难度 | 简单 | 复杂(需管理对话状态机) |
工程陷阱(面试高频追问)
| 陷阱 | 解决方案 |
|---|---|
| 参数类型漂移 | 模型可能输出字符串而非数字,需加类型转换层 |
| 幻觉参数 | 模型编造不存在的字段名,必须配合 Schema Validation |
| 函数命名不一致 | 不同厂商 API 格式不同(OpenAI vs Anthropic vs OpenRouter),建议抽象一层统一适配 |
| 无限循环 | 模型反复调用同一函数,需设置最大调用次数限制 |
| 权限控制 | 不是所有函数都应暴露给模型——敏感操作(删除/转账)需鉴权层 |
代码示例(生产级 Function Calling 封装)
python
class SafeToolExecutor:
def __init__(self, tools: list[dict]):
self.tools = {t["name"]: t for t in tools}
self.max_calls_per_turn = 5
async def handle_tool_call(self, message: dict) -> list[dict]:
"""处理 tool_calls,安全执行并返回结果"""
results = []
call_count = 0
for tc in message.get("tool_calls", []):
if call_count >= self.max_calls_per_turn:
break
call_count += 1
func_name = tc["function"]["name"]
func_def = self.tools.get(func_name)
if not func_def:
results.append({"role": "tool", "tool_call_id": tc["id"],
"content": f"Error: function '{func_name}' not found"})
continue
try:
args = json.loads(tc["function"]["arguments"])
# 🔒 Schema Validation — 防止参数注入
validated = validate_schema(args, func_def["parameters"])
result = await execute_func_safely(func_name, validated)
results.append({"role": "tool", "tool_call_id": tc["id"], "content": json.dumps(result)})
except Exception as e:
results.append({"role": "tool", "tool_call_id": tc["id"],
"content": f"Execution error: {str(e)}"})
return results面试话术:
"Function Calling 是让模型从'聊天机器人'变成'智能体'的关键桥接技术。它的工作流很清晰:声明工具 schema → 模型判断是否调用 → 生成结构化 JSON → 应用执行 → 结果回传 → 综合回答。核心优势是不需要微调模型就能扩展功能。但工程上有不少坑:参数类型漂移、无限循环、权限控制——我在项目里封装了一个 SafeToolExecutor,做了 Schema Validation、调用次数上限和错误降级。"