上周给公司做一个内部工具,需要让 AI 能查数据库、发通知、改配置。我以为 Function Calling 很简单,结果踩了整整两天坑,最后才搞明白为什么 AI 总是乱调用工具,以及怎么让整个流程稳定跑起来。
这篇文章把我踩过的坑全记录下来,顺便附上完整可运行的代码。
先说需求背景
我们有个内部运维工具,需要支持自然语言查询:
- "查一下昨天的订单量" → 查数据库
- "把告警发到钉钉" → 调通知接口
- "把 config.yaml 里的超时时间改成 30s" → 修改配置文件
这种场景用 Function Calling 是最合适的,让 AI 决定调哪个工具、传什么参数,我只负责实际执行。
第一坑:工具描述写得太随意
最开始我的工具描述是这样的:
python
tools = [
{
"type": "function",
"function": {
"name": "query_database",
"description": "查询数据库",
"parameters": {
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "SQL语句"
}
},
"required": ["sql"]
}
}
}
]
结果 AI 直接生成了 SELECT * FROM orders WHERE date = '2026-04-21',然后我的代码执行了这条 SQL,返回了几千行数据。
问题在哪?描述太模糊了。AI 不知道这个函数的边界,也不知道应该生成什么样的 SQL。
修复方案:描述要写清楚 "能做什么"、"不能做什么"、"参数格式是什么":
python
tools = [
{
"type": "function",
"function": {
"name": "query_orders",
"description": "查询订单统计数据,只支持聚合查询(COUNT、SUM、AVG),不支持返回原始行数据。时间范围必须指定。",
"parameters": {
"type": "object",
"properties": {
"start_date": {
"type": "string",
"description": "开始日期,格式 YYYY-MM-DD"
},
"end_date": {
"type": "string",
"description": "结束日期,格式 YYYY-MM-DD"
},
"metric": {
"type": "string",
"enum": ["count", "amount", "avg_amount"],
"description": "查询指标:count=订单数量,amount=总金额,avg_amount=平均金额"
}
},
"required": ["start_date", "end_date", "metric"]
}
}
}
]
用 enum 限制参数取值,用具体的参数名代替开放的 SQL,AI 就不会乱来了。这一步花时间把描述写清楚,后面省的调试时间是十倍。
第二坑:没处理并行工具调用
Claude 和 GPT-4 都支持一次返回多个工具调用(parallel tool use)。我最开始的代码只处理了单个调用:
python
# 错误写法:只处理第一个工具调用
response = client.chat.completions.create(...)
tool_call = response.choices[0].message.tool_calls[0] # 只取第一个!
用户问 "查一下昨天的订单量,同时把结果发到钉钉",AI 会同时返回两个工具调用,我的代码直接丢掉了第二个。
正确写法:
python
import json
import openai
client = openai.OpenAI(
base_url="https://api.ofox.ai/v1",
api_key="sk-xxx"
)
def run_agent(user_message: str, tools: list) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg)
# 没有工具调用,直接返回
if not msg.tool_calls:
return msg.content
# 处理所有工具调用(可能有多个)
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
result = execute_tool(func_name, func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
# 继续循环,让 AI 根据工具结果生成回复
关键点:用 while True 循环,让 AI 可以多轮调用工具,直到它不再需要工具为止。
第三坑:工具执行出错没有优雅处理
工具执行失败时,我最开始直接抛异常,整个流程就挂了。更好的做法是把错误信息返回给 AI,让它自己决定怎么处理:
python
def execute_tool(func_name: str, func_args: dict) -> dict:
try:
if func_name == "query_orders":
return query_orders(**func_args)
elif func_name == "send_notification":
return send_notification(**func_args)
else:
return {"error": f"未知工具: {func_name}"}
except Exception as e:
# 不抛异常,把错误信息返回给 AI
return {"error": str(e), "success": False}
AI 收到错误信息后,通常会告诉用户 "查询失败,原因是...",而不是让整个程序崩溃。
完整实战代码
把上面的坑都规避掉,完整代码如下:
python
import json
import openai
from datetime import datetime
client = openai.OpenAI(
base_url="https://api.ofox.ai/v1",
api_key="sk-xxx"
)
TOOLS = [
{
"type": "function",
"function": {
"name": "query_orders",
"description": "查询订单统计数据,只支持聚合查询,必须指定时间范围",
"parameters": {
"type": "object",
"properties": {
"start_date": {"type": "string", "description": "开始日期 YYYY-MM-DD"},
"end_date": {"type": "string", "description": "结束日期 YYYY-MM-DD"},
"metric": {
"type": "string",
"enum": ["count", "amount", "avg_amount"],
"description": "count=订单数, amount=总金额, avg_amount=均价"
}
},
"required": ["start_date", "end_date", "metric"]
}
}
},
{
"type": "function",
"function": {
"name": "send_notification",
"description": "发送通知消息到钉钉群",
"parameters": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "通知内容"},
"title": {"type": "string", "description": "通知标题"}
},
"required": ["message"]
}
}
}
]
def query_orders(start_date: str, end_date: str, metric: str) -> dict:
mock_data = {"count": 1234, "amount": 98765.50, "avg_amount": 80.04}
return {
"success": True,
"data": {metric: mock_data[metric]},
"period": f"{start_date} ~ {end_date}"
}
def send_notification(message: str, title: str = "运维通知") -> dict:
print(f"[钉钉通知] {title}: {message}")
return {"success": True, "message": "通知已发送"}
def execute_tool(func_name: str, func_args: dict) -> dict:
try:
if func_name == "query_orders":
return query_orders(**func_args)
elif func_name == "send_notification":
return send_notification(**func_args)
else:
return {"error": f"未知工具: {func_name}"}
except Exception as e:
return {"error": str(e), "success": False}
def run_agent(user_message: str) -> str:
messages = [
{
"role": "system",
"content": "你是一个运维助手。今天日期是 " + datetime.now().strftime("%Y-%m-%d") + "。"
},
{"role": "user", "content": user_message}
]
for _ in range(5): # 防止死循环
response = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
msg = response.choices[0].message
messages.append({
"role": "assistant",
"content": msg.content,
"tool_calls": msg.tool_calls
})
if not msg.tool_calls:
return msg.content
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
result = execute_tool(func_name, func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "超过最大轮次,请重试"
if __name__ == "__main__":
print(run_agent("查一下昨天的订单数量,然后把结果发到钉钉"))
几个让工具调用更稳定的技巧
1. 给 AI 加上今天的日期
用户说 "昨天"、"上周",AI 需要知道今天是几号才能算出具体日期。在 system prompt 里加上当前日期,能避免很多时间相关的错误。
2. 工具数量别超过 10 个
工具太多,AI 选择困难,容易调错。如果工具很多,可以按场景分组,每次只传相关的工具。
3. 用 tool_choice 控制调用行为
python
# 强制调用某个工具
tool_choice={"type": "function", "function": {"name": "query_orders"}}
# 禁止调用工具,只生成文本
tool_choice="none"
# 默认:AI 自己决定
tool_choice="auto"
4. 多模型对比测试
不同模型对 Function Calling 的支持程度不一样。我在项目里试了 Claude Sonnet、GPT-4o 和 DeepSeek V3,Claude 在工具描述理解上表现最好,DeepSeek 在中文场景下参数提取更准。
我现在用 ofox.ai 统一管这几个模型的调用,一套代码切换模型,方便做对比测试,省得维护多个 key。
什么时候不该用 Function Calling
踩完坑之后,我反而更清楚 Function Calling 的边界了:
- 适合:需要 AI 决策 "调哪个工具"、"传什么参数" 的场景
- 不适合:工具调用顺序固定的场景(直接写代码更清晰)
- 不适合:工具很多但每次只用一个(用 RAG 检索工具描述更好)
如果你的场景是 "用户说一句话,AI 需要组合调用 3-5 个工具才能完成",Function Calling 是最合适的方案。如果只是 "用户选一个功能,执行固定逻辑",普通的 if-else 反而更可控。
Function Calling 本身不复杂,但要做到稳定可靠,工具描述的质量是关键。花时间把工具描述写清楚,比后面调试省时间多了。