上周 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 的多轮对话消息格式有点绕,我之前一直搞错。
正确的流程是:
- 用户消息
- 模型返回 assistant 消息(含 tool_calls)
- 你执行工具,把结果以 tool role 追加
- 再次调用 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,防偶发格式问题
代码都是我项目里实际跑过的,应该能直接用。有问题欢迎评论区交流。