ARTICLE · 1196595
AI 工具调用写了 3 个月,Java 后端踩的 5 个坑都在这
AI 工具调用写了 3 个月,Java 后端踩的 5 个坑都在这
◆先说结论:工具调用,是把「一句话」变成「一次函数调用」
工具调用,老名字叫 Function Calling,做的事其实很朴素:你告诉模型「我这有几个方法能用」,模型在回答前先判断「这题要不要动手、动哪个手」,要动手就吐出一次调用意图(调哪个工具、传什么参数),框架替你执行,再把结果喂回模型,最后由模型组织成一句人话。
举个例子。用户问「帮我查下订单 12345678 到哪了」。模型不会去编一个假物流,它会回一个意图:我要调 `getOrderStatus("12345678")`。Spring AI 接住这个意图,真的去调你的 Java 方法,把返回值塞回上下文,模型再据此回你「这单昨天已从杭州发出,预计后天到」。
这就是它为什么是 Agent 的地基:模型从「会聊天」升级成「能动手」。客服自动查单、工单自动改状态、BI 里自动拉数——共同点都是「模型得去碰你系统里的真实数据」,光靠嘴是碰不到的。
好消息是,Spring AI 把这层做成了注解,你压根不用自己拼 schema。你写的方法上打一个 `@Tool`,参数上打 `@ToolParam`,框架负责两件事:把你的方法信息生成一段「说明书」塞进提示词给模型看;拿到模型返回后,把 JSON 里的参数映射成你方法要的类型再执行。
于是你要写的业务代码,就剩三件事:写方法、打注解、把它注册进 ChatClient。注册那步就一行:
看着简单,对吧?坑全在细节里。下面 5 个,一个比一个贵。
◆坑 1:@Tool 的描述当注释写,模型就当没看见
这是最便宜、也最致命的坑。
很多人把 `@Tool` 后面那个字符串,当成给人看的注释随手写:「查询订单」。你猜模型看到这个描述会怎么理解?它不知道这工具是「用户给了订单号才能用」还是「任何时候都能用」,也不知道返回的是状态还是明细。于是在该调的时候它不调,不该调的时候乱调。
记住一句话:`@Tool` 的 description 不是注释,是给模型看的指令。你写「何时用」,它才会在该用的时候用。
描述写得好不好,直接决定模型「选对工具」的概率。我的土办法是:把它当成在跟一个刚入职的实习生交代事情——「什么情况下你该干这个活,需要什么东西」。你越像交代人,模型越准。
◆坑 2:参数类型对不上,模型递给你的是字符串
第二个坑,是 Java 人最容易本能踩的:觉得「我方法参数当然要用最合适的类型」。
错。模型吐回来的是 JSON,里面的值首先都是字符串或数字。你方法参数写成 `int`、`LocalDate`、枚举、甚至自定义对象,框架就得替你做一轮类型转换。转得成就转,转不成就抛异常(或者模型直接收到一个 400 不知道怎么办)。日期格式、枚举取值、布尔值的真假表达,全是重灾区。
我的经验是一条土律:工具入参尽量收成 `String`,进方法后自己转。你要的严格校验,放在 Java 里做,写在方法第一行;别指望模型「乖乖按格式给你」。
如果是枚举,别偷懒。要么参数收 String、在方法里 `Enum.valueOf`,要么在 `@ToolParam` 的描述里把可选值一个个列出来,让模型只能在给定的几个里挑。你少写这一句,模型就可能给你传一个「订单状态是"已完成"还是"complete"」的薛定谔值。
◆坑 3:工具返回值太大,一次把上下文顶爆
这个坑专坑「数据多」的业务。
有个兄弟写了个工具 `listOrders(userId)`,返回 `List<Order>` 直接 JSON 序列化,一个用户 800 单,序列化出来几万字符,全被 Spring AI 塞回上下文。结果就是:token 账单暴涨,模型还被一堆冗余字段(创建时间、更新时间、内部 id)淹没,回答质量肉眼可见地塌。更糟的是,几次调用叠起来,直接把模型的上下文窗口撑爆,接口开始报错。
工具返回值的正确姿势是:只返回「模型做这次决策需要的最小信息」。大结果别原样回,先在你的 Java 层做裁剪、汇总,或者落库、给一个 id + 摘要。
一句话原则:工具返回值是「给模型看的简报」,不是「给程序解析的数据」。想给程序解析的,走另一个接口,别混进工具返回值里。
◆坑 4:工具一抛异常,整条链路直接 500
第四个坑,是线上最容易被投诉的:工具方法里一个 `RuntimeException`,用户那头直接看到 500。
为什么会这样?因为工具是被框架在执行你 `.call()` 的线程里同步调用的。你的方法一抛异常,异常就顺着调用栈一路穿到最外层,`call()` 抛出去,你的 Controller 没人接,就是 500。整个过程里,模型压根没机会「知道出错了、然后自己换个说法或换个参数」。
正确做法是:把「可预期的失败」在工具内部消化掉,返回一段人话给模型,让它去决定怎么办。只有那种真的没法继续的系统级异常,才往外抛。
区别在哪?第一种写法,用户看到冷冰冰的 500;第二种写法,用户看到的是模型顺着你的提示回一句「没有找到这个订单号,方便我再确认一下号码吗」。同一份数据、同一个模型,体验差出一个档次。这就是为什么我说,工具调用的质量,一大半是「错误处理」的质量。
◆坑 5:工具描述重叠,又没有步数上限,模型原地打转
最后一个坑,通常出现在「工具变多之后」。
两个问题叠在一起:一是工具描述彼此重叠——你有一个 `getOrderStatus`「查订单状态」,又有一个 `getOrderTracking`「查订单物流」,两个描述都写了「查订单」,模型面对用户一句「查下我的订单」就懵了,来回试;二是你没有任何轮数上限,模型拿到一样的结果还会接着调同一个工具,陷入死循环,token 哗哗烧。
解法有两个层面。工具层面:每个工具的描述必须划清边界,用「只在……时调用」把它的适用场景钉死。
工程层面:别让模型无限调。一是在业务侧给一次对话设个「最多调 N 次工具」的闸,超了就强制收口让它用已有信息回答;二是别一次给模型挂上几十个工具,工具越多,选错的概率越高——按业务场景分组,聊天入口只挂这一组要用的。我见过有人一口气挂 40 个工具,模型基本全程在那抽奖。
顺带说个认知:工具幂等也很重要。你把「创建工单」这种有副作用的工具暴露给模型,又没有幂等保护,模型一次重试就可能给你建两张工单。有副作用的工具,一定要能防重。
◆最小可跑代码(贴进 IDE 能编译)
前面 5 个坑讲完,给你一份把「正确姿势」全写进去的最小可跑版本。三个文件:工具类、组装 ChatClient 的服务、一个普通 Controller。
跑起来之后,命令行验证一下就行:
你会看到模型先调 `getOrderStatus`,拿到状态后再组织成人话回你。到这一步,工具调用就算真正跑通了——你后面做 Agent、做 RAG 里的「按需取数」,长的都是这根主枝。
◆依赖版本清单(照抄,别乱升)
版本冲突是 Java 人收藏一篇文章的第一个理由,我直接给你一组在 2026 年 10 月验证能跑的版本,照抄,别自己升。
| 组件 | 版本 | 说明 |
|---|---|---|
两个必须记住的铁律。第一,`@Tool` 的注解包名在 GA 版是 `org.springframework.ai.tool.annotation.Tool`,你在网上搜到的老博客如果写的是 M 版(M1–M6)的包路径,照抄必挂——认准 1.0.0 GA,见「M」字样的版本号绕道。第二,Spring AI 的大版本必须和 Spring Boot 对得上,1.0.0 认 3.4.x,你硬上 3.2 或 3.5 都可能踩到 API 微调,先在 release notes 里翻一眼 Breaking Changes,能省一下午。
◆一页速查表:@Tool 这套 API 的坑位对照
这张表把你 90% 会踩到的地方覆盖了,建议截图存下来。
| 你想干的事 | 怎么写 | 坑位(踩过) |
|---|---|---|
记住一句话:工具调用的坑,八成不在「模型聪不聪明」,在「有没有把方法名、描述、参数、返回值、异常这五样交代清楚」。你把工具当接口设计,模型才用得顺。
◆跑通之后别急着上生产
这篇给的是「能跑、能验证」的版本,生产里你还得补两件事:一是给工具加权限校验,别让模型调到你没授权的接口(模型的调用意图不可信,落库、扣款、改状态这类工具,必须再过一遍你后端的鉴权);二是给整条链路配超时和轮数上限,模型偶尔会「上头」,不设闸就会一直调下去烧你的 token。
最后说句掏心窝的:技术号我从来不看点赞。你愿意收藏,才是真觉得这些东西能抄走用。这篇的 5 个坑,你要是踩过其中任何一个,底下扣个字母,我下一篇照着你最容易栽的那个往深了写。
关于我:Java 讲师,一人公司实践中。知识星球「AI搞钱实验室」里整理了 100+ 个真实的 AI 搞钱案例和可复制的 SOP,68 元/年。

📊 本期投票
最后说句掏心窝的:技术号我从来不看点赞。你愿意收藏,才是真觉得这些东西能抄走用。这篇的 5 个坑,你要是踩过其中任何一个,底下扣个字母,我下一篇照着你最容易栽的那个往深了写
A:我想要一份能直接跑的 GitHub 模板(OrderTools + ChatClient 注册 + Controller)
B:我卡在「多个工具互相打架」,想看多工具怎么编排、怎么防模型选错
C:我卡在「工具返回值太大 / 上下文爆炸」,想看大结果怎么裁剪
👉 评论区扣字母 + 聊聊你的看法
本文数据来自公开资料,仅供参考,不构成任何投资建议。
作者:秋天
关注「程序员秋天」

扫码关注「程序员秋天」,完整版在星















































































