Claude Opus 4.7 实战踩坑:Function Calling 这些坑我替你踩了

5 阅读4分钟

上周 Claude Opus 4.7 发布,我当天就开始迁移项目,结果被 Function Calling 的几个变化搞得焦头烂额,调了整整两天才顺起来。

这篇文章把我踩过的坑全记下来,希望能帮你省点时间。

先说背景

我在做一个内部工具,让非技术同事用自然语言查数据库。核心逻辑就是:用户输入问题 → LLM 解析意图 → 调用预定义函数查 DB → 返回结果。

之前用 Claude Sonnet 4.6 跑得挺稳,这次升级 Opus 4.7 本来以为无缝切换,结果...

坑一:tool_choice 行为变了

以前我用 tool_choice: {"type": "auto"} 让模型自己决定要不要调工具,大部分情况没问题。

升级之后发现 Opus 4.7 在某些模糊问题上会直接回答文字,不走 Function Calling,导致我的解析逻辑拿到 None 直接崩。

import openai
import json

client = openai.OpenAI(
    base_url="https://api.ofox.ai/v1",  # 我用的这个,国内直连
    api_key="sk-xxx"
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "query_database",
            "description": "查询数据库,获取业务数据",
            "parameters": {
                "type": "object",
                "properties": {
                    "sql": {
                        "type": "string",
                        "description": "要执行的 SQL 查询语句"
                    },
                    "params": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "SQL 参数列表,防止注入"
                    }
                },
                "required": ["sql"]
            }
        }
    }
]

def ask_db(user_question: str):
    response = client.chat.completions.create(
        model="claude-opus-4-7",
        messages=[
            {
                "role": "system",
                "content": "你是数据库查询助手。用户提问时,你必须调用 query_database 函数来获取数据,不要直接回答。"
            },
            {"role": "user", "content": user_question}
        ],
        tools=tools,
        # 关键改动:强制必须调用工具
        tool_choice={"type": "required"}
    )
    
    message = response.choices[0].message
    
    if message.tool_calls:
        tool_call = message.tool_calls[0]
        args = json.loads(tool_call.function.arguments)
        return args
    
    return None

改成 tool_choice: {"type": "required"} 之后稳多了,模型必须调工具,不会绕过去直接回答。

但这里有个副作用:如果用户问的问题完全不需要查数据库(比如"你好"),模型会强行生成一个奇怪的 SQL,所以要在 system prompt 里加好边界说明。

坑二:parallel_tool_calls 默认开启

这个坑更隐蔽。Opus 4.7 默认开启了并行工具调用,一次可能返回多个 tool_calls。

我原来的代码只处理 message.tool_calls[0],结果有时候模型一次返回两个查询,我只执行了第一个,数据就不完整。

def execute_tool_calls(message):
    results = []
    
    if not message.tool_calls:
        return results
    
    for tool_call in message.tool_calls:  # 遍历所有,不只取第一个
        if tool_call.function.name == "query_database":
            args = json.loads(tool_call.function.arguments)
            
            # 实际执行查询(这里用伪代码)
            result = run_query(args["sql"], args.get("params", []))
            
            results.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "content": json.dumps(result, ensure_ascii=False)
            })
    
    return results

如果你不想要并行调用,可以显式关掉:

response = client.chat.completions.create(
    model="claude-opus-4-7",
    messages=messages,
    tools=tools,
    parallel_tool_calls=False  # 关掉并行,一次只调一个
)

我的场景是查询之间有依赖关系(先查用户再查订单),所以关掉更合适。

坑三:多轮对话的消息格式

Function Calling 的多轮对话消息格式有点绕,我之前一直搞错。

正确的流程是:

  1. 用户消息
  2. 模型返回 assistant 消息(含 tool_calls)
  3. 你执行工具,把结果以 tool role 追加
  4. 再次调用 API,模型根据工具结果生成最终回答
def full_conversation(user_question: str):
    messages = [
        {"role": "system", "content": "你是数据库查询助手,通过调用工具来回答用户问题。"},
        {"role": "user", "content": user_question}
    ]
    
    # 第一轮:让模型决定调什么工具
    response = client.chat.completions.create(
        model="claude-opus-4-7",
        messages=messages,
        tools=tools,
        tool_choice={"type": "required"}
    )
    
    assistant_message = response.choices[0].message
    
    # 把 assistant 消息加入历史(注意要转成 dict)
    messages.append({
        "role": "assistant",
        "content": assistant_message.content,
        "tool_calls": [
            {
                "id": tc.id,
                "type": "function",
                "function": {
                    "name": tc.function.name,
                    "arguments": tc.function.arguments
                }
            }
            for tc in (assistant_message.tool_calls or [])
        ]
    })
    
    # 执行工具并追加结果
    tool_results = execute_tool_calls(assistant_message)
    messages.extend(tool_results)
    
    # 第二轮:让模型根据工具结果生成最终回答
    final_response = client.chat.completions.create(
        model="claude-opus-4-7",
        messages=messages,
        tools=tools
    )
    
    return final_response.choices[0].message.content


# 测试
if __name__ == "__main__":
    answer = full_conversation("最近 7 天注册用户有多少?")
    print(answer)

之前我踩的坑是把 assistant_message 直接 append 进去,但 Pydantic 对象不能直接序列化,要手动转成 dict。

关于多模型管理

做这个项目期间我同时在测 GPT-5.4 和 Gemini 3,三个模型来回切换,API Key 管理很烦。

后来换用了 ofox.ai 聚合平台,一个 Key 搞定所有模型,base_url 统一,切模型只改 model 参数,省了不少事。

延迟实测在 300ms 出头,比我预期好,国内直连不需要折腾网络。

坑四:arguments 偶尔不是合法 JSON

这个是偶发问题,概率不高但真的遇到过。模型生成的 tool_call.function.arguments 有时候会有尾随逗号或者注释,json.loads 直接报错。

import re

def safe_parse_args(arguments: str) -> dict:
    try:
        return json.loads(arguments)
    except json.JSONDecodeError:
        # 尝试清理常见问题
        cleaned = re.sub(r',\s*}', '}', arguments)  # 去掉尾随逗号
        cleaned = re.sub(r',\s*]', ']', cleaned)
        try:
            return json.loads(cleaned)
        except json.JSONDecodeError:
            # 实在不行就返回空,让上层处理
            return {}

加个 fallback 处理,别让这种偶发问题把整个流程搞崩。

总结

升级 Opus 4.7 之后 Function Calling 能力确实更强了,理解复杂意图的准确率明显提升,但有几个行为变化需要注意:

  • tool_choice 建议显式指定,别依赖 auto 的默认行为
  • 并行工具调用默认开启,遍历 tool_calls 而不是只取第一个
  • 多轮对话消息格式要严格,assistant 消息转 dict 再 append
  • arguments 解析加 try/except,防偶发格式问题

代码都是我项目里实际跑过的,应该能直接用。有问题欢迎评论区交流。