ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多智能体+RAG实战:Spring AI Alibaba构建企业知识库问答

多智能体+RAG实战:Spring AI Alibaba构建企业知识库问答

多智能体加RAG这两个词放在一起,很多人第一反应是"是不是有点过度设计"。我最初也这么想——一个检索增强生成的应用,单Agent跑得好好的,为什么要拆成多个智能体?直到我接手一个内部知识库问答项目,文档横跨产品手册、运维记录、合同模板、故障复盘四大类,单Agent方案在意图识别上就开始翻车:用户问"上次那个接口超时怎么处理的",它跑去检索产品手册里的接口文档,答得头头是道但完全跑偏。那次之后我才认真研究多智能体编排这条路,用Spring AI Alibaba落地了一版,跑下来效果确实不一样。这篇就把整个设计思路、核心实现和踩过的坑完整摊开讲,适合有一定Java后端基础、想在企业知识问答场景里把RAG做扎实的同行参考,小白也能看懂整体逻辑,照着骨架能搭出自己的版本。

1. 整体架构设计与方案选型思路

1.1 为什么单Agent撑不住复杂知识场景

先说清楚问题在哪。RAG的本质是"先检索、再生成",单Agent模式下,检索和生成是串行绑死的:一个查询进来,走同一个检索器、同一个提示词模板、同一个大模型。这套结构在知识域单一的时候非常香,代码少、链路短、调试直观。但企业知识库的典型特征是异构——不同来源的文档,语言风格、术语体系、甚至"正确答案"的形态都不一样。

我拿实际项目举例。产品手册类问题,用户要的是精确参数和操作步骤,检索时应该偏向结构化片段,生成时要求严格引用原文;故障复盘类问题,用户要的是"当时怎么定位、怎么解决"的叙事逻辑,检索要跨多篇复盘做关联,生成时允许归纳总结;合同模板类问题,用户要的是条款原文,一个字都不能改,这时候生成环节甚至应该退化成"抽取+拼接"。你让一个Agent用同一套检索参数和提示词去覆盖这三类,必然有一类被牺牲。

多智能体的核心价值就在这里:把"理解意图"和"执行任务"解耦。一个路由智能体专门判断"这个问题属于哪一类、该交给谁",下游每个专职智能体带着自己的检索策略、提示词模板、甚至不同的大模型配置去干活。这跟微服务拆分的逻辑一模一样——不是为拆而拆,是因为不同业务的SLA和实现路径确实不同。

1.2 Spring AI Alibaba在这个方案里扮演什么角色

选型这块我对比过几套方案。纯手写编排(自己写状态机调大模型API)灵活但重复劳动多;LangChain4j生态成熟但Java侧的Agent编排抽象还在演进;Spring AI Alibaba的优势在于它把模型接入、提示词管理、工具调用、对话记忆这几层做了统一抽象,而且和Spring生态无缝衔接——你项目里已有的Bean、配置中心、监控埋点都能直接复用。

具体到多智能体场景,它提供的几个能力是关键:

  • ChatClient的统一封装:每个智能体本质上是一个配置了不同系统提示词和工具的ChatClient,创建成本极低,可以按需实例化多个。
  • Advisor机制:检索增强可以通过Advisor挂载到ChatClient上,意味着"检索策略"变成了可插拔的组件,不同智能体挂不同的Advisor即可。
  • 工具调用(Function Calling):路由智能体可以把下游智能体包装成工具,让大模型自己决定调哪个,这是实现"智能路由"最省事的方式。
  • 对话记忆:多轮对话的上下文管理有现成抽象,不用自己维护会话状态。

提示:Spring AI Alibaba的版本迭代较快,Advisor和Tool的API在不同小版本间有过调整,落地前务必锁定版本并核对官方示例,别直接抄半年前的博客代码。

1.3 整体链路长什么样

我把整个应用拆成四层,从上到下依次是:接入层(统一对话入口)、路由层(意图识别与分发)、执行层(多个专职RAG智能体)、基础层(向量库、文档解析、模型服务)。用户的一次提问,先经过路由层判断意图,再分发给对应的专职智能体,专职智能体完成"检索-重排-生成"后返回,路由层负责汇总和兜底。

这个分层的好处是每一层都能独立演进。比如后来我要给"故障复盘"智能体加一个"按时间线排序"的后处理,只动执行层那一个类,路由层和接入层完全不受影响。下面几节就按这个结构逐层拆。

2. 路由层:意图识别到底怎么做才靠谱

2.1 三种路由策略的取舍

路由是整个多智能体方案的咽喉,路由错了后面全错。我试过三种做法,各有适用场景:

策略实现方式优点缺点适用场景
关键词规则路由正则/关键词匹配零延迟、可控覆盖不全、维护成本高意图极固定的场景
分类模型路由微调小模型做分类准确率高、快需要标注数据、训练成本意图类别稳定且数据充足
大模型路由让大模型判断意图灵活、零训练有延迟、偶发不稳定意图类别多、边界模糊

我最终选的是大模型路由为主、关键词规则兜底的混合方案。原因很实际:项目初期没有标注数据,分类模型无从谈起;而纯关键词规则在"接口超时怎么处理"这种既像运维又像产品的问题上直接失效。大模型路由虽然多一次调用(实测增加300-800ms),但意图判断的准确率从规则版的六成多提到了九成以上,这个延迟换准确率我认为值。

2.2 用工具调用实现智能路由

具体实现上,我把每个下游智能体包装成一个"工具",交给路由智能体的大模型去选择。核心思路是给路由智能体一个精简的系统提示词,明确列出每个工具的职责边界,然后让模型输出工具调用。

// 路由智能体的系统提示词(精简版) String routerPrompt = """ 你是一个知识库问答的路由器。根据用户问题,选择最合适的处理工具。 可选工具: - productManualAgent:产品参数、操作步骤、功能说明类问题 - opsRecordAgent:故障处理、运维操作、历史事件类问题 - contractAgent:合同条款、法律文本原文查询类问题 - generalAgent:无法明确归类时的通用问答 只输出工具名,不要解释。 """;

这里有个细节值得说:提示词里必须写清"边界"而不是"能力"。早期我写的是"productManualAgent处理产品相关问题",结果模型把"产品接口超时"也判给了它。改成"涉及具体参数、操作步骤、功能说明的才归它,涉及故障现象和处理的归opsRecordAgent"之后,误判明显下降。边界的描述要具体到"什么情况不归我管",这比正面描述有效得多。

2.3 路由结果的校验与兜底

大模型路由不是百分百可靠的,必须做校验。我的做法是:路由智能体返回工具名后,先做一次合法性校验(是不是在预设的工具列表里),不合法就落到generalAgent;同时对置信度做一层判断——如果模型在回复里带了犹豫性表述(比如"可能""大概"),也降级到通用智能体。

public String route(String question) { String toolName = routerClient.prompt() .system(routerPrompt) .user(question) .call() .content() .trim(); // 合法性校验 if (!VALID_AGENTS.contains(toolName)) { log.warn("路由结果非法,降级到通用智能体: {}", toolName); return "generalAgent"; } return toolName; }

注意:路由层一定要有"通用兜底智能体",它不追求精准,但保证任何问题都有回应。没有兜底的路由系统,一旦模型抽风返回空或乱码,整个对话就断了,用户体验极差。

3. 执行层:每个专职智能体的差异化设计

3.1 检索策略为什么要按智能体分开

这是多智能体RAG和单Agent RAG最本质的区别。单Agent只有一个检索器,多智能体则是每个智能体一套检索配置。我按知识类型做了三套差异化配置:

产品手册智能体:文档结构规整、术语统一,检索用较小的分块(300-500字)保证片段精确,相似度阈值设高(0.75以上),宁可少召回也不要引入噪声。Top-K取3-5即可,因为答案通常集中在少数几个片段。

故障复盘智能体:复盘文档是叙事性的,一个故障的完整处理过程可能横跨几千字,分块要放大到800-1200字,否则"现象-定位-解决"的因果链会被切断。相似度阈值可以放低到0.6,Top-K取8-10,因为需要跨多篇复盘做关联归纳。

合同条款智能体:这个最特殊,检索精度要求极高,但生成环节要"克制"。分块按条款自然边界切(一条一款),检索用混合检索(向量+关键词),因为法律术语的精确匹配很重要。生成时提示词强制要求"只引用检索到的原文,不得改写"。

这套差异化配置不是拍脑袋定的,是拿真实问题集反复调出来的。我建议你也这么做:先准备50-100条真实问题,标注好正确答案来源,然后针对每类智能体单独调参,看召回率和答案准确率的变化。

3.2 用Advisor挂载检索增强

Spring AI Alibaba的Advisor机制让检索增强变成了可插拔组件。每个智能体创建自己的ChatClient时,挂载不同的QuestionAnswerAdvisor(或自定义的检索Advisor),检索逻辑就绑定在这个智能体上了。

// 产品手册智能体 ChatClient manualAgent = ChatClient.builder(chatModel) .defaultSystem("你是产品手册助手,回答必须基于检索到的原文,给出具体参数和步骤。") .defaultAdvisors( new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder() .topK(4) .similarityThreshold(0.75) .build()) ) .build(); // 故障复盘智能体 ChatClient opsAgent = ChatClient.builder(chatModel) .defaultSystem("你是运维复盘助手,综合多篇复盘记录,按现象、定位、解决三段式回答。") .defaultAdvisors( new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder() .topK(10) .similarityThreshold(0.6) .build()) ) .build();

这里的关键认知是:Advisor不只是"检索",它是"检索+注入提示词"的完整链路。QuestionAnswerAdvisor会把检索到的文档拼进用户消息的上下文里,模型看到的是"问题+参考资料"。所以不同智能体挂不同Advisor,等于同时定制了检索参数和上下文注入方式。

3.3 提示词模板的差异化设计

提示词这块我踩过最大的坑是"一套模板打天下"。早期所有智能体共用一个"请基于以下资料回答问题"的模板,结果合同智能体开始自由发挥改写条款,这在法务场景是灾难。后来我给每个智能体写了独立的系统提示词,核心差异在三个维度:

  • 引用要求:产品手册要求"标注参数出处",合同要求"逐字引用原文并标注条款号",故障复盘允许"归纳总结但需列出参考的复盘编号"。
  • 拒答策略:产品手册检索不到时明确说"资料中未找到该参数",合同检索不到时直接拒答"未检索到相关条款",故障复盘可以基于相似案例给"参考性建议"但需声明。
  • 输出结构:产品手册用"参数表+步骤列表",故障复盘用"现象/定位/解决"三段式,合同用"条款原文+适用说明"。
String contractSystem = """ 你是合同条款查询助手。严格遵守: 1. 只输出检索到的条款原文,不得改写、不得归纳。 2. 每条引用标注来源文档名和条款号。 3. 检索不到相关条款时,直接回复"未检索到相关条款",不得推测。 """;

提示:拒答策略一定要在提示词里写死,并且用测试用例验证。大模型天生"乐于助人",你不明确禁止,它检索不到也会编一个看起来合理的答案,这在企业场景里比不回答更危险。

4. 基础层:文档处理与检索质量优化

4.1 文档解析与分块的真实难点

多智能体方案对文档质量的要求比单Agent更高,因为每个智能体的检索精度都依赖分块质量。我在文档解析上花的时间比写Agent代码还多,几个真实难点:

PDF表格解析:产品手册里大量参数表,用通用PDF解析器出来是一堆错位的文本,参数和值对不上。后来针对表格区域单独处理,用结构化提取把表格转成"参数名: 参数值"的键值对再入库,检索准确率立竿见影。

跨页段落:故障复盘经常一个处理过程跨两三页,按页切块会把因果链切断。我的做法是先做全文拼接,再按语义边界(标题、空行、编号)切块,而不是按页切。

术语一致性:不同文档对同一概念叫法不同(比如"接口超时"和"请求超时"),纯向量检索可能漏召回。我在入库前做了一层术语归一化,维护一个同义词表,把变体统一映射到标准术语,检索时也做同样映射。

4.2 分块参数的计算与选择

分块大小不是拍脑袋,我按一个简单公式估算:分块大小 ≈ 单个答案的平均长度 × 1.5。比如产品参数类答案平均200字,分块取300字左右;故障复盘答案平均600字,分块取900字左右。1.5的系数是给上下文留余量,保证答案完整落在块内。

重叠(overlap)我统一取分块大小的15%-20%。这个比例的经验来源是:答案很少恰好落在块边界,重叠太小会切断关键句,太大则冗余检索增多。实测15%在多数场景够用,叙事性文档可以提到20%。

文档类型分块大小重叠比例相似度阈值Top-K
产品手册300-500字15%0.753-5
故障复盘800-1200字20%0.68-10
合同条款按条款切10%0.7(混合检索)5

4.3 检索质量的两道保险

光靠向量检索不够,我加了两道保险。第一道是混合检索:向量检索负责语义相似,关键词检索(BM25类)负责精确术语匹配,两路结果做加权融合。合同场景对这道保险依赖最重,因为"第X条第Y款"这种精确引用,纯向量检索经常召回语义相近但条款号不对的内容。

第二道是重排(Rerank):初检召回Top-20,再用重排模型精排取Top-5。重排模型对"问题-片段"的相关性判断比向量相似度更准,尤其在片段较长时优势明显。这一步会增加100-300ms延迟,但对答案准确率的提升在故障复盘场景实测有十几个百分点,值得。

// 混合检索的融合逻辑(简化示意) List<Document> vectorResults = vectorSearch(question, 20); List<Document> keywordResults = keywordSearch(question, 20); List<Document> merged = weightedMerge(vectorResults, keywordResults, 0.6, 0.4); List<Document> reranked = rerankModel.rerank(question, merged, 5);

5. 多智能体协同与上下文管理

5.1 智能体之间要不要互相调用

这里有个设计决策:智能体之间是"完全隔离"还是"允许协作"。完全隔离实现简单,但遇到复合问题就抓瞎——比如"这个接口超时了,产品文档里怎么说的,历史上怎么处理的",这需要产品手册智能体和故障复盘智能体都参与。

我的方案是路由层支持多工具调用:路由智能体判断出问题涉及多个领域时,可以返回多个工具名,由编排层依次调用后汇总。实现上不是让智能体互相调用(那会形成复杂的依赖图),而是让路由层做"扇出-汇总"。

// 多工具扇出执行(简化示意) List<String> agents = router.routeMulti(question); // 可能返回多个 List<String> answers = agents.stream() .map(name -> agentRegistry.get(name).answer(question)) .toList(); String finalAnswer = synthesizer.synthesize(question, answers);

汇总环节我用一个轻量的"综合智能体",它的职责是把多个智能体的回答整合成一段连贯的话,并标注各部分来源。这里要注意:综合智能体不做新的检索,只做整合,避免引入未经验证的内容。

5.2 多轮对话的上下文怎么管

多轮对话在多智能体场景下有个特殊问题:上下文该跟着谁走。用户第一轮问产品参数(路由到产品手册智能体),第二轮问"那它的故障率呢"(应该路由到故障复盘智能体),但"它"指代的是第一轮的产品,这个指代关系必须传递过去。

我的做法是维护一份会话级共享上下文,记录最近几轮的问题、路由结果和关键实体。路由时把这份上下文一起喂给路由智能体,让它结合历史判断当前意图;执行时把相关实体注入到当前智能体的提示词里。

// 会话上下文结构(简化) class SessionContext { List<Turn> history; // 历史轮次 Map<String, String> entities; // 提取的关键实体,如产品名、故障编号 }

实体提取我用的是轻量方案:每轮对话后让模型抽取一次关键实体(产品名、系统名、时间等),存进entities。下一轮路由和执行时,把这些实体作为"已知信息"注入。这套机制让"那它的故障率呢"能正确理解为"某产品的故障率",路由到故障复盘智能体。

注意:共享上下文要设上限,我限制只保留最近5轮和最多10个实体,否则上下文越滚越长,既增加token成本又稀释关键信息。

5.3 并发与性能的实际考量

多智能体扇出执行时,串行调用会明显拖慢响应。我的做法是扇出的多个智能体并行执行,用CompletableFuture做异步编排,最后统一等待汇总。

List<CompletableFuture<String>> futures = agents.stream() .map(name -> CompletableFuture.supplyAsync( () -> agentRegistry.get(name).answer(question), executor)) .toList(); List<String> answers = futures.stream() .map(CompletableFuture::join) .toList();

实测下来,两个智能体扇出并行比串行快40%左右(因为单个智能体的检索+生成本身有几百毫秒到一两秒)。但要注意线程池隔离——不同智能体可能调用不同模型服务,共用一个线程池容易互相拖累,我给每个智能体配了独立的小线程池。

6. 常见问题与排查实录

6.1 路由误判怎么定位和修正

路由误判是最常见的问题,排查思路是先看日志再看提示词。我在路由层打了详细日志:原始问题、路由结果、模型原始输出、耗时。误判发生时,先看模型原始输出是不是带了多余解释(导致解析失败),再看是不是提示词边界描述不清。

一个典型case:用户问"合同里的违约责任怎么写的",被路由到了产品手册智能体。查日志发现模型输出是"contractAgent(因为涉及合同)",带了括号解释导致字符串匹配失败,落到了兜底。修正方法是提示词里强调"只输出工具名,不要任何其他字符",同时在解析时做一次清洗(提取第一个匹配的工具名)。

6.2 检索召回不准的三层排查法

召回不准我按三层排查:分块层、检索层、重排层。

  • 分块层:把召回片段打印出来,看答案是不是被切断了。如果是,调大分块或重叠。
  • 检索层:看正确答案所在片段有没有进初检Top-20。没进,说明向量或关键词检索有问题,检查术语归一化和混合检索权重。
  • 重排层:正确答案在初检里但被重排挤掉了,说明重排模型对该类问题判断有偏,考虑调整重排的输入格式或换模型。
现象可能原因排查动作解决方向
答案被截断分块过小打印召回片段调大分块/重叠
正确答案未召回术语不一致检查同义词映射补同义词表
召回噪声多阈值过低看相似度分布提高阈值
精确引用错纯向量检索检查是否走混合检索加关键词检索
重排后变差重排模型不适配对比重排前后换模型/调输入

6.3 大模型"编答案"的抑制手段

这是RAG最顽固的问题。我的组合拳是:提示词强制拒答 + 检索结果为空时短路 + 答案后校验。

短路逻辑很关键:如果检索返回的片段相似度全部低于阈值,直接返回"未找到相关资料",根本不调生成模型。这一步能挡掉相当一部分编造。答案后校验则是让一个轻量模型检查"生成的答案是否都能在检索片段里找到依据",找不到依据的句子标记出来或直接剔除。

// 检索为空时短路 List<Document> docs = retrieve(question); if (docs.isEmpty() || docs.get(0).getScore() < MIN_THRESHOLD) { return "未在知识库中检索到相关资料,建议换个问法或联系人工。"; }

提示:拒答话术要友好且给出下一步,别只甩一句"不知道"。我用的模板是"未检索到相关资料,你可以尝试:换个关键词、缩小范围,或转人工"。用户体验差别很大。

6.4 多智能体扇出时的超时与降级

扇出并行时,只要有一个智能体超时,整个汇总就被拖住。我的处理是给每个智能体设独立超时(比如3秒),超时的直接跳过并在最终答案里标注"某部分暂未获取到",其余正常汇总。这样保证整体响应时间可控,不会因为一个慢智能体拖垮全局。

降级策略也要提前设计:模型服务不可用时,路由层降级到关键词规则路由,执行层降级到"只返回检索原文不生成"。这些降级路径平时用不到,但故障时能保证系统"可用"而不是"全挂"。

7. 落地过程中的经验与参数调优心得

7.1 从单Agent迁移到多智能体的渐进路径

如果你手上已经有一个跑着的单Agent RAG,别一上来就全量重构。我的建议是先加路由层,再拆执行层。第一步,在现有单Agent前面加一个路由智能体,把明显不属于当前知识域的问题分流出去(哪怕分流后只是给个提示),先验证路由准确率。第二步,把分流出去的那类问题做成第一个专职智能体,跑通后再逐个拆。这样每一步都有可回退的余地,风险可控。

我当初就是先拆了故障复盘这一类(因为它和产品手册的检索策略差异最大,收益最明显),跑了两周确认效果后才拆合同类。渐进迁移的另一个好处是,你能在真实流量下积累每类问题的样本,为后续调参提供依据。

7.2 参数调优的优先级排序

参数一大堆,先调哪个?我的经验优先级是:分块质量 > 检索阈值 > Top-K > 重排 > 提示词。分块是地基,分块不好后面全白搭;阈值决定召回质量的下限;Top-K影响成本和噪声;重排是锦上添花;提示词虽然重要但调整成本低,可以随时改。

调参一定要有评估集。我维护了一个200条真实问题的评估集,每条标注了正确答案来源文档,每次调参跑一遍,看召回率和答案准确率两个指标。没有评估集的调参就是盲调,改了一个参数不知道是变好还是变坏。

7.3 成本与延迟的平衡

多智能体天然比单Agent贵——路由多一次模型调用,扇出可能多次检索多次生成。我的成本控制手段有三个:路由用轻量模型(意图判断不需要强模型);扇出只在路由判断为复合问题时触发(多数问题单智能体就够);重排只对初检Top-20做,不对全量做。

延迟方面,路由的300-800ms是硬开销,但可以通过流式输出缓解感知——路由完成后立即开始流式返回,用户感知到的首字延迟并不高。扇出并行则把多智能体的额外延迟压到了可接受范围。实测端到端P95延迟在3-5秒,对于知识问答场景是可以接受的。

8. 这套方案适合谁、不适合谁

多智能体RAG不是银弹。它适合知识域明显异构、对答案准确性要求高、有一定工程能力维护多套配置的场景,比如企业内部知识库、专业领域问答、多产品线客服。它不适合知识域单一、追求极简架构、团队规模小的场景——那种情况下单Agent加好的分块和提示词,性价比高得多。

我在实际项目里最大的体会是:多智能体的复杂度主要不在代码,而在配置管理和效果评估。每个智能体一套检索参数、一套提示词、一套评估标准,维护成本是实打实的。所以上这套方案前,先问自己一句:我的知识域真的异构到单Agent搞不定了吗?如果答案是肯定的,那这套分层路由加专职智能体的结构,确实能把RAG的效果往上抬一个台阶。后续如果要扩展,我打算在综合智能体那层加一个"答案溯源"能力,把每个结论对应的原文片段直接展示给用户,让可信度再往上走一步。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进