Function Calling实战:让大模型调用外部工具
── 定义工具(用 JSON Schema 描述) ──tools = ["description": "获取指定城市的当前天气,包括温度、天气状况和湿度","city": {"description": "城市名称,如 北京、上海"},},"description": "执行数学计算,支持 +、-、*、/ 和括号","description": "数学表达式,如 '23 * 47 + 15 *
Function Calling:让 AI 学会用工具
LLM 再聪明也只是「脑子」,没有手和脚。Function Calling 就是给 AI 装上手脚——让它能查天气、发邮件、操作数据库。这是 Agent 开发最核心的一课。
一、LLM 的先天缺陷
LLM 训练数据有截止日期,它不知道今天几号、你的数据库里有什么、你的 API 能不能用。直接问它:
问:帮我查一下北京现在的天气
答:抱歉,我无法获取实时天气信息。建议您查看天气预报网站...
但如果你告诉它「有一个 get_weather 函数可以用」,它就能输出一个结构化的调用请求,由你的程序去真正执行。
二、Function Calling 的工作原理
┌──────────┐ ① 用户提问 + 工具描述 ┌──────────┐
│ │ ──────────────────────────→ │ │
│ 你的程序 │ │ LLM │
│ │ ←────────────────────────── │ │
└──────────┘ ② LLM 返回:应该调哪个工具 └──────────┘
│ 调什么参数
│
③ 真的去执行
│
▼
┌──────────┐ ④ 执行结果 → LLM ┌──────────┐
│ 天气 API │ ──────────────────────────→ │ LLM │
│ 数据库 │ │ │
│ 文件系统 │ ←────────────────────────── │ │
└──────────┘ ⑤ LLM 用自然语言回复用户 └──────────┘
LLM 不执行工具,它只是说「应该调哪个工具、传什么参数」。真正执行的是你的代码。
三、完整实战代码
3.1 定义工具
import json
import httpx
from openai import AsyncOpenAI
client = AsyncOpenAI(
api_key="your-api-key",
base_url="https://api.deepseek.com/v1"
)
# ── 定义工具(用 JSON Schema 描述) ──
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气,包括温度、天气状况和湿度",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 北京、上海"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "执行数学计算,支持 +、-、*、/ 和括号",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,如 '23 * 47 + 15 * 8'"
}
},
"required": ["expression"]
}
}
}
]
3.2 工具的实际实现
async def get_weather(city: str) -> str:
"""模拟天气查询(生产环境替换为真实 API)"""
# 实际项目用 httpx 调天气 API
# response = await httpx.AsyncClient().get(f"https://api.weather.com?city={city}")
weather_db = {
"北京": "晴,25°C,湿度 40%",
"上海": "多云,28°C,湿度 65%",
"深圳": "阵雨,30°C,湿度 80%",
}
return weather_db.get(city, f"未找到 {city} 的天气数据")
def calculate(expression: str) -> str:
"""安全执行数学计算"""
# ⚠️ 生产环境不要用 eval!这里仅作示例
import ast
import operator
allowed_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"计算错误:{e}"
3.3 Agent 主循环
import asyncio
# 工具名 → 实际函数
available_functions = {
"get_weather": get_weather,
"calculate": calculate,
}
SYSTEM_PROMPT = """你是一个智能助手,可以查询天气和执行计算。
- 用户问天气时,调用 get_weather 工具
- 用户问计算时,调用 calculate 工具
- 获取结果后,用友好的中文回复用户"""
async def agent_loop(user_input: str):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input}
]
# 第一步:询问 LLM 是否需要调工具
response = await client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools,
tool_choice="auto" # 让 LLM 自己决定要不要用工具
)
msg = response.choices[0].message
# 如果 LLM 决定调工具
if msg.tool_calls:
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"🔧 调用工具:{func_name}({func_args})")
# 真正执行
func = available_functions[func_name]
if asyncio.iscoroutinefunction(func):
result = await func(**func_args)
else:
result = func(**func_args)
# 把工具调用的结果追加到对话中
messages.append(msg) # LLM 的工具调用消息
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# 第二步:把结果交给 LLM,让它生成友好回复
final_response = await client.chat.completions.create(
model="deepseek-chat",
messages=messages
)
return final_response.choices[0].message.content
# 如果不需要工具,直接返回
return msg.content
# ── 测试 ──
async def main():
# 测试 1:天气查询
print("=" * 50)
print("用户:北京现在天气怎么样?")
reply = await agent_loop("北京现在天气怎么样?")
print(f"助手:{reply}")
print()
# 测试 2:计算
print("=" * 50)
print("用户:23 × 47 + 15 × 8 等于多少?")
reply = await agent_loop("23 × 47 + 15 × 8 等于多少?")
print(f"助手:{reply}")
print()
# 测试 3:不需要工具
print("=" * 50)
print("用户:你好,请介绍一下你自己")
reply = await agent_loop("你好,请介绍一下你自己")
print(f"助手:{reply}")
asyncio.run(main())
运行结果:
用户:北京现在天气怎么样?
🔧 调用工具:get_weather({'city': '北京'})
助手:北京现在是晴天,温度 25°C,湿度 40%,很适合出门哦!
用户:23 × 47 + 15 × 8 等于多少?
🔧 调用工具:calculate({'expression': '23 * 47 + 15 * 8'})
助手:23 × 47 + 15 × 8 = 1201
用户:你好,请介绍一下你自己
助手:你好!我是智能助手,可以帮你查天气、做计算...
四、Function Calling 的进阶技巧
4.1 并行调用
LLM 可以一次返回多个 tool_call:
# 用户问:"北京和上海今天天气怎么样?"
# LLM 一次返回两个 tool_call:
tool_calls = [
{"name": "get_weather", "arguments": {"city": "北京"}},
{"name": "get_weather", "arguments": {"city": "上海"}}
]
# 并行执行
import asyncio
tasks = [get_weather(c["arguments"]["city"]) for c in tool_calls]
results = await asyncio.gather(*tasks)
4.2 工具描述就是「API 文档」
工具 JSON Schema 写得越详细,LLM 选工具越准:
# ❌ 太模糊
{"name": "search", "description": "搜索东西", "parameters": {"q": {"type": "string"}}}
# ✅ 精确
{
"name": "search_harmonyos_docs",
"description": "搜索华为 HarmonyOS 官方文档,返回相关的 API 说明和代码示例。关键词应使用中文技术术语。",
"parameters": {
"query": {"type": "string", "description": "中文搜索关键词,如 '状态管理'、'页面路由'"},
"api_version": {"type": "string", "enum": ["API12", "API11"], "default": "API12"}
}
}
4.3 错误处理:工具调用失败怎么办
try:
result = func(**args)
except Exception as e:
result = f"工具调用失败:{e}。请告诉用户暂时无法完成此操作。"
# LLM 拿到错误信息后会自动向用户解释
五、Function Calling vs MCP
这是面试高频题:
| 维度 | Function Calling | MCP |
|---|---|---|
| 是什么 | LLM 的推理能力 | 通信协议 |
| 谁定义 | 使用方(你的代码) | 工具提供方(MCP Server) |
| 复用性 | 每次要重新定义 | 写一个 Server,到处用 |
| 关系 | LLM 的输出格式 | 工具的标准接入方式 |
MCP 底层也会触发 Function Calling。MCP 解决的是「工具从哪来」,Function Calling 解决的是「LLM 怎么选工具」。
六、生产实战:这些坑你迟早会踩
6.1 工具调用失败后的重试策略
Agent 最怕的不是工具失败,是工具失败了 LLM 以为成功了。
# 三种重试策略
async def execute_tool_with_retry(func_name: str, args: dict, max_retries: int = 2):
"""工具调用 + 智能重试"""
last_error = None
for attempt in range(max_retries + 1):
try:
result = await available_functions[func_name](**args)
return {"success": True, "result": result}
except TimeoutError:
last_error = f"超时(第 {attempt+1} 次尝试)"
await asyncio.sleep(2 ** attempt) # 指数退避
except ValueError as e:
# 参数错误 → 不用重试,让 LLM 修正参数
return {"success": False, "error": f"参数错误:{e}", "retry_with_fix": True}
except Exception as e:
last_error = str(e)
return {"success": False, "error": last_error}
6.2 并行调用的陷阱
# ❌ 错误:串行调天气
for city in ["北京", "上海", "深圳", "广州"]:
weather = await get_weather(city)
# ✅ 正确:并行调天气
results = await asyncio.gather(
get_weather("北京"),
get_weather("上海"),
get_weather("深圳"),
get_weather("广州"),
return_exceptions=True # 一个失败不影响其他的
)
生产环境经验:10 个城市并行查天气,串行要 2 秒(每个 200ms),并行只要 200ms。但要注意 API rate limit——并行太多可能被限流。
6.3 工具 Schema 写得不好 = LLM 选错工具
# ❌ 两个工具描述太像,LLM 不知道该用哪个
{"name": "search_docs", "description": "搜索文档"}
{"name": "search_code", "description": "搜索代码"}
# ✅ 差异化描述
{
"name": "search_harmonyos_docs",
"description": "搜索华为官方 HarmonyOS 开发文档,返回 API 说明和代码示例。适用场景:用户问 API 用法、组件属性等。"
}
{
"name": "search_csdn_articles",
"description": "搜索 CSDN 博客的技术文章,返回实战经验和踩坑记录。适用场景:用户问怎么做、有什么坑等。"
}
经验法则:如果你在描述里加一个「不适用场景」,LLM 选错的概率降 30%。
6.4 流式输出 + 工具调用
Agent 对话中用户期待实时反馈,但工具调用是阻塞的:
async def agent_loop_streaming(user_input: str):
"""流式 Agent,工具调用时发送状态更新"""
yield {"type": "status", "text": "正在分析你的问题..."}
# ... LLM 决定调用工具 ...
yield {"type": "status", "text": f"正在调用 {tool_name}..."}
result = await execute_tool(tool_name, tool_args)
yield {"type": "tool_result", "text": f"{tool_name} 返回:{result[:100]}..."}
# ... LLM 生成最终回答 ...
async for chunk in llm.chat_stream(messages):
yield {"type": "chunk", "text": chunk}
用户盯着空白界面等 3 秒会觉得卡。每步发一个状态更新,体验完全不同。
六、总结
- Function Calling 是 Agent 的核心——没有它,LLM 只是个聊天机器人
- LLM 不执行——它只说用什么工具,真正执行的是你的代码
- 工具描述决定准确率——JSON Schema 写得好,LLM 才能选对工具
- 并行调用——多个独立工具可以同时执行,提升性能
- Function Calling 是 MCP 的底层机制——理解它才能理解 Agent 生态
下一篇:《RAG:给 LLM 装上知识库》——向量数据库、Embedding、Chunking,从原理到完整可运行的 RAG 系统。
系列文章:00-总纲 → ①-LLM 原理 → ②-Prompt 工程 → ③-Function Calling → ④-RAG → ⑤-Agent 模式 → ⑥-LangGraph → ⑦-MCP → ⑧-Multi-Agent
更多推荐


所有评论(0)