这期带来一篇关于 RAG(Retrieval-Augmented Generation)功能实现逻辑的完整拆解

大部分人觉得RAG只是简单的检索+生成两步走。但如果真的做过一个能上线、能抗压、能解决实际问题的系统,就会发现。rag功能需要设计一条完整的工程链路,示例代码使用java的langchain4j框架。

官网:LangChain4j 中文文档 | LangChain4j 中文文档

中文社区版:LangChain4j 中文教程 | LangChain4j 中文文档

目录

数据处理

分块策略

问题改写

检索策略

回话记忆


数据处理

我们要处理的数据源文件一般是多种多样的。常见输入包括:

  • PDF
  • Word(.docx)
  • PPT
  • HTML
  • Markdown
  • 扫描件(甚至 OCR)

在大部分ai框架中,一般具有一些默认的处理的方法。但是针对不同类型,我们一般还是自己实现一下。下面举例一下langchain4j处理html文件的逻辑。

自定义一个DocumentTransformer处理器,用于处理文件



/**
 * 自定义html文档转换器
 */
public class HtmlToTextDocumentTransformer implements DocumentTransformer {
    @Override
    public Document transform(Document document) {
        return Document.from(removeHtmlTags(document.text()));
    }

    // 使用正则表达式清除 HTML 标签
    public static String removeHtmlTags(String html) {
        if (html == null || html.isEmpty()) {
            return "";
        }
        // 定义正则表达式
        String regex = "<[^>]+>";
        // 替换所有匹配的标签为空字符串
        return html.replaceAll(regex, "").trim();
    }

    @Override
    public List<Document> transformAll(List<Document> documents) {
        List<Document> list = new ArrayList<Document>();
        documents.stream().forEach(document -> {
            list.add(this.transform(document));
        });
        return list;
    }
}

加载html文件

Document htmlDoc = Document.from(
        "<html><body><p>manba <b>out</b>!</p></body></html>"
);

// 文档转换器
DocumentTransformer transformer = new HtmlToTextDocumentTransformer();
Document cleanedDoc = transformer.transform(htmlDoc);

System.out.println(cleanedDoc.text());

最终输出了

manba out!


分块策略

切太大检索不精确,切太小上下文丢失,不同文档需要不同的策略。langchain4j提供了一个叫DocumentSplitter的工具。给我们自定义各种切割规则。

// 设置分割器
DocumentSplitter splitter = DocumentSplitters.recursive(
    200,  // 每块约200字符
    20    // 重叠区防止知识断裂
);
List<TextSegment> segments = splitter.split(document); // 文档瞬间变拼盘

具体有哪些类型的DocumentSplitter,可以查看langchain4j中文社区

RAG(检索增强生成) | LangChain4j 中文文档


问题改写

用户问“那他怎么配置”,不进行上下文补充,根本不知道用户在询问什么

QueryTransformer负责在检索前对用户问题进行优化,LangChain4j的DefaultRetrievalAugmentor允许配置多个QueryTransformer来组合使用。

实现/策略描述适用场景
CompressingQueryTransformer利用ChatModel将对话历史和当前问题压缩成一个独立、清晰的问题多轮对话中,解决指代不明(如“那他怎么配置?”)
ExpandingQueryTransformer利用ChatModel将一个查询扩展为多个相关的查询增加检索的召回率,从不同角度寻找相关信息
RepeatingQueryTransformer在高级RAG管道中重复使用检索查询配合ReRankingContentAggregator等组件使用
自定义实现实现QueryTransformer接口,编写特定逻辑(如同义词替换)针对特定业务场景的个性化查询优化

示例:使用压缩型查询转换器

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.rag.query.transformer.CompressingQueryTransformer;
import dev.langchain4j.rag.query.Query;

// 假设已有ChatLanguageModel实例 (如OpenAiChatModel)
ChatLanguageModel model = OpenAiChatModel.builder()
        .apiKey("your-api-key")
        .modelName("gpt-3.5-turbo")
        .build();

// 1. 创建压缩型转换器
QueryTransformer compressor = new CompressingQueryTransformer(model);

// 2. 模拟对话历史和当前问题
List<ChatMessage> chatMemory = List.of(
    SystemMessage.from("你是一个Java技术专家。"),
    UserMessage.from("如何在Spring Boot项目中配置OpenAI API Key?"),
    AiMessage.from("可以在application.properties中设置`openai.api.key`。")
);
String currentQuery = "那怎么把它设置为环境变量呢?";

// 3. 转换查询
Query originalQuery = Query.from(currentQuery, chatMemory);
Collection<Query> transformedQueries = compressor.transform(originalQuery);

transformedQueries.forEach(q -> System.out.println("优化后的问题: " + q.text()));
// 输出可能为: "如何在Spring Boot项目中把OpenAI API Key设置为环境变量?"


检索策略

纯向量检索对精确匹配很弱,用户问一个订单号,向量见多可能完全找不到,混合检索怎么融合,需不需要进行重排序等等,都要进行取舍

LangChain4j的DefaultRetrievalAugmentor提供了一套模块化的RAG管道,允许我们灵活组合查询转换、多路检索和结果重排等高级策略。

  • 混合检索:结合关键词匹配(BM25)和语义相似度(向量检索),LangChain4j通过与Azure AI Search等向量数据库集成,原生支持混合搜索。

  • 结果重排:使用ReRankingContentAggregator,在合并多路检索结果后,通过一个ScoringModel对内容进行重新打分排序,将最相关的结果排在前面

示例:构建一个带查询压缩、混合检索和重排的RAG管道

import dev.langchain4j.rag.DefaultRetrievalAugmentor;
import dev.langchain4j.rag.content.aggregator.ReRankingContentAggregator;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.query.transformer.CompressingQueryTransformer;
import dev.langchain4j.model.scoring.ScoringModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.model.embedding.EmbeddingModel;

public class AdvancedRagPipeline {
    public static void main(String[] args) {
        // 1. 配置组件 (实际需要具体实现类)
        ChatLanguageModel chatModel = ...; // 你的ChatModel
        EmbeddingStore embeddingStore = ...; // 你的向量存储,如InMemoryEmbeddingStore
        EmbeddingModel embeddingModel = ...; // 你的嵌入模型
        
        // 2. 配置混合检索器 (此处示例为向量检索,混合检索需特定集成)
        ContentRetriever vectorRetriever = EmbeddingStoreContentRetriever.builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(embeddingModel)
                .maxResults(15)
                .minScore(0.6)
                .build();
        // 3. 配置ScoringModel用于重排 (以Cohere为例)
        ScoringModel scoringModel = CohereScoringModel.builder()
                .apiKey("your-cohere-api-key")
                .modelName("rerank-english-v3.0")
                .build();

        // 4. 构建高级检索增强器
        DefaultRetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
                // 添加查询转换器:将多轮对话压缩为独立查询
                .queryTransformer(new CompressingQueryTransformer(chatModel))
                // 添加内容检索器:可以有多个,进行多路召回
                .contentRetriever(vectorRetriever)
                // 添加内容聚合器:对多路结果进行重排
                .contentAggregator(new ReRankingContentAggregator(scoringModel))
                .build();

        System.out.println("高级RAG管道构建完成。");
    }
}


回话记忆

当我们不断进行绘画时,上下文内容越来越长。token成本越来越大。我们需要选择带那几次的上下文。如何控制上下文取舍等等。这些都是要考虑的事情。

ChatMemory接口负责管理对话历史,LangChain4j提供了两种开箱即用的策略来控制上下文长度,控制Token消耗和延迟。

策略描述特点
MessageWindowChatMemory保留最近N条消息的滑动窗口简单高效,但无法精确控制Token消耗
TokenWindowChatMemory保留最近N个Token,并尽可能保留完整消息可精确控制Token数,是生产环境的推荐选择

示例:使用TokenWindowChatMemory

import dev.langchain4j.memory.chat.TokenWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiTokenizer;

// 1. 创建基于Token窗口的聊天记忆,最大容量为4000个Token
ChatMemory chatMemory = TokenWindowChatMemory.builder()
        .maxTokens(4000, new OpenAiTokenizer("gpt-3.5-turbo"))
        .id("user-session-123") // 可为不同用户/会话设置不同ID
        .build();

// 2. 模拟对话
chatMemory.add(UserMessage.from("LangChain4j是什么?"));
chatMemory.add(AiMessage.from("LangChain4j是一个Java库..."));
chatMemory.add(UserMessage.from("它支持RAG吗?"));

// 3. 获取记忆中的消息(会自动进行淘汰)
List<ChatMessage> messagesToSend = chatMemory.messages();
System.out.println("即将发送给LLM的消息数量: " + messagesToSend.size());

// 4. 如需持久化,可实现ChatMemoryStore接口并注入到ChatMemory中
// ChatMemoryStore persistentStore = new MyPersistentStore();
// ChatMemory chatMemory = MessageWindowChatMemory.builder()
//         .id("user-123")
//         .maxMessages(10)
//         .chatMemoryStore(persistentStore)
//         .build();

MessageWindowChatMemory适合快速验证,TokenWindowChatMemory适合成本敏感的生产环境。ChatMemory只管理发送给LLM的"记忆",不保证存储完整的对话"历史"


以上就是本期关于使用langchain4j实现企业级RAG功能设计的全部内容。如果这篇博客对你有帮助,可以点赞,收藏加关注一下,你们的支持就是我们更新的最大动力。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐