Spring Boot 4 + Spring AI:多模型多Agent平台搭建实战

Spring Boot 4 + Spring AI:多模型多Agent平台搭建实战 简介这是一套面向Java后端开发者与AI工程化实践者的企业级智能体平台开源实现聚焦AI应用落地中的多模型调度、Agent编排、RAG知识增强、长期记忆管理与技能模块化等核心难题。资源基于Spring Boot 4与Spring AI深度集成提供开箱即用的后台管理界面与标准化OpenAPI支持快速接入大模型服务并开展二次开发。压缩包共672个文件含554个Java业务与配置类覆盖Agent生命周期、向量检索、记忆存储等核心逻辑、49个前端交互JS脚本、39个XML配置及12个CSS样式文件整体仅2.26MB轻量且结构清晰。目前已有23人学习下载读者可直接获取完整可运行项目、多层级模块划分的工程目录、RAG与记忆功能的实现实例以及适配主流向量库与大模型的抽象封装层显著降低AI Agent平台从0到1的搭建门槛。 接手这个项目的时候我的第一反应是又是一个把“RAG、Agent、多模型”这些热词堆在一起的平台。但真正拆开做完之后我发现这个选题背后的工程量确实不小——Spring Boot 4 作为底座Spring AI 负责统一模型抽象上面再叠多 Agent 编排、记忆、技能、向量检索这其实是当下 AI 应用落地最典型的一块拼图。今天这篇就把我做这个平台时的整体思路、关键实现和踩坑记录整理出来希望对正在搞 Spring AI 或准备做企业内部 AI 能力平台的同学有参考价值。1. 平台整体设计与模块拆解1.1 为什么是 Spring Boot 4 Spring AI而不是自己封一层 HTTP很多团队做多模型接入的时候第一反应是写一个统一的 ChatService里面用 HttpClient 调各家模型的 REST 接口然后自己管理密钥、超时、重试、上下文裁剪。这条路前期很快但一旦要处理流式输出、工具调用、embedding、向量库对接、以及不同模型返回格式的差异代码量会迅速失控。Spring AI 的核心价值在于它把“模型调用”这件事抽象成了一组稳定的接口ChatModel、EmbeddingModel、ImageModel、AudioModel。你面向的是接口而不是某一家厂商的 SDK。换模型就是改配置或换一个 Bean业务代码基本不用动。Spring Boot 4 则提供了更干净的自动配置机制和更强的原生镜像支持对部署侧的友好度是实打实的。这个平台最终定位是“开箱即用的多模型、多 Agent 管理平台”所以架构上从一开始就不是给某一个业务写死逻辑而是拆成几条纵向能力模型接入层、Agent 编排层、RAG 知识库层、记忆层、技能编排层以及外围的运维与监控管理后台。1.2 宏观架构五个核心模块的协作关系我在实际搭建时把所有功能拆成了五个独立的 Maven 模块彼此通过 Spring 的 ApplicationContext 和事件机制协作而不是做成一个大杂烩工程model-gateway多模型接入和路由。负责各家模型的密钥管理、模型实例注册、路由策略按业务线、按成本、按优先级。agent-runtimeAgent 的生命周期管理和编排。一个 Agent 在运行时就是一次“模型 工具 记忆 技能”的调度过程。rag-service知识库的完整链路。包含文档解析、切块、embedding、向量存储和检索、重排序。memory-core会话记忆的存取和管理。支持内存实现和 Redis 实现按用户、按会话隔离。skill-studio技能编排的可视化配置和引擎。技能本质上是“一系列可复用的步骤模板”Agent 执行时按模板调用。模块之间通过接口通信例如 agent-runtime 要检索知识库时只依赖 rag-service 暴露的 RerankSearchService不关心底层是 Elasticsearch 还是 PostgreSQL 的 pgvector。这样做的首要考虑是“可替换性”。企业内部落地 AI 项目时底层组件往往受运维和成本约束你不能强制让团队必须上某个向量数据库。接口隔离之后替换成本就从“改代码”降到了“加依赖 配置”。1.3 模型接入层为什么必须做成动态路由如果你只接入 OpenAI 或只接入一个国内大模型动态路由的意义不大。但凡是做平台用户一定会提三个需求第一不同业务线想用不同的模型第二要对成本敏感希望低价模型优先、复杂任务走高配模型第三某个模型不稳定时可以一键切换。动态路由的基本设计是一个 RouterChatModel 实现了 Spring AI 的 ChatModel 接口内部持有多个具体模型实例和一个路由策略。路由策略支持两种按规则路由比如请求参数里带 modelxxx 就强制指定模型和按加权路由给每个模型配权重用一致性哈希或轮询分发。这个阶段不需要做太重的流量调度规则路由先跑起来后续再扩展。密钥也是一样的道理。我没有把密钥写到 application.yml 里而是把模型接入信息存到数据库表 model_provider 中包含 provider 类型、baseUrl、apiKey 的加密密文、模型名称、最大 Token 数等字段。平台启动时从库里加载配置动态创建模型 Bean。改配置不用重启服务这个体验对运维非常友好。2. RAG、记忆、技能编排等核心能力的方案选型与工程实现2.1 RAG 链路从文档解析到混合检索RAG 是这个平台引用频率最高的能力。它的价值无需多讲我再强调一个观点RAG 的效果主要取决于两件事一是切块策略二是检索质量embedding 模型只排第三。文档解析阶段我用的是 Spring AI 自带的 DocumentReader 体系按文档类型装配不同的 Reader。PDF 用 PDF 段落读取Word 和 Markdown 用文本解析。中文场景下解析服务端文档时强烈建议保留标题层级信息。切块阶段如果能把标题作为元数据注入每个 chunk后续检索时就能用于“按章节过滤”。切块策略我最终没有用固定的固定长度切法而是实现了一个结合标题层级与段落边界的结构化切块器。策略是先按标题H1/H2/H3分割全文保证语义完整性。每个标题段落再按“句号、问号、感叹号”断句以句子为基本单位。结合 embedding 模型的 max input tokens常见是 512 tokens动态组装句子保证相邻 chunk 有 50 到 100 个字符的重叠。每个 chunk 附加上一级标题、文档来源、页码等元数据。召回阶段我同时接入了向量检索和 BM25 关键词检索做多路召回再用一个重排序模型把两路的候选结果合并排序。为什么要混合检索这个问题的根本原因在于向量检索擅长语义相似但不擅长精确匹配。实际业务中大量检索词是产品名、订单号、人名这类专有名词用户问“AP-2024-0112 的部署文档”embedding 模型很可能把“AP-2024-0112”识别得模模糊糊但 BM25 对这种 token 级别的精确匹配非常稳。重排序阶段我先用相对轻量的 bge-reranker 模型过滤一遍再按分数截取 top-k。重排序千万不能省我试过直接把多路召回的结果合并后丢给大模型效果非常不稳定尤其是两路结果里都混着低相关片段时大模型会被带偏。2.2 向量存储选型pgvector 是当前性价比最高的默认选项向量数据库是 RAG 里讨论度最高的组件了。项目选型时我对比了专门的向量数据库和关系型数据库的插件两种路线。对于多数企业内部平台我最终选了 PostgreSQL pgvector核心原因有三个不需要额外引入一个独立的向量数据库集群运维成本低。业务元数据文档、知识分类、权限标记本来就可以存在同一个库里方便按租户或标签过滤后再查向量。pgvector 的 HNSW 索引在中低并发场景下性能和专门向量库的差距没有想象中大。当然如果数据量真到了千万级向量以上、且高并发 QPS 要求很硬Milvus 或 Qdrant 这类专门的向量库还是更优解。我在接口设计上留了 VectorStore 的抽象切换时对上层透明。在 pgvector 建表时embedding 维度要和选择的 embedding 模型保持一致。我用的模型输出是 768 维所以表定义大致是这个结构简化版CREATE TABLE knowledge_chunk ( id BIGSERIAL PRIMARY KEY, doc_id VARCHAR(128) NOT NULL, chunk_text TEXT NOT NULL, chunk_meta JSONB NOT NULL DEFAULT {}, embedding VECTOR(768) NOT NULL, tenant_id VARCHAR(64) NOT NULL DEFAULT default, created_at TIMESTAMP DEFAULT now() ); CREATE INDEX idx_chunk_embedding ON knowledge_chunk USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);注意 HNSW 索引的参数不能随便拍脑袋。m 表示每个节点的最大连接数ef_construction 是构建索引时的候选集大小。m 越大查询精度越高但内存和构建时间也越大ef_construction 在构建时影响索引质量查询时用 SET hnsw.ef_search 控制候选集大小。通常情况下我把 ef_search 设置在 40 到 100 之间超过 100 收益极速下降反而白白增加延迟。2.3 记忆不能只存聊天记录要分层管理多 Agent 场景下“记忆”不是一个简单的消息列表。我的方案是把记忆分成三层短期会话记忆当前会话内的对话消息用于多轮追问的上下文。摘要记忆当短期记忆超出窗口后自动压缩成摘要保留关键信息。用户画像记忆从历史对话中提取用户偏好、常用术语、项目中关注点长期保存跨会话生效。短期会话记忆的实现比较简单。Spring AI 的 ChatMemory 接口可以直接用按 conversationId 维度存消息列表。我做了两层实现单机调试用 InMemoryChatMemory部署环境用 Redis 实现带过期时间。Redis 里我用了一个 Hash 结构key 是会话 IDfield 是消息序号value 是消息 JSON。每次取的时候按序号排序避免并发写入造成的乱序。摘要记忆的触发不能只按“轮数”判断因为单轮内容的长短差异极大。我实现了一个触发器当累计 token 数超过模型窗口的 60% 时自动把最早的历史消息喂给一个便宜的摘要模型生成压缩摘要然后丢弃被压缩的原始消息。这个操作要用一个异步任务执行避免用户等待摘要生成。用户画像记忆是平台差异化最好的切入点但也是最容易做脏的。我建议用结构化的 Profile 对象保存而不是一句一句存原文。例如记录用户的“业务领域”“常用名词”“技术偏好”等字段通过在对话结束时异步调用模型抽取。这里要加一条规则抽取出来的用户画像信息必须经过人工确认后才能进入长期记忆池否则模型幻觉会污染后续所有会话的判断依据。2.4 Agent 与工具调用理解 Tool Calling 的本质Spring AI 的 Agent 实现并不花哨核心就是 Function CallingOpenAI 叫工具调用Spring AI 里是通过 Tool 注解和 ToolCallback 抽象暴露方法给模型。一个 Agent 在运行时的逻辑是将用户请求、系统提示词、可用工具的 JSON Schema 一起发给模型。模型返回两种结果要么直接输出最终回答要么返回“需要调用某个工具”的参数。如果模型返回了工具调用请求平台执行对应的 Java 方法比如查数据库、查库存、调 HTTP 接口把执行结果作为 ToolMessage 返回给模型。模型拿到工具执行结果后继续生成回答或再次发起工具调用。循环直到模型给出最终答案或达到最大迭代次数。这段逻辑写起来不难难点在于工具的描述质量。很多 Agent 效果差不是因为模型不行而是工具名和描述写得含糊模型根本不知道这个工具是干嘛的、参数怎么填。我总结了一套工具定义规范工具名称用“动词 业务对象”query_order_status、create_incident_ticket而不是 order、doSave 这种。description 必须说清楚功能边界和适用场景比如“仅用于查询订单快递物流状态商品价格查询请使用 query_product_price”。参数必须有明确的单位、格式、取值范围枚举值全部列出。能拆的接口尽量拆细一个工具只做一件事拆得越细模型调用越准确。工具调用最大的坑是“循环空转”。模型可能因为某个工具返回结果不符合预期反复调用同一个工具白白消耗 token。我给 AgentRuntime 加了一个工具调用计数器和熔断机制单个 Agent 单次请求中工具调用次数超过 8 次就强制中断并让模型基于已有信息作答。2.5 技能编排不是工作流引擎是步骤模板这个平台里的“技能编排”我参考了 Spring AI Alibaba 的思路同时也做了自己的取舍。Agent 在回答问题的时候不能每次都把“检索知识库 调用工具 组织答案”这些逻辑全部塞进提示词。更好的做法是把这些编排逻辑固化成“技能模板”。我的技能编排引擎定义了一个简单的 DAG。每个技能包含多个节点节点类型定义了相对比较全的集合检索节点调用 RAG、工具节点调用平台注册的工具、LLM 节点执行一步模型推理、条件判断节点、循环节点。技能之间也可以嵌套一个技能内部可以调用另一个技能作为子步骤。举个例子一个“内部产品技术支持助手”的技能模板是这么编排的节点 A检索节点根据用户问题检索产品文档知识库返回 top-5 文档片段。节点 B条件节点如果检索结果相关性分数低于 0.55直接转入人工工单创建节点否则进入节点 C。节点 C工具节点调用 query_order_status 工具查询用户的订单和授权信息。节点 DLLM 节点将知识库片段、订单信息、用户问题组装成最终 answer。这个模板的好处显而易见。业务方不需要每次都跟模型强调你先看看知识库再查一下订单状态而是直接指定一个技能名称Agent 引擎按模板执行。实验结果也证实了这种方式的效果——相同问题用固定技能模板的准确率要比纯靠提示词引导高一大截而且延迟更稳定。这里要特别强调一点技能编排不等于工作流引擎。工作流引擎比如 Flowable、Camunda解决的是状态机和人工审批流转而技能编排解决的是“模型推理步骤的编排”。两者的业务目标完全不同不能混用。2.6 MCP让 Agent 接入外部系统更标准平台里我还预留了 MCPModel Context Protocol客户端的集成位。MCP 的目的是让 Agent 工具接入有一个统一标准避免每个工具写一套自定义协议。Spring AI 的 MCP 客户端抽象做得已经比较完善可以自动发现 MCP 服务器暴露的工具列表并把工具注册到 Agent 的 ToolCallback 池中。我在实际使用时把内部的工单系统、CMDB、发布平台各包装成独立的 MCP ServerAgent 就能像调用本地工具一样调用这些远程服务。如果你的平台要接十几个内部系统MCP 带来的标准化收益值得投入。不过 MCP 也有自己的问题工具发现后如果没有权限控制外部 Agent 可能会调用到不安全的方法。平台在 MCP 工具注册层加了一层 ACL 过滤每个 Agent 只能看到自己有权限的 MCP 工具。这一点在落地时务必要考虑否则内部系统暴露面会被无限放大。3. 实操过程从零搭建到跑通第一个 Agent3.1 环境搭建与依赖引入我使用的是 Java 21 Spring Boot 4.x Spring AI 1.0 正式版GA 之后 API 已经基本稳定不像 0.8.x 时代三天两头破坏性变更。新建工程时选择 Maven 管理依赖Spring Boot 4 的版本管理依然好用直接在 parent 里引入 BOMparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version4.0.x/version /parent properties java.version21/java.version spring-ai.version1.0.x/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement依赖项按模块引入模型网关模块引入 OpenAI、DashScope 等 starter。这里提醒一句Spring AI 1.0 的模块划分比早期版本清晰很多Modelfoundry 相关的 starter 按需添加即可不要一股脑全引进去否则自动配置的 Bean 太多排查问题会非常痛苦。3.2 多模型接入的最小配置在 application.yml 里先配置一个 OpenAI 兼容的模型接入几乎所有云厂商都提供了 OpenAI 兼容接口这是最省事的接入方式spring: ai: openai: base-url: https://your-endpoint.example.com api-key: ${LLM_API_KEY} chat: options: model: qwen-plus temperature: 0.6 max-tokens: 2048但这只是单模型配置。平台层面的多模型接入需要自己实现动态模型注册。我在项目里定义了一个 ModelProviderRegistry启动时从数据库加载 provider 配置通过一个泛型工厂创建 ChatModel 实例。如果是 OpenAI 兼容接口就复用 OpenAiApi 的构建器如果是本地 vLLM 或 Ollama就用对应的 starter。这里有个细节不同模型的参数能力差异很大统一封装时要注意。比如有的模型不支持 temperature有的模型对 JSON mode 支持不完整。我们的封装策略是ModelGateway 对外暴露统一请求体内部适配时对“不支持的能力”做降级或剔除。否则模型改配置时报错信息会很难看。3.3 跑通 RAG 最小链路我用 Spring AI 内置的向量存储抽象 pgvector 实现跑通 RAG 的最小链路。先配置 pgvectorspring: datasource: url: jdbc:postgresql://localhost:5432/ai_platform username: ai_user password: ${DB_PASSWORD} ai: vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 768Embedding 模型我接的是千问的 text-embedding 或者本地的 bge-m3本地化部署时用 Ollama 拉一个 embedding 模型非常方便。核心代码就三步第一步文档加载与切块var reader new PagePdfDocumentReader(classpath:/docs/产品手册.pdf); ListDocument documents reader.get(); var splitter TitleBasedTokenTextSplitter.builder() .withTokenLimit(512) .withOverlapTokens(80) .withTitleMetadataKey(doc_title) .build(); var chunks splitter.apply(documents);第二步向量化写入vectorStore.add(chunks);第三步检索问答SearchRequest request SearchRequest.builder() .query(AP-2024-0112 的部署步骤是什么) .topK(10) .similarityThreshold(0.42) .build(); var results vectorStore.similaritySearch(request);这段代码跑通之后平台的 RAG 主链路已经通了。后续就是在这个基础上去加混合检索、重排序、权限过滤这些工程化逻辑。3.4 一个带工具调用的 Agent 实例我用一个实际案例来演示开发一个“订单查询助手”。它需要能识别用户问题、判断是否需要调用订单接口、然后组织回答。先定义一个工具Spring AI 里直接用 Tool 注解Component public class OrderTools { Tool(description 根据订单号查询订单当前状态和物流信息仅支持内部订单订单号格式为 AP-数字) public OrderInfo queryOrderStatus(String orderNo) { return orderService.queryOrder(orderNo); } }然后构建 AgentConfiguration public class AgentConfig { Bean public ChatClient orderAgent(ChatModel chatModel, ListToolCallback toolCallbacks) { return ChatClient.builder(chatModel) .defaultSystem(你是一个订单助手。请先通过工具查询订单状态再结合结果回答用户。) .defaultTools(toolCallbacks.toArray(new ToolCallback[0])) .build(); } }在 Controller 中调用时通过 ChatClient 的 prompt 传入用户消息和会话 ID就能实现多轮对话 工具调用的闭环。注意 ChatClient 是 Spring AI 1.0 里推荐的统一入口它比直接使用 ChatModel 更易用把工具调用、提示词模板、记忆都封装好了。3.5 多 Agent 协作的最小实现多 Agent 协作是这个平台标题里比较有分量的内容。我的实现方式是给每个 Agent 一个明确的角色边界然后通过一个“调度 Agent”按需转发请求。转发机制是动态的调度 Agent 接收问题后先判断属于哪个领域订单、售后、技术咨询再把原始请求转发给对应领域的 Agent。这样做的好处是每个领域 Agent 的 Prompt 可以集中优化工具列表也不用手忙脚乱的塞一堆。如果领域 Agent 返回的结果不完整调度 Agent 可以发起二次追问或转交给另一个 Agent。为了控制复杂度我限制两层转发最多一个调度层加一个执行层避免 Agent 之间无限循环。在实现层面我借助了 Spring AI 的 Advisor 机制在 PreAdvice 阶段由调度 Agent 决定路由目标然后替换执行链路。这比硬编码 if-else 要灵活很多。4. 常见踩坑与排查实战4.1 向量检索召回结果差先怀疑切块而非索引这个坑我印象很深。第一版 RAG 上线后用户反馈很多专业问题答非所问。当时我先怀疑 embedding 模型不够好、向量库参数不对折腾了半天最后发现是切块时把一个完整的操作步骤拦腰切断了导致检索到的片段逻辑不完整。切块检查有一个笨办法但非常有效把切出的 chunk 按顺序拼回来看看是否能无缝还原原文档。如果拼回时发现语句表达断裂说明切块策略需要调整。另一些常见问题包括PDF 表格被切割成无意义字符、代码块的缩进在 embedding 前被 Markdown 解析器吃掉、标点符号被切到 chunk 边界外。这些都需要在切块器里增补规则。4.2 工具调用频繁失败的排查方法Agent 调工具时最常见的报错是“参数格式错误”或“必填参数缺失”。第一次遇到时我以为是模型理解能力不行后来逐条对比发现根本原因是部分工具的参数名起得太抽象。比如我有一个工具参数叫“input”description 写得也敷衍模型每次都在猜这个参数到底该传什么。排查工具调用问题的套路是打开 Spring AI 的日志把请求和响应的完整 JSON 输出到日志文件仔细看模型返回的工具调用参数和工具定义是否吻合。这里有个小技巧在开发环境配置 logging.level.org.springframework.aiDEBUG就能看到完整的请求体、响应体以及工具执行结果基本够用。如果日志正常但工具执行抛异常优先看异常信息的根因不要被模型生成的“抱歉我遇到错误”误导。模型这时候只会客套话真实的错误藏在工具方法内部。4.3 Spring Boot 4 虚拟线程与 AI 异步调用的相处之道Spring Boot 4 默认开启虚拟线程对 AI 场景的高 IO 等待非常友好。但要注意不要在有线程本地变量ThreadLocal的代码路径里跨虚拟线程传播上下文比如数据库事务和部分安全上下文。AI 模型调用本身是纯 IO 等待用虚拟线程收益很大但工具方法内部如果涉及事务或线程绑定资源建议在装配层切换为普通线程池避免踩到线程复用的坑。另外一个老问题异步调用 Agent 时一定要显式设置超时时间。我和大模型服务商之间的网络抖动、模型推理排队都可能导致调用时间远超预期。平台里配置了三个超时连接超时 8 秒、读取超时 60 秒、整体 Agent 执行超时 90 秒。超过 90 秒直接向用户返回“处理超时请稍后重试”保住用户体验。4.4 技能编排中 DAG 死循环的防御技能编排引擎上线后遇到过一个问题一个技能节点 A 依赖节点 B 的输出节点 B 又声明依赖节点 A配置在了可视化页面上直接导致引擎跑完 A 后永远不会到达 B。后来我在配置保存时增加了一个基于 DFS 的环检测所有技能保存前必须先通过校验。同时引擎执行时也加了节点执行次数上限默认 50一旦超过就中断整个技能并报错。4.5 记忆污染与切换模型后的上下文异常如果用户前几轮对话用的模型 A中途因为服务商故障切换到模型 B记忆上下文里的历史消息可能会包含模型 A 特有的系统标签或工具调用格式模型 B 解析不了导致回答风格突变甚至报错。我的处理方式是切换模型时将上下文中的系统消息和工具消息清理掉只保留用户消息和最终的助手回复重新组织成一个干净的短期记忆。这虽然牺牲了一部分上下文信息但换来的是模型切换后的稳定输出。5. 个人总结与扩展这个平台做完以后我最大的一点体会是Spring AI 的抽象层把模型接入成本压得非常低真正决定项目成败的其实是你对业务场景的建模深度——知识怎么切块、工具怎么定义、技能怎么编排、记忆怎么管理这些才是 AI 应用的核心工程问题。模型会越来越强但工程化的组织方式不会随模型换代而作废。如果要在这个平台基础上继续扩展我建议优先做两件事一是把模型调用和检索链路的监控数据收集起来搞清楚每一次回答的 token 消耗、延迟、召回质量用数据指导优化二是把技能编排的可视化界面做得更好用让业务运营同学能自己配置 Agent 技能而不是每次都要开发介入。最后分享一个实操小技巧多模型平台的“模型路由”不要一上来就搞复杂的智能路由先按业务线配置规则比如“这些接口固定用模型 X这些场景用模型 Y”。跑一段时间后你会积累出不同模型在不同任务上的实际表现数据那时候再做动态智能路由才有依据。绝大部分项目根本用不上智能路由规则路由已经足够解决 80% 的诉求。本文还有配套的精品资源点击获取