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

相关推荐
qcx232 分钟前
【系统学AI】25 论文导读 ①:两篇改变 AI 的开山之作——Attention Is All You Need & ReAct
前端·人工智能·react.js·transformer
Black蜡笔小新10 分钟前
自动化AI算法训练服务器DLTM制造业AI质检工作站助力制造业实现AI智检
人工智能·算法·自动化
川冰ICE13 分钟前
⑮ AI音乐与音频:工具详解与创作流程
人工智能·音视频
米小虾19 分钟前
2026 年多模态大模型全面爆发:从「看懂图」到「听懂世界」的技术跃迁
人工智能
米小虾24 分钟前
AI Agent 进入协议时代:MCP、A2A、AG-UI 三大协议全景解析
人工智能·agent
蝎子莱莱爱打怪25 分钟前
🚀 🚀🚀2026年5月GitHub月榜精选:17个项目中挑出10个推荐,实操4个!
人工智能·后端·ai编程
升鲜宝供应链及收银系统源代码服务34 分钟前
升鲜宝AI助手项目源码集成开发步骤(一)---升鲜宝生鲜配送供应链管理系统源代码服务
人工智能·生鲜配送系统·生鲜物流线路规划·生鲜电商订单系统·生鲜供应链系统·生鲜系统架构设计·生鲜配送ai功能集成
yjcode78937 分钟前
探索游戏充值新纪元:友价源码技术革新之旅
大数据·人工智能·游戏·游戏交易
冬奇Lab1 小时前
Agent 系列(11):A2A 协议——Agent 与 Agent 如何协作
人工智能·agent
snow@li1 小时前
AI:理解 大数据、算法、算力、电力、生成式AI、token 之间的关系
大数据·人工智能·算法