Spring AI Alibaba 实战:多模型、工具调用、RAG 与 Agent 编排全解析
这篇是 Spring AI Alibaba 实战的下篇接着上篇那个能跑通的 HelloWorld 往后聊。上篇的内容不算多就是把依赖配好、模型服务连上、让 ChatClient 回一句话。但真正从 Demo 走到业务你会发现事情远没有那么简单一个应用里可能要用好几个模型模型得会调服务端的工具有些回答还必须返回固定结构的 JSON再加上知识库和 Agent 的编排随便一环出问题得到的结果就会很不靠谱。这篇就是围绕这几个点把我实际踩过的坑和验证过的方案整个捋一遍。适合已经跑通快速开始准备把 Spring AI Alibaba 用到真实项目里的同学。如果你连 ChatClient 都还没调通建议先回头看上篇否则后面配置里的很多概念会显得很跳。接下来我尽量用“一段代码加一段解释”的方式不讲空理论全是实打实验证过的操作。1. 决定能不能上线的三个核心能力1.1 上篇留下的问号上篇的 Demo 让模型回了一句“你好呀”看起来挺顺利但它本质上只是把一段文本发给模型再把模型输出打出来。真实的业务场景里我们真正需要解决的是三个问题第一对话之外模型如何访问到业务系统里的实时数据第二模型返回的字符串如何直接变成下游能消费的 JSON 对象第三当需要给模型加记忆、加知识库、加多步任务时代码结构要怎么写才不容易崩。很多刚接触 Spring AI Alibaba 的人会有种错觉觉得它就是一个 HTTP 客户端封装调一下接口就完事了。实际上这个框架把“模型能力”和“业务能力”之间那条缝补上了不少。比如工具调用你只需要定义一个普通方法加个注解模型就能在你允许的范围内“使用”这个方法像给一个刚入职的新员工配了内部系统账号一样它自己去查、自己去算然后给你结论。再比如结构化输出你告诉它“我要一个 UserInfo 对象”它就会尝试按字段填数据而不是甩给你一段需要正则解析的原文。1.2 这篇的适用场景与阅读建议如果你是做架构评审、技术选型或者正在写一个 AI 功能的方案这篇也能帮上忙。里面涉及的不只是 API 怎么调更多的是边界怎么划分哪些活应该让模型自己判断哪些必须由代码兜底知识库的召回到底应该信多少Agent 的步数怎么限制。这些直接决定上线之后是省心还是修 Bug 修到怀疑人生。读的时候我建议你先把代码跑通再回来看注意事项。代码部分我用的是 Spring AI Alibaba 当前主分支的常见写法不同版本之间方法名可能会有调整但思路基本一致。后面每一章我都会先说做法再解释“为什么这么做”最后把容易踩坑的点单独挑出来提醒。2. 多模型接入与运行时切换绕开 Bean 冲突是关键2.1 为什么一个应用里要放多个模型我在实际项目里很少只用一个模型。原因很直接不同任务对模型的要求完全不同。抽标题、做分类这种任务结果相对固定用轻量模型就够了速度快、成本低深度问答、复杂 Agent 推理则必须上效果更好的大模型不然答案质量没法看。如果所有流量都走同一个最强模型成本会飞速上涨响应时间也扛不住。Spring AI Alibaba 把模型能力抽象成了ChatModel接口。它是整个大模型调用的入口你可以像操作一个 Service 一样去操作它。默认情况下框架会根据你引入的依赖自动创建一个ChatModelBean。但如果你需要多个模型就不能只依赖默认装配了得手动把不同配置的ChatModel分别注册成 Bean然后按业务场景选择。一个比较清晰的配置方式是在配置类里给每个模型起一个业务名字。示意代码如下Configuration public class MultiModelConfig { Bean(lightChatModel) public ChatModel lightChatModel(ChatModelBuilder builder) { return builder .defaultOptions(ChatOptions.builder() .model(light-model) .temperature(0.2) .maxTokens(1024) .build()) .build(); } Bean(heavyChatModel) public ChatModel heavyChatModel(ChatModelBuilder builder) { return builder .defaultOptions(ChatOptions.builder() .model(heavy-model) .temperature(0.8) .maxTokens(2048) .build()) .build(); } }这里我没有写具体服务商相关配置因为不同模块需要的 key 和地址不一样但整体套路就是这样通过 Builder 设置模型名和默认参数再把构建好的对象注册为 Bean。注意不要直接定义两个普通ChatModelBean 然后不指定名字Spring 启动时会因为不知道注入哪个而报NoUniqueBeanDefinitionException这个错我在搭建环境时碰到过好几次。2.2 按业务场景做运行时路由有了多个 Bean 之后下一步是怎么在请求里动态选择模型。最简单的方案是用MapString, ChatModel注入Spring 会把容器里所有ChatModel类型的 Bean 放到这个 Map 里key 就是 Bean 名称。调用时根据业务参数取出对应的那个模型取不到就落到默认值上。RestController public class ChatController { private final MapString, ChatModel modelMap; public ChatController(MapString, ChatModel modelMap) { this.modelMap modelMap; } PostMapping(/chat) public String chat(RequestBody ChatRequest req) { ChatModel model modelMap.getOrDefault(req.modelKey(), modelMap.get(lightChatModel)); if (model null) { throw new IllegalArgumentException(modelKey 不存在); } return model.call(new Prompt(req.message())) .getResult() .getOutput() .getContent(); } }这里有两个细节。第一Bean 名称来自配置类里的方法名所以Bean(lightChatModel)里的名字一定要和业务约定对齐别搞个modelA、modelB过几天自己都忘了是干嘛的。第二不要想当然用modelMap.get一旦前端传了不存在的 key直接空指针很难看。先getOrDefault再判断或者干脆在 gateway 层做白名单校验。如果你需要更灵活的路由规则比如根据用户等级、当前并发数、费用策略来决定走哪个模型可以把这段逻辑封装成一个ModelRouterService对外只暴露chat(routerKey, prompt)方法。这样上层就不需要知道模型列表的细节。2.3 temperature、maxTokens 怎么调才合理模型参数里最容易纠结的两个是temperature和maxTokens。我的经验是凡是需要“确定性”的任务比如实体抽取、意图分类、关键词提取temperature调到 0.1~0.3越低越好没特殊需求别超过 0.5。而需要发散、生成文案的任务可以调高到 0.8 左右但不要超过 1否则模型会开始自由发挥甚至胡编。maxTokens这个参数很多人一开始不设置结果模型输出到一半被截断接口返回的内容不完整。我建议提前估算如果要求生成一段 2000 字左右的周报maxTokens至少给到 2500 以上因为中文状态下每个 token 对应字数并不固定保守一点更稳。框架返回结果里有个finishReason如果它等于length就代表输出被上限截断了。代码里可以针对这个做处理要么换更大上限的模型要么给用户提示“生成内容过长”。var response chatModel.call(new Prompt(req.message(), ChatOptions.builder().maxTokens(2048).build())); String finishReason response.getResult().getMetadata().getFinishReason(); if (length.equals(finishReason)) { // 处理截断逻辑而不是直接把残缺文本返回给前端 }参数配置不是越多越好很多情况下能少碰就少碰。模型调用方最怕的是参数口径不统一同一个功能有人传了temperature0.8有人没传线上表现就很随机。所以我会在默认配置里把基础参数定好个别场景再用运行时参数覆盖。3. 工具调用与结构化输出让模型真正成为接口3.1 用注解声明一个可被调用的工具模型本身是不了解业务系统数据的它不知道你的数据库里有哪些订单也不知道外部接口的返回格式。想让模型具备访问这些信息的能力最标准的方式是工具调用英文里叫 function calling。Spring AI Alibaba 里实现起来的体验比较直观一个普通方法加个Tool就能让自己“变成”模型可调用的功能。举个例子假设我要让模型回答“北京今天适合穿短袖吗”它必须先知道北京的天气所以我写一个查天气的工具Component public class WeatherTool { Tool(description 根据城市名获取当天的天气情况) public String getWeather(String city) { // 这里替换成真实天气接口调用 return city 当前天气晴温度 26℃微风; } }然后在构建ChatClient时把这个工具类注册进去ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(WeatherTool.class) .build(); String content chatClient.prompt(北京今天适合穿短袖吗) .call() .content();当模型接收到“北京今天适合穿短袖吗”这个问题时它会根据工具描述判断“我需要先获取北京的天气”然后返回一个工具调用请求。框架拿到这个请求后在本地找到WeatherTool.getWeather方法并执行再把执行结果打包给模型模型最终给出“北京今天 26℃适合穿短袖”这样的回答。整个过程对用户来说完全透明。这里我特别想提醒一点Tool方法尽量使用基本类型作为参数。模型在输出工具参数时是基于文本的如果参数过多且类型复杂出错概率会变大。能拆成两个简单工具就不要硬塞成一个复杂工具。3.2 工具调用的幕后循环搞清楚才能不迷路很多同学会疑惑模型到底是怎么知道要调用工具的其实它并没有真正“运行”代码它只是在生成的文本里选择了一个我们约定的 JSON 格式这个格式描述了方法名和参数值。比如模型可能会返回这样一段结构{name: getWeather, arguments: {city: 北京}}。框架收到后负责解析、反射调用、拿到返回值然后追加到上下文中再发给模型。整个过程就像是一个“模型提需求、框架执行、结果反馈给模型”的循环。理解这一点很重要。工具描述写得越清晰模型就越知道什么场景要用它返回值越是结构化模型就越容易继续处理。以前我把一个工具描述写成“获取数据”结果模型压根没调用后来改成“根据城市名获取当天的天气情况如果城市参数缺失返回错误提示”效果立竿见影。所以工具描述不是写给人看的是写给模型看的要把触发条件、参数含义说清楚。3.3 让模型直接返回 JSON 对象如果说工具调用是模型“走进”了业务系统那结构化输出就是让模型的返回结果能被程序直接消费。实现方式很简单使用ChatClient的entity方法public record UserInfo( String name, String address, String urgency ) {} UserInfo info ChatClient.builder(chatModel) .build() .prompt(张三住杭州需要马上处理) .entity(UserInfo.class);调用之后框架会引导模型输出符合UserInfo结构的 JSON并反序列化成一个对象。以前我遇到过一个场景用户的留言里包含“名字”“地址”“紧急程度”如果用正则去抠各种情况防不胜防。用结构化输出以后代码里直接拿对象省了一堆字符串处理。不过结构化输出也有翻车的时候。类字段如果没有语义明确的注释模型可能填错字段比如把“紧急”填进“地址”。解决办法很简单给字段加JsonProperty标注或者在 prompt 里给一个完整的示例。另外如果设置了temperature比较高JSON 格式偶尔会不稳定出现多余的逗号或缺失引号所以抽取任务一定要把温度调低。框架通常也支持指定responseFormat为 JSON 对象遇到格式不对时可以显式开启。3.4 工具调用里的三个“不要”我在这块踩过不少坑挑三个最典型的分享。第一不要在工具方法里放长事务或重计算。模型可能会在一轮对话里连续调用多个工具如果每个工具都去操作数据库并开启事务并发一高连接池很快被耗尽。工具方法应该是轻量的尽量只做查询和计算必要时把耗时的数据更新放到异步任务里。第二不要盲目相信工具返回的内容。模型拿到工具返回值后可能为了最终答案的连贯性对数值做“润色”把不确定的数据说得很确凿。所以我建议关键数据在工具内部就完成校验并返回结构化错误让模型在拿到错误后重新询问或给出“暂时无法获取”的回应而不是编造结果。第三不要用两个描述相似的工具。比如一个叫“获取订单”一个叫“查询订单详情”模型很容易选错。宁可少定义工具把逻辑合并也别让模型去做模棱两可的选择题。4. 知识库与 RAG 落地让回答有据可循4.1 先决定用哪种向量库再动手RAG检索增强生成是目前让模型回答专业知识最实用的一种路径。思路是把业务文档切成片段用向量模型转成向量存进向量库用户提问时先把问题变成向量在库里找最接近的片段再把这些片段拼到模型输入里让模型基于这些参考资料回答。Spring AI Alibaba 对向量库做了抽象比如VectorStore接口所以底层实现可以换。本地开发阶段我用的是内存版向量库零搭建成本进程一启动就能用。数据量上去了可以切到支持持久化的向量库比如 PGVector、Redis 等。我建议不要一上来就接太重的中间件先拿内存库把流程跑通确认检索效果可以了再迁迁移只影响配置和索引初始化那部分代码业务逻辑基本不用动。4.2 Embedding 模型和切分策略直接决定检索质量很多人关心选哪个 embedding 模型却没有花时间设计切分策略。实际上切分不合理再好的向量模型也救不回来。拿一个帮助文档举例如果按“每 200 个字符切一段”很可能把两个无关的 FAQ 拼在一起或者把一个完整的操作步骤从中间切开检索到的片段语义不完整。我现在的做法是优先按照文档结构切分。有明确章节的按标题切每一条 FAQ 单独作为一段合同或规范类文档尽量保留条款编号再按自然段落合并到 2000 字以内的完整语义块。切分之后还有一个容易忽略的点元数据。给每个Document加上来源、标题、章节号等字段一方面可以在检索时做过滤另一方面能够让模型在回答时知道“这个信息来自哪份文档”。生成结果时把来源也输出给前端会显著提高可信度。还有一点必须记住写入时用的 embedding 模型和查询时用的 embedding 模型要保持一致。如果中途换了模型向量空间变了旧数据不重新生成的话检索结果会非常随机。这个坑我踩过表现是“之前能搜到换模型之后什么都搜不到”。4.3 一个最小可运行的 RAG 流程示例代码上并不复杂。我先定义一个VectorStore的 Bean然后写入文档再查询。Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); } // 初始化数据 vectorStore.add(List.of( new Document(Spring AI Alibaba 支持工具调用可以让模型获取实时业务数据。, Map.of(source, doc-1)) )); // 查询 SearchRequest request SearchRequest.builder() .query(模型能不能获取实时数据) .topK(3) .build(); ListDocument docs vectorStore.similaritySearch(request);这里是一个全链条query先被 embedding 成向量然后在向量库里做相似度检索返回最接近的 3 个文档片段。下一步就是把docs的内容拼到 prompt 里交给ChatClient生成答案。有一点要特别注意不要在每次用户请求里去加载文件、切分和 embedding。这些都是相对重的操作。文档应该启动时加载一次或者定时增量更新。否则你的 RAG 接口瓶颈不在模型而在你自己的文档解析流程。4.4 TopK 和重排别让答案被无关片段带偏检索回几个片段合适不是越多越好。topK如果太大会把一些相似度偏低的无关内容也塞给模型模型反而容易被错误信息带偏。我的经验是topK取 3~5 个并且要结合相似度阈值过滤低于某个分数就直接不要。如果你想进一步提高答案质量可以加一步重排。也就是先把候选片段用topK10都召回来再用一个专门的 rerank 模型或者简单的关键词加权重新排序最后只把得分最高的 3 个片段给模型。没有重排能力时可以在提示词里加一句“只根据提供的资料回答如果资料没有提到就明确说不知道”。实测下来这句提示能明显减少模型一本正经地胡说八道。5. 拆解一个完整 Agent 示例从工具调用到流程编排5.1 Agent 的本质是什么Agent 听起来很高深但其实本质就是“模型 工具 循环决策”。模型本身不会执行操作它负责理解目标、规划步骤工具负责真正干活每执行完一个工具模型会观察结果再决定下一步做什么。这个过程可以重复多轮直到模型认为目标已经完成。Spring AI Alibaba 帮我们做了大量底层工作开发者不需要自己维护“调模型、解析 tool call、执行、再调模型”这个循环。但在写业务代码时你仍然要清楚这个循环的存在否则你会想不通“为什么一个简单问题模型要调好几次工具”。我比较喜欢把模型比作一个刚毕业但脑子转得很快的新员工工具则是给他开的内部系统权限。他能不能把活干好一半靠他的能力一半靠你把权限设计得是否顺手。如果权限边界不清今天能查订单明天也能删数据那迟早出事。5.2 一个“行程规划 Agent”的代码骨架我用一个最常见的规划类需求来演示让 Agent 帮用户规划一日游。它至少需要三个信息天气情况、景点门票、路线是否合理。对应三个工具我把它们注册到同一个ChatClient里。public record TourPlan( String city, ListString spots, String route, double budget ) {}三个工具分别定义WeatherTool.getWeather(city)、TicketTool.getTicketPrice(spot, date)、RouteTool.calculateRoute(spots, startPoint)。然后构建调用TourPlan plan ChatClient.builder(chatModel) .defaultTools(WeatherTool.class, TicketTool.class, RouteTool.class) .build() .prompt(帮我规划杭州一日游预算 500 元从西湖出发) .entity(TourPlan.class);模型在生成最终TourPlan前会先依次触发几个工具查一下天气查一下几个景点的门票再算一下路线。框架会自动把每个工具的结果回填给模型。最后模型整合所有信息输出一个结构化的行程。这里有一个小技巧prompt里最好把约束条件写完整比如“从西湖出发”。否则模型缺少起点信息可能会默认从市中心开始算导致路线结果和用户预期不一致。你交代的信息越完整Agent 规划出来的结果就越可用。5.3 Agent 的边界和兜底比模型能力更影响体验Agent 在实际使用中最大的风险不是“不聪明”而是“不可控”。模型可能会为了完成用户指令编造一个根本不存在的景点门票价格也可能在缺参数时自己脑补一个参数值继续跑。所以每个工具的入参都要做校验缺参数就返回“缺少某某参数”这样的结构化错误让模型知道信息不够需要追问用户。另外一个很容易忽略的问题是迭代次数。如果模型在某个工具调用上反复失败它可能会无限循环下去。虽然框架本身有保护机制但应用层还是应该加一个计数器或超时控制比如超过 5 轮工具调用直接终止返回“处理超时请重新描述需求”。在我维护的一个内部 AI 服务里很大一部分异常都来自 Agent 第一步就选错了工具后来我调整了工具描述的顺序和措辞错误率才明显降下来。边界意识和兜底机制决定了 Agent 是拿来“演示”还是拿来“上线”。不要指望模型每次都能完美地自主规划很多实际需求需要在关键动作前跟用户确认一下。这种“人机协作”的模式在工程上更稳定也是我越来越倾向的做法。6. 性能、安全与问题排查上线前必看的实录6.1 并发场景下的线程池与超时配置大模型请求是典型的 IO 密集型操作单次耗时动辄几百毫秒到几秒。如果在同步 Servlet 线程里直接去等模型返回Tomcat 线程很容易被打满。我见过有些应用上线后并发一上来所有请求都卡在模型调用上其他普通的接口也跟着超时。这个问题的根源不是模型慢而是线程模型不合理。一个可行的做法是把模型调用放到独立的异步线程池里Web 层用异步返回。同时给外部模型调用配置两套超时连接超时和读取超时。连接超时一般设 5 秒以内读取超时根据业务可以放到 30 秒左右要看模型服务商的实际响应时间。如果同一个服务要承载多个模型还需要给每个模型做独立的限流别让一个高并发场景把另一个模型的路由也拖垮。6.2 常见异常速查表异常现象可能原因处理办法启动时报 Bean 冲突定义了多个ChatModel且没有区分名称给每个 Bean 加上Bean(xxx)用Map注入调用模型超时网络慢、输出内容过长、服务商拥堵调大读取超时减少maxTokens增加重试工具始终没有被调用工具描述不清晰或者提示词没有触发场景重写Tool描述补充触发条件返回内容不是合法 JSON温度太高、字段语义不明确降低 temperature字段加JsonProperty指定 responseFormatRAG 检索结果差切分不合理、embedding 模型不一致按语义重新切分统一 embedding 模型上下文超长多轮历史全部塞给模型裁剪历史做摘要压缩或使用记忆机制这张表是我维护的项目里按真实故障频率整理出来的每一条背后都有对应的线上问题。排查时先按表格定位方向再去看日志能省很多时间。6.3 从一次线上故障里学到的事我之前负责过一个小型 AI 问答服务测试环境一切正常一上线就出现大量慢请求。最初以为是模型服务出了问题后来把调用日志打开才发现时间主要消耗在文档处理上。原来每次请求进来代码都重新读取原始文件、切分、embedding、写入向量库等于做了一遍 RAG 的初始化过程。模型本身只花了两秒但前期的文档处理已经花了十几秒。从那以后我养成了几个检查习惯第一外部调用必须单独记录耗时不能混在业务日志里第二工具调用里如果有同步 IO要确认是不是每次都执行了第三上线前会用并发脚本压一遍模型路由和 RAG 接口而不是只跑通单个请求第四看到返回结果里finishReason异常时先判断是不是参数配置问题再怀疑模型能力。把这些检查项变成习惯之后我的 AI 服务上线后出问题的频率降低了很多。这篇写到的多模型切换、工具调用、RAG、Agent其实每一步到最后都指向同一个核心把模型的不可控性用工程手段尽量约束住。我自己在实际操作中的体会是Spring AI Alibaba 给了很好的编排能力但真正的项目质量还是取决于你对工具边界和异常场景的设计。希望这些踩坑记录能帮你少走几段弯路。