Function Calling / Tool Use 报错怎么办?参数格式、超时、不触发全解析

Function Calling / Tool Use 报错怎么办?参数格式、超时、不触发全解析

Function Calling 让大模型能调用外部函数------你把函数描述通过 tools 参数传给模型,模型决定何时调用、传什么参数,你的代码负责执行并把结果喂回去。这个闭环里任何一个环节出问题,都会报错。实际开发中踩得最多的坑集中在四类:tools 参数格式不对、模型压根不触发工具、工具执行超时、返回结果解析失败。

一、tools 参数格式错误

这是最高频的报错来源。OpenAI 的 tools 参数有严格的 JSON Schema 规范,写错一个字段或者套错层级,模型要么直接忽略你的工具定义(静默失败),要么返回 400 错误。

常见写法错误

错误示例:用了已废弃的 functions 参数

python 复制代码
# ❌ 旧写法,openai 1.0+ 已废弃
response = client.chat.completions.create(
    model="YOUR_MODEL",
    messages=[{"role": "user", "content": "今天天气怎么样?"}],
    functions=[{...}],        # 废弃参数
    function_call="auto"      # 废弃参数
)

正确写法:用 tools + tool_choice

python 复制代码
# ✅ 当前标准写法
from openai import OpenAI
import json

client = OpenAI(api_key="YOUR_API_KEY", base_url="YOUR_BASE_URL")

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市名,如:北京、上海"
                    }
                },
                "required": ["location"],
                "additionalProperties": False
            },
            "strict": True
        }
    }
]

response = client.chat.completions.create(
    model="YOUR_MODEL",
    messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
    tools=tools,
    tool_choice="auto"
)

关键格式要点

几个容易忽略的细节:

  • type 字段必须是 "function" ,外层包一个 function 对象,里面才是 namedescriptionparameters
  • additionalProperties: false 在 strict 模式下是必须的,否则 API 会拒绝请求
  • required 数组必须列出 properties 中的所有字段 ,可选字段用 ["string", "null"] 联合类型表示
  • strict: true 开启后,模型生成的参数会 100% 符合你定义的 Schema,不会出现缺字段或类型不对的情况

strict 模式的限制

strict 模式不支持所有 JSON Schema 特性。以下关键字不支持

  • $ref / $defs(不能用引用)
  • patternProperties / dependencies
  • format 中的部分值(只支持 emailuridate-time 等少量格式)
  • minLength / maxLength / minimum / maximum 可以用,但 API 不会强制校验------你的代码需要自己验证

二、模型不触发工具调用

定义了工具,模型却直接用文字回答了,finish_reason 返回的是 "stop" 而不是 "tool_calls"。这种情况排查起来让人头疼,因为没有报错,只是工具没被调用。

原因排查

原因 1:tool_choice 设置问题

tool_choice 默认是 "auto",模型自己决定要不要调用工具。如果模型觉得直接回答就行,它就不会调。这种情况下你不会收到 tool_calls

python 复制代码
# 强制模型必须调用至少一个工具
response = client.chat.completions.create(
    model="YOUR_MODEL",
    messages=[...],
    tools=tools,
    tool_choice="required"  # 强制触发
)

# 或者强制调用某个特定函数
response = client.chat.completions.create(
    model="YOUR_MODEL",
    messages=[...],
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "get_weather"}}
)

原因 2:函数描述太模糊

模型完全依赖 description 字段来判断工具该不该用。如果你的描述写的是 "获取数据",模型根本不知道什么场景该调。

python 复制代码
# ❌ 描述太模糊
"description": "获取数据"

# ✅ 描述具体明确
"description": "根据城市名称查询当前天气,返回温度、湿度、风力等级。用户询问天气相关问题时使用此工具。"

原因 3:参数描述缺失或不清晰

模型需要从用户输入中提取参数值,如果参数描述写得含糊,模型可能不知道该传什么。

python 复制代码
# ❌ 参数描述缺失
"location": {
    "type": "string"
}

# ✅ 带示例的参数描述
"location": {
    "type": "string",
    "description": "城市名称,例如:北京、上海、深圳"
}

原因 4:工具数量过多

OpenAI 官方建议单次请求不超过 20 个工具定义。工具太多会让模型选择困难,准确率下降。如果你的系统有几十个工具,应该根据上下文做一次预筛选,只传相关的工具子集。

诊断代码

python 复制代码
def diagnose_response(response):
    """诊断模型为什么没有触发工具调用"""
    choice = response.choices[0]
    
    print(f"finish_reason: {choice.finish_reason}")
    print(f"message content: {choice.message.content}")
    print(f"tool_calls: {choice.message.tool_calls}")
    
    if choice.finish_reason == "tool_calls":
        if not choice.message.tool_calls:
            print("⚠️ finish_reason 是 tool_calls 但 tool_calls 为空")
        else:
            for tc in choice.message.tool_calls:
                print(f"  工具: {tc.function.name}")
                print(f"  参数: {tc.function.arguments}")
    elif choice.finish_reason in ["stop", "end_turn"]:
        print("⚠️ 模型选择直接回答,未调用工具")
        print("   检查:1) tool_choice 是否为 auto")
        print("         2) 函数描述是否清晰")
        print("         3) 用户输入是否匹配工具用途")
    elif choice.finish_reason == "length":
        print("⚠️ 响应被 max_tokens 截断")

三、工具执行超时

模型触发了工具调用,但你的函数执行太慢,导致整个请求链路超时。这在调用外部 API、查数据库、做文件处理的场景里很常见。

问题表现

  • API 返回 504 Gateway Timeout
  • 客户端报 APITimeoutErrorAPIConnectionError
  • 多轮对话中,工具结果还没返回,模型已经"忘了"之前的状态

解决方案

方案 1:异步执行 + 超时控制

python 复制代码
import asyncio
import json
from openai import OpenAI

client = OpenAI(api_key="YOUR_API_KEY", base_url="YOUR_BASE_URL")

async def execute_tool_with_timeout(tool_name, args, timeout_seconds=10):
    """带超时的工具执行"""
    try:
        result = await asyncio.wait_for(
            call_tool(tool_name, args),
            timeout=timeout_seconds
        )
        return json.dumps({"result": result}, ensure_ascii=False)
    except asyncio.TimeoutError:
        return json.dumps({"error": f"工具 {tool_name} 执行超时"}, ensure_ascii=False)

async def call_tool(name, args):
    # 替换为你的实际工具实现
    if name == "get_weather":
        await asyncio.sleep(2)  # 模拟 API 调用
        return {"temp": 25, "condition": "晴"}
    raise ValueError(f"未知工具: {name}")

方案 2:给工具结果加上错误处理

工具执行失败时,不要直接抛异常中断流程,把错误信息作为工具结果返回给模型,让模型自己决定怎么处理:

python 复制代码
def safe_execute_tool(tool_name, args_dict):
    """安全执行工具,捕获所有异常"""
    try:
        result = TOOL_REGISTRY[tool_name](**args_dict)
        return json.dumps({"result": result}, ensure_ascii=False)
    except KeyError:
        return json.dumps({"error": f"工具 {tool_name} 不存在"}, ensure_ascii=False)
    except TypeError as e:
        return json.dumps({"error": f"参数错误: {e}"}, ensure_ascii=False)
    except Exception as e:
        return json.dumps({"error": f"执行失败: {e}"}, ensure_ascii=False)

方案 3:Responses API 的 5 分钟窗口

如果你用的是 OpenAI Responses API,requires_action 状态有 5 分钟的有效期。工具执行超过 5 分钟后提交结果会失败。解决方案是轮询检查状态,超时后重新创建请求:

python 复制代码
import time

MAX_TIMEOUT = 300  # 5 分钟

start = time.time()
while response.status == "requires_action":
    if time.time() - start > MAX_TIMEOUT:
        # 超时,重新创建请求
        response = client.responses.create(...)
        start = time.time()
        continue
    # 执行工具并提交结果
    ...

四、返回格式解析失败

模型返回了 tool_calls,但 arguments 字段的 JSON 解析不了。这个问题的根因有三个,每个的修法不同。

原因 1:响应被 max_tokens 截断

模型生成参数的 token 数超过了 max_tokens 限制,JSON 被拦腰截断。这种情况下 finish_reason 会是 "length" 而不是 "tool_calls"

python 复制代码
choice = response.choices[0]

if choice.finish_reason == "length":
    # 参数被截断了
    print("⚠️ 工具参数被 max_tokens 截断")
    print("解决方案:增大 max_tokens 或精简 Schema")
    
    # 增大 max_tokens 重试
    response = client.chat.completions.create(
        model="YOUR_MODEL",
        messages=[...],
        tools=tools,
        max_tokens=4096  # 增大
    )

原因 2:JSON 解析异常

tool_call.function.arguments 是一个字符串,不是字典,必须用 json.loads() 解析。即使开了 strict 模式,在 token 压力下偶尔还是会出问题(gpt-4o-mini 约 2-5% 的概率)。

python 复制代码
import json

def parse_tool_arguments(response):
    """安全解析工具参数"""
    choice = response.choices[0]
    
    if not choice.message.tool_calls:
        return None, "没有工具调用"
    
    results = []
    for tc in choice.message.tool_calls:
        raw_args = tc.function.arguments
        
        if not raw_args or raw_args.isspace():
            results.append({
                "tool": tc.function.name,
                "error": "参数为空"
            })
            continue
        
        try:
            args = json.loads(raw_args)
            results.append({
                "tool": tc.function.name,
                "args": args
            })
        except json.JSONDecodeError as e:
            results.append({
                "tool": tc.function.name,
                "error": f"JSON 解析失败: {e}",
                "raw": raw_args
            })
    
    return results, None

原因 3:Schema 过于宽松

Schema 没有约束好,模型生成的参数虽然能解析,但内容不对------缺少字段、类型不匹配、枚举值越界。

python 复制代码
def validate_args(args, schema):
    """客户端校验参数,API 不会替你做这件事"""
    props = schema["properties"]
    
    for param, value in args.items():
        if param not in props:
            return False, f"未知参数: {param}"
        
        param_def = props[param]
        
        # 枚举校验
        if "enum" in param_def and value not in param_def["enum"]:
            return False, f"{param} 不在允许值中: {param_def['enum']}"
        
        # 类型校验
        expected_type = param_def["type"]
        if isinstance(expected_type, list):
            # 联合类型,如 ["string", "null"]
            if value is not None and not isinstance(value, str):
                return False, f"{param} 类型错误"
        elif expected_type == "string" and not isinstance(value, str):
            return False, f"{param} 应为 string"
        elif expected_type == "number" and not isinstance(value, (int, float)):
            return False, f"{param} 应为 number"
    
    # 必填字段校验
    for req in schema.get("required", []):
        if req not in args:
            return False, f"缺少必填字段: {req}"
    
    return True, "校验通过"

完整的多轮工具调用示例

把上面的防御措施整合起来:

python 复制代码
import json
from openai import OpenAI

client = OpenAI(api_key="YOUR_API_KEY", base_url="YOUR_BASE_URL")

def run_agent(user_query, tools, tool_registry, max_rounds=5):
    messages = [{"role": "user", "content": user_query}]
    
    for round_num in range(max_rounds):
        response = client.chat.completions.create(
            model="YOUR_MODEL",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        
        choice = response.choices[0]
        
        # 情况 1:模型直接回答,没有工具调用
        if choice.finish_reason in ["stop", "end_turn"]:
            return choice.message.content
        
        # 情况 2:响应被截断
        if choice.finish_reason == "length":
            messages.append({"role": "user", "content": "请继续"})
            continue
        
        # 情况 3:工具调用
        if choice.finish_reason == "tool_calls" and choice.message.tool_calls:
            messages.append(choice.message)  # 必须先加 assistant 消息
            
            for tc in choice.message.tool_calls:
                try:
                    args = json.loads(tc.function.arguments)
                except json.JSONDecodeError:
                    result = '{"error": "参数解析失败"}'
                else:
                    func = tool_registry.get(tc.function.name)
                    if func:
                        try:
                            result = json.dumps(func(**args), ensure_ascii=False)
                        except Exception as e:
                            result = json.dumps({"error": str(e)}, ensure_ascii=False)
                    else:
                        result = '{"error": "工具不存在"}'
                
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": result
                })
        else:
            messages.append({"role": "user", "content": "请使用工具来回答"})
    
    return "达到最大轮次限制"


# 工具注册表
TOOL_REGISTRY = {
    "get_weather": lambda location: {"temp": 25, "condition": "晴"}
}

# 运行
answer = run_agent("北京天气怎么样?", tools, TOOL_REGISTRY)
print(answer)

注意上面代码里的一个关键细节:提交工具结果前,必须先把 assistant 消息(包含 tool_calls)追加到 messages 列表里 。漏掉这一步会报 400 错误,提示 messages with role 'tool' must be preceded by a message with a tool_calls field

快速排错表

症状 可能原因 排查方法 修复方案
API 返回 400 tools 参数格式错误 打印 tools 列表检查 JSON 结构 按官方 Schema 格式重写
finish_reasonstop 模型未触发工具 检查 tool_choice 和函数描述 设为 required 或优化描述
finish_reasonlength 参数被 max_tokens 截断 检查 max_tokens 设置 增大 max_tokens 或精简 Schema
tool_calls 为 None 模型选择直接回答 打印完整 response.choices0.message 优化 prompt 或强制 tool_choice
json.loads() 报错 arguments 字符串不完整 打印 raw arguments 字符串 检查是否被截断,加 try-except
400: tool result 无效 没先追加 assistant 消息 检查 messages 顺序 messages.append(choice.message)
tool_call_id 不匹配 ID 对应错误 检查每个 tool_call 的 id 严格用 tc.id 不要手写
504 超时 工具执行太慢 给工具加超时日志 异步执行 + 超时控制
工具被调用但参数错 Schema 太宽松 打印实际参数对比 Schema 开 strict 模式 + 客户端校验
strict 模式被拒绝 Schema 不合规 查看 API 返回的具体错误信息 检查 additionalProperties 和 required

配置检查清单

开发 Function Calling 功能时,逐项过一遍这个清单:

  • tools 参数格式 :外层 type: "function",内层 function 对象包含 namedescriptionparameters
  • parameters 结构type: "object",有 propertiesrequiredadditionalProperties: false
  • strict 模式 :生产环境开启 strict: true,所有字段在 required 中,可选字段用联合类型
  • 函数描述:描述写清楚什么时候该用这个工具、返回什么数据,不要写一句话的模糊描述
  • 参数描述:每个参数有清晰的 description,复杂参数带示例值
  • tool_choice :确认是 auto(模型决定)、required(必须调用)还是指定函数
  • max_tokens:设置足够大,避免参数生成到一半被截断
  • JSON 解析tool_call.function.argumentsjson.loads() 解析,加 try-except
  • 消息顺序:工具结果前必须先追加 assistant 消息(含 tool_calls)
  • tool_call_id :每个 tool 结果的 tool_call_id 必须精确匹配模型返回的 ID
  • 错误处理:工具执行失败时返回 JSON 错误信息,不要直接抛异常
  • 超时控制:工具执行加超时限制,避免整个请求链路卡死
  • 客户端校验:API 不强制校验 enum/min/max,你的代码要自己做参数校验
  • 工具数量:单次请求不超过 20 个工具,太多会降低准确率
  • 日志记录:记录每次工具调用的名称、参数、执行时间、返回结果,方便线上排查
相关推荐
RunProof可证工程2 小时前
AI 帮我写的代码,上线前到底能不能信?我跑了一轮代码安全体检
ai编程
RunProof可证工程2 小时前
AI 生成的代码能跑,为什么不能直接上线?
ai编程
undsky_3 小时前
【n8n教程】:Email Trigger IMAP节点,实现邮件自动化处理
人工智能·ai·aigc·ai编程
用户3610588626124 小时前
讲透 Embedding 本质:从 one-hot 到表示学习,词嵌入的四个技术前提
openai·ai编程
BehaviourBlogs4 小时前
ChatGPT Work 推出个人写作风格学习功能:从「通用助手」到「个人化写作代理」
gpt·aigc·ai编程
全栈弄潮儿4 小时前
中级开发者用 AI,最容易掉进的 5 个坑
aigc·openai·ai编程
打呵欠的猫5 小时前
我把 20 个页面的权限控制从"硬编码"改成"配置驱动",AI 帮我生成了 80% 的迁移代码
前端·ai编程
console.log('npc')5 小时前
2026 实测:Grok 4.5 与 Grok 4.6 怎么选?前端开发、教程写作、Figma 还原选型指南
前端·大模型·ai编程·figma·grok
超级架构师6 小时前
连接企业系统,不等于把接口直接交给 Agent:LIMENORA 的集成边界
网络·人工智能·架构·ai编程