# 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 Search Content Retriever 【Web 内容搜索】
使用 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;
}