ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI Alibaba实战:构建带工具调用的智能客服Agent

Spring AI Alibaba实战:构建带工具调用的智能客服Agent 先说结论Spring AI Alibaba 这个项目是 Spring 官方 AI 框架与阿里云通义千问模型体系的一次深度整合。我用它从零搭了一个带工具调用能力的客服 Agent整个过程比我预想的顺滑很多。这篇文章会直接从项目设计思路讲到代码落地的完整过程包括我在工具定义、上下文管理、模型响应解析上踩过的坑以及最后总结出来的排查方法。如果你已经会 Spring Boot 的基础用法又想找一个省心、不折腾、Java 生态内就能搞定的方式接入大模型能力这篇文章尤其适合你。1. 内容整体设计与思路拆解1.1 项目为什么选 Spring AI Alibaba而不是直接用 HTTP 调模型接口先回答一个很多人都会问的问题既然大模型厂商都提供了 HTTP API直接用 RestTemplate 或者 WebClient 调不就完了为什么还要引入一个框架我最初也是这么想的。但真正动手写的时候发现一个完整的 Agent 并不只是把用户的问题发给大模型这么简单。你至少需要处理三件麻烦事第一是对话上下文的维护。大模型本身是无状态的你每次调用都要把之前的对话历史拼好再发过去这个工作一旦手工做很快就变成灾难。你需要关心消息列表怎么存、怎么裁剪、多轮对话的格式怎么拼。第二是工具调用的协议解析。现在的 Agent 大多需要调用外部工具比如查订单、查天气、查数据库。模型会返回一个结构化的我想调用某某工具参数是什么的指令你需要解析这个指令、执行真正的工具方法、再把结果返回给模型。这一来一回的协议格式每家模型厂商还有细微差别。第三是模型厂商的切换问题。你写完一个版本老板说换一个更便宜的模型你要是直接写 HTTP 调用那基本上所有代码都要重写。Spring AI Alibaba 解决的正是这三个痛点。它提供了一套 ChatModel 的统一抽象你换模型厂商只需要换依赖和改配置它内置了 ChatMemory 来做对话上下文管理它通过 Tool 注解帮你把工具调用协议的解析和组装完全封装掉了。说白了框架帮你把那些每个项目都要重复写一遍的脏活累活干掉了。1.2 一个合格 Agent 的完整闭环在动手写代码之前我先把 Agent 的核心闭环画了一遍用户输入进来Agent 判断需不需要调用工具如果需要就生成一个工具调用指令执行完工具拿到结果后再把结果交给模型做最终回复。这个过程可能循环好几次直到模型认为信息足够了才直接输出给用户。这个判断-执行-再判断的循环是 Agent 智能程度的根本来源。它不是一次 prompt 就能完成的而是一个动态决策过程。Spring AI Alibaba 的 Agent 模型帮你把这一层串起来了你要做的就是定义清楚工具然后让模型自己去决定什么时候调用、调用哪个、参数是什么。举个例子用户问帮我查一下订单 A10086 的物流状态。模型收到这个请求后会先看自己有没有查询物流这个工具有的话就返回一个调用请求框架帮你执行工具方法拿到已签收这样的结果再把它和用户的原始问题一起交给模型模型最终生成您的订单已于昨天签收这样的自然语言回复。2. 核心细节解析与实操要点2.1 Spring AI Alibaba 到底给你提供了什么Spring AI Alibaba 作为一个 Spring AI 的增强实现它的核心组件可以分为这么几层模型接入层。它统一封装了多种大模型的接入方式你通过配置项就可以切换不同的模型供应商。比如用通义千问的 qwen-plus配置项写spring.ai.dashscope.chat.options.modelqwen-plus就行。如果你不想用阿里云的模型也有 OpenAI 兼容模式的接入方式。聊天记忆层。它提供了ChatMemory接口内置了InMemoryChatMemory这类实现。你可以在构建 ChatClient 的时候指定一个 advisor让它自动把历史消息塞进每次请求里。这里要特别注意默认的内存实现只在应用运行期间有效服务一重启上下文就没了如果要持久化需要自己实现接口。工具调用层。这是 Agent 能力最核心的部分了。你只需要在普通方法上加一个Tool注解框架就会帮你完成工具描述注册、参数 JSON Schema 生成、模型调用结果解析这一整条链路。这个对 Java 开发者来说真的非常友好你定义一个普通 Java 方法它就能变成一个 Agent 可以调用的手。Prompt 模板层。它兼容 Spring AI 的 PromptTemplate你可以用占位符的方式把用户输入、系统指令、参考数据动态拼进 prompt。模板文件可以放在 resources 目录下也可以直接写在代码里。2.2 三个关键的配置项直接影响 Agent 的智商配置这一块我最想提醒你的是下面这三个参数它们对 Agent 行为的影响比很多代码逻辑都大模型温度temperature。这个参数控制的是输出的随机性取值范围一般是 0 到 2。做客服问答、信息抽取这类需要确定性高的场景建议调到 0.3 以下如果是让 Agent 帮你写文案、做头脑风暴可以调到 0.8 左右。实测下来Agent 做工具调用决策的时候温度越低越不容易发疯。最大令牌数maxTokens。这是控制模型单次回复的最大输出长度。如果 Agent 要生成比较长的分析报告这个值要设大一点比如 2000 以上如果只是做简短的工单分类几百就够。设太小的后果是回复被截断JSON 解析直接失败。流式输出stream。Agent 的工具体验和流式输出天然是矛盾的。允许流式输出时模型的中间思考过程和工具调用过程都会被打断。如果你需要做比较复杂的多轮工具调用建议先关掉流式输出等整个链路跑通了再考虑优化体验。2.3 Maven 依赖引入的几个版本坑Spring AI Alibaba 的版本号比较特殊它不完全是跟着 Spring Boot 的版本走的。我第一次搭建的时候直接用了 Spring Boot 3.2.0 加上最新版本的 spring-ai-alibaba-starter结果启动直接报 Bean 创建异常。后来查文档才发现特定版本的 Spring AI Alibaba 对 Spring Boot 的版本有明确要求。我稳定的配置是这样的Spring Boot 3.2.4Spring AI 版本 1.0.0Spring AI Alibaba 版本 1.0.0.2。你们在搭的时候一定先去官方文档确认一下兼容矩阵避免一上来就踩版本坑。另外DashScope 的 API Key 建议配置在环境变量里不要直接写死在 application.yml 里特别是代码要提交到 Git 仓库的情况下。这不仅是个好习惯更是安全底线。3. 实操过程与核心环节实现3.1 项目初始化与基础依赖配置第一步还是创建一个标准的 Spring Boot 项目。我用的是 IntelliJ IDEA 直接初始化Java 版本选的 17Spring Boot 版本 3.2.4。构建工具用的 Maven原因无他就是公司环境里 Maven 是标准配置团队协作时大家都不用额外学 Gradle。pom.xml 里需要引入这几个核心依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.4/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0.2/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies需要注意一个细节Spring AI Alibaba 的野心很大它会自动配置很多内容包括消息转换器、工具上下文等。如果你的项目中还有其他 AI 相关的依赖最好检查一下是否有 Bean 冲突。我见过有人同时引入了 spring-ai-openai 和 spring-ai-alibaba结果模型调用指向完全混乱了。3.2 application.yml 的完整配置示例我的配置文件长这样spring: application: name: ai-agent-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.2 max-tokens: 2048 chat: memory: enabled: true size: 20这里面spring.ai.chat.memory.size20是控制聊天记忆轮数的我默认设为 20 轮实测对大多数日常客服场景够用。如果设太大会增加 token 消耗设太小则 Agent 容易失忆。这里要特别提醒一下如果你是在本地开发调试没有配 DASHSCOPE_API_KEY 环境变量服务是起不来的。你可以临时在 IDEA 的 Run Configuration 里加一下 Environment variables或者直接在配置里写临时值但千万别往 Git 里提交。3.3 构建可解析 Java 方法的智能体核心服务接下来是重头戏。我设计了一个 AgentService 类它的职责是接收用户消息通过 ChatClient 调起大模型把工具列表注册进去让模型自行决定是否调用。先看工具类的定义。我用Tool注解来实现 Agent 调用真实业务服务的能力Service public class OrderToolService { private static final Logger log LoggerFactory.getLogger(OrderToolService.class); Tool(name query_order_status, description 根据订单号查询订单的最新物流状态和签收时间) public String queryOrderStatus(String orderId) { log.info(调用工具查询订单状态, orderId{}, orderId); // 这里模拟调用真实的订单服务 if (A10086.equals(orderId)) { return 订单已签收签收时间为2025年1月15日 14:23; } return 未查询到该订单信息请确认订单号是否正确; } Tool(name cancel_order, description 根据订单号取消未发货的订单) public String cancelOrder(String orderId) { log.info(调用工具尝试取消订单, orderId{}, orderId); return 订单A10086已成功取消退款将在3个工作日内原路返还; } }这里我最想说的是Tool注解的 description 字段。很多人不重视这个字段随手一写实际上它就是告诉大模型什么情况下应该调用这个工具的说明书。描述写得越精确模型判断就越准。比如你写查询订单状态模型可能在某些闲聊场景下也去调用但你写成根据订单号查询订单的最新物流状态和签收时间模型就能准确判断出这不是闲聊场景该干的事。再来看 AgentService 的完整实现Service public class AgentService { private final ChatClient chatClient; public AgentService(ChatClient.Builder builder, OrderToolService orderToolService) { this.chatClient builder .defaultSystem(你是一个智能客服助手负责处理用户的订单查询和售后服务问题。回答要简洁准确工具调用结果要完整转述给用户。) .defaultTools(orderToolService) .build(); } public String chat(String userId, String message) { return chatClient.prompt() .user(message) .call() .content(); } }注意到我没有在这个服务里手动做上下文管理。原因是在配置文件中启用了 ChatMemoryChatClient 会自动按 sessionId 维护对话历史。如果你需要区分不同用户可以在 prompt 里加.user(() - message)的同时指定.advisors(a - a.param(chatId, userId))这样每个用户就有独立的对话记忆空间了。否则所有用户共享一份上下文那就会闹出张三问的问题李四能看到上下文的乌龙。3.4 编写一个测试控制器验证整个链路为了验证 Agent 是否真的能自己决定调用工具我写了一个简单的 ControllerRestController RequestMapping(/agent) public class AgentController { private final AgentService agentService; public AgentController(AgentService agentService) { this.agentService agentService; } GetMapping(/chat) public String chat(RequestParam String userId, RequestParam String message) { return agentService.chat(userId, message); } }启动服务后我用 curl 做了几次测试curl http://localhost:8080/agent/chat?userIdu001message帮我查一下订单A10086的物流状态模型返回您查询的订单 A10086 已签收签收时间为2025年1月15日 14:23。第二次测试curl http://localhost:8080/agent/chat?userIdu001message把这个订单取消掉模型返回订单 A10086 已成功取消退款将在3个工作日内原路返还。这整个过程我没有写任何 if-else 分支去判断用户意图是模型自己根据工具描述选择了合适的工具并且正确解析了参数。这就是框架带给你的核心价值。3.5 集成 NL2SQL 工具让 Agent 能查数据库热词里频繁出现 spring ai alibaba nl2sql这确实是 Spring AI Alibaba 非常有价值的使用场景。我额外加了一个工具方法让 Agent 能根据用户的自然语言问题自动生成 SQL 并查询数据库。Tool(name query_database, description 根据用户问题生成SQL查询数据库返回查询结果集合的JSON字符串) public String queryDatabase(String question) { // 构造一个简化的提示词让大模型帮助生成SQL String sqlPrompt 请根据下面的数据库表结构针对用户问题生成一条SQL查询语句。 表结构orders(id, user_id, order_no, amount, status, created_at) 用户问题%s 只输出SQL语句不要输出其他内容。 .formatted(question); String sql generateSql(sqlPrompt); log.info(生成SQL: {}, sql); // 使用 JdbcTemplate 执行SQL并返回结果 ListMapString, Object result jdbcTemplate.queryForList(sql); return JSON.toJSONString(result); } private String generateSql(String prompt) { return chatClient.prompt() .user(prompt) .call() .content(); }整个流程走下来用户直接对 Agent 说查一下昨天下了多少笔订单Agent 就会自己生成 SQL、执行查询、把结果返回。这个能力对内部数据看板、运营后台这种系统的改造非常有想象空间。不过要注意让大模型生成 SQL 然后直接执行是存在安全风险的。生产环境一定要做 SQL 白名单校验至少也要限制为只读操作。我这里的演示代码没有做这些防护真实项目中必须加上。4. 常见问题与排查技巧实录4.1 模型不调用工具满口胡说怎么办这是我遇到最多的一个现象。模型面对一个明显需要查询数据库或调用工具的问题却用我无法获取实时数据这样的回答糊弄用户。排查下来原因通常是这几个工具描述写得太模糊。如果你的 description 只写了查询订单模型可能不知道这个工具应该用在哪个具体的环节。我的做法是把工具描述写得像需求文档一样清楚包括入参是什么、返回值是什么、什么场景下调用。另外一个技巧是给工具方法起一个动词名词风格的名字比如queryOrderStatus而不是process1这对模型的意图判断很有帮助。系统提示词没有给模型明确的指令约束。我在 System Prompt 里明确加了当用户询问订单相关问题时你必须调用工具获取信息不要凭空编造。不要低估这句话的作用显式指令比隐式推断的效果好得多。4.2 工具调用了但模型输出的内容还是胡编的工具执行成功数据也拿回来了但模型在组织最终回复的时候仍然没有完全基于工具结果而是夹杂了一些自己脑补的内容。这个问题的根源在于工具调用结果在发给模型的时候prompt 拼接可能出问题了。后来我调整了工具方法返回的文案让它不仅是机器结果而是可以直接作为答案给用户的话术。比如工具返回的不是一个对象 JSON而是订单A10086已签收签收时间为2025年1月15日 14:23。这样模型在组织语言时更倾向于直接使用工具返回的内容而不会另起炉灶编造。4.3 上下文越长越乱Agent 前后矛盾我测过一个比较极端的场景让用户连续问了二十几个不同的问题结果模型到后面开始把前面订单的状态弄混了。排查下来的原因有几个方面。一是单轮对话里塞了太多历史消息超过了模型的有效关注窗口。我的解决方法是结合 ChatMemory 的 size 参数控制历史消息的条数同时可以通过 PromptTemplate 把历史消息做摘要压缩。二是缺少 chatId 的隔离我在最初测试的时候没有传 chatId导致所有用户的请求都在共享同一个上下文窗口。加上.advisors(a - a.param(chatId, userId))之后问题就解决了。4.4 流式输出和工具调用打架这是一个非常典型的问题。一旦你用了流式输出模型因为要边生成边输出工具调用过程的中间状态就会被打断甚至出现工具根本不被触发的情况。我后来统一改成先关闭流式等服务内部的 Agent 链路完全跑完拿到完整结果之后再对外做一次性输出。如果你确实需要打字机效果建议仔细阅读文档确认你用的模型供应商对 streaming 模式下工具调用的支持程度实测下来不同模型差异很大。4.5 问题排查速查表现象可能原因解决办法Agent 不调用任何工具工具描述太模糊System Prompt 缺少指令约束重写工具 description在系统提示词中显式要求调用工具调用工具后回复内容胡编工具返回的文本不够人性化让工具方法直接返回可直接播报的话术上下文混乱、前后矛盾未按用户隔离 ChatMemory历史条数过多按 chatId 隔离调低 memory size工具调用和流式输出冲突流式输出打断决策链路关掉流式或选用兼容工具调用的模型版本启动报 Bean 创建异常Spring Boot 与 Spring AI Alibaba 版本不匹配确认版本兼容矩阵锁定稳定版本组合4.6 日志排查技巧工具调用的调试很大程度上依赖日志。我在application.yml里把 Spring AI 相关包的日志级别调成了 DEBUGlogging: level: com.alibaba.cloud.ai: DEBUG org.springframework.ai: DEBUG打开 DEBUG 日志后你能看到模型返回的原始响应结构包括工具调用的具体参数。这个信息比什么调试工具都管用。遇到工具调用语义不对的问题先看原始响应里模型到底想调用什么就知道是工具描述的问题还是模型本身的问题了。5. 总结一下这套方案的扩展空间整套代码下来核心依赖就一个 starter核心代码就三个类工具类、服务类、控制器。但它已经具备了一个 Agent 的基本能力能理解用户意图能自己决定调用哪些工具能基于工具结果给出最终回复。我个人实测下来最明显的感受是Spring AI Alibaba 把 Agent 开发的复杂度降到了一个 Java 开发完全不用焦虑的水平。传统的 Agent 框架动不动就让你学一堆新概念配料表一样长的依赖而在 Spring 生态里你只要关注业务逻辑本身就好。后续想在这个基础之上继续深入可以做这么几件事一是让工具方法连接真实的 RAG 知识库让 Agent 能基于私有文档回答问题二是把工具调用链加长让多个工具组合完成复杂业务比如查订单、算价格、生成优惠券一体化完成三是为生产环境接入持久化的 ChatMemory比如用 Redis 存储历史消息让 Agent 在服务重启后依然记得老用户之前的对话。这几个方向后续我会逐个分享实操过程欢迎一起折腾交流。
返回列表