尧图精选

Spring AI工具调用实战:从订单查询到库存校验的完整落地指南

🕒 发布时间:2026/10/1 5:13:25 📁 来源:尧图网络
Spring AI 的工具调用Tool Calling这块我前后踩了小半个月的坑才算是真正玩明白。网上现在的资料基本都停在“Hello World”级别的示例定义一个加法工具、让模型算一下 11然后就没有然后了。但真实项目里压根不是这么回事——工具怎么描述才不会被模型忽略、参数怎么设计才不容易被解析错、多个工具之间怎么编排、调用失败怎么兜底这些才是真正决定能不能上生产环境的细节。这篇文章我不打算给你复述官方文档而是把我在一个餐饮 SaaS 项目里把 Spring AI 工具调用落地到订单查询和库存校验的完整过程拆开讲。你会看到工具调用的原理本质、为什么这么设计、实际编码时的要点以及我踩过的一堆坑。如果你正准备在自己的 Spring Boot 项目里接入 AI Agent或者正在纠结“到底用 Spring AI 还是 LangGraph4j”这篇文章应该能帮你省下不少时间。1. 工具调用是什么先把这块拼图的位置搞清楚1.1 从“对话模型”到“能动手的模型”先说个最基础的问题为什么需要工具调用大语言模型本质上是一个概率预测器它只能根据输入的 token 预测下一个 token。这意味着它有两个天然缺陷第一训练数据有截止时间它不知道当下这一刻的天气、库存、订单状态第二它没有权限执行任何操作不能真的去查数据库、调接口、发消息。早期要做 AI 应用只能靠提示词让模型“假装”知道或者把数据一股脑塞进上下文——前者是胡编后者是浪费 token而且上下文窗口有限。工具调用Tool Calling / Function Calling解决的就是这个问题让模型在推理过程中识别出“此刻需要外部信息或外部操作”然后输出一个结构化的调用请求由我们的代码去真正执行再把执行结果回传给模型让模型基于真实结果继续生成回答。用生活化的类比来说你请了一个知识渊博的助理但这个助理没有电话、没有电脑、不能出门。工具调用就是给他配上了这些“手脚”——他遇到不知道的事会写一张纸条递给你你去查完再把结果告诉他他基于结果给你最终答复。1.2 Spring AI 在这块做了什么Spring AI 是 Spring 生态官方推出的 AI 应用开发框架它做了一件很重要的抽象把各家大模型厂商的差异尽量抹平。你写一套工具定义OpenAI、通义千问、DeepSeek、Ollama 本地模型都能用只是底层适配不同。具体到工具调用Spring AI 的核心封装包括两层把 Java 方法自动转换成模型可识别的 JSON Schema 描述包括方法名、参数名、参数类型、注释说明在对话循环中自动处理模型的工具调用请求由框架调用你的方法再把结果放回对话上下文。你不需要手写繁琐的 JSON Schema也不需要自己维护多轮调用的消息历史拼接框架把这些脏活都干了。这点非常关键因为我一开始手写过 OpenAI 格式的 tools 定义那会儿光维护参数格式就够呛模型一换格式就变Spring AI 至少在格式适配层帮你把活揽下来了。1.3 为什么说它是 Agent 的基石没有工具调用的模型再聪明也只是个“键盘侠”有了工具调用模型才真正具备了执行能力。你看现在所有的 Agent 架构——无论是 LangChain 系的、LangGraph 系的、还是 Spring AI 自己的 Agent——底层核心都是这个模式用户输入 → 模型判断是否需要工具 → 发起工具调用请求 → 代码执行工具 → 结果回传 → 模型继续推理 → 循环直到给出最终答案这个“判断-调用-回传-再判断”的循环英文叫 Agent Loop。Spring AI 的工具调用就是 Agent Loop 的最小闭环。你先把这一层玩透了后面再去理解多工具编排、子 Agent、多步规划都会变得顺理成章。2. 设计思路什么场景用工具什么场景别硬凑2.1 工具调用、RAG、提示词三者的边界我见过不少团队一上来就堆工具把什么都塞给模型去调结果效果稀烂还费钱。其实工具调用不是万能的在动手之前你得先判断目标场景到底适合哪种方案。RAG检索增强生成解决的是“知识不在模型脑子里”的问题。比如企业内部的制度文档、产品手册、历史工单这些内容体量大、需要溯源应该走向量检索把相关片段检索出来塞进上下文而不是做成工具。因为把整本手册塞给工具参数显然不现实让模型“翻书”找答案也远不如向量检索精准。提示词解决的是“规则明确、不需要外部数据”的问题。比如让模型帮你润色文案、提取关键词、做情感分类这些纯粹靠模型自身能力就能完成工具调用反而是画蛇添足。工具调用适合的是“信息在系统里、操作由代码执行”的场景。典型的有查订单状态、查实时库存、查天气、发审批通知、调用第三方 API、执行数据库查询。一句话总结凡是需要实时数据或真实操作的才考虑用工具凡是知识性的、规则性的优先用提示词或 RAG。2.2 工具粒度的设计粗一点还是细一点工具设计是个典型的“度”的把握。粒度太细工具数量爆炸模型在多个工具之间犹豫不决还容易调错粒度太粗一个工具啥都能干参数一堆模型反而不知道该怎么填。我在餐饮 SaaS 项目里的经验是按用户的业务意图来划分工具而不是按后端接口来划分。比如后端可能有selectOrderById、selectOrderByPhone、selectOrderByShopId三个接口但如果做成三个工具模型就会纠结用户说的“帮我查一下订单”到底该调哪个。我更建议合并成一个工具叫queryOrder参数里给一个灵活的条件结构。同时在工具描述里写清楚当用户提到订单号时填写 orderId提到手机号时填写 phone提到店铺时填写 shopId。这样模型面对的是“业务动作”而不是“接口实现”调用准确率会明显提升。反过来也有反面案例。我最初做过一个工具叫executeSql意图是让模型直接写 SQL 查询数据库。听起来很灵活但实际用起来非常可怕——模型生成的 SQL 经常语法有问题甚至有超时风险。后来改成queryOrder、queryDish、queryInventory等几个语义明确的工具后稳定性大幅提升。2.3 工具描述的写作诀窍工具调用能不能成功很大程度上取决于你怎么写工具描述。模型看不到你的 Java 代码它只能看到你写在注解里的描述文字和参数说明。这段描述是模型做决策的唯一依据。我的写法参考了一个原则站在模型的角度描述触发条件。不要写“查询订单信息”这种干巴巴的话而要写“当用户询问任何与订单相关的问题包括订单状态、订单金额、配送进度、订单详情时使用此工具。如果用户提供了订单号请填入 orderId 参数如果提供了手机号请填入 phone 参数”。参数说明同样重要。比如status参数是枚举值你要把每个枚举值对应的含义写清楚。比如status0 表示待支付1 表示已支付2 表示配送中不然模型极有可能填一个它自己“认为”合理的值。我还习惯在工具描述里加上“不要使用此工具的场景”比如“当用户只是想要下单操作说明时不要调用此工具直接回复使用步骤”。这个负面排除项对降低误调用率非常有效。实测下来加上负面描述后工具误调用率能降低至少三成。3. 核心原理模型是怎么知道该调哪个工具的3.1 工具描述如何变成上下文抛开黑盒焦虑工具调用的内部机制其实不神秘。在每次请求模型之前Spring AI 会把所有已注册工具的信息——方法名、功能描述、参数名、参数类型、参数描述、是否必填——转换成 JSON Schema 数组混入系统消息里发送给模型。模型的注意力机制会阅读这些工具定义然后在生成回答时做两件事的判断当前用户的这个问题需不需要调用工具如果需要该调用哪一个、参数应该填什么当模型判断需要调用时它不会生成正常的文本回答而是输出一个结构化的工具调用指令格式类似{name: queryOrder, arguments: {\orderId\: \12345\}}。Spring AI 的底层适配器会解析这个输出识别出工具名和参数 JSON然后通过注册中心的映射找到对应的 Java 方法用反射把参数绑定进去执行。执行完毕后的返回值会被包装成一条ToolResponseMessage附加到对话历史里再次发给模型。模型看到“工具结果”后生成面向用户的最终答复。3.2Tool注解背后发生了什么Spring AI 最常用的方式是在方法上打Tool注解。这个注解的妙处在于它会读取方法签名、参数注解、以及参数对象里的字段描述自动生成 JSON Schema。比如这样写Tool(name queryOrder, description 查询订单信息当用户询问订单状态、金额、配送进度时使用) public String queryOrder(ToolParam(description 订单号用户提供时必填) String orderId, ToolParam(description 下单手机号) String phone) { // 业务逻辑 }Spring AI 会把name作为工具名description作为工具描述ToolParam的 description 作为参数描述。如果你的参数是一个对象类型Spring AI 会递归解析对象字段同样按照字段名和ToolParam描述生成嵌套的 JSON Schema。这一点对复杂参数的建模非常友好你不需要手动写任何 JSON 结构定义。这里有个容易被忽略的细节Tool也可以标注在类上表示这个类里所有带Tool注解的方法都注册为该类的工具集。我习惯把同一业务域的工具放到同一个类里比如OrderTools里放queryOrder和cancelOrderInventoryTools里放queryInventory和updateInventory这样注册和管理都清晰。3.3 注册方式与模型指定注册工具有两种常用方式。第一种是基于 Spring Bean 的自动发现只要Tool标注的方法所在的类是一个 Spring Bean并且你在ChatClient构建时调用了.defaultTools()或.tools()方法Spring AI 就会自动找出这些方法注册为工具。推荐用这种方式因为它把工具的发现和管理交给 Spring 容器依赖注入也方便。比如你的工具方法里需要注入OrderService、JdbcTemplate直接Autowired或构造器注入就好了模型每次调用都会走完整的 Spring Bean 生命周期。还有一点值得注意不同模型的工具调用能力有差异。OpenAI 系的模型gpt-4o、gpt-4o-mini工具调用非常稳定通义千问的 qwen-plus 也支持DeepSeek 的 Function Calling 也不错但是小参数量的本地模型比如 7B 以下的量化模型工具调用效果就很不稳定经常不按格式输出或者乱填参数。我的建议是如果你要做严肃的工具调用至少用 13B 以上或云端中大型模型本地小模型做玩具可以生产环境还是别冒险。4. 实操记录把工具调用接进 Spring Boot 项目4.1 配置环境和依赖我用的是 Spring Boot 3.2 Spring AI 1.0.0这套写法在 Spring AI 0.8.x 上也基本兼容只是个别 API 名有差异。Maven 依赖如下dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency如果你是接通义千问用spring-ai-tongyi-spring-boot-starter接 DeepSeek 的话DeepSeek 兼容 OpenAI 协议直接用 OpenAI starter 然后改 base-url 就行spring: ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7注意temperature 建议设置在 0.2~0.7 之间。工具调用内部的参数生成需要一定的稳定性温度过高容易导致模型在工具名和参数上“发挥创意”温度太低又会让模型过于保守甚至拒绝调用工具。我在实际项目中一般用 0.3~0.5 这个区间。4.2 从 ChatClient 到工具调用的完整代码Spring AI 1.0 之后的推荐用法是ChatClient链式调用非常爽快。我直接贴一个完整的示例这既是我在餐饮项目里实际使用的简化版也是你能直接跑通的最小可复现代码。先定义一个订单查询工具Component public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService orderService; } Tool(name queryOrder, description 根据订单号或手机号查询订单信息。 当用户询问订单状态、订单金额、配送进度、订单详情时使用此工具。 如果用户提供了订单号优先填入orderId如果只提供了手机号填入phone。) public String queryOrder( ToolParam(description 订单号例如 O20250601001) String orderId, ToolParam(description 下单时使用的手机号11位数字) String phone) { if (orderId ! null !orderId.isBlank()) { return JSON.toJSONString(orderService.getByOrderId(orderId)); } if (phone ! null !phone.isBlank()) { return JSON.toJSONString(orderService.listByPhone(phone)); } return 参数不足既没有订单号也没有手机号; } }然后构建 AI 服务Service public class AiOrderAssistant { private final ChatClient chatClient; public AiOrderAssistant(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个餐饮SaaS客服助手可以帮用户查询订单信息。) .defaultTools(queryOrder) .build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }注意.defaultTools(queryOrder)这一行传入的是Tool注解里定义的工具名。你还可以用.defaultTools(OrderTools.class)批量注册某个类下所有工具。启动项目后你调用这个接口输入“帮我查一下手机号 13800138000 的最近一笔订单”模型会先调用queryOrder工具拿到订单数据再组织语言回复。整个过程你完全不需要手动处理工具调用的中间状态框架都帮你搞定了。4.3 自定义工具返回结果不是所有返回都能直接给模型这个点是我踩坑之后才意识到的。工具方法的返回值最终会被塞回上下文再发给模型所以返回值必须满足两个要求字数要精简内容要结构化。最开始我偷懒直接把数据库实体类序列化返回结果一次查出来 200 多个字段的大 JSON模型读是读懂了但 token 消耗感人而且响应时间变长。后来我改成了专门为工具调用设计的精简 VO只返回模型真正需要的信息订单号、状态、金额、商品列表、配送地址、预计送达时间。对模型来说“够用”比“完整”更重要它能基于这些字段回答用户绝大部分问题。另外工具方法里做业务操作失败时不要直接抛异常给 Spring AI。因为异常一旦抛出整个对话就会中断模型没有机会做补充解释。更优雅的做法是返回一个错误描述字符串比如“订单号不存在请确认后重试”让模型基于这个结果组织给用户的回复。这样用户体验要友好得多。4.4 多工具场景下的顺序编排单工具是入门多工具才是日常。比如用户问“我想把订单 O20250601001 里的辣椒炒肉退掉顺便看看这家店还有没有奶茶”这里涉及查订单、查菜品、查库存三个工具的协同。Spring AI 的默认行为是一次请求里模型可以并行发起多个工具调用取决于底层模型是否支持并行调用。你不需要手动编排顺序模型会自己判断哪些工具可以同时调用。但这里有个隐含前提你的所有工具之间不能有强依赖关系。如果工具 B 的执行依赖工具 A 的结果那模型通常会先调 A看到结果后再调 B它会自动多轮循环。这也是 Agent Loop 的意义所在——模型自己控制调用的节奏。我在项目里给餐饮连锁店做过一个“复购推荐”场景就涉及三个工具queryUserOrders查历史订单、queryDishById查菜品详情、queryShopById查门店信息。模型在多轮里依次唤起这些工具最终给用户推荐了一套“根据你上次点的菜推测你会喜欢”的组合。这个效果只靠提示词永远做不到因为菜单信息是实时变化的必须靠工具去拉取。4.5 增加业务约束同一个工具材料不同结果就不同很多人忽略了工具方法里可以做权限校验。模型决定调用工具但工具内部可以校验“这个用户有没有权限查这笔订单”。我在工具方法里注入了当前登录用户的信息把 userId 通过上下文传入然后在queryOrder里先判断订单归属是否与当前用户一致不一致就直接返回“无权查看该订单”。这样做的好处是模型层不需要做任何权限逻辑它只负责理解用户意图权限这种硬规则写在代码里永远比让模型自觉可靠。这条经验来自一次真实的教训早期我没做权限校验模型只要知道订单号就能查任何订单。在一个内部测试环境里还好但到了生产环境一旦有用户拿别人的订单号来试探就泄漏了数据。这是个非常严重的安全问题。所以工具调用落地时一定要把权限、限流、审计都当作一等公民放进工具方法里而不是寄希望于模型。5. 常见问题与排查技巧实录5.1 工具没被调用模型直接编答案这是最最常见的现象。我排查的思路分三步。第一步检查模型是否能识别当前问题属于你的工具能解决的范畴。把用户的原始问题、你的工具定义都打印出来看看是不是因为描述太窄导致模型判断“不需要用工具”。比如店员问“我们这个店一天开几单”你的工具描述只写了“查询订单状态”那模型自然想不到该调它。解决方法是把工具描述和触发场景写得更宽泛一些。第二步确认工具是否真的被注册进去了。在构建ChatClient之后可以打印一下它实际注册的工具列表。Spring AI 支持通过ChatClient的配置来查看如果你不确定可以在工具方法里打一条日志调用时能看到。如果日志没出现说明模型压根没发起工具调用问题多半在描述或模型能力上。第三步看看是不是模型版本太弱。同一段代码我换用 gpt-4o 一切正常换用某个 7B 本地模型就完全不调用工具。这不是配置问题是模型本身能力不够。升级模型或者换更强的服务是唯一的出路。5.2 工具被调用了但参数解析错了最常见的问题是数字、日期、枚举这类参数被模型填了异常值。比如一个工具参数限定枚举值为PENDING、PAID、DELIVERING模型却填了已支付。这是因为模型只是从描述里“猜”参数格式它不是严格遵守者。我的解决方式有几个在ToolParam描述里把合法值的取值范围写全比如 “可选值PENDING待支付、PAID已支付、DELIVERING配送中”在工具方法内部做容错对枚举值多写一个匹配方法比如已支付.equals(status)也映射到PAID对简单场景用 Boolean 或 Integer 替代字符串枚举降低模型理解成本。还有一个细节如果你的参数对象里用了 LocalDateTime 这类格式模型的输出极有可能不符合 Java 的序列化格式。我建议所有工具参数和返回值都尽量避免复杂的时间格式统一转成字符串“2025-06-01 12:30:00”传输然后在方法内部解析。5.3 工具调用超时和性能问题工具方法里如果做了慢操作——查数据库、调外部 API、执行复杂的计算——整个对话的响应时间会被拖长。因为模型在等待工具返回结果的过程中用户是干等的。我的优化思路是工具方法内部能缓存就缓存能走索引就走索引。还有一个技巧把工具调用设计成“先返回关键信息再补充详情”的两阶段模式。比如queryOrder先返回订单的基本状态和金额模型基于这些信息先给用户一个大致的答复如果用户追要明细模型再发起queryOrderDetail获取完整数据。这样大部分请求都能快速响应。5.4 工具调用结果太长了模型反而不会总结这是我在双十一大促场景踩过的坑。外卖平台的订单量巨大用手机号查订单时返回了 50 条订单记录结果模型在回传时因为上下文过长生成的回复质量直线下降还经常截断。解决办法是在工具方法内做好“预聚合”和“限制条数”。比如只返回最近 5 条订单或者直接在 SQL 层做统计订单数、总金额、近一周趋势然后把统计结果返回给模型。模型更擅长解读总结好的数据而不是从一百行原始 JSON 里找规律。记住一个原则工具返回的应该是“加工后的结果”不是“原始数据”。5.5 Spring AI 还是 LangGraph4j用场景说话这个热搜词我看到了估计很多人都在纠结。我的看法很简单如果你的技术栈是 Spring Boot 全家桶而且你需要的工具调用复杂度是“模型根据意图调几个方法、做多轮循环”Spring AI 完全够用甚至是最省事的选择。它贴近 Spring 生态配置简单团队上手成本低。但如果你的场景是复杂的 DAG 编排、多智能体协作、有明确的状态机流转和人工介入节点LangGraph4j 这类流程框架更合适。它是把 LangGraph 的图执行概念移植到了 Java适合把每个 Agent 步骤拆成图节点精细控制流转和状态。我个人在餐饮 SaaS 项目里的经验是90% 的工具调用需求用 Spring AI 就够了而且它的抽象层让你以后换模型厂商不用改业务代码。剩下的 10% 才需要考虑上更重的 Agent 编排框架。还是那句话技术选型要跟着场景走别为了炫技上重型武器。5.6 NL2SQL 和 RAG 能不能结合起来Spring AI Alibaba 有nl2sql的能力就是让模型把自然语言转成 SQL。我试过之后发现一个规律直接用工具调用的方式包装 SQL 执行虽然灵活但风险高更稳妥的做法是把限定条件、表结构说明都放在工具描述里让模型只能基于白名单表来生成查询。比如我定义了一个工具queryStatistics描述里写明“当前可查询的指标有营业额、订单量、客单价可按时间范围、门店维度筛选”。模型只在这个指标白名单内生成查询条件不会自己去创造 SQL。这种有界面的 NL2SQL比让模型自由发挥安全得多。至于 RAG它和工具调用不是替代关系而是配合关系。比如用户在问“你们平台怎么处理退款”这种制度问题时走 RAG 检索手册在问“我这个订单退款到哪了”这种实时问题时走工具调用。我在项目里是把两者都注册到同一个ChatClient里模型自己判断该走哪条路。6. 从单工具到 Agent走向更复杂的能力编排6.1 把工具调用串成 Agent Loop当你理解了工具调用之后想往上再走一步就涉及 Agent 的完整循环。Spring AI 的ChatClient默认会做多轮工具调用循环但如果你需要更多控制——比如限制最大迭代次数、在每一轮之间插入日志、做结果校验并重试——就需要自己实现一个循环。我自己写过一个简单的循环核心逻辑大约是for (int i 0; i maxIterations; i) { ChatResponse response chatClient.prompt() .messages(messages) .call() .chatResponse(); if (response.hasToolCalls()) { // 执行工具调用 ListToolResponseMessage toolResults executeToolCalls(response.getToolCalls()); messages.addAll(toolResults); } else { // 模型给出了最终文字回复 return response.getTextContent(); } }这个模式虽然粗但能帮你理解 Agent 的本质在“思考”和“执行”之间反复横跳直到得出最终答案。Spring AI 内部其实也是类似逻辑只不过它帮你隐藏了循环细节。6.2 工具注册的边界什么时候该收敛工具的粒度不仅影响模型调用准确性还影响维护成本。我有个原则叫**“一个业务场景最多注册 5 到 8 个工具”**。一旦超过这个数模型面对一堆相似工具时就会出现张冠李戴的问题而且你很难通过描述把所有工具的边界讲清楚。如果业务确需要很多能力我建议把工具分组按会话阶段或用户意图加载不同的工具集。比如用户在“查单”阶段只注册订单类工具在“下单”阶段注册购物车和结算类工具阶段切换由你的业务逻辑把控而不是一股脑全塞给模型。这本质上是一种轻量级的“路由”能显著提升大规模工具场景下的准确率。6.3 从工具调用出发的后续扩展方向工具调用玩熟之后可以尝试的方向包括把工具结果接入业务系统的事件总线实现 AI 驱动的自动化操作比如模型识别到用户投诉自动创建售后工单结合定时任务 工具调用做主动式 AI 助手每天定时检查订单履约情况异常时通过工具下发通知把多个工具编排成可复用的“技能包”配合向量检索实现动态装配——模型先检索到合适的技能描述再动态加载对应的工具。这些其实都是 Agent 理论的工程化落地。归根结底工具调用就是 Agent 世界里“行动力”的具体实现。我在实际项目里还有个小技巧每个工具方法都写一个AuditLog之类的注解记录模型每次调用的入参和出参。这些日志在调试时价值极高——你能看到模型“为什么这么填参数”、哪一步出了问题。有个阶段模型老是查错店铺靠日志才发现是因为工具描述里没写清楚 shopId 和门店名称的对应关系改完描述准确率一下上来了。所以记住工具调用排错的第一步永远不是猜模型在想什么而是看日志。整体走下来Spring AI 的工具调用已经算很成熟了只要分清场景、克制设计、守住权限底线它能帮你做出真正“能干活”的 AI 功能而不是一个只会聊天的玩具。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →