尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot 4 + Spring AI 实战:多模型接入、RAG 与 Agent 编排

Spring Boot 4 + Spring AI 实战:多模型接入、RAG 与 Agent 编排 简介Snail AI 定位为企业级 AI 智能体平台基于 Spring Boot 4 与 Spring AI 构建面向需要统一接入多模型、编排智能体流程、搭建 RAG 知识库、管理长期记忆和技能插件的 Java 技术团队尤其适合在企业内部落地智能客服、知识问答、自动化助手等场景。压缩包共 672 个文件整体仅 2.26MB以 554 个 Java 源码文件为主体承担后端核心逻辑49 个 JavaScript 与 12 个 CSS 组成后台管理界面的前端资源39 个 XML 配置用于应用装配与配置文件另含 3 个 imports 导入描述、SQL 数据库脚本、Dockerfile 容器部署文件、proto 接口定义文件等结构清晰、轻量易读可作为企业级 Agent 平台的二次开发基线。目前已有 25 人浏览学习适合具备 Spring Boot 基础、希望深入 Agent/RAG 实践或研究智能体平台架构的中高级开发者。资源涵盖多模型管理、智能体编排、RAG 检索、记忆持久化等功能的后端实现并配套后台管理界面与 OpenAPI 接口便于直接切换大模型、维护向量知识库与自定义技能借助 Dockerfile 可快速搭建本地运行环境减少从零集成的重复工作也能帮助团队更快理解现代 Java AI 应用的工程组织适合私有化部署与深度定制场景。1. 从“能跑”到“能落”Spring Boot 4 Spring AI 这套平台到底解决了什么先说一个反直觉的结论现在很多团队做 AI 应用卡住的不是模型能力而是“模型接入”和“上下文管理”这两件脏活。你拿 ChatGPT 写个 demo 很容易但一旦要接企业知识库、要多个 Agent 协作、要会话里有记忆、要按技能编排调用链就会发现 prompt 拼来拼去、向量库换来换去、线程池被 LLM 调用堵死——这些事每个项目都在重复造轮子。Spring Boot 4 Spring AI 这个组合本质就是把“多模型接入、RAG、记忆、技能编排、Agent 协作”这些 AI 应用里的高频能力收敛成一套可以开箱即用的平台底座。它不是一个前端 demo 项目而是一个让后端团队可以快速把 AI 能力嵌进现有 Spring Boot 服务体系里的中间件层。这篇文章我会站在一线开发的视角把这套平台的架构拆开讲清楚 Spring Boot 4 和 Spring AI 各自的角色、多模型接入怎么配、RAG 和向量检索落地时那些坑、Agent 编排用哪种模型设计、以及线上调度和扩缩容怎么处理。所有配置和代码都是可抄作业的但我也会明确说哪些参数是默认值就够哪些必须根据你的业务调免得你照着抄完发现线上翻车。适合的读者是已经有 Spring Boot 基础、想在项目里引入 AI 能力的后端开发以及被 LangChain 那套 Python 生态搞烦了、想在 JVM 体系里统一 AI 能力的团队。2. Spring Boot 4 与 Spring AI 的选型逻辑为什么是它们俩组合2.1 Spring Boot 4 带来了什么不只是一个版本号Spring Boot 4 不是一个简单的数字升级。它基于 Spring Framework 7底层把 Jakarta EE 的基线标到了 Servlet 6.1这意味着你在用 Boot 4 时容器环境、内嵌 Tomcat、依赖管理都跟 Boot 3 有本质差别。很多团队升级踩坑第一刀就切在 javax 到 jakarta 的包名迁移上但 Spring Boot 4 直接把这个问题写进了基线新的代码不需要再担心老包名。从应用角度Spring Boot 4 最值得关注的是它对 GraalVM 原生镜像的支持进一步成熟配合 Spring AI 的场景可以做到很低的启动延迟和内存占用。AI 应用里 LLM 调用是 IO 密集型但 Agent 编排和技能编排往往是 CPU 密集型这两类负载混在一个应用里原生镜像的启动时间从十几秒压到几百毫秒对弹性扩缩容非常友好。如果你用的是 JDK 17建议直接上 21Spring Boot 4 对 21 的支持是完整且经过充分测试的。另外Spring Boot 4 的自动配置机制更强调条件化装配Spring AI 正好利用了这一点。你引入某个模型 Starter它的自动配置类只有在对应的 API Key 配置存在时才生效不会像以前那样默认加载一堆用不到的 Bean。这就让多模型接入的配置面变得非常干净。2.2 Spring AI 在 JVM 生态里的定位不是 LangChain 的替代是另一种解法Spring AI 的官方定位是给 Spring 生态提供 AI 应用抽象层包括ChatModel、EmbeddingModel、VectorStore、Memory这些核心接口。它跟 LangChain 最大的区别是LangChain 是一套独立的框架有自己的链和代理抽象Spring AI 是建立在 Spring 的依赖注入和自动配置之上的它的可观测性、事务管理、重试机制天然跟 Spring 体系融合。选择 Spring AI 而不是 LangChain4j核心理由有两条第一Spring AI 是 Spring 官方孵化的项目后续跟 Spring Boot 4 的版本兼容性有保障不用自己处理第三方库和 Boot 版本之间的微妙冲突第二Spring AI 的Advisor机制比 LangChain4j 的过滤器更贴近 Spring 开发者的习惯你可以在调用链路上插入鉴权、日志、RAG 增强、记忆管理像写 Spring AOP 一样自然。不过要清醒一点Spring AI 的生态成熟度还比不上 LangChain尤其是 Agent 部分很多高级编排能力还在快速迭代中。我的做法是把 Spring AI 当作模型接入和 RAG 的稳定底座Agent 编排层用自己写的协调器包一层这样既拿到官方支持又不会被框架的未稳定 API 绑架。2.3 平台的整体模块划分接入层、编排层、存储层这套平台按职责可以拆成三层。接入层负责统一所有模型提供方的 API 差异包括 OpenAI、Azure OpenAI、智谱、通义、Ollama 本地模型等对外暴露统一接口让上层不感知具体供应商。编排层负责 RAG 管道的组装、Agent 的规划与执行、技能调用的路由这是整个平台的核心。存储层负责向量索引、会话记忆、技能定义和权限策略的持久化。模块划分上有两个关键决策。第一向量库和业务库分开不要为了省事把向量塞进 PostgreSQL 的 JSON 字段里除非你的数据量在十万条以下且对召回精度不敏感。第二Agent 执行引擎和 Web 服务模块物理隔离因为 Agent 的循环调用会长时间占用工作线程如果把 Tomcat 线程池和 Agent 执行线程混在一起并发一高就会出现线程饥饿普通请求被 Agent 任务堵死。3. 多模型接入的最小配置从 OpenAI 到本地 Ollama 的完整落地3.1 引入依赖与起步配置一个可抄作业的最小工程不管接哪个模型起步步骤是一样的引入spring-ai-starter和你需要的具体模型 Starter。以智谱和 Ollama 为例下面是pom.xml里的核心依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-zhipu/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-ollama/artifactId /dependency这里注意Spring AI 的 GroupId 从早期版本开始就是org.springframework.ai但具体模块的命名在不同版本里变过几次尤其是spring-ai-starter-model-*这种命名是从 1.0 之后才稳定下来的。如果你们的仓库管理严格建议用 BOM 统一管理版本避免不同模块版本不一致导致的 NoSuchMethodError。然后是配置文件用application.yml管理多模型的关键点在于每个模型的base-url和api-key要独立配置同时要为每个模型定义一个业务别名后续代码里通过别名注入不直接依赖具体实现类。spring: ai: zhipu: base-url: https://open.bigmodel.cn/api/paas/v4 api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4-plus temperature: 0.7 ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b temperature: 0.3配置完这两段Spring AI 的自动配置会在容器里创建两个ChatModelBeanBean 名称默认是zhipuChatModel和ollamaChatModel。如果你想要自己的别名可以用Qualifier或者在配置类里重新包装。这里有个很容易忽视的点temperature的默认值在不同模型上表现差异很大智谱的 GLM 系列对temperature的敏感度比 OpenAI 高实测同样的 0.7 在 GLM 上回答会更跳跃所以业务场景不同要单独调不能用一套参数套所有模型。3.2 统一调用接口与多模型 Router 的实现当你接了三五个模型之后业务上最头痛的是每个模型返回格式微有差异比如有的模型返回content字段有的返回text字段有的会带reasoning内容。Spring AI 的ChatModel.call()已经统一了返回类型为ChatResponse但实际开发中你需要做一层自己的AiRouter按业务路由到不同模型同时记录延迟和 token 消耗。这里我直接给出一个可用的多模型 Router 骨架Service public class AiRouter { private final ChatModel zhipuChatModel; private final ChatModel ollamaChatModel; private final MeterRegistry meterRegistry; public AiRouter(Qualifier(zhipuChatModel) ChatModel zhipuChatModel, Qualifier(ollamaChatModel) ChatModel ollamaChatModel, MeterRegistry meterRegistry) { this.zhipuChatModel zhipuChatModel; this.ollamaChatModel ollamaChatModel; this.meterRegistry meterRegistry; } public ChatResponse route(String modelAlias, String prompt) { long start System.currentTimeMillis(); ChatModel target resolve(modelAlias); ChatResponse response target.call(new Prompt(prompt)); meterRegistry.timer(ai.call.duration, model, modelAlias) .record(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS); return response; } private ChatModel resolve(String alias) { return switch (alias) { case zhipu - zhipuChatModel; case ollama - ollamaChatModel; default - zhipuChatModel; }; } }这段代码有几个值得注意的设计意图。第一通过Qualifier注入具体 Bean绕开了 Spring AI 自动装配时可能出现的多个ChatModel冲突问题如果你不写QualifierSpring 会因为同时存在多个ChatModel类型 Bean 而启动失败。第二用 Micrometer 的MeterRegistry记录每次调用的耗时这是后续做模型降级和成本分析的基础数据别等到线上模型超时了才开始埋点。Router 做好了之后业务代码里调用aiRouter.route(zhipu, userPrompt)就行。这个时候你还可以顺手加一个简单的熔断逻辑连续超时超过 3 次就切到备用模型实现非常简单用CircuitBreaker注解包一层即可。3.3 模型提供的稳定性与降级策略不能把命交给单家厂商AI 应用的线上事故里模型厂商接口抖动占了很大比例。平台层面要做的是多模型自动降级而不是等客服电话。我的做法是定义一个ModelChaosExecutor在调用失败时按优先级切换Component public class ModelFailoverExecutor { private final ListModelRoute routes List.of( new ModelRoute(zhipu, 0), new ModelRoute(ollama, 1) ); public ChatResponse executeWithFailover(String prompt) { for (ModelRoute route : routes) { try { return route.chatModel().call(new Prompt(prompt)); } catch (Exception e) { log.warn(model {} failed, switching to next, error{}, route.name(), e.getMessage()); } } throw new IllegalStateException(all models unavailable); } }这里有个血泪教训降级策略的关键是「超时也要兜底」很多模型 SDK 内部没有默认读超时或者默认超时长达 120 秒。你必须在全局配置里把连接超时和读超时压下来通常连接 3 秒、读 30 秒是合理值。否则一旦上游抖动你的 Agent 线程会被挂住几分钟。超时配置在 Spring AI 里可以通过RestClient的配置覆盖但最省心的做法是在接入层统一设置一个ResponseErrorHandler把非 2xx 响应快速转换成异常避免 SDK 的默认行为把错误体包装成通用异常丢失信息。3.4 本地模型与云端模型混合部署的实操清单很多团队会在开发环境用 Ollama 跑本地模型生产环境切云端 API这套平台的配置方式天然支持这种混合部署。本地模型的优势是隐私和数据不出内网劣势是显卡资源有限并发能力弱。混合部署的最佳实践是低并发内部工具用本地模型高并发用户请求走云端把本地模型当作降级后备而不是主力。Ollama 接入要特别注意base-url必须指向http://localhost:11434而不是https://ollama.com很多人第一次配错就是去查 ollama.com 的 API 地址。另外 Ollama 拉取的模型名称要跟 Spring AI 里的model配置完全一致比如qwen2.5:7b和qwen2.5:7b-instruct-q4_K_M是两个不同的模型串配错会导致 404。4. RAG 落地的核心链路向量检索与知识库增强不能只调一个 API4.1 RAG 的最小工作流从文档加载到回答生成的五个环节RAG 不是简单地把用户问题发给模型再拼接一段知识它是一条完整的数据管道。最少可用链路需要五个环节文档解析与加载、文本切分、向量化、向量检索入库、带上下文的生成。每个环节都有独立的配置和调优空间。我一般会把 RAG 分为离线管道和在线管道。离线管道负责把新文档处理成向量并入库在线管道只负责从向量库召回内容再走模型生成。这样做的好处是离线任务失败不影响线上回答质量文档更新可以走定时任务批量处理。Spring AI 里文档加载用DocumentReader切分用DocumentSplitter向量化用EmbeddingModel存储用VectorStore。下面是一段典型的离线管道代码Service public class RAGIngestionService { private final VectorStore vectorStore; private final EmbeddingModel embeddingModel; public RAGIngestionService(VectorStore vectorStore, EmbeddingModel embeddingModel) { this.vectorStore vectorStore; this.embeddingModel embeddingModel; } public void ingest(String filePath) { // 1. 读取文档 ListDocument documents new PagePdfDocumentReader(filePath).get(); // 2. 按 512 字符切块重叠 64 字符 TokenTextSplitter splitter new TokenTextSplitter(512, 64); ListDocument chunks splitter.apply(documents); // 3. 向量化并入库 vectorStore.add(chunks); } }4.2 切分策略的参数调优为什么固定字符数不是好选择TokenTextSplitter的构造参数是块大小和重叠大小直接决定了召回质量。切分太小语义不完整检索召回一堆残句切分太大模型上下文膨胀超出 context window反而降低了后续生成的精确度。这块没有标准答案但有几个经验参数可以参考中文场景建议块大小 300500 字重叠 3080 字代码类文档建议按方法或类切分而不是按固定字符切否则一个类被切得四分五裂表格类文档建议整表保留不切开。如果你用的是 PDF 或 Word 的复杂排版还要处理表格和图片。常见做法是用规则检测到表格区域后单独提取做成 Markdown 表格再切分而不是把 PDF 解析出的纯文本直接喂给切分器。遇到混合排版页面宁可这个页面整页入库也不要让切分器把文本流切断在一个无意义的位置。这套平台的默认切分器是基于 token 数切分的但 token 数和字符数在中文场景下不是等价的一个中文字符在 Llama 系分词器里可能占 0.61.5 个 token所以你需要对你的语料做一次抽样统计跑一遍实际切分结果看 chunk 语义是否完整。不要相信默认参数直接上线这个环节是 RAG 项目翻车的高发地。4.3 向量库选型与数据库连接配置PostgreSQL 还是专用向量库向量检索需要一个 connectorSpring AI 支持多种VectorStore实现包括 PGVector、Milvus、Qdrant、Chroma、Redis 等。选型时可以从查询延迟、过滤能力、部署运维成本三个维度权衡。小团队且已有 PostgreSQL 实例直接选 PGVector 最省事数据量大且需要复杂过滤Milvus 或 Qdrant 更专业。这里给出一张选型对比表向量库适合数据量过滤器支持运维成本延迟表现PGVector百万级以下较好低一般Qdrant千万级优秀中优秀Milvus十亿级优秀高优秀Redis十万级一般低优秀我个人的建议是没有作死需求就别一开始上 MilvusPGVector 先扛住上线等数据量真的涨到百万级再迁移。迁移的代价主要是重新向量化一次所有文档而这本身也是离线管道的一次重跑成本可控。如果你的平台已经定义好了VectorStore接口切换数据库只需要改配置和依赖业务代码几乎不动。比如从 PGVector 切到 Qdrant只需要替换 starter 依赖和修改连接参数spring: ai: vectorstore: qdrant: host: localhost port: 6333 collection-name: ai_platform_docs但注意切换向量库时Embedding 模型尽量保持一致否则同一个文档在不同向量库里的向量空间完全不兼容检索会变成瞎撞。向量库切换必须连同全量重建一起做这是切库的默认前提。4.4 Agentic RAG 与普通 RAG 的差异该不该让模型决定查几次如果你的平台目标用户是需要复杂问题分解的场景那要区分普通 RAG 和 Agentic RAG。普通 RAG 是固定的一路检索加生成Agentic RAG 是让模型自己决定检索几次、每次检索什么关键词、是否需要调整检索策略。后者效果上限高但失败模式和成本也更难控制。以“对比上季度和本季度的销售数据”这种问题为例普通 RAG 会把问题整体向量化检索一堆混合内容生成时可能只从中挑一段答信息不完整。Agentic RAG 会把问题拆成两个子查询“上季度销售总额”“本季度销售总额”每个子查询独立走检索再把两个结果拼在一起让模型汇总。这样的回答质量通常明显提升。Spring AI 里的Advisor机制可以帮你实现最基础的 Agentic RAG 增强通过自定义 Advisor 在每次 LLM 调用前动态拼接检索结果。但更复杂的多跳检索一般还是要靠 Agent 框架来实现这个我在第 5 章展开。4.5 知识库更新与同名向量冲突增量入库的三个注意点知识库不是一次性建好就结束的。当你更新某个文档旧 chunk 还留在向量库里新旧内容重复检索召回会出现语义污染。平台需要注意以下几点第一文档级别维护一个 version入库时带上自定义 metadata 标记 source 和 version检索时优先按 version 过滤。Spring AI 的Filter机制支持这种 metadata 过滤比如vectorStore.similaritySearch(SearchRequest.query(...).withFilterExpression(source contract_2024.pdf))。第二删除旧版本时要通过 source 定位所有相关 chunk而不是按 chunk id 删。因为切分器每次运行产生的 chunk id 可能随机变化按 chunk id 删容易漏。第三如果文档更新很频繁建议用异步任务在低峰期重建索引而不是在线调用里同步更新。同步更新会阻塞请求线程而且 embedding 调用可能要花好几秒用户等不起。5. 多 Agent 与技能编排构建可协作的 Agent 工作流5.1 Agent 和 Skill 的边界别让 Agent 变成一个巨型 if-else技能编排是这套平台最容易被误解的部分。很多人把 Agent 理解为“一个能自主决策的大模型”但实际落地时你会发现单 Agent 做不了复杂任务多 Agent 协作又有通信和状态同步的成本。更可行的方案是「一个 Supervisor Agent 多个 Skill Worker」Supervisor 负责理解意图、拆分任务、调度工人Skill Worker 每个只做一件具体的事比如“翻译”“摘要”“代码生成”“数据库查询”。这个设计能落地的关键是要在代码层面明确Skill的接口。每个 Skill 必须有独立的名称、描述、输入输出 schemaSupervisor 通过 LLM 能力选择调用哪个 Skill。这个 schema 很关键LLM 的 function calling 依赖准确的参数描述schema 写得含糊模型就会传错参数。一个常见的反模式是把五六个工具函数拼进一个大 Prompt让模型自己选。短期内可用但一旦工具数量超过十个模型选择准确率暴跌同时还容易互相干扰。更科学的是用 Spring AI 的Tool注解定义技能利用框架自动生成 function calling schema再交给 Agent 调度。下面是定义技能的最小示例Component public class SearchSkills { Tool(description 根据关键词搜索企业内部知识库返回最相关的文档片段) public String searchKnowledge(String keyword, int topK) { // 调用 vectorStore 查询 return 检索结果: keyword; } Tool(description 将文本翻译成目标语言languageCode 取值为 zh/en/fr) public String translate(String text, String languageCode) { // 调用翻译模型的接口 return text; } }5.2 用 Spring AI 构建 Supervisor Agentloop 的终止条件与 Token 控制Spring AI 里实现 Agent loop 不是开箱即用的官方给了ChatClient的结构化输出和工具调用能力但循环控制需要自己写。我一般会做一个AgentCoordinator核心是一个 while 循环每轮让模型决定下一步动作直到模型输出 final answer 或达到最大轮数。Service public class AgentCoordinator { private final ChatClient chatClient; public AgentCoordinator(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个任务规划助手必须严格按技能列表选择工具不得随意编造。) .build(); } public String execute(String userTask) { int maxSteps 5; Message currentMessage new UserMessage(userTask); for (int i 0; i maxSteps; i) { ChatResponse response chatClient.call(new Prompt(List.of( currentMessage, new ToolCallMessage-Agent... ))); // 解析 response 里的 tool call // 执行工具 // 把工具结果追加为新的消息 // 判断是否生成 final answer } throw new IllegalStateException(agent loop exceeded max steps); } }不要把上面的伪代码当作可以直接运行的东西但这里的关键点是每一轮的 history 必须包含上一轮的工具结果否则模型失去上下文会重复调用同一个工具。必须设置maxSteps否则模型可能在复杂问题上无限循环token 费用快速爆炸。5 步是一个保守值复杂任务可以提至 8但不要超过 10。每轮的 prompt 里都要带上全部技能清单和它们的描述这是 function calling 模式的要求但也意味着 token 开销会累积。平台层面最好把技能描述缓存起来避免每次构造 Prompt 都拼接一遍。5.3 多 Agent 协作时的上下文传递与记忆共享多 Agent 之间不能各自为政否则协作就退化成“把一堆模型输出硬拼在一块”。最简单可靠的模式是所有 Worker 都共享同一个ConversationMemorySupervisor 在派发任务时把相关记忆片段一并传给 Worker。这里注意不是共享全部原始会话而是共享「经过摘要或抽取的上下文」。平台的记忆管理要分层短期记忆存当前会话的最近 N 轮原始消息长期记忆存跨会话的用户偏好和任务结论工作记忆存当前 Agent 执行过程中的中间产物。三层分开存储避免互相污染。我常用的实现是 Redis 里存短期记忆键名是sessionId:recent值为 JSON 数组过期时间设成会话空闲超时。长期记忆存 PostgreSQL用 metadata 标记用户 ID、标签、时间检索时按用户过滤。Spring AI 的MessageMemory接口提供了基础能力但它的默认实现是内存存储多实例部署下会丢失记忆必须封装一层 Redis 实现替换。技能编排的另一个坑是技能调用的鉴权。不要把数据库查询技能暴露给所有普通用户必须在 Agent 层加权限过滤。Supervisor 在派发技能前检查当前用户的角色组如果没有该技能权限直接拒绝并告知用户而不是把权限校验逻辑写在技能内部否则绕过 Agent 直接调技能接口就泄露权限。5.4 Agent 的失败恢复如何优雅处理工具异常与模型幻觉Agent 执行过程中工具异常是常态而不是异常。比如搜索技能连不上向量库、翻译技能超时、数据库查询返回空。处理策略是工具异常要反馈给模型让模型判断是否换一种方式重试而不是直接终止整个 Agent。一个典型的错误处理循环长这样try { Object result executeSkill(skillName, args); return result; } catch (Exception e) { return 工具执行失败错误信息: e.getMessage() 。请尝试换一个技能或改变参数重试。; }这个返回值会作为 tool response 回到模型那里模型可能会说“那我试试关键词模糊搜索”。这种自我纠正能力是 Agent 有价值的体现。但要注意不能无限重试否则一个坏工具会把 Agent 拖进死循环。常见的做法是记一个toolErrorCount超过 2 次就强制切给人工兜底。模型幻觉在 Agent 里表现得特别明显模型可能声称执行了某个技能并返回了一个虚构结果但实际上根本没调用工具。缓解办法有两个一是在 Prompt 里强调“只能使用工具结果回答不得自行编造”二是用输出约束让模型返回结构化结果其中必须包含source_tool字段平台层面校验该字段是否真实存在。这个方法虽然不能 100% 消幻觉但能堵住最恶劣的例子。6. 线上部署要避开的 6 个常见坑从线程配置到向量库连接6.1 线程池被 LLM 调用占满默认 Tomcat 线程池不适合 AI 场景这是线上最容易翻车的地方。普通 web 请求处理很快Tomcat 默认 200 线程足够。但 Agent 的循环调用一次可能耗时 30 秒以上如果 50 个用户同时发起 Agent 任务线程池直接撑爆其他所有请求排队。解决思路是把 LLM 调用统一放到独立的ExecutorService中用 CompletableFuture 异步编排不让阻塞的模型调用直接占用 Tomcat 工作线程。平台里要做的是定义一个专门的AiExecutor可控并发、可排队、可拒绝Bean(aiTaskExecutor) public ExecutorService aiTaskExecutor() { return new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new ThreadFactoryBuilder().setNameFormat(ai-worker-%d).build(), new ThreadPoolExecutor.CallerRunsPolicy() ); }参数怎么定核心线程数建议为 CPU 核心数乘以 2最大线程数不超过 16队列长度 100。如果并发任务超过队列容量拒绝策略用CallerRunsPolicy会让调用线程亲自执行任务虽然违背了异步原则但至少在负载极高时降级为同步不会丢失任务。另一个方案是用AbortPolicy并返回 429但这对用户不友好。6.2 向量检索耗时异常默认精确检索在高数据量下不可用PGVector 的默认索引是 IVFFlat 还是 HNSW直接决定大数据量下的检索速度。Spring AI 的 PGVector Starter 默认创建索引时用的是 ivfflat 还是 hnsw不同版本不一样。一段常见翻车现场是数据量从几万涨到几十万检索延迟从几十毫秒涨到好几秒原因是索引没建或者索引参数不合适。正确做法是建 HNSW 索引并设置合适的m和ef_construction参数CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);m控制每个节点的最大连接数越大精度越高但内存占用越大ef_construction控制构建索引时的搜索范围越大索引质量越好但构建时间越长。m16, ef_construction64是精度和资源的均衡值。另外检索时也要指定ef_searchSpring AI 的 PGVector 支持通过请求参数传入如果没传默认值可能很低影响召回质量。6.3 Embedding 模型不一致导致的召回率丧失换库必须全量重建这个坑特别隐蔽。你在开发时用 OpenAI 的text-embedding-3-small向量化了一批文档后来因为成本改用智谱的 embedding结果发现原来能召回的内容全都召不回了。原因很简单不同模型的向量空间不同余弦相似度没有可比性。换 embedding 模型等于切换了整个向量空间旧向量全部作废。所以平台上线前就要定好 embedding 模型尽量不要中途更换。真必须换时全量文档重新向量化是唯一解法没有后悔药。在线管道和离线管道里要保持 embedding 模型的配置一致不要一个用 A 模型一个用 B 模型。6.4 会话记忆在分布式环境下的失效本地内存不是记忆Spring AI 默认的InMemoryChatMemory只在单实例内有效。如果你部署了两个副本用户第一次请求打到实例 A记忆存在 A第二次请求负载均衡到实例 B模型完全没上下文用户感觉“这 AI 是不是失忆了”。平台里必须把记忆存储外置到 Redis以下是使用 Spring AIChatMemory接口自定义 Redis 实现的思路Component public class RedisChatMemory implements ChatMemory { private final StringRedisTemplate redisTemplate; Override public ListMessage get(String conversationId, int lastN) { ListOperationsString, String ops redisTemplate.opsForList(); ListString jsonList ops.range(chat: conversationId, -lastN, -1); // 反序列化成 Message 列表 } Override public void add(String conversationId, Message message) { redisTemplate.opsForList().rightPush(chat: conversationId, serialize(message)); redisTemplate.expire(chat: conversationId, Duration.ofHours(24)); } }这样每个实例都从 Redis 读记忆彻底解决分布式失效。这里有个次要问题Redis 列表按时间顺序存取如果用户消息和辅助消息交替出现反序列化时要保留messageType字段否则模型分不清哪些是用户说的、哪些是工具返回的推理会混乱。6.5 Agent 技能列表过长的 Token 压力动态裁剪技能描述当技能数量增长到二三十个每个技能的 schema 描述都很长一轮 function calling 的 prompt 可能就有好几千 token成本飙升且模型对技能的注意力和选择准确率下降。平台需要按用户意图动态裁剪技能列表。常见的做法是先让模型对用户问题做一次轻量意图分类再根据意图只注入相关技能子集。比如用户问“翻译一段合同”就只注入翻译技能和文档技能其他技能全部排除。这个分类调用成本很低用一个小模型就能做却可以大幅降低主模型的 token 消耗。另外技能描述写得好也能省 token描述控制在 20 字以内参数的 description 控制在 10 字以内格式统一为“动词 对象 场景”。不要写“用于从知识库中检索与关键词相关的文档片段并返回前 topK 个结果适合企业内部搜索场景”直接写“搜索知识库返回前 topK 个文档片段”即可。冗长表述除了浪费 token 外还可能让模型误解触发条件。6.6 本地大模型的内存不足Ollama 模型的 OOM 与 swap 抖动最后这条属于硬件玄学但也必须说Ollama 拉取 7B 模型并加载到 GPU 显存或者 CPU 模式下加载到内存都要占用极大资源。一个 7B 的量化模型在 CPU 推理时需要至少 8GB 内存如果平台部署在一台只有 8GB 的云主机上Ollama 启动后系统就会疯狂 swap然后所有接口都变慢包括与 AI 无关的普通接口。排查方法是看dmesg里有没有 OOM killer 记录以及free -h确认 swap 是否被大量占用。解决上要么换更大的机型要么给 Ollama 设置环境变量限制并发推理数量比如OLLAMA_NUM_PARALLEL1让它一次只处理一个请求避免十几个并发切割内存。开发环境本地跑 7B 模型没问题但生产环境想用本地模型做主力请先算清楚物理内存账不要只看模型文件只有 4GB 就以为 8GB 刚好够。7. 从可用到好用验证 RAG 召回质量的三个量化指标与一套评测脚本RAG 系统上线后你还需要一个可验证的质量基线。不要问“效果怎么样”要问你“hit rate 是多少、MRR 是多少、生成幻觉率是多少”。我用三个指标来验证这套平台的核心链路Hit Rate在测试集里针对每个问题预先标注正确答案所属的文档 chunk检索结果 top K 里是否包含该 chunk。包含则命中。这个指标衡量检索环节的能力。MRR (Mean Reciprocal Rank)对每个问题看答案第一次出现在检索结果中的位置取倒数后求平均。它衡量排序质量比 hit rate 更能反映真实体验。Faithfulness生成答案中每个关键论点是否能在检索到的文档里找到对应依据。人工或用一个评估模型判断。下面是一段用于批量评测 hit rate 的最小脚本跑在独立测试工程里不干扰线上服务import requests import json KB_CHUNKS [ {chunk_id: doc1_3, text: 本季度销售总额为 2300 万元...}, {chunk_id: doc2_7, text: 上季度销售总额为 1800 万元...}, ] def evaluate_hit_rate(questions_with_gold): hit 0 total len(questions_with_gold) for q, gold_chunk_id in questions_with_gold: resp requests.post(http://localhost:8080/api/v1/retrieve, json{query: q, top_k: 5}) results json.loads(resp.text)[chunks] ids [item[chunk_id] for item in results] if gold_chunk_id in ids: hit 1 return hit / total test_cases [ (本季度销售总额是多少, doc1_3), (上季度销售与对比, doc2_7), ] print(Hit Rate 5 , evaluate_hit_rate(test_cases))这个脚本只是框架你需要准备至少 50 组标注好的 QA 对才能得到有统计意义的结果。要持续做回归测试每次调整切分参数、换 embedding 模型、改检索逻辑后都跑一遍防止某次优化让召回变差还不自知。我自己的习惯是每周跑一次评测集把 hit rate 和 MRR 记录到一个表格里跟本周改动绑定。如果某个改动让 hit rate 掉超过 3 个百分点立即回滚对比而不是继续叠加。RAG 调优的黑匣子效应很强很多参数看着是优化实测却是负向只有长期跟踪这条评测曲线才不会在优化中迷失方向。这套平台做到这个阶段其实你的收获不只是代码而是一套“接入多模型、管理知识库、编排 Agent、持续观测质量”的完整方法论。把这三个指标当作你团队的质量卡点所有功能改动都先过一遍评测能为你省下大量跟业务方扯皮的时间。希望这些踩过的坑和验证方法能帮到你让你的 Spring Boot 4 Spring AI 平台真正从能演示的 demo变成敢于扛线上流量的底座。本文还有配套的精品资源点击获取
返回列表