ARTICLE · 1138295
AI调工具,怎么不翻车
AI调工具,怎么不翻车
LLM 会写字不算本事,会调工具才算长手;可工具调用,是后端接 AI 翻车最多的那一关。
01 | Function Calling,到底在调什么
很多人把 Function Calling 和"让模型吐 JSON"混为一谈,这是第一个翻车点。
• Structured Output:你让模型每次都按固定格式吐数据(提价值息),没有副作用,9 月 7 日那篇已经讲透。 • Function Calling / Tool Use:你给模型一批工具,它按需决定调哪个、传什么参数、要不要调,调完拿结果继续说人话——这才是"长手"。
一句话:Structured Output 是"每次吐数据",Function Calling 是"按需触发动作"。前者其实是后者的退化版(工具数变 1、参数固定)。理解这层包含关系,后面坑都不会跑偏。
命名差异先说清楚,免得切文档切懵,字段以各厂官方文档为准:
tools[].function | ||
tools[].input_schema | ||
functionDeclarations |
别拿 OpenAI 的 tool_calls 往 Anthropic 上套——返回结构完全不同,这是第二大翻车点。
02 | 一个完整循环,到底几步
工具调用不是一次请求,是一个循环。把它想成一次请求就完事,是第三个翻车点。
用户提问 ↓模型 → 返回"我要调 getWeather,参数=东京" ↓你的应用 → 真的去调 getWeather("东京") → 拿到 18° ↓把 18° 塞回 tool_result → 模型 → "东京现在 18 度" ↓(模型可能再发起一轮,直到它决定回纯文本,循环结束)关键:这个 while 循环得你自己写。模型返回工具调用时,结束信号是"我有工具要调"(OpenAI 的 tool_calls、Anthropic 的 tool_use block),不是对话到底;只有它回纯文本,这轮才算完。新手最常见错误:拿到 tool_calls 直接 return 给用户,结果把"我要调工具"的 JSON 原样抛到了前端。
骨架代码(伪流程,字段以官方 SDK 为准):
# 伪代码:工具调用循环骨架whileTrue: resp = client.chat(messages=messages, tools=tools)ifnot resp.has_tool_calls: # 模型回纯文本了break messages.append(resp.assistant_message)for call in resp.tool_calls: # 可能多个,见 04 result = dispatch(call) # 你的执行器 messages.append(tool_result(call.id, result))return resp.text03 | 三家返回结构,别张冠李戴
这块最容易写错,把"模型要调工具"在各家返回里长什么样列清楚:
OpenAI:工具调用挂在 message.tool_calls 数组里,每个含 id、function.name、function.arguments(字符串形式 JSON)。结果用 role: "tool" + tool_call_id 回传。
Anthropic:工具调用是 content 里的一个 tool_use block(含 id、name、input 对象)。结果回传用 role: "user" + tool_result block,tool_use_id 对上。
Gemini:工具调用是 parts 里的 functionCall,结果用 functionResponse 回传。Gemini 3 起每次调用带唯一 id,多轮里 functionResponse 带对 id 才稳(来源:Google 文档相关说明)。
三个一看就明白:返回结构、result 的 role、id 机制都不一样。你做的抽象层只针对一家写死,换厂商等于重写。生产里的工具层,要么用框架(Spring AI / LangChain4j)抹平,要么自己封一层中立 ToolCall / ToolResult 对象。
一组第三方对比数据(2026 Q1 非官方评测,数字会变,仅供参考):工具调用可靠度 Anthropic 8.4、Google 7.9、OpenAI 6.3——即同一句话,OpenAI 更容易选错工具或参数缺斤少两。这只是趋势参考,不是你选型的硬依据。
04 | 并行工具调用:省一整轮延迟的加速器
这是大多数人没用起来、用了又翻车的能力。
模型可以一次返回多个工具调用(问"北京和东京天气"→ 一次返回两个 getWeather)。你的应用并发执行,全部结果回传,模型再继续——省掉"调一个、回一轮、再调下一个"的整轮往返,延迟砍半。
OpenAI 多数模型默认开 parallel_function_calls;Anthropic 默认允许并行(要关才显式设 disable_parallel_tool_use)。
但有顺序依赖时并行会出大事:工具 B 的入参要靠 A 的返回,模型却把 A、B 一次都调了,B 拿不到真实参数只能瞎编。怎么办?两条路:
• 系统提示写明"必须先调 A 拿到结果再调 B"; • 关掉并行,强制模型串行。
判断准则:独立工具才并行,有依赖的工具拆步。别为了快无脑开并行——选错工具的代价远大于多等一轮。
05 | 九个坑,一张表
第 5、8 两个最致命:第 5 把异常当结果,让模型把报错当"正常返回"继续编;第 8 是 Agent 安全老问题——删数据、转账、发邮件这种不可逆动作模型拍板就执行,上线就是事故。记住:工具能力越强,HITL 越不能省。
06 | 下一代:让模型自己找工具
前面九坑里 7、3 号(工具爆炸、选错工具)根子是:你把所有工具定义一次性塞进上下文。Agent 要接几十个 MCP server、上百个工具时,定义和结果能吃掉 5 万 token 以上(Anthropic 工程博客原话),模型在噪声里更容易选错。
Anthropic 给的三个新方向(官方工程博客,beta 阶段,细节以官方文档为准):
• 动态工具发现:工具不预先塞进 context,模型按任务按需加载相关工具。接 500 个 MCP 工具时只加载当下要的那几个,省上下文也降低选错率。 • 代码执行调用工具:让模型用代码编排"循环、条件、数据转换"那类逻辑,而不是每个中间步都过一次推理。每个工具调用都要一次 inference,中间结果堆在 context 里越来越脏——代码天然适合做这种 orchestration。 • 从样例学习用法:JSON Schema 只能保证"结构合法",说不出"这个可选参数什么时候该传、哪些组合有意义"。这种用法约定,靠 few-shot 示例教,比靠 schema 约束更管用。
这三件套直接拆解第 7、3 号坑。趋势很清楚:工具调用正在从"我把工具定义全塞给你"走向"你自己按需找工具、用代码编排、看样例学规矩"。
07 | Java 落地两件套
生产里别手撸 HTTP 抹平三家差异,用框架。两套都支持 @Tool 注解把普通方法直接挂成工具(方法签名跟版本走,照你那份 javadoc 来)。
Spring AI(核实时最新 v2.0.1):
// 骨架:方法即工具,字段以 javadoc 为准@BeanToolCallbackProvider weatherTools(WeatherSvc svc) {return MethodToolCallbackProvider.builder() .toolMethods(newWeatherTools(svc)).build();}classWeatherTools {@Tool(description = "查某城市当前天气") String getWeather(@ToolParam("城市名") String city) {return svc.current(city); }}LangChain4j(核实时最新 1.20.0):
// 骨架:接口+注解,AI 对象即 AgentinterfaceWeatherAgent {@SystemMessage("你是天气助手,按需调工具") String ask(String q);}WeatherAgentagent= AiServices.builder(WeatherAgent.class) .chatLanguageModel(model) .tools(newWeatherTools(svc)) .build();两者差异:Spring AI 把工具方法做成 ToolCallbackProvider Bean,适合熔进现有 Spring DI;LangChain4j 用 AiServices 接口代理,更接近"声明一个 Agent"的心智。挑哪个看你团队栈,不是能力高下。
什么时候别上 Function Calling
不是所有 AI 接入都要工具调用。几种情况别硬上:
• 纯文本生成:写文案、总结、改写,直接补全就够,加工具是给自己挖坑。 • 固定流程的 RAG:检索→拼 prompt→回答,没有"决策调啥"的空间,工具调用多余(Rerank 见 9-16)。 • 无副作用查询:只是查库取数,结构化输出(9-07)已够,套工具壳只增复杂度。
反过来,只要模型需要"决定下一步动作"——调 API、查多源、按结果分支——Function Calling 就是正确武器。
写在最后
工具调用把 LLM 从"会说话的引擎"变成"能干活的 Agent"。它不是一次请求,是一个循环;不是一种格式,是三家不一样的协议;不是工具塞越多越强,是塞太多反而选错。把循环写对、并行用对、HITL 守住,再往"动态发现"那一代演进——这关就过了。
觉得有用?点赞 + 在看 + 转发 ❤️