Function Calling 踩坑实录:让 AI 真正动手帮你干活

上周给公司做一个内部工具,需要让 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 本身不复杂,但要做到稳定可靠,工具描述的质量是关键。花时间把工具描述写清楚,比后面调试省时间多了。

相关推荐
十铭忘2 小时前
InfoGCN++:通过预测未来学习表征以实现在线骨架人体动作识别
人工智能
漫游的渔夫2 小时前
从 Fetch 到 RAG:为什么你的 AI 知识库总是“胡言乱语”?
前端·人工智能
Jempo M2 小时前
为GitHub Copilot手搓一个可调用工具的AI Agent
人工智能·github·copilot
产品人卫朋2 小时前
AI硬件产品怎么做?Nova Sphere桌面设备
人工智能·产品经理·创业
探物 AI2 小时前
【感知实战·数据增强篇】深度解析目标检测中的图片数据增强算法,多图演示效果
人工智能·算法·目标检测
QYR-分析2 小时前
全地形轮足机器人行业发展分析:分类、格局与市场机遇
大数据·人工智能·机器人
Codigger官方2 小时前
生态破局:从孤岛工具到协同奇点
开发语言·人工智能·程序人生
竹之却2 小时前
【Agent-阿程】AI先锋杯·14天征文挑战第14期-第14天-OpenClaw 全配置目录结构与核心配置文件详解
人工智能·openclaw
Wenzar_2 小时前
**发散创新:基于算子融合的深度学习推理优化实战**在现代AI推理场景中,模型性能瓶颈往往不是由单一算子决定的,而是多个连续算子之间数
java·人工智能·深度学习