8.1 相关概念
8.1.1 检索系统
检索系统(Information Retrieval System, IR System)是一个为了满足用户信息需求,从大规模、非结构化的数据集合中,自动、高效地查找、排序并返回相关信息的计算机系统。
它的核心任务是:在正确的时间,以正确的方式,将正确的信息传递给正确的人。最常见的例子就是:搜索引擎(如Go0gle、百度)。
随着大型语言模型的流行,检索系统已成为人工智能应用(例如RAG)的重要组成部分。且存在多种关系数据库【不同类型】的检索系统,包括:
- 关系数据库:关系数据库是许多应用程序中使用的结构化数据存储的基本类型。数据存储在行(记录)和列(属性)中,可以通过SQL(结构化查询语言)进行高效的查询和作。关系数据库擅长维护数据完整性、支持复杂查询以及处理不同数据实体之间的关系。
- 词法搜索索引:许多搜索引擎基于将查询中的单词与每个文档中的单词进行匹配,这种方法称为词法检索。即一个单词经常出现在用户的查询和特定文档中,那么这个文档可能是一个很好的匹配。这通常使用【倒排索引】实现。
- 向量数据库:向量存储不使用字频,而是使用【嵌入模型】将文档转换为高维向量表示。这允许使用余弦相似度等。数学运算对嵌入向量进行有效的相似性搜索。
8.1.2 检索器
检索器是检索系统中的一个核心组件,它接收来自用户接口的查询(Query ),检索出包含查询关键词的候选文档集合。

我们可以使用上面提到的任何检索系统实现方式创建检索器! 如关系数据库、向量数据库等。
由于其重要性和多样性,LangChain提供了一个统一的接口来与不同类型的检索系统进行交互。LangChain的检索器接口非常简单:
- 输入:查询字符串 Query
- 输出:文档列表(标准化的 LangChain 文档对象 Document )

例如,使用关系数据库的检索系统,检索器可以将问题转换为SQL语句,并执行查询,最后将查询结果响应用户。(该示例将来会在 LangGraph 中讲解,在这里帮助理解概念即可)

下面,我们来演示如何使用向量存储作为检索器。
8.2 使用++向量数据库++作为检索器
8.2.1 基本使用
向量存储是索引和检索非结构化数据的一种强大而有效的方法。可以通过调用向量数据库的as_retriever方法,将向量存储用作检索器。在这里我们使用Redis向量存储。
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 配置 Redis 客户端
redis_url = "redis://192.168.100.238:6379"
config = RedisConfig(
index_name="qa",
redis_url=redis_url,
metadata_schema=[
{"name": "category", "type": "tag"},
{"name": "num", "type": "numeric"},
],
)
# Redis 存储初始化
vector_store = RedisVectorStore(embeddings, config=config)
retriever = vector_store.as_retriever()
docs = retriever.invoke("数据库表怎么设计的? ")
for doc in docs:
print("*" * 30)
print(doc.page_content[:30])
# 输出
******************************
提供两种方案有以下好处:
降低门槛:单机版让用户快速体验核
******************************
数据库:MySQL + MyBatis
MySQL 和 M
******************************
不同状态(上架/已租/下架)的查询频率不同,拆分可减少锁争用
******************************
分库分表:按user_id哈希分库,按月份分表(如chat_
可以看到 LangChain 检索器是一个Runnable 的对象,它是LangChain组件的标准接口。这意味着它有一些常用方法,包括invoke,用于与其交互。默认情况下,向量存储检索器使用相似性搜索。
as_retriever 方法也支持我们传递相关参数修改搜索结果,如:
-
search_type:设置相似算法,包括:"similarity"(默认)、 "similarity_score_threshold"(相似性分数阈值)或"mmr"
-
search_kwargs:k:限制检索器返回的文档 k 数量。
retriever = vector_store.as_retriever(search_kwargs={"k":2})
fetch_k:要传递给MMR算法的文档量。
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={
"k": 2,
"fetch_k": 10
}
)
【注意】Retrievers检索器虽然是Runnable对象,但其不提供任何流式处理 ,因为它本身常是同步的、阻塞的操作。如下所示:
chunks = [] for chunk in retriever.stream("数据库表怎么设计的? "): chunks.append(chunk) print(chunk, end="|", flush=True)调用可以发现,它是一次性返回,与 invoke 调用无异。
8.2.2 使用 @chain 自行创建"检索器"
除了使用as_retriever方法,我们还可以自行创建一个"检索器"。回想一下检索器的特点:
- LangChain检索器是一个Runnable的对象
- LangChain检索器输入为查询字符串,输出为文档列表(标准化的LangChain文档对象Document)
综上所述,我们可以:
@chain
def retriever(query: str) -> List[Document]:
return vector_store.similarity_search(query, k=2)
docs = retriever.invoke("数据库表怎么设计的? ")
for doc in docs:
print("*" * 30)
print(doc.page_content[:30])
上面定义了一个函数,使用 @chain 修饰,该修饰可以使其成为Runnable函数,且满足检索器输入输出的要求。在函数中,我们依旧使用向量数据库的相似性搜索方法,这样灵活性也更高,想要进行元数据筛选也更方便。
【注意】这并不是真正的检索器,检索器是一个Runnable对象,而我们定义的只是一个函数,具备其特点罢了。
8.3 RAG案例
RAG是当前大语言模型应用的核心模式。当用户向LLM提问时,系统首先使用嵌入模型在知识库中进行语义搜索,找到最相关的内容,然后将这些内容和问题一起交给LLM来生成答案。这极大地提高了答案的准确性和时效性。
我们来完成一个最简单的RAG案例,将会完成:由于 LangChain检索器 是一个 Runnable 的对象,我们便可以方便的使用链完成相关的调用。
1.根据Query搜索最相关的4篇文档。
2.将相关的文档转换为字符串,以便后续发送给聊天模型。
3.将Query与文档字符串发送给聊天模型。
4.聊天模型依据输出解析器格式输出内容。
# 定义聊天模型
model = ChatOpenAI(model="gpt-4o-mini")
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 配置 Redis 客户端
redis_url = "redis://192.168.100.238:6379"
config = RedisConfig(
index_name="qa",
redis_url=redis_url,
metadata_schema=[
{"name": "category", "type": "tag"},
{"name": "num", "type": "numeric"},
],
)
# 定义 Redis 向量存储
vector_store = RedisVectorStore(embeddings, config=config)
# 生成检索器
retriever = vector_store.as_retriever()
# 定义提示词模板
prompt = ChatPromptTemplate.from_messages(
[
(
"human",
"""你是负责回答问题的助手。使用以下检索到的上下文片段来回答问题。如果你不知道答案,就说你不知道。最多只用三句话,回答要简明扼要。
Question: {question}
Context: {context}
Answer:""",
),
],
)
# 将文档转换为字符串
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# 定义链
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
# 执行链,流式输出
for chunk in rag_chain.stream("数据库表怎么设计的? "):
print(chunk, end="|", flush=True)
# 输出:
|数据库|表|设计|可以|通过|水平|分|表|来|优化|性能|,|特别|是|当|单|表|数据|量|过|大|时|,|可以|减少|锁|争|用|和|提升|查询|效率|。|实现|方式|是|将|状态|字段|独立|存储|在|house|_status|表|中|,|并|通过|house|_id|进行|关联|查询|。|使用|My|SQL|和|My|Batis|进行|数据|存储|与|访问|,|同时|可以|利用|Redis|缓存|来|进一步|提高|性能|。|||
上述代码中,RunnablePassthrough() 我们之前还没有见过,简单来说,这是一个 "伪"Runnable,它的主要作用是在链(Chain)中透明地传递输入数据,而不做任何修改。
当我们需要将原始输入和另一个处理过程的输出一起传递给下一个步骤时,就需要RunnablePassthrough。例如代码中,我们需要将【Query】与【通过检索出来的文档转换的字符串】同时发送给提示词模板。
上面讲过检索器不提供任何流式处理。但这里可以使用流式输出。我们可以把像RAG这样的链的执行过程想象成两个阶段:
阶段一:准备阶段(同步、阻塞):接收输入、检索文档、构建提示
阶段二:生成阶段(可流式):调用LLM
流式输出的是最终答案,而非中间过程。