LangChain4j系列:Advanced RAG 核心组件源码分析与实践

# LangChain4j系列:LangChain4j Easy-Rag示例及RAG APIs详解 文章中介绍了LangChain4j Easy-RAG的使用,并详细介绍了实现 RAG 的所有组件,本文将继续深入 Advanced RAG 的核心组件并进行实践。

核心组件及协作关系

  • QueryTransformer 查询转换器
  • QueryRouter 查询路由器
  • ContentRetriever 内容检索器
  • ContentAggregator 内容聚合器
  • ContentInjector 内容注入器

组件之间的协作关系图:

Retrieval Augmentor 检索增强器,是 Advanced RAG 管道的入口,主要分为以下几个步骤;

第一步,将 UserMessage 转换为 Query。

第二步,通过 QueryTransformer 组件将用户的 Query 转换为一个或者多个 Query。

第三步,通过 QueryRouter 将第二步转换的 Query 路由到其对应的 Content Retriever 检索内容。

第四步,ContentAggregator 将 Content Retriever 检索的内容合并到一个最终的排名列表中。

最后,将第四步产生的 Content 与 UserMessage组合,发给大模型。

在整体上了解了 Advanced RAG 的主要内容,下面我们将详细的介绍每个组件的能力以及解决的实际问题。

Retrieval Augmentor 【检索增强器】

Retrieval Augmentor 是 RAG 管道的入口,负责使用多个来源信息来扩充 Chat Message。 在创建 AI Services 时指定一个Retrieval Augmentor。

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)  
        ...  
        .retrievalAugmentor(retrievalAugmentor)  
        .build();

RetrievalAugmentor 源码

java 复制代码
@Experimental
public interface RetrievalAugmentor {
    // 默认实现,
    default AugmentationResult augment(AugmentationRequest augmentationRequest) {
        // 传入的 ChatMessage 必须为 UserMessage 实例
        if (!(augmentationRequest.chatMessage() instanceof UserMessage)) {
            throw runtime("Please implement 'AugmentationResult augment(AugmentationRequest)' method " +
                    "in order to augment " + augmentationRequest.chatMessage().getClass());
        }

        UserMessage augmented = augment((UserMessage) augmentationRequest.chatMessage(), augmentationRequest.metadata());

        return AugmentationResult.builder()
                .chatMessage(augmented)
                .build();
    }
    // 方法废弃,不建议使用,使用augment(AugmentationRequest)替代
    @Deprecated
    UserMessage augment(UserMessage userMessage, Metadata metadata);
}

RetrievalAugmentor 是一个接口,默认实现 augment(AugmentationRequest),LangChain4j提供一个默认的 DefaultRetrievalAugmentor。默认的 Augmentor 适应大部分的 RAG 场景,但是用户实现满足自身业务场景的 Augmentor。

默认 Augmentor 实现源码

实现思路也是参考 解构 RAG 这篇文章和一篇论文

java 复制代码
public class DefaultRetrievalAugmentor implements RetrievalAugmentor {
    // 查询转换器
    private final QueryTransformer queryTransformer;
    // 查询路由
    private final QueryRouter queryRouter;
    // 内容聚合
    private final ContentAggregator contentAggregator;
    // 内容注入
    private final ContentInjector contentInjector;
    // 线程池
    private final Executor executor;
    
    // 方法, augment 方法是核心的操作流程和步骤,与上图相对应。其它方法大家自行阅读源码。
    @Override
    public AugmentationResult augment(AugmentationRequest augmentationRequest) {
        // 获取 Chat Message
        ChatMessage chatMessage = augmentationRequest.chatMessage();
        
        // 获取元数据
        Metadata metadata = augmentationRequest.metadata();

        // ChatMessage 转换为 Query
        Query originalQuery = Query.from(chatMessage.text(), metadata);
        // 通过使用 QueryTransformer 将 Query 转换为一组 Query
        Collection<Query> queries = queryTransformer.transform(originalQuery);
        
        // 记录 Query 相关的日志信息
        logQueries(originalQuery, queries);
       
        // 根据 QueryRouter 路由到对应的 ContentRetriever 查询对应的内容
        Map<Query, Collection<List<Content>>> queryToContents = process(queries);
        // 内容排序聚合
        List<Content> contents = contentAggregator.aggregate(queryToContents);
        // 记录日志
        log(queryToContents, contents);
        // 内容注入器将 contents 与 chatMessage 组合为一个新的 ChatMessage
        ChatMessage augmentedChatMessage = contentInjector.inject(contents, chatMessage);
        log(augmentedChatMessage);

        return AugmentationResult.builder()
            .chatMessage(augmentedChatMessage)
            .contents(contents)
            .build();
    }
}

Query 【查询】

Query 代表用户一次查询,其中包含查询文本和查询元数据。

java 复制代码
// 查询对象
public class Query {
    // 查询文本
    private final String text;
    // 查询元数据
    private final Metadata metadata;
}

// 元数据对象
public class Metadata {
    // 原始用户消息,需要增强
    private final UserMessage userMessage;
    // 聊天历史消息ID
    private final Object chatMemoryId;
    // 聊天历史消息列表,有助于访问 Query 上下文
    private final List<ChatMessage> chatMemory;
}

Query Transformer 【查询转换器】

Query Transformer 将给定的 Query 转换一个或者多个 Query,目标是通过修改或扩展原始 Query 来提高检索质量。

有一些已知的改善检索的方法;

  • Query compression 查询压缩
  • Query expansion 查询扩展
  • Query re-writing 查询重写
  • Step-back prompting 后退提示
  • HyDE 假设文档嵌入。
    • 核心思想是先让大语言模型(LLM)根据用户的查询,生成一个"假想"的答案,然后将这个答案向量化,用于检索真正相关的文档。

QueryTransformer 源码

java 复制代码
@Experimental
public interface QueryTransformer {
    // 将一个 Query 转换为一组 Query
    Collection<Query> transform(Query query);
}
  • DefaultQueryTransformer 默认实现 QueryTransformer,不做任何处理
  • CompressingQueryTransformer 压缩查询转换器,使用 LLM 将一个 Query 重写为简洁、易懂且自包含的新的 Query。
  • ExpandingQueryTransformer 扩展查询转换器,使用 LLM 将一个 Query 扩展为多个 Query。这个比较有用,LLM 通过各种方式改写和重新表述 Query。

先对 CompressingQueryTransformer 和 ExpandingQueryTransformer 的源码进行简单的了解,后续会详细介绍并使用

CompressingQueryTransformer

java 复制代码
public class CompressingQueryTransformer implements QueryTransformer {
    // 默认的提示词,意思就是说分析用户的输入 并理解根据用户和大模型的交流历史,把用户的输入重写为简洁
    // 易懂并且自包含的一个新的查询。
    public static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = PromptTemplate.from(
            """
                    Read and understand the conversation between the User and the AI. \
                    Then, analyze the new query from the User. \
                    Identify all relevant details, terms, and context from both the conversation and the new query. \
                    Reformulate this query into a clear, concise, and self-contained format suitable for information retrieval.
                    
                    Conversation:
                    {{chatMemory}}
                    
                    User query: {{query}}
                    
                    It is very important that you provide only reformulated query and nothing else! \
                    Do not prepend a query with anything!"""
    );
    
    // 实现的方法也是相当的简单
    @Override
    public Collection<Query> transform(Query query) {

        List<ChatMessage> chatMemory = query.metadata().chatMemory();
        if (chatMemory.isEmpty()) {
            // no need to compress if there are no previous messages
            return singletonList(query);
        }

        Prompt prompt = createPrompt(query, format(chatMemory));
        String compressedQueryText = chatLanguageModel.generate(prompt.text());
        Query compressedQuery = query.metadata() == null
                ? Query.from(compressedQueryText)
                : Query.from(compressedQueryText, query.metadata());
        return singletonList(compressedQuery);
    }
}

ExpandingQueryTransformer 源码

java 复制代码
public class ExpandingQueryTransformer implements QueryTransformer {
    // 提示词意思是说:根据用户的查询生成多个不同的查询
    public static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = PromptTemplate.from(
            """
                    Generate {{n}} different versions of a provided user query. \
                    Each version should be worded differently, using synonyms or alternative sentence structures, \
                    but they should all retain the original meaning. \
                    These versions will be used to retrieve relevant documents. \
                    It is very important to provide each query version on a separate line, \
                    without enumerations, hyphens, or any additional formatting!
                    User query: {{query}}"""
    );

    @Override
    public Collection<Query> transform(Query query) {
        // 创建提示词, 将 Query 中内容设置到提示词模板中
        Prompt prompt = createPrompt(query);
        
        // 大模型根据提示词模板生成新的查询
        String response = chatLanguageModel.generate(prompt.text());
        
        // 解析响应
        List<String> queries = parse(response);
        
        // 将返回的内容返回一组 Query
        return queries.stream()
                .map(queryText -> query.metadata() == null
                        ? Query.from(queryText)
                        : Query.from(queryText, query.metadata()))
                .collect(toList());
    }
    
    // 解析响应
    protected List<String> parse(String queries) {
        return stream(queries.split("\n"))
                .filter(Utils::isNotNullOrBlank)
                .collect(toList());
    }
}

Content 【内容】

Content 表示与用户 Query 相关的内容,现在 LangChain4j【0.36.1】 还仅支持文本(TextSegment)。将来会支持多种形式,比如(图片、音频、视频等)。

java 复制代码
public class Content {
    // Content对象确实是仅包含一个 TextSegment
    private final TextSegment textSegment;
    
    // 通过文本创建 Content 对象
    public Content(String text) {
        this(TextSegment.from(text));
    }
    // 通过 TextSegment 创建 Content 对象
    public Content(TextSegment textSegment) {
        this.textSegment = ensureNotNull(textSegment, "textSegment");
    }
    // 静态方法创建 Content 对象
    public static Content from(String text) {
        return new Content(text);
    }

    public static Content from(TextSegment textSegment) {
        return new Content(textSegment);
    }
}

Content Retriever 【内容检索器】

ContentRetriever 使用给定的 Query 从数据源中检索 Content(内容)。 数据源几乎是任何内容;

  • 嵌入存储 (Embedding store)
  • 全文搜索引擎 (Full-text search engine)
  • 混合检索 (Embedding + Full-text search)
  • Web 搜索引擎 (Web Search Engine)
  • 知识图谱 (Knowledge graph)
  • 关系数据库 (SQL)
java 复制代码
public interface ContentRetriever {
    // 根据查询获取相关内容
    List<Content> retrieve(Query query);
}

Embedding Store Content Retriever 【嵌入内容获取】

核心源码,retrieve(Query query)

java 复制代码
public class EmbeddingStoreContentRetriever implements ContentRetriever {
    @Override
    public List<Content> retrieve(Query query) {
        // 通过 embedding model 将查询内容向量化
        Embedding embeddedQuery = embeddingModel.embed(query.text()).content();
        
        // 构建嵌入查询对象
        EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
                // 查询内容
                .queryEmbedding(embeddedQuery)
                // 返回的最多匹配的条数
                .maxResults(maxResultsProvider.apply(query))
                // 匹配的最低得分
                .minScore(minScoreProvider.apply(query))
                // 根据查询定义过滤器,默认为 null
                .filter(filterProvider.apply(query))
                .build();
        // 去向量数据库中查询匹配的内容
        EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);
        // 结果处理,将查询的内容转换为List返回
        return searchResult.matches().stream()
                .map(EmbeddingMatch::embedded)
                .map(Content::from)
                .collect(toList());
    }
}

使用示例:

java 复制代码
    // 指定向量存储,根据业务需求选择不同的嵌入向量存储,比如 PGVector
    EmbeddingStore embeddingStore = ...  
    // 嵌入模型,可以选择不同厂商提供的嵌入模型
    EmbeddingModel embeddingModel = ...  
    // 定义一个内容获取查询器
    ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()  
    // 向量存储
    .embeddingStore(embeddingStore)  
    // 嵌入模型
    .embeddingModel(embeddingModel)  
    // 返回的最大条数
    .maxResults(3)  
    // 根据查询动态指定返回的最大条数
    .dynamicMaxResults(query -> 3)  
    // 返回最小的匹配的分数
    .minScore(0.75)
    // 根据查询动态指定匹配的最小分数
    .dynamicMinScore(query -> 0.75)  
    // 指定一个过滤器
    .filter(metadataKey("userId").isEqualTo("12345"))  
    // 根据查询动态指定过滤器
    .dynamicFilter(query -> {  
        String userId = getUserId(query.metadata().chatMemoryId());  
        return metadataKey("userId").isEqualTo(userId);  
    })  
    .build();

使用 Web搜索引擎从 Web 检索相关内容。LangChain4j框架支持如下搜索引擎;

  • Google Custom Search 谷歌自定义搜索引擎
  • SearchApi 是一个实时的 SERP API。
    • google search web
    • google news
    • bing
    • bing news
    • baidu
  • SearNEX searxng
  • Tavily tavily
java 复制代码
WebSearchEngine googleSearchEngine = GoogleCustomWebSearchEngine.builder()  
    .apiKey(System.getenv("GOOGLE_API_KEY"))  
    .csi(System.getenv("GOOGLE_SEARCH_ENGINE_ID"))  
    .build();  
  
ContentRetriever contentRetriever = WebSearchContentRetriever.builder()  
    .webSearchEngine(googleSearchEngine)  
    .maxResults(3)  
    .build();

后面会详细的实践

SQL Database Content Retriever 【数据库内容获取器】

SqlDatabaseContentRetriever 使用 DataSource 和LLM 为给定的自然语言查询生成和执行 SQL 查询。在0.36.0版本中还标注为实验性质。

Neo4j Content Retriever

Neo4jContentRetriever 是与 Neo4j 图形数据库的集成。它将自然语言查询转换为 Neo4j Cypher 查询,并通过在 Neo4j 中运行这些查询来检索相关信息。可以在 langchain4j-neo4j 模块中找到它。

Query Router 【查询路由】

上一小节介绍了多种 Content Retriever,那么 Query Router 的作用就是将不同的查询路由到不同的 Content Retriever。

java 复制代码
@Experimental
public interface QueryRouter {

    /**
     * 将给定的查询路由到一个或者多个内容查询器。
     */
    Collection<ContentRetriever> route(Query query);
}

Default Query Router

DefaultQueryRouter 默认实现的查询路由,它将每个 Query 路由到所有已配置的 ContentRetriever

java 复制代码
public class DefaultQueryRouter implements QueryRouter {

   private final Collection<ContentRetriever> contentRetrievers;

   public DefaultQueryRouter(ContentRetriever... contentRetrievers) {
       this(asList(contentRetrievers));
   }

   public DefaultQueryRouter(Collection<ContentRetriever> contentRetrievers) {
       this.contentRetrievers = unmodifiableCollection(ensureNotEmpty(contentRetrievers, "contentRetrievers"));
   }

   @Override
   public Collection<ContentRetriever> route(Query query) {
       return contentRetrievers;
   }
}

Language Model Query Router

LanguageModelQueryRouter 使用 LLM 来决定将给定的 Query 路由到那个内容查询器。

java 复制代码
public class LanguageModelQueryRouter implements QueryRouter {
    // 
    public static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = PromptTemplate.from(
            """
                    Based on the user query, determine the most suitable data source(s) \
                    to retrieve relevant information from the following options:
                    {{options}}
                    It is very important that your answer consists of either a single number \
                    or multiple numbers separated by commas and nothing else!
                    User query: {{query}}"""
    );

    protected final ChatLanguageModel chatLanguageModel;
    protected final PromptTemplate promptTemplate;
    protected final String options;
    protected final Map<Integer, ContentRetriever> idToRetriever;
    protected final FallbackStrategy fallbackStrategy;
}

Content Aggregator 内容聚合器

总结

相关推荐
Dyan_csdn1 小时前
【Java项目】基于SpringBoot的【新生宿舍管理系统】
java·开发语言·spring boot
Q_19284999061 小时前
基于Spring Boot的智能笔记的开发与应用
spring boot·笔记·后端
武昌库里写JAVA2 小时前
Golang设计模式目录
数据结构·vue.js·spring boot·算法·课程设计
躲在没风的地方3 小时前
spring cloud微服务分布式架构
java·spring boot·spring cloud·微服务·架构
半夏知半秋3 小时前
python中常用的内置函数介绍
服务器·开发语言·笔记·后端·python·学习
开心工作室_kaic3 小时前
springboot551三国之家网站设计(论文+源码)_kaic
前端·spring boot·html·生活·html5
IManiy3 小时前
自动生成RESTful API——Spring Data Rest
后端·spring·restful
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS海滨学院班级回忆录系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·maven·intellij-idea
编程、小哥哥4 小时前
Spring Boot项目中分布式锁实现方案:Redisson
spring boot·分布式·后端
m0_748245924 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端