1. 从传统RAG到Agentic RAG:为什么需要“反思”?
如果你最近在折腾RAG(检索增强生成),大概率已经踩过不少坑了:用户问“苹果公司的市值”,系统却给你返回一堆关于水果“苹果”的营养价值文档;或者用户输入一个拼写错误或模糊的查询,比如“LangChian4j怎么用”,检索器直接懵圈,要么召回无关内容,要么干脆空手而归。传统的RAG流程——检索(Retrieve)然后生成(Generate)——就像一个老实巴交的办事员,你给什么指令,他就按部就班地去资料库(向量数据库)里找最“像”的文档,然后一股脑塞给大模型去生成答案。这个过程缺乏一个关键的环节:对检索动作本身的“质检”与“反思”。
这就是Agentic RAG,或者说“具有代理能力的RAG”要解决的核心问题。它不再把RAG看作一个僵化的管道,而是赋予其一个“智能体”(Agent)的视角。这个智能体具备感知-决策-行动-反思的循环能力。具体到检索环节,“反思”能力意味着系统能够评估当前检索结果的质量,判断其是否足够相关、完整,甚至能识别出用户查询本身可能存在的问题(如歧义、错误),并据此动态调整策略,比如触发重写查询、进行网络搜索补充,或者直接承认知识库的不足。
而CRAG(Corrective Retrieval Augmented Generation,纠错检索增强生成)正是实现这种“反思”与“纠错”能力的一种前沿架构范式。它核心的思想是引入一个“检索评估器”,在得到初步检索结果后,不是直接送去生成,而是先“踩一脚刹车”,问问自己:“我这次检索干得怎么样?”如果评估认为结果不佳,就会触发一个“纠正”动作,这个动作可能就是重写查询、切换检索方法(如从向量检索切到关键词检索)、或引入外部知识源。LangChain4j作为一个强大的Java集成框架,为我们实现这套复杂但精妙的流程提供了得心应手的工具。
所以,这次我们不谈空洞的概念,直接进入实战。我将手把手带你,利用LangChain4j,构建一个具备CRAG思想的Agentic RAG系统。你会看到,如何让RAG系统学会“三思而后行”,从而显著提升回答的准确性和可靠性。
2. 战场准备:LangChain4j环境与核心组件搭建
在开始构建复杂系统之前,我们必须把战场打扫干净,工具准备齐全。LangChain4j虽然API设计优雅,但依赖管理不慎也会让人头疼。这里我基于一个真实的Spring Boot项目来搭建,你可以直接复用。
2.1 依赖管理与版本锁定
首先,pom.xml里的依赖声明是重中之重。LangChain4j模块众多,我们只需要核心的几个。特别注意版本号,不同版本间API可能有细微差别,我强烈建议锁定一个稳定版本,避免后续踩坑。
<properties> <langchain4j.version>0.31.0</langchain4j.version> <!-- 使用一个稳定的版本 --> </properties> <dependencies> <!-- LangChain4j 核心 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- 用于连接OpenAI等大模型 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- 用于本地嵌入模型(生成向量),可选,但推荐用于测试和降成本 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- 内存向量数据库,方便快速原型验证 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings-store-filter</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings-store-in-memory</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- Spring Boot基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>注意:
langchain4j-embeddings-all-minilm-l6-v2集成了一个轻量级的本地嵌入模型,无需API调用即可生成向量,对于快速验证和成本敏感的场景非常有用。生产环境可以考虑更强大的本地模型(如通过langchain4j-embeddings-local-ai连接LocalAI)或商用嵌入API。
2.2 核心Bean配置:模型、嵌入与存储
接下来,在Spring的配置类或主应用类中,我们需要初始化几个核心Bean。这里我采用一个@Configuration类来集中管理。
import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class LangChain4jConfig { // 你的OpenAI API Key,务必通过环境变量或配置中心管理,不要硬编码! private final String openAiApiKey = System.getenv("OPENAI_API_KEY"); /** * 配置对话模型(用于最终的答案生成和Agent的决策逻辑) * 这里使用GPT-3.5 Turbo,平衡性能与成本。 */ @Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .apiKey(openAiApiKey) .modelName("gpt-3.5-turbo") // 可根据需要换为 gpt-4 .temperature(0.2) // 较低的温度使输出更确定,适合任务执行 .maxTokens(500) .build(); } /** * 配置嵌入模型(用于将文本和查询转换为向量)。 * 方案一:使用OpenAI的嵌入模型(效果好,但有成本) */ @Bean public EmbeddingModel openAiEmbeddingModel() { return OpenAiEmbeddingModel.builder() .apiKey(openAiApiKey) .modelName("text-embedding-3-small") // 性价比高的新模型 .build(); } /** * 方案二:使用本地嵌入模型(零成本,适合原型和特定场景) * 取消下面的注释,并注释掉上面的openAiEmbeddingModel Bean即可切换。 */ // @Bean // public EmbeddingModel localEmbeddingModel() { // return new AllMiniLmL6V2EmbeddingModel(); // } /** * 配置向量存储。 * 这里使用内存存储,重启后数据丢失。生产环境请换为Pinecone、Milvus、PGVector等持久化存储。 * 注意:InMemoryEmbeddingStore本身不带检索功能,需要配合EmbeddingStoreIngestor和EmbeddingStoreRetriever使用。 */ @Bean public EmbeddingStore<TextSegment> embeddingStore() { return new InMemoryEmbeddingStore<>(); } }这个配置类奠定了我们系统的基础。ChatLanguageModel是我们的“大脑”,负责复杂的逻辑判断和内容生成;EmbeddingModel是“翻译官”,把文字世界映射到向量空间;EmbeddingStore是“资料库”,存放着我们处理好的知识片段。选择内存存储纯粹是为了演示简便,在实际项目中,这绝对是第一个需要替换的组件。比如对于专利检索、法律条文查询这类严肃场景,向量数据库的持久化、可扩展性和检索性能至关重要。
3. 实现CRAG核心:检索评估与动态纠错流程
现在进入最核心的部分:如何让RAG系统拥有“反思”能力?CRAG的论文提出了一种优雅的架构,我们将其落地到LangChain4j中。整个流程可以分解为几个可管理的步骤。
3.1 第一步:构建知识库与基础检索器
在系统能“反思”之前,它得先会“检索”。我们首先需要有一个灌入了知识的数据源,以及一个基础检索器。
import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.nio.file.Path; import java.nio.file.Paths; @Component public class KnowledgeBaseLoader { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public KnowledgeBaseLoader(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } /** * 项目启动时加载知识文档并存入向量库。 * 这里以加载一个Markdown文件为例。 */ @PostConstruct public void initKnowledgeBase() throws Exception { // 1. 加载文档。可以从文件系统、URL、数据库等多种来源加载。 Path documentPath = new ClassPathResource("knowledge/company_faq.md").getFile().toPath(); Document document = FileSystemDocumentLoader.loadDocument(documentPath); // 2. 分割文档。这是关键步骤,分割策略直接影响检索精度。 // 使用递归分割器,按最大token数分割,尽量保证语义完整性。 List<TextSegment> segments = DocumentSplitters.recursive(300, 30).split(document); // 3. 生成嵌入向量并存储。 EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); ingestor.ingest(segments); System.out.println("知识库加载完成,共存入 " + segments.size() + " 个文本片段。"); } }有了知识库,基础检索器就很简单了。LangChain4j提供了EmbeddingStoreRetriever,它负责计算查询的向量,并在向量库中寻找最相似的片段。
@Component public class BasicRetriever { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public BasicRetriever(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } /** * 基础检索:将查询向量化,并从存储中召回最相关的N个片段。 * @param query 用户查询 * @param maxResults 最大返回结果数 * @return 相关文本片段列表 */ public List<TextSegment> retrieve(String query, int maxResults) { // 将查询文本转换为向量 Embedding queryEmbedding = embeddingModel.embed(query).content(); // 在向量库中进行相似度搜索 List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(queryEmbedding, maxResults); // 提取匹配的文本片段 return matches.stream().map(EmbeddingMatch::embedded).collect(Collectors.toList()); } }至此,一个传统RAG的检索部分就完成了。但它是“盲目的”,无论查询是“苹果市值”还是“苹果好吃吗”,它都会尽力去找最相似的向量。接下来,我们要给它装上“质检员”。
3.2 第二步:设计检索评估器——系统的“质检员”
检索评估器是CRAG的灵魂。它的任务是给本次检索的结果打分,判断其是否可靠。论文中常用一个轻量级的大模型(如GPT-3.5)来完成这个任务。我们设计一个RetrievalEvaluator类。
import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.output.structured.Description; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import lombok.Data; public interface RetrievalEvaluator { /** * 评估检索结果的相关性。 * @param query 用户查询 * @param retrievedContext 检索到的上下文(拼接后的字符串) * @return 评估结果,包含分数和理由 */ EvaluationResult evaluate(String query, String retrievedContext); } @Data class EvaluationResult { @Description("相关性评分,范围0-1,越高表示越相关") private double relevanceScore; @Description("评估理由,解释为什么给出这个分数") private String reasoning; @Description("建议的后续动作:PROCEED(直接生成), REWRITE(重写查询), SUPPLEMENT(补充检索)") private String suggestedAction; }我们需要一个实现类,利用大模型进行结构化输出。LangChain4j的@SystemMessage和@UserMessage注解,配合结构化输出,能让代码非常清晰。
import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.output.structured.Description; import dev.langchain4j.service.AiServices; @Component public class LLMRetrievalEvaluator implements RetrievalEvaluator { private final EvaluationAgent evaluationAgent; // 通过AiServices创建一个代理,专门负责评估任务 interface EvaluationAgent { @SystemMessage("你是一个检索质量评估专家。你的任务是根据用户查询和检索到的上下文,判断上下文是否足够相关和完整以回答问题。请严格按格式输出。") EvaluationResult evaluateRelevance(@UserMessage String query, @UserMessage String context); } public LLMRetrievalEvaluator(ChatLanguageModel chatLanguageModel) { this.evaluationAgent = AiServices.create(EvaluationAgent.class, chatLanguageModel); } @Override public EvaluationResult evaluate(String query, String retrievedContext) { // 如果检索结果为空,直接判定为不相关 if (retrievedContext == null || retrievedContext.trim().isEmpty()) { EvaluationResult result = new EvaluationResult(); result.setRelevanceScore(0.0); result.setReasoning("检索结果为空,无法提供任何相关信息。"); result.setSuggestedAction("REWRITE"); // 建议重写查询或尝试其他检索方式 return result; } // 调用大模型进行评估 return evaluationAgent.evaluateRelevance(query, retrievedContext); } }这个评估器会接收用户查询和检索到的文本,让大模型扮演专家,输出一个结构化的评估结果。suggestedAction字段是关键,它决定了流程的走向。这里我定义了三种动作:PROCEED(结果很好,直接生成答案)、REWRITE(结果不太行,需要优化查询)、SUPPLEMENT(结果部分相关,需要补充信息,比如调用网络搜索)。
3.3 第三步:构建查询重写器与备用检索器
如果评估器建议REWRITE,我们就需要优化原始查询。同样,我们可以用一个大模型代理来完成。
@Component public class QueryRewriter { interface RewriteAgent { @SystemMessage("你是一个查询优化专家。用户提供了一个原始查询和不太相关的检索结果。请分析原因,并生成一个更清晰、无歧义、更利于检索的新查询。只返回优化后的查询文本。") String rewriteQuery(@UserMessage String originalQuery, @UserMessage String poorContextSnippet); } private final RewriteAgent rewriteAgent; public QueryRewriter(ChatLanguageModel chatLanguageModel) { this.rewriteAgent = AiServices.create(RewriteAgent.class, chatLanguageModel); } public String rewrite(String originalQuery, String poorContext) { return rewriteAgent.rewriteQuery(originalQuery, poorContext); } }对于SUPPLEMENT动作,我们需要一个备用检索器。在真实场景中,这可能是:
- 关键词检索(如BM25):当向量检索因表述方式不同而失效时,传统的关键词匹配可能更有效。你可以集成Lucene或Elasticsearch。
- 混合检索:结合向量检索和关键词检索的分数。
- 网络搜索:当知识库信息不足时,调用必应(Bing)或谷歌搜索API获取最新信息。注意:实现网络搜索时,务必使用合规的API接口,并遵守相关服务条款。
这里为了简化,我演示一个“模拟”的备用检索器,它假设在向量检索失败时,尝试一个更宽泛的检索策略(比如减少相似度阈值,召回更多结果)。
@Component public class FallbackRetriever { private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; public FallbackRetriever(EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; } /** * 备用检索策略:降低相似度阈值,召回更多可能相关的结果。 */ public List<TextSegment> fallbackRetrieve(String query, int maxResults) { Embedding queryEmbedding = embeddingModel.embed(query).content(); // 注意:InMemoryEmbeddingStore的findRelevant方法可能不支持直接设置阈值。 // 这里为了演示逻辑,假设我们召回双倍数量,然后在业务逻辑里处理。 // 实际使用支持阈值的数据库(如Milvus、Pinecone)时,可以设置score_threshold。 List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(queryEmbedding, maxResults * 2); // 可以在这里根据匹配分数进行过滤,模拟阈值效果 return matches.stream() .map(EmbeddingMatch::embedded) .collect(Collectors.toList()); } }3.4 第四步:组装CRAG Agent——指挥整个流程
现在,所有零件都准备好了,我们需要一个“指挥官”来串联整个流程。这就是我们的CRAGAgent。
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Slf4j @Service public class CRAGAgent { private final BasicRetriever basicRetriever; private final RetrievalEvaluator retrievalEvaluator; private final QueryRewriter queryRewriter; private final FallbackRetriever fallbackRetriever; private final ChatLanguageModel chatModel; // 用于最终答案生成 // 构造函数注入所有依赖... public CRAGAgent(BasicRetriever basicRetriever, RetrievalEvaluator retrievalEvaluator, QueryRewriter queryRewriter, FallbackRetriever fallbackRetriever, ChatLanguageModel chatModel) { this.basicRetriever = basicRetriever; this.retrievalEvaluator = retrievalEvaluator; this.queryRewriter = queryRewriter; this.fallbackRetriever = fallbackRetriever; this.chatModel = chatModel; } public String answer(String userQuery) { log.info("用户查询: {}", userQuery); String finalContext; List<TextSegment> retrievedSegments; // --- 第一轮:基础检索 --- retrievedSegments = basicRetriever.retrieve(userQuery, 3); // 先尝试召回3个 String initialContext = concatenateSegments(retrievedSegments); log.info("初始检索结果: {}", initialContext); // --- 第二轮:评估与反思 --- EvaluationResult evaluation = retrievalEvaluator.evaluate(userQuery, initialContext); log.info("检索评估: 分数={}, 建议动作={}, 理由={}", evaluation.getRelevanceScore(), evaluation.getSuggestedAction(), evaluation.getReasoning()); // --- 第三轮:决策与纠正 --- switch (evaluation.getSuggestedAction()) { case "PROCEED": finalContext = initialContext; log.info("评估通过,使用初始上下文。"); break; case "REWRITE": String rewrittenQuery = queryRewriter.rewrite(userQuery, initialContext); log.info("查询被重写为: {}", rewrittenQuery); // 用重写后的查询重新检索 retrievedSegments = basicRetriever.retrieve(rewrittenQuery, 3); finalContext = concatenateSegments(retrievedSegments); log.info("重写后检索结果: {}", finalContext); // 可选:对重写后的结果再次评估,这里简化处理 break; case "SUPPLEMENT": log.info("尝试补充检索..."); List<TextSegment> fallbackSegments = fallbackRetriever.fallbackRetrieve(userQuery, 5); // 合并初始结果和补充结果 retrievedSegments.addAll(fallbackSegments); // 简单去重(根据内容哈希) retrievedSegments = retrievedSegments.stream() .distinct() .collect(Collectors.toList()); finalContext = concatenateSegments(retrievedSegments); log.info("补充后合并上下文: {}", finalContext); break; default: log.warn("未知的建议动作,使用初始上下文。"); finalContext = initialContext; } // --- 最终:生成答案 --- // 构建最终提示词,明确要求基于给定上下文回答 String prompt = String.format(""" 请基于以下上下文信息,回答用户的问题。如果上下文信息不足以回答问题,请明确说明你不知道。 上下文信息: %s 用户问题:%s 请给出准确、简洁的答案: """, finalContext, userQuery); String answer = chatModel.generate(prompt); log.info("最终答案: {}", answer); return answer; } private String concatenateSegments(List<TextSegment> segments) { if (segments == null || segments.isEmpty()) { return "[未检索到相关信息]"; } return segments.stream() .map(TextSegment::text) .collect(Collectors.joining("\n\n")); } }这个CRAGAgent的answer方法清晰地展示了整个“检索-评估-纠正”的闭环流程。它首先进行常规检索,然后引入“质检员”(评估器)进行反思。根据反思结果,系统会做出不同的决策:直接通过、优化查询、或补充检索。这个动态决策过程,正是Agentic RAG智能性的体现。
4. 实战测试与效果对比:看“反思”如何提升效果
理论说再多,不如跑个例子看看。我们假设知识库(company_faq.md)里包含以下内容:
Q: 公司的旗舰产品是什么? A: 我们的旗舰产品是“智能数据分析平台X1”,专注于实时大数据可视化。 Q: 如何联系技术支持? A: 请发送邮件至 support@example.com,或拨打400-xxx-xxxx。 Q: 公司最近的融资情况如何? A: 我司于2023年完成了B轮融资,由红杉资本领投。4.1 测试案例一:处理模糊与错误查询
用户查询:“你们那个数据分析工具叫啥来着?我忘了具体名字。”
- 传统RAG流程:向量检索可能会匹配到“智能数据分析平台X1”这个片段,但也可能因为“工具”、“叫啥”等口语化表述导致相似度不高,召回失败或召回其他不相关的内容(如“技术支持”)。
- 我们的CRAG流程:
- 基础检索器召回相关片段(假设召回了“旗舰产品”和“技术支持”两个片段)。
- 评估器分析:“用户问数据分析工具的名字,上下文提到了‘智能数据分析平台X1’,高度相关。但同时也包含了不相关的技术支持信息。” 它可能给出一个较高的分数(如0.85),并建议
PROCEED。 - 系统直接使用包含产品名的上下文生成答案:“我们的旗舰产品是‘智能数据分析平台X1’。”
用户查询:“蓝链4j怎么集成?”(一个明显的拼写错误,“蓝链”应为“LangChain”)
- 传统RAG流程:查询向量与知识库中任何片段的向量都不相似,很可能返回空结果或极低分的无关结果,导致大模型胡编乱造。
- 我们的CRAG流程:
- 基础检索器召回的结果质量极差(空或无关)。
- 评估器分析:“检索结果与查询‘蓝链4j怎么集成’完全不相关。查询可能存在拼写错误或指代不明。” 给出低分(如0.1),建议
REWRITE。 - 查询重写器工作。它看到原始查询和糟糕的上下文,可能会输出:
“LangChain4j 集成方法”。 - 用重写后的查询重新检索,这次很可能成功召回关于LangChain4j集成的知识(如果知识库里有的话)。
- 生成正确答案。
4.2 测试案例二:处理知识库未覆盖的查询
用户查询:“公司CEO最近在公开场合发表了什么观点?”
- 传统RAG流程:知识库中没有CEO相关言论,检索结果为空或勉强匹配到“公司”、“融资”等词。大模型要么说“我不知道”,更糟糕的情况下会基于“融资”这个微弱关联开始编造CEO的言论(幻觉)。
- 我们的CRAG流程:
- 基础检索器召回的结果不相关(比如只召回“融资情况”)。
- 评估器分析:“用户询问CEO的公开观点,但上下文中只提及了融资信息,无法回答该问题。知识库可能缺乏此信息。” 给出低分,建议
SUPPLEMENT。 - (这里是关键)在我们的演示中,
FallbackRetriever只是扩大了召回范围。但在一个更完善的系统里,SUPPLEMENT动作应该触发网络搜索。系统可以调用合规的搜索API(例如必应搜索的企业级API),获取关于该公司CEO的最新报道或演讲摘要。 - 将内部检索结果和网络搜索结果融合,形成最终上下文。
- 生成一个基于真实网络信息的答案,或者诚实地说明“根据内部资料未找到,但根据公开报道...”。
通过这两个案例,你可以直观地感受到CRAG带来的提升。它通过一个简单的“评估-决策”循环,极大地增强了RAG系统对复杂、模糊、错误查询的鲁棒性,并提供了引入外部知识源的入口,缓解了知识库覆盖不足的问题。
5. 深入优化与生产级考量
上面的实现是一个可运行的原型,但要投入生产环境,还有大量的优化工作需要做。这里分享几个我从实际项目中总结的关键点。
5.1 评估器的优化:成本、延迟与准确性平衡
让大模型评估每一次检索,虽然效果好,但会增加成本和延迟。优化策略包括:
- 轻量化评估:不是所有查询都需要评估。可以设置一个“快速过滤器”,例如,如果检索到的top1片段的向量相似度分数超过一个很高的阈值(如0.95),直接认为检索成功,跳过评估。这能节省大量简单查询的LLM调用。
- 小模型评估:评估任务相对简单,可以考虑使用更小、更快的模型(如GPT-3.5 Turbo,甚至专门微调的小模型)来完成,而不是使用昂贵的GPT-4。
- 缓存评估结果:对于相同或相似的查询,可以缓存评估结果,在一定时间内复用。
5.2 纠错策略的扩展:超越重写与补充
我们只实现了REWRITE和SUPPLEMENT。更复杂的策略可以包括:
- 多路检索与融合:同时使用向量检索和关键词检索(BM25),评估器可以决定更相信哪一路的结果,或者将两路结果融合(如加权平均)。
- 迭代式重写:重写查询后,可以再次评估,如果还不理想,可以基于新的结果进行第二轮重写,形成一个迭代优化循环。
- 子问题分解:对于复杂查询(如“对比产品X1和竞争对手Y2在价格和性能上的差异”),评估器可以判断其复杂性,并触发一个“规划”步骤,将大问题分解成多个子问题,分别检索再综合。
5.3 与成熟向量数据库的集成
InMemoryEmbeddingStore只是玩具。生产环境需要:
- 持久化与可扩展性:使用Milvus、Pinecone、Weaviate或PGVector(如果你用PostgreSQL)。这些数据库支持海量向量数据的持久化存储和高效检索。
- 高级检索功能:它们支持过滤(Filtering)、混合搜索(Hybrid Search,结合向量和标量字段)、分页等,能更好地实现我们
SUPPLEMENT策略中的复杂逻辑。 - 性能:专门的向量数据库针对大规模相似性搜索进行了优化。
以集成Milvus为例,你需要引入对应的LangChain4j模块,并配置连接:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-store-embedding-milvus</artifactId> <version>${langchain4j.version}</version> </dependency>配置Bean时,使用MilvusEmbeddingStore替代InMemoryEmbeddingStore,并配置主机、端口、集合名等参数。
5.4 监控、评估与持续迭代
一个上线的RAG系统不是一劳永逸的。必须建立监控和评估体系:
- 关键指标监控:检索成功率、评估器调用比例(重写/补充率)、平均响应延迟、Token消耗成本。
- 答案质量评估:可以定期用一批标注好的测试问题(Q&A对)来跑系统,计算答案的准确率(Accuracy)、忠实度(Faithfulness,答案是否严格基于上下文)、相关性(Relevance)。LangChain4j也提供了一些评估工具链的集成。
- 日志与溯源:像我们代码中那样,详细记录每一轮的查询、检索结果、评估分数、决策动作和最终答案。这对于排查bad case、理解系统瓶颈至关重要。当用户反馈答案不对时,你能快速定位是检索失败了,还是评估器误判了,或者是大模型生成时产生了幻觉。
构建一个真正健壮、智能的Agentic RAG系统,是一个将精妙算法思想与扎实工程实践相结合的过程。LangChain4j提供了优秀的抽象和模块化设计,让我们能专注于业务逻辑的编排。从基础的RAG管道,到引入检索评估的CRAG,再到未来可能集成更多工具调用(Tool Use)和复杂规划(Planning)的智能体,这条路充满了挑战,但每一步的进化,都让我们的系统离“真正理解用户意图”更近一步。