ARTICLE DETAIL

资讯详情

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

Spring AI多模型多Agent平台实战:从接入路由到RAG落地避坑

Spring AI多模型多Agent平台实战:从接入路由到RAG落地避坑 简介Snail AI 是一套基于 Spring Boot 4 与 Spring AI 构建的企业级 AI 智能体平台面向需要多模型接入与智能体编排的 Java 开发者及企业团队适用于知识库问答、智能客服、自动化任务等场景。平台开箱即用地提供 RAG 知识库、长期记忆、技能编排、向量检索等核心能力并配有完善的后台管理界面和 OpenAPI 接口便于快速集成和二次开发。资源包共含 672 个文件约 2.26MB以 554 个 Java 源码为主覆盖服务、API、智能体调度等后端逻辑另有 JS/CSS 构成管理控制台XML/YAML 承担配置SQL 初始化数据库Dockerfile 支持容器化部署目录结构清晰。内置技能编排与向量检索示例可帮助理解 RAG 切片、向量化召回和记忆持久化的工程实现也能作为企业级 AI 中台的起步模板。资源目前已有 25 人学习对希望掌握 Spring AI 实战与智能体平台架构的开发者具有直接参考价值。1. Spring AI 多模型多 Agent 平台先看清它解决什么问题团队里模型越接越多GPT 类模型、智谱、通义千问各留一套调用代码半年后主服务里堆了十几个模型 Service换一个 Key 要改两处加一个模型要复制一整个类——这还是在没用上 Agent 的前提下。我拆这套基于 Spring Boot 4 Spring AI 的多模型多 Agent 管理平台时目的很明确把这段最乱的接入层收编成可配置、可管理的东西。模型供应商统一接入Agent 独立注册RAG 和记忆各归各的技能用注解暴露给 Agent 调度。这套东西适合两拨人一是 Java 团队想在企业应用里落地 RAG 和 Agent受够了 Python 服务与 Java 业务系统之间来回跳二是已经在用 Spring AI但多模型切换靠改代码、Agent 逻辑全写在 Controller 里的人。它真正的价值不是某个算法多深而是把 Spring AI 里那些概念落成了可操作的模块——模型接入、Agent 生命周期、会话记忆、向量检索、技能编排每一块都能在配置层和接口层改。后文我会从架构拆起然后讲怎么跑起来、RAG 链路怎么调最后把最容易翻车的五个点过一遍。看完你能直接对着源码改出自己的版本。2. 多模型接入与 Agent 生命周期先把 Spring AI 的边界拆清楚2.1 多模型接入ChatModel 抽象与多供应商配置的取舍Spring AI 本身对模型做了统一抽象所有模型供应商都实现ChatModel接口上层通过ChatClient调用。这个设计在单模型场景下很舒服但一旦你在application.yml里同时配置了智谱和百炼Spring Boot 的自动配置会创建两个ChatModelBean此时直接Autowired ChatModel启动就会报NoUniqueBeanDefinitionException。常见做法是手动维护一个路由层把多个 ChatModel 收进 Map按请求里的模型名分发Component public class RouterChatModel implements ChatModel { private final MapString, ChatModel models; public RouterChatModel( Qualifier(zhipuAiChatModel) ChatModel zhipuAiChatModel, Qualifier(dashScopeChatModel) ChatModel dashScopeChatModel) { this.models Map.of( zhipu, zhipuAiChatModel, dashscope, dashScopeChatModel); } Override public ChatResponse call(Prompt prompt) { // 从 Prompt 的 options 里取模型名默认走 dashscope String modelName resolveModelName(prompt); ChatModel target models.getOrDefault(modelName, models.get(dashscope)); return target.call(prompt); } private String resolveModelName(Prompt prompt) { // 从 prompt.getOptions().getModel() 解析兼容不同供应商的模型名写法 String model prompt.getOptions().getModel(); if (model null) return dashscope; if (model.contains(glm)) return zhipu; return dashscope; } }这里有两个关键点。第一Qualifier必须写清楚否则 Spring 自己猜注入目标会直接报错第二resolveModelName这个解析逻辑是 Router 的核心我一般建议在Prompt的options里显式传一个自定义参数而不是靠模型名字符串去猜——不同供应商的模型名经常带前缀靠 contains 匹配迟早出问题。配置层面长这样智谱和百炼的 Key 都放进环境变量代码里不落任何明文密钥spring: ai: chat: dashscope: api-key: ${DASHSCOPE_API_KEY:} options: model: qwen-plus temperature: 0.7 zhipuai: api-key: ${ZHIPU_API_KEY:} options: model: glm-4-air temperature: 0.6注意temperature是供应商侧参数部分模型不支持时会静默忽略如果你发现同一个值在 A 供应商有效、B 供应商没反应优先查该模型官方文档不要怀疑 Spring AI 的代码。另外如果你的模型走的是 OpenAI 兼容格式的自建网关配置方式完全一样把base-url指向网关地址就行Spring AI 的ChatModel实现会按兼容协议解析。2.2 Agent 生命周期注册、启停、状态与技能绑定Agent 在这套设计里不是简单的提示词 模型封装它有独立的状态和技能列表。一个 Agent 至少包含id、name、systemPrompt、绑定的模型名、短期记忆窗口、技能列表、运行状态。状态用枚举管理IDLE 和 RUNNING 是核心两个技能编排或异步任务里很容易出现并发复用同一个 Agent 导致上下文串线所以状态必须显式维护。Service public class AgentService { private final MapString, Agent agents new ConcurrentHashMap(); public Agent register(Agent agent) { agents.put(agent.id(), agent); return agent; } public ChatResponse execute(String agentId, String userMessage) { Agent agent agents.get(agentId); if (agent null || agent.status() ! AgentStatus.IDLE) { throw new IllegalStateException(Agent 不存在或正在执行: agentId); } agent.status(AgentStatus.RUNNING); try { UserMessage message new UserMessage(userMessage); ChatMemory memory agent.memory(); memory.add(message); Prompt prompt new Prompt(memory.messages(), agent.options()); return agent.chatModel().call(prompt); } finally { agent.status(AgentStatus.IDLE); } } }register是给管理后台用的新 Agent 通过接口注册进来不用改代码execute是核心执行路径注意finally里必须把状态复位否则一次异常后这个 Agent 永远处于 RUNNING后续请求全部被拒。这是我在实际项目里踩过的坑——当时一个外部接口超时把状态卡死在 RUNNING整个 Agent 池三小时后才被监控发现。Agent 的技能列表用注解暴露Spring AI 会把带Tool的方法转成模型可调用的工具描述。技能绑定发生在注册阶段我一般会在Agent构造时把技能列表注入并给每个技能一个name做唯一标识。多个 Agent 可以共享同一个技能类实例但要注意技能实现类内部不要持有会话级状态否则并发下会串数据。2.3 会话与记忆线程级 MessageWindow 与持久化召回记忆是 Agent 最容易做糊的部分。这套项目的设计把记忆分两层短期记忆用 Spring AI 自带的MessageWindowChatMemory只负责当前会话内的多轮上下文长期记忆落到向量库或数据库按对话主题召回历史片段。// 短期记忆固定窗口大小超出后丢弃最旧消息 ChatMemory shortTerm MessageWindow.builder() .maxMessages(16) .build(); // 长期记忆会话结束后把摘要和关键结论写入向量库 ConversationRecord record ConversationRecord.builder() .conversationId(session-001) .summary(summaryText) .build(); vectorStore.add(List.of(record));maxMessages不是越大越好。我经验值是 8 到 16超过 16 条以后模型对早期消息的注意力明显下降响应变慢且费用上升。长期记忆的召回发生在会话开始时——把当前用户问题先向量化去库里检索相关的历史会话摘要作为上下文拼进系统提示词。这套方案的优点是短期记忆不落盘、速度快长期记忆按需召回不会把整个历史一股脑塞给模型。缺点是两个层之间需要同步策略否则会出现短期记忆和长期召回内容互相矛盾这个我在避坑章再展开。3. 把平台跑起来环境准备、配置与第一个 Agent 对话3.1 环境清单与版本基线先明确版本基线。标题写的是 Spring Boot 4实际拆解时我用的 Spring Boot 4.x 配套 Spring AI 1.0这两个大版本在依赖管理上是配套的不需要你手动对齐很多版本号。如果你的生产环境还压在 Spring Boot 3.5也可以跑只要把 spring-ai BOM 版本降下来代码层面基本不用动。组件版本/用途说明JDK21Spring Boot 4 默认支持虚拟线程Agent 这种 IO 密集场景直接受益Maven3.9构建和依赖管理MySQL8.0Agent 注册信息、技能配置、会话记录的持久化Redis7.x短期记忆、分布式锁、接口级缓存pgvector / Milvus按调研选型向量检索RAG 的存储底座模型 API Key智谱 / 百炼至少一个两个都配才能演示多模型路由向量库选型是很多人一开始就卡住的问题。项目默认可以用 pgvector因为它不需要额外部署一套服务在 MySQL 旁边的 PostgreSQL 实例里建个扩展就能跑适合团队里第一次上 RAG 的场景。如果你们的向量数据量级到了千万级以上、或对检索延迟有硬性要求再上 Milvus。别为了高性能一上来就部署独立向量库运维成本会被低估。3.2 配置与启动从 yml 到第一个 Agent 对话把项目拉下来后第一步不是启动是把配置和基础设施准备好。我有一次直接mvn spring-boot:run结果启动到一半就报数据库连不上——因为没看 README 的依赖要求。现在按这个顺序来spring: application: name: ai-agent-platform datasource: url: jdbc:mysql://localhost:3306/ai_agent_platform username: root password: ${MYSQL_PASSWORD:root} driver-class-name: com.mysql.cj.jdbc.Driver ai: chat: dashscope: api-key: ${DASHSCOPE_API_KEY:} options: model: qwen-plus vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE data: redis: host: localhost port: 6379初始化数据库表结构项目里带了 SQL 脚本# 初始化 MySQL 表结构 mysql -uroot -p sql/init.sql # 启动应用 mvn spring-boot:run启动成功后日志里会出现 Spring AI 相关的初始化信息。验证最小链路不要直接问复杂的业务问题先用一个最简单的请求确认模型连通调用 Agent 执行接口传agentIddefault-agent消息内容写请回复连接正常。如果模型返回了这四个字说明从应用到模型供应商的链路是通的。这一步很多人跳过直接测 RAG结果 RAG 有问题时根本分不清是模型的问题还是检索的问题。3.3 同时接入智谱与百炼两个 Key 的共存配置单模型跑通后多模型共存是下一个坎。把 2.1 里的 RouterChatModel 注册成 Bean配置里同时放两个供应商spring: ai: chat: dashscope: api-key: ${DASHSCOPE_API_KEY:} options: model: qwen-plus temperature: 0.7 zhipuai: api-key: ${ZHIPU_API_KEY:} options: model: glm-4-air temperature: 0.6这段配置完成后Spring 容器里会有两个 ChatModel Bean。如果 2.1 的 Router 没有生效启动时会在任何注入ChatModel的地方报NoUniqueBeanDefinitionException。判断依据很简单报错信息里出现expected single matching bean but found 2就是没走 Router。此时要么把 Router 组件补上要么在所有注入点写Qualifier(zhipuAiChatModel)——但这等于把路由逻辑散落在业务代码里后续维护会非常难受。多模型切换真正的价值在故障降级。比如百炼限流了Router 里加一个简单的降级策略try-catch捕获限流异常后自动切换到备选模型这个过程对调用方透明。我在生产环境里就是这么用的某个模型供应商不稳定时切换不需要发版。4. RAG 与向量检索落地文档分块、入库与召回参数调优4.1 从文档到向量分块、Embedding 与入库链路RAG 的第一步是把文档变成可检索的向量。整个过程有四个环节文档解析、分块、向量化、入库。项目里常用TokenTextSplitter做分块它在中文场景下按 token 数切比按字符或按段落切更可控。// 分块参数块大小 512重叠 100 TextSplitter splitter new TokenTextSplitter(512, 100); // 原始文档可以来自 PDF、Word、Markdown统一转成 Document Document doc new Document(fileContent, Map.of(source, tutorial-001)); ListDocument chunks splitter.split(doc); // 向量化并写入 pgvector EmbeddingModel embeddingModel new OpenAiEmbeddingModel(...); VectorStore vectorStore new PgVectorStore(jdbcTemplate, embeddingModel, 1024); vectorStore.add(chunks);分块参数直接决定检索质量这个没有理论最优值但有几个实际规律。块越小检索越精确但上下文碎片化严重模型可能看不到完整逻辑块越大上下文越完整但检索噪声也越大。中文技术文档我一般从 512 起步重叠设 100。重叠的意义在于保证跨块的关键信息不会被拦腰切断比如一个术语在前一块结尾、下一块开头没有重叠就两个块都搜不到完整含义。分块大小适用场景问题256问答型 FAQ、短条款长逻辑丢失需要额外拼接上下文512技术文档、操作手册较均衡中文场景常用起点1024长章节、合同条文噪声增加检索准确率下降PgVectorStore构造方法里那个1024是向量维度。这个数字必须与 Embedding 模型输出维度严格一致否则入库或检索会直接报维度不匹配。不同 Embedding 模型的维度差别很大用智谱的 embedding-3 和用阿里百炼的 text-embedding-v3维度不一样切换模型前必须先建新表或改字段。4.2 检索链路TopK、相似度阈值与相关性重排入库之后是检索。RAG 项目里经常有个错觉向量检索结果一定相关。实际完全不是这样向量的相似度排序和人类理解的相关差距很大所以检索链路要做三件事召回、过滤、重排。// 检索参数topK 召回 10 条相似度阈值 0.35 SearchRequest request SearchRequest.builder() .query(question) .topK(10) .similarityThreshold(0.35) .build(); ListDocument hits vectorStore.similaritySearch(request);topK是召回数量不是最终结果数量。10 是一个稳妥的起步值召回太少容易漏太多则噪声变大后续重排阶段会再把质量差的排下去。similarityThreshold是过滤阈值pgvector 的 cosine 距离在 0 到 2 之间值越小越相近。0.35 是个经验起步点如果你的检索结果总是空先看是不是阈值设太高反过来如果结果一堆明显不相关的内容就把阈值往上提到 0.5 左右。如果对结果还不满意就做重排。重排有两种落地方式一是调独立的 rerank 模型把召回文本逐条打分重新排序二是规则重排——把用户问题里的关键词提取出来对召回结果做关键词命中加权。规则重排不需要额外模型成本低在英文文档上效果一般但中文技术文档里关键词命中非常有效因为专业术语的区分度很高。顺着这个链路往深走就是现在讨论比较多的 Agentic RAG——不是一次检索完事而是让 Agent 判断当前召回结果够不够回答不够就改写查询再搜一轮。项目里这套能力是可以落地的在 Agent 技能里加一个search_docs工具让模型自主决定是否调用。它的收益在复杂问题上很明显但要注意控制循环次数不然一次提问可能引发四五轮检索延迟和成本都是问题。4.3 用 Hit Rate 和 MRR 量化 RAG 效果RAG 最玄学的部分就是感觉效果变好了——感觉不靠谱。我建议项目里建一个固定的小测试集50 个左右的问题每个问题标注标准答案所在的文档 ID。每次改检索参数、换分块策略或换 Embedding 模型后跑一遍测试集看两个指标指标计算方式看什么Hit Rate正确答案出现在检索结果中的比例召回有没有漏MRR正确答案在结果中的排名倒数取平均排得够不够靠前比如 50 个问题里40 个的正确答案出现在 Top 10 召回内Hit Rate 就是 0.8。正确答案平均排在第 2 位MRR 就是约 0.5。改进的方向就可以量化了Hit Rate 低优先改分块和重排MRR 低且 Hit Rate 高优先调 topK 和阈值。这套评估跑起来以后RAG 参数调整就不是玄学而是有数据反馈的迭代。5. 避坑手册Spring AI RAG 最常见的 5 个翻车现场5.1 技能注解不生效Agent 不调用 Tool 方法现象代码里写好了Tool注解的方法Agent 的对话里也能看到工具描述但模型就是死活不调用或者调用时报参数解析错误。原因最常见的是工具方法注入了容器但 Agent 构建时没有把ToolCallback列表传进去Spring AI 只有在ToolCallback出现在 Prompt 的toolCallbacks里时才会把工具描述发给模型。其次是工具方法的参数类型太复杂用了自定义对象且没有合理的 JSON Schema 映射。解决把技能类实例传给 Agent 构造器并确认方法参数只用基础类型、String、或简单record复杂对象在模型侧会生成一堆无法解析的 JSON 字段直接导致调用失败。5.2 多模型配置后启动失败NoUniqueBeanDefinitionException现象配置里加完第二个模型供应商应用启动直接报expected single matching bean but found 2。原因Spring AI 自动配置把两个供应商都注册成了ChatModelBean而项目里至少有一处注入ChatModel没有指定Qualifier。解决动手前先全局搜索ChatModel注入点统一改成经过 Router 调度的方式。不要在某个业务 Service 里单独注入Qualifier(zhipuAiChatModel)——短时间内能跑侧后面加第三个模型时又是一轮全局改动。我现在的习惯是所有业务代码只依赖RouterChatModel这个门面。5.3 向量检索结果为空或乱维度、距离类型、阈值现象文档成功入库但检索时要么什么也查不到要么返回一堆完全不相关内容。原因三个方向排查。第一Embedding 模型维度与库表维度不一致这个问题通常在入库时报错但也有静默失败的数据库实现第二距离类型设置错误比如用EUCLIDEAN_DISTANCE却按 cosine 的阈值标准过滤第三similarityThreshold设得过高把本应该召回的结果全过滤了。解决先确认维度再确认distance-type与SearchRequest的阈值量纲一致最后把阈值降到 0.2 试一次——如果降阈值后能召回说明是阈值问题还不行就检查入库时有没有执行 Embedding。排查 RAG 问题时先把链路拆成入库→检索→生成三段逐段验证效率最高。5.4 会话记忆越聊越乱短期窗口与长期召回互相矛盾现象同一个用户连续问同一个话题Agent 的表现在第三轮开始明显退化或者长期记忆召回了三个月前的内容和当前对话上下文冲突回答里出现自相矛盾。原因短期记忆的MessageWindow没有按会话隔离多个会话共用了同一个ChatMemory实例或者长期记忆召回时没有携带会话 ID 过滤条件把其他用户的记录也召回了。解决确保每个会话创建独立的MessageWindow用conversationId做维度长期记忆的向量检索条件里加入 session 过滤字段。我一般会在ConversationRecord里打上userId和conversationId两个标签检索时在SearchRequest的 filter 里同时限定这个过滤步骤不能省。5.5 Spring Boot 4 与 Spring AI 版本依赖错位现象项目一启动就报NoSuchMethodError或类定义找不到明显是三个不同模块用了同一个类的不同版本。原因Spring AI 不同 minor 版本对 Spring Boot 4 的适配进度不一样有些三方依赖又间接引了旧版 spring-ai。解决用 BOMBill of Materials统一管理版本不要在每个模块单独写 spring-ai 版本。我把spring-ai-bom和spring-boot-dependencies都在父 POM 的dependencyManagement里声明子模块只写 groupId 和 artifactId。版本冲突排查时跑一遍mvn dependency:tree看哪些 jar 被重复引入再在 BOM 覆盖。6. 进阶玩法自定义多 Agent 路由与技能编排的两种实现6.1 意图路由把用户请求分给最合适的 Agent多 Agent 平台不是说把所有 Agent 暴露给前端让用户选而是让系统判断请求该交给谁。我在项目里的做法是定义一个 Router Agent它的唯一职责是从候选 Agent 列表里选目标String routePrompt 根据用户问题从以下 Agent 中选择最合适的一个 1. sql-agent处理数据查询、报表类问题 2. rag-agent处理文档知识库问答 3. chat-agent日常闲聊、开放对话 只返回 agent 名称不要解释。 用户问题%s .formatted(userMessage);这个方案简单直接模型的选择准确率与候选 Agent 的系统提示词描述质量强相关。如果你的 Agent 描述写得模糊路由结果会随机跳动。另一个方向是用向量匹配做路由——把每个 Agent 的典型问题样例向量化用户问题来了先做向量相似度匹配命中率达不到再用模型判断。6.2 技能编排把查询能力封装成 Tool 链技能编排的落地方式是让业务能力变成模型可调用的工具。我给项目加过一个 NL2SQL 技能Agent 收到自然语言问题后先生成 SQL再执行查询最后把结果组织成回答。这里有两个关键设计——SQL 生成用专用模型或专用提示词模板避免把数据库结构暴露给主模型执行结果必须做长度限制不然一次查询返回上千行上下文瞬间被撑爆。从那以后我每次接入新的模型供应商或改 RAG 参数都强制自己走一遍完整流程先看配置清单再启动启动了先测连通性再测业务改完参数跑一遍测试集看 Hit Rate 和 MRR。这个习惯帮我少走了很多弯路也希望帮到你。本文还有配套的精品资源点击获取
返回列表