news 2026/11/4 7:12:36

Java 后端 AI 转型实录 Day6:Spring AI1.1.0 Tool‑Calling实战@Tool注解、反射调用、会话记忆生产踩坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 后端 AI 转型实录 Day6:Spring AI1.1.0 Tool‑Calling实战@Tool注解、反射调用、会话记忆生产踩坑

Java 后端 AI 转型实录 Day6:让大模型调用我的 Java 方法|深挖 Spring AI Tool‑Calling 底层反射机制

AI 转型系列・Day6

很多人跑通 Tool‑Calling 就觉得完事,但很少有人去深究框架背后到底发生了什么。
今天不仅完成 Spring AI 1.1.0 Tool‑Calling 完整跑通,还通过javap -c反汇编字节码,拆解完整调用链路,厘清记忆共享、参数编译开关这些极易踩坑的生产隐患。全部为实测结论,可以直接复制代码使用。

开工前置校验:先确认模型是否真的支持工具调用

遵循本周实战准则:不要直接上手写业务代码,先验证模型本身能力。
我没有直接写 Spring 代码,先用 Ollama 原生 Chat 接口做能力探针,模拟工具调用入参,提前确认 qwen3.8:27b‑mlx 是否可以输出标准tool_calls结构。

curl http://localhost:11434/api/chat -d '{ "model":"qwen3.8:27b-mlx", "messages":[ { "role":"user", "content":"帮我查询订单10086物流" } ], "stream":false }'

实测得到 3 个关键信息:

  1. 模型可以输出标准tool_calls,不是文本假装函数调用,模型能力没问题,不需要做降级兼容方案;
  2. 参数抽取效果稳定,用户输入订单10086,可以正确解析出参数{"orderId":"10086"};
  3. 模型返回携带thinking推理字段,后续日志看到该字段属于正常现象,不必恐慌。

💡重要:Tool‑Calling 会发起两轮大模型请求,这个耗时是正常现象,不要误判为程序卡死。

这一步属于预判验证,不是线上踩坑。写博客要实事求是,不能把前期验证包装成踩坑事故。

30 分钟跑通最小 Tool‑Calling 示例

定义工具 Bean,使用@Tool注解声明可供大模型调用的 Java 业务方法。

重点:description描述文本质量直接决定模型会不会、什么时候调用这个函数。

importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.stereotype.Component;@ComponentpublicclassOrderTools{@Tool(name="getOrderStatus",description="根据订单号查询订单的物流状态。当用户询问订单物流、快递进度、货到哪了的时候调用该工具。入参为订单号字符串。")publicStringgetOrderStatus(StringorderId){returnswitch(orderId){case"10086"->"已发货,顺丰 SF1234567,预计明天送达";default->"订单号不存在,请核对订单编号";};}}

编写 Controller,.tools()传入工具 Bean,完成最小 Demo 接口:

importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/api")publicclassToolChatController{privatefinalChatClientchatClient;privatefinalOrderToolsorderTools;publicToolChatController(ChatClientchatClient,OrderToolsorderTools){this.chatClient=chatClient;this.orderTools=orderTools;}@GetMapping("/chat/tools")publicStringchatWithTools(@RequestParamStringq){returnchatClient.prompt().user(q).tools(orderTools).call().content();}}

测试两条 case:

# 场景1:命中工具,模型主动调用getOrderStatus curl -sG "http://localhost:8080/api/chat/tools" --data-urlencode "q=我的订单10086到哪了" # 场景2:和工具业务无关,直接闲聊,不触发工具调用 curl -sG "http://localhost:8080/api/chat/tools" --data-urlencode "q=今天天气如何"

现象:

  • 查询订单:模型识别意图,调用 Java 工具,拿到模拟物流结果再整理自然语言返回;
  • 询问天气:不触发任何工具,直接返回对话结果。

模型是否调用工具完全自主决策,决策依据来源于我们写的description描述。

Demo 跑通只是开始,真正容易埋坑的都藏在框架内部。下面抛出三个问题,全部通过字节码反汇编找答案。

三问底层:跑通 Demo 之后再深挖

Q1:.tools(orderTools)是怎么执行到我写的 Java 方法?

很多初学者会误以为.tools()这行就直接执行了方法,其实完全不是。

结论:.tools()仅仅做注册登记,不会执行业务逻辑;真正执行我们业务方法依靠 JDK 反射Method.invoke()。是否调用、调用哪一个,决策权交给大模型,方法执行发生在本地 JVM 内部。

我反编译了spring‑ai‑model、spring‑ai‑client‑chat、spring‑ai‑ollama三个 Jar 包,完整调用链路梳理如下:

.tools(orderTools)└─ToolCallbacks.from(orderTools)└─MethodToolCallbackProvider.builder().toolObjects(obj).build().getToolCallbacks()扫描类内部所有@Tool注解方法,封装成MethodToolCallbacktoolMethod :被@Tool标记方法的反射Method对象 toolObject :Spring容器中的Bean实例 toolDefinition :组装name、description、入参JSONSchema[第1次向大模型发起请求]把工具的name、描述、参数Schema随Prompt一起传给模型[大模型侧决策返回]返回ToolCall,携带工具名称、JSON格式入参参数[SpringAI框架内部执行,DefaultToolCallingManager主循环]· 打印DEBUG日志:Executingtool call:xxx · 根据工具name匹配对应的MethodToolCallback;匹配不到直接抛出IllegalStateException,不会静默失败 · callback.call(arguments):1.将工具返回JSON解析为Map2.通过反射拿到方法形参名,从Map取出对应参数值3.做类型转换,调用 toolMethod.invoke(toolObject,args)👉 执行我们自己写的业务方法[第2次向大模型发起请求]将工具执行结果追加进对话历史,交给大模型生成最终自然语言回答

对于 Java 后端开发者,这套模式非常熟悉,可以类比 SpringMVC:

表格

Spring MVCSpring AI Tool‑Calling
@RequestMapping@Tool
HandlerMapping 根据 URL 匹配处理器根据工具 name 匹配 MethodToolCallback
HandlerAdapter 通过反射执行目标 Controller 方法Method.invoke () 执行 @Tool 标记的业务方法
用户浏览器输入 URL大模型输出工具名称与参数

本质逻辑没有创新,唯一变化:选择执行哪个函数的权力,交给了大模型输出的结果。
也正是这个原因,@Tool里面 description 写得好不好,直接决定整个工具调用成功率。

Q2:接口没有写任何记忆代码,连续提问为什么还保留上下文?

测试中发现,连续多次请求/api/chat/tools,模型可以记住上一轮对话内容,但 Controller 接口代码中,完全看不到任何会话记忆相关代码。

结论:会话记忆不是绑定在单个接口上,而是绑定在全局单例ChatClientBean 上。我们之前配置的MessageChatMemoryAdvisor对使用这个 Bean 的所有接口全部生效。

大模型本身是无状态,不存在 “记忆”。每一次请求到来,Advisor 会从存储器把历史会话全部读取出来,拼接进 Prompt 再传给模型。

梳理MessageChatMemoryAdvisor内部执行逻辑:

  1. 请求执行前 (before):根据conversationId读取历史消息,把历史消息合并到当前 Prompt;同时把当前用户消息存入存储。

⚠️注意:用户消息在大模型还没返回结果前就入库。如果中间工具调用发生异常,数据库会残留只有用户提问、没有 AI 回复的 “孤儿消息”。

  1. 请求执行后 (after):拿到大模型最终回答,把 assistant 回复存入会话存储。

框架防御性设计:工具调用中间过程(tool_calls、工具返回数据)不会存入会话记忆,只会保存最终用户‑assistant 对话。如果把中间工具消息持久化,下一轮提交给 Ollama 会直接报 400 参数错误。

几个实测得到的重要细节:

  1. 如果不传conversationId,默认使用字符串default作为会话 ID;
  2. 生产重大隐患:多个不同用户不传递 cid,全部共用 default 会话池,发生会话互相串数据,用户隐私会互相泄露!
  3. 当前使用 H2 内存数据库,应用重启,全部会话记忆直接丢失。

小实验:在/api/chat接口说一句暗号,再访问/api/chat/tools接口,模型可以读取到之前对话;只有传入不同的conversationId,会话才会彻底隔离。

Q3:ChatClientConfig 配置类什么时候执行?

结论:仅在 Spring 容器启动、Bean 实例化阶段执行一次。后续每一次 HTTP 请求不会再次执行配置类逻辑。

每次调用chatClient.prompt()会生成一个全新请求级别的 Spec 对象,.tools()、临时参数全部挂载在这个临时对象上,请求结束就销毁。

表格

对象创建时机生命周期
chatClient、chatMemory、AdvisorSpring 容器启动全局单例,整个进程复用
prompt () 返回的 Spec、.tools () 注册的临时工具每一次 HTTP 请求请求级别,请求完成就销毁

这也解释了之前踩坑:修改@Bean配置、yml 配置之后,必须重启服务。旧的单例对象依旧存活在 Spring 容器,不重启代码修改不会生效。

Tool‑Calling 高频踩坑清单

表格

现象解决方案
网上搜到FunctionCallback相关代码编译报错Spring AI1.1.0 已经重构工具调用体系,优先使用@Tool注解 + ToolCallback,复制代码前核对 Jar 包版本签名
工具调用整体响应很慢,误以为程序卡死Tool‑Calling 会两轮调用大模型;首次模型加载会额外增加耗时,热态 5‑8s 属于正常区间
工具名称匹配失败框架直接抛出IllegalStateException,不会静默返回错误结果;核对@Tool的 name,和模型返回工具名称保持一致
工具拿到的参数全部为 null,参数匹配异常方法形参名依赖编译时‑parameters编译开关。spring‑boot‑starter‑parent 默认开启;Gradle、Ant 构建默认关闭,关闭之后方法参数名全部变成arg0,参数解析直接失效。这个坑排查难度极高。
会话互相串消息,多用户上下文混乱业务场景下必须传入独立 conversationId 做会话隔离,禁止依赖 default 默认会话;持久化不要使用 H2 内存库。

Day6 实战总结

  1. Demo 跑通只是入门门槛。调用一行.tools()人人都可以做到,但弄懂框架内部发生了什么,才是普通 API 使用者和 AI 应用开发工程师的分水岭。今天三个问题,很多是写 Demo 不会遇到,上线生产才会爆发的隐患。
  2. javap / javap -c反汇编校验这套工作方式继续发挥价值。AI 框架文档更新往往滞后版本,看字节码,比网上教程、AI 生成代码更加可靠。
  3. Spring AI 的 Tool‑Calling 没有很多想象中的黑魔法。本质:注解扫描 + 名称匹配 + JDK 反射执行,全是 Spring 生态传统技术;唯一变化,调用哪个方法的决定权交给了大模型。把新技术拆解成熟悉的老技术,学习成本会大幅降低。

下一篇预告:多工具编排实战,注册多个 @Tool 工具,观察模型如何自主选择;做一组 description 删减对照实验,验证工具描述文本对调用成功率的影响。


本文是「Java 后端 AI 转型实录」系列第 6篇。项目后续开源在 GitHub(链接见主页),每周更新。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 7:11:11

RK3588算力扩展方案,近期有点火!

边缘AI项目最头疼:设备快要定型了,突然新增算力需求。多路相机、更大模型、更高帧率,如果主控算力不够,就要改板、重走测试认证,很可能多出两三个月的工作量。创龙科技RK3588工控机,靠M.2 Key M接口外接AI算…

作者头像 李华
网站建设 2026/11/4 7:11:11

AI大模型应用开发工程师:小白也能入局的高薪赛道,速收藏

AI行业风向转变,重心从“造模型”转向“用模型”。AI大模型应用开发工程师应运而生,核心是用现成大模型做二次开发,解决实际问题。该岗位需求旺盛,薪资高,技能门槛相对较低,是普通人入局AI行业的最佳机会。…

作者头像 李华
网站建设 2026/11/4 7:10:57

流程全绿,工件精度却对不上?先排查这几个隐形断点

工业现场最让人头皮发麻的场景,不是机器冒烟报警,而是所有指示灯都绿着,生产流程一秒不差地跑完了,结果卡尺一量:尺寸超差。复测两次,第一次偏左0.1毫米,第二次偏右0.05毫米,第三次居…

作者头像 李华
网站建设 2026/11/4 7:09:13

【共创稿事节】HarmonyOS 7.0 Grid网格布局:从接入到生产验证

状态管理系列讲了七个装饰器(State/Prop/Link/Provide/Inject/AppStorage/Observed),这篇补上最后一块拼图:Environment(系统环境变量注入)和跨页面状态架构的整体设计。Environment 解决的是"系统设置…

作者头像 李华
网站建设 2026/11/4 7:04:17

Listen1 音乐聚合插件:一个搜索框搜遍 7 个平台,免费开源

Listen1 音乐聚合插件:一个搜索框搜遍 7 个平台,免费开源 【免费下载链接】listen1_chrome_extension one for all free music in china (chrome extension, also works for firefox) 项目地址: https://gitcode.com/gh_mirrors/li/listen1_chrome_ext…

作者头像 李华