本文主要讲解LangChain的核心组件(输出解析器,文档加载器,文本分割器,文本向量,检索器)。
1.输出解析器
负责获取模型的输出,并将输出转换为结构化的格式。他们将LLM的非结构话文本输出转换为结构化格式。这使得LLM的交互从模糊的文本对话变成了精确的数据API调用。
1.1与with_structured_output()的区别
维度不同:输出解析器是一个功能性组件,而with_structured_output()是聊天模型的一个方法。
用法不同:输出解析器的工作流程可以是链式的。而with_structured_output()不行,只能手动调用,返回一个新的具备了结构化输出能力的Runnable
1.2使用示例:
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
model=ChatOpenAI(model="gpt-4o-mini")
chain=model|StrOutputParser()
for chunk in chain.stream("写⼀⾸夏天的诗词,50字以内。"):
print(chunk,end="|")
1.3解析结构化对象输出
要结构化对象,需要用到的输出解析器是PydanticOutputParser。
其参数如下:
pydantic_object:要解析的pydantic模型。
内置方法:
invoke():将单个输入转化为输出。
get_format_instructions()->str:重要!
作用:生成一个指令字符串这个字符串会被添加到发给LLM的提示的末尾。
目的:告诉LLM应该以什么样的格式返回他的响应。
代码示例如下:
from typing import Optional
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel,Field
model=ChatOpenAI(model="gpt-4o-mini")
class Joke(BaseModel):
"""给用户讲一个笑话"""
setup:str=Field(description="这个笑话的开头")
punchline:str=Field(description="这个笑话的妙语")
rating: Optional[int]=Field(
default=None,description="从1到10分,给这个笑话评分"
)
#设置解析器
parser=PydanticOutputParser(pydantic_object=Joke)
#提示词模板
prompt=PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
# #partial_variables:提⽰模板携带的部分变量的字典,⽆需在每次调⽤提⽰时都传⼊它们。
# #类型为 Mapping[str, Any],传⼊template携带的部分变量的字典。
partial_variables={"format_instructions":parser.get_format_instructions()},
)
chain=prompt|model|parser
for chunk in chain.stream({"query":"给我讲一个关于唱歌的笑话"}):
print(chunk,end="|")
1.4解析JSON输出
要输出JSON格式,需要用到的输出解析器是JsonOutParser。其参数如下:
pydantic_object:用于验证Pydantic对象。如果为空则不执行任何验证。
内置方法:
invoke():将单个输入转换为输出。
get_format_instructions()->str:重要!
作用:生成一个指令字符串,这个字符串会被添加到发送给LLM的提示的末尾。
目的:告诉LLM应该以什么样的格式返回他的响应。
代码如下:
from typing import Optional
from langchain_core.output_parsers import PydanticOutputParser, JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel,Field
model=ChatOpenAI(model="gpt-4o-mini")
class Joke(BaseModel):
"""给用户讲一个笑话"""
setup:str=Field(description="这个笑话的开头")
punchline:str=Field(description="这个笑话的妙语")
rating: Optional[int]=Field(
default=None,description="从1到10分,给这个笑话评分"
)
#设置解析器
parser=JsonOutputParser()
#提示词模板
prompt=PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
# #partial_variables:提⽰模板携带的部分变量的字典,⽆需在每次调⽤提⽰时都传⼊它们。
# #类型为 Mapping[str, Any],传⼊template携带的部分变量的字典。
partial_variables={"format_instructions":parser.get_format_instructions()},
)
chain=prompt|model|parser
print(chain.invoke({"query":"给我讲⼀个关于唱歌的笑话"}))
2.文档加载器
2.1RAG介绍
对于AI大模型它最擅长的是语义理解和文本总结,最不擅长的是提取实时信息。
对于搜索引擎来说,他最擅长的是获取实时信息,缺点是信息分散,每次都需要人为进行总结。
因此大模型和搜索引擎的结合,就是给AI配备了一个活字典,让AI可以随时进行查阅。

搜索引擎可以帮助我们解决实时数据的获取,但获取到的数据也是受限的。它只能获取到公开在网络中的数据而无法获取到一些本地数据,或企业内部的私有数据等。
解决办法是:使用RAG技术。当用户向LLM提问的时候,系统首先在知识库中进行语义搜索找到最相关的内容,然后将这些内容核问题一起交给LLM来生成答案。
2.2RAG流程
RAG流程分为离线数据处理和在线检索两个过程。
RAG知识库可以是本地文档,公司内部文档等一些私有化数据。但这些私有数据或文档实际上并不能很好地被直接进行检索访问。需要将这些私有化数据建成可以被检索的知识库,这就是离线数据处理要做的事情。而在线检索则是我们依赖知识库查询通过大模型生成结果的过程。

首先第一步是文档加载:加载多种不同来源加载文档,LangChain提供了100多种不同的文档加载器。
第二步是文本分割:他是把Documents切分为指定大小的块。
第三步是存储:它涉及到两个环节。分别是将切分好的文档块进行嵌入,即将文档块转换为向量的形式。同时将嵌入后的向量数据,存储到数据库中。
第四步是检索:数据存入向量数据库后。当我们需要进行数据检索的时候,会通过某种检索算法找到与输入问题相似的文档块。
第五步是输出:把问题以及检索出来的文档快一起交给LLM,它会通过问题和检索出来的提示一起生成更加合理的答案。
2.3Doucment文档类
要想实现RAG,首先就需要从源中获取到数据,即加载数据或文档。这是通过LangChain的文档加载器完成的。LangChain文档加载器可以将各种数据源加载成一系列的文档对象Document。class langchain_core.documents.base.Document用于存储一段文本和相关元数据的类,我们可以直接定义LangChain文档列表,如下所示:
from langchain_core.documents import Document
documents = [
# 单个Document对象通常表⽰较⼤⽂档的⼀个块
Document(
# 内容字符串
page_content="狗是很好的伴侣。",
# 元数据字典
# 元数据属性可以捕获有关⽂档源、与其他⽂档的关系以及其他信息的信息。
metadata={"source": "mammal-pets-doc"},
),
Document(
page_content="猫是独⽴的宠物。",
metadata={"source": "mammal-pets-doc"},
)
]
这里我们定义了一个documents文档列表,其内包含了两个Document文档对象。通常单个Document对象表示较大文档的一个块/页。每个Document对象包含了以下参数:
id。可选的文档标识符。理想情况下,这应该在整个文档集合中是唯一的,并格式化为UUID。
page_count:字符串文本。
metadata:与内容关联的任意元数据。类型为dict [Optional]。
2.4加载PDF文档
将本地的PDF加载到LangChain中其实就是将PDF文档转换为一个个Document对象。这时候就需要我们使用PyPDFLoader文档加载器完成这一个功能。PyPDFLoader类有以下关键函数:
init()初始化函数,入参file_path,表示要加载的PDF文档路径。
load()->list[Document]:将数据加载到文档对象中,返回文档对象列表。
现在让我们加载一个本地PDF文档看下效果。
from langchain_community.document_loaders import PyPDFLoader
loader=PyPDFLoader(file_path="./docs/pdf/26王道《操作系统》.pdf")
docs=loader.load()
print(f"PDF文档总页数为:{len(docs)}\n")
print(f"第一页的文本内容是:{docs[0].page_content[:200]}\n")
print(f"第一页的元数据字典是:{docs[0].metadata}")
2.5加载Markdown文件
将本地的Markdown文档加载到LangChain中需要我们使用UnstructuredMarkdownLoader文档加载器完成这一个功能,他又以下关键函数:
init()初始化函数,所需参数:file_path:表示路径,mode:加载文档所使用的模式,可以是single或者elements。默认为single。
load()->list[Document]:将数据加载到文档对象中,返回文档对象列表。
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
loader=UnstructuredMarkdownLoader("./docs/markdown/Linux线程.md")
data=loader.load()
text_splitter=CharacterTextSplitter(
separator="\n\n ",
chunk_size=400,
chunk_overlap=50,
length_function=len,
is_separator_regex=False,
)
documents=text_splitter.split_documents(data)
for document in documents[:10]:
print("*"*30)
print(document)
3.文档分割器
我们已经知道通过文档加载器完成各种数据源的加载将其转换为数据源的加载,将其转换为文档对象Document。那么接下来要做的是文档拆分。
文档拆分是将大的文本分解为更小的易于管理的块。这对于索引数据并将其传递到模型中都很有用。因此大块更难搜索并且不适合模型的有限上下文窗口。拆分可以提高搜索结果的粒度,从而更精确的将查询与相关文档部分进行匹配。
我们可以根据文档的长度进行拆分:基于字符长度拆分和基于token长度拆分。
3.1基于字符长度来拆分
还是上面的例子:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
loader=UnstructuredMarkdownLoader("./docs/markdown/Linux线程.md")
data=loader.load()
text_splitter=CharacterTextSplitter(
separator="\n\n ",
chunk_size=400,
chunk_overlap=50,
length_function=len,
is_separator_regex=False,
)
documents=text_splitter.split_documents(data)
for document in documents[:10]:
print("*"*30)
print(document)
其中separator代表选择分隔符。chunk_size代表目标块的大小。chunk_overlap代表块之间的重叠大小。length_function代表使用测量长度的函数。is_separator_regex代表是否为正则表达式。
3.2基于Token长度进行拆分
之前我们讲过LLM大模型并不是直接接收字符串,而是需要现做token切分编码。这里我们可以借助tiktoken分词器,来进行token的切分编码。
import tiktoken
# 定于cl100k_base编码⽅式的分词器
enc = tiktoken.get_encoding("cl100k_base")
# 进⾏切分编码
enc_output = enc.encode("my name is LiHua!")
# 打印结果
print(f"编码后的token:{str(enc_output)}")
for token in enc_output:
print(f"将token: {str(token)} 变成⽂本:{str(enc.decode_single_token_bytes(token))}")
在LangChain中我们可以使用from_tiktoken_encoder()方法来定义根据tiktoken分词器拆分文本
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)
4.文本向量
4.1嵌入模型
嵌入的核心思想是将人类世界的符号转换为计算机能够理解的数值形式,并且要求这种转换能够保留原始的符号语义和关系。它是把人类语言翻译成计算机的数学语言。
嵌入模型是表示型模型。它的目的不是生成文本,而是为输入的文本创建一个最佳的富含语义的数值表示。
4.2嵌入式模型应用场景:
1.语义搜索
语义搜索时将查询和文档库中的所有文档转换为向量,然后计算查询向量与所有文档向量的相似度,返回最相似的文档。借助嵌⼊模型进⾏⽂档搜索的过程:1)为多文档生成其各自的向量。2)为搜索查询语句生成向量。3)衡量查询向量与每个文档向量之间的相似性,得到相似度最高的文档。
2.检索增强生成
当用户向LLM提问时,系统首先使用嵌入模型在知识库中进行语义搜索,找到最相关的内容,然后将这些内容和问题一起交给LLM来生成答案。
4.3Embeddings嵌入模型类
1.定义嵌入模型
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-large", # 是 OpenAI 2024年发布的最新嵌⼊模型,⽣成3072维
的⾼质量向量
)
他其中设计了两个核心方法来处理文本嵌入。
.embed_documents():用于处理文档。它的输入是多个文本,例如将一个知识库里的所有段落都转换为向量后存入数据库就会使用这个方法。
它返回一个二维列表。外层列别哦啊的每个元素对应一个输入文档,内层则是该文档的向量表示。
.embed_query():用于处理查询query。它的输入是单个文本。例如当用户提出一个问题时,需要将这个问题转换为向量,以便在数据库中搜索相似的文档段落,就会使用这个方法。
它返回一个一维列表,里面是浮点数,代表单个查询文本的向量。
之所以设计成两个方法是因为某些嵌入模型提供商会针对被搜索的文档和搜索查询本身采用不同的优化策略和模型。即使底层是同一个模型,也可能对两者进行不同的预处理,以获得更好的搜索效果。
2.嵌入文档列表
embed_documents的语义是索引。它的目的是预处理大量文本,为他们创建向量表示,以便后续被搜索。这一般是一个离线批量处理的过程,代码如下:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
markdown_path="./docs/markdown/Linux线程.md"
#single模式加载后默认只有一个Document对象
loader=UnstructuredMarkdownLoader(markdown_path)
data=loader.load()
#生成分割器
text_splitter=CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=200,
chunk_overlap=50
)
#分割文档
documents=text_splitter.split_documents(data)
#定义嵌入模型
embeddings=OpenAIEmbeddings(model="text-embedding-3-large")
#嵌入文档列表,生成向量列表
texts=[doc.page_content for doc in documents]
documents_vector=embeddings.embed_documents(texts)
print(f"文档数量为:{len(documents)},生成了{len(documents_vector)}个向量列表")
print(f"第一个文档向量维度:{len(documents_vector[0])}")
print(f"第二个文档向量维度:{len(documents_vector[1])}")
3.嵌入单个查询
embed_query的语义是"搜索"。它的目的是在用户发起请求时,实时地将一个问题或者指令转换为向量,用于在索引的文档向量中检索,这是一个在线,实时,按需处理的过程。
为单个查询生成向量的代码如下:
from langchain_openai import OpenAIEmbeddings
#定义嵌入模型
embeddings=OpenAIEmbeddings(model="text-embeddings-3-large")
query_vector=embeddings.embed_query("在项目中遇到了哪些挑战?如何解决?”)
print(f"向量维度:{len(query_vector)}")
print(f"向量前五个数值为:{query_vector[:5]")
4.4向量存储
在LangChain中实际并不需要我们直接手动调用嵌入模型生成向量然后手动比较向量,实际上向量是被管理在专门的向量存储介质中,如向量数据库。向量存储的核心任务是解决一个传统数据库不擅长的问题:基于内容的相似性搜索,而不是基于精确匹配的查询。向量数据库提供了专门用于高效存储管理和检索高维向量的能力。其核心是:高效的组织和检索这些数据。
常见的向量数据库核心机制如下:
1.专门的索引。
这是向量数据库的灵魂。它们不会使用暴力搜索而是预先为向量构建一种特殊的索引结构。
常见的方法有近似最近邻搜索:未来追求极致的速度,它愿意牺牲一点点的精度。它通过聚类分层压缩等算法技术,将搜索范围从整个数据库缩小到几个最可能的候选集。
2.向量相似度计算优化。
向量数据库底层使用高度优化的库来进行向量计算,如FAISS向量数据库,它能快速处理大规模数据并支持在高维空间进行相似性的搜索。
这些库充分利用了CPU的SIMD指令集和GPU的并行计算的能力,让大规模的向量计算速度极快。
我们可以利用专门的向量数据库,通过构建索引,优化计算流程并提供丰富的过滤和管理功能,从而对海量高维的向量数据进行快速可扩展的相似性检索的一套完整技术方案。
4.5内存存储
LangChain中的大多数向量存储时接收嵌入模型作为参数。如下表示:
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
# 定义嵌⼊模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 内存存储初始化
vector_store = InMemoryVectorStore(embedding=embeddings)
添加文档
我们可以使用add_documents方法,向内存存储中添加文档。要注意的是该方法为添加的文档编排索引,索引列表随着该方法返回。
当我们相对某个文本进行数据检索的时候的两个步骤:
1.编制索引:用于从源中摄取数据并为其编排索引。
2.检索与生成:接收用户查询并从索引中检索相关数据,然后将其传递给模型。
其中第一步我们通过下面代码完成:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
from TEST45 import vector_store
#生成分割器
text_splitter=CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=200,
chunk_overlap=50
)
#加载文档,输出成 LangChain 统一的 Document 对象
data=UnstructuredMarkdownLoader(
"./docs/markdown/Linux线程.md",
).load()
#分割文档
documents=text_splitter.split_documents(data)
#添加文档
ids=vector_store.add_documents(documents=documents)
print(f"共编排了{len(ids)}个文档索引")
print(f"前三个文档索引是:{ids[:3]}")
获取文档
使用get_by_ids的方法,通过索引列表获取对应的文档列表。如下所示:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
from TEST45 import vector_store
#生成分割器
text_splitter=CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=200,
chunk_overlap=50
)
#加载文档,输出成 LangChain 统一的 Document 对象
data=UnstructuredMarkdownLoader(
"./docs/markdown/Linux线程.md",
).load()
#分割文档
documents=text_splitter.split_documents(data)
#添加文档
ids=vector_store.add_documents(documents=documents)
doc3=vector_store.get_by_ids(ids[:3])
#把 doc3 里前 3 个文档的内容,拿出来组成列表,再打印出来
print(f"{[doc.page_content for doc in doc3]}")
#print(f"共编排了{len(ids)}个文档索引")
#print(f"前三个文档索引是:{ids[:3]}")
4.6相似性搜索
想要获取根据相似性搜索的结果即嵌入单个查询,并查找相似的文档并将他们作为文档列表返回。这可以使用similarity_search方法来实现。代码如下:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
from TEST45 import vector_store
#生成分割器
text_splitter=CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=500,
chunk_overlap=50
)
#加载文档,输出成 LangChain 统一的 Document 对象
data=UnstructuredMarkdownLoader(
"./docs/markdown/Linux线程.md",
).load()
#分割文档
documents=text_splitter.split_documents(data)
#添加文档
ids=vector_store.add_documents(documents=documents)
doc3=vector_store.get_by_ids(ids[:3])
#把 doc3 里前 3 个文档的内容,拿出来组成列表,再打印出来
#print(f"{[doc.page_content for doc in doc3]}")
#print(f"共编排了{len(ids)}个文档索引")
#print(f"前三个文档索引是:{ids[:3]}")
search_docs = vector_store.similarity_search(query="Linux如何查看线程信息?", k=1)
for doc in search_docs:
print("*" * 30)
print(doc.page_content)
那么这时候我们就有疑问,为什么不直接是问题的答案而只是相似的文档呢?
此时我们可以将最相关内容和我们提出的问题一起交给LLM生成答案,这便能极大地提高答案的准确性。以上流程叫做检索增强生成(RAG)这是当前大语言模型应用的和新模式。
虽然向量数据库实现了搜索算法,来有效地搜索所有嵌入的文档以找到最相似的文档。但现实场景中,我们还希望通过先根据元数据进行过滤,来帮助缩小搜索范围。例如从特定来源或日期范围检索文档。
这次,我们给搜索方法加入了filter参数,它接受一个bool的值,表示我们可以根据条件选择是否过滤某些文档,再去进行搜索。
4.7元数据过滤
每个Document对象,都包含了以下参数:
id:可选的文档标识符。理想情况下这应该是整个文档集合中是唯一的。
page_contetnt:字符串文本
metadata:与内容关联的任意元数据。类型为dict[Optional]
虽然向量数据库实现了搜索算法,来有效地搜索所有嵌入地文档以找到最相似的文档。但现实场景中我们还是希望先根据元数据进行过滤来帮助缩小搜索范围。
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
from langchain_core .documents import Document
from TEST45 import vector_store
#生成分割器
text_splitter=CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=500,
chunk_overlap=50
)
#加载文档,输出成 LangChain 统一的 Document 对象
data=UnstructuredMarkdownLoader(
"./docs/markdown/Linux线程.md",
).load()
#分割文档
documents=text_splitter.split_documents(data)
#添加文档
ids=vector_store.add_documents(documents=documents)
doc3=vector_store.get_by_ids(ids[:3])
#把 doc3 里前 3 个文档的内容,拿出来组成列表,再打印出来
#print(f"{[doc.page_content for doc in doc3]}")
#print(f"共编排了{len(ids)}个文档索引")
#print(f"前三个文档索引是:{ids[:3]}")
def _filter_function(doc:Document)->bool:
return doc.metadate.get("source")=="hahaha"
search_docs = vector_store.similarity_search(query="Linux如何查看线程信息?", k=1,filter=_filter_function)
for doc in search_docs:
print("*" * 30)
print(doc.page_content)
4.8Redis向量存储
我们还可以使用Redis来存储向量。从本质上来讲,Redis是一种键值型的NoSQL数据库,除了传统用例以外。它还提供了诸如搜索和查询功能等额外能力,允许用户在Redis内创建二级索引结构。
4.8.1基本概念
RediSearch是Redis官方提供的一款高性能搜索与全文索引引擎模块。它基于Redis构建,使用户能够直接在Redis数据库中执行复杂的搜索和分词查询,无需引入外部搜索引擎。
Index是RediSearch模块的概念用于定义一个查询目录。它是一个独立的数据结构建立在多个Redis Keys(Hash类型)之上,这专门为了激素执行文本搜索过滤和聚合而设计。
Index Fields是在创建索引时,明确指定的那些需要被索引的字段。metadata schema则是用来描述元数据的结构声明。
4.8.2环境设置
使用Redis来存储向量,首先需要将相关环境配置好。
第一步,启动Redis服务端:使用Docker启动Redis实例。
使用:
docker run -d -p 6379:6379 -it redis:latest
第二步,安装Redis客户端包,以便将来定义客户端,以及运行搜索和查询命令。
pip install redis
第三步,在LangChain中想要使用Redis向量库,需要安装langchain-redis包
pip install -qU langchain-redis
第四步,定义Redis连接URL,客户端连接Redis需要使用
第五步,测试连接
import redis
redis_url="redis://localhost:6379"
redis_client=redis.from_url(redis_url)
print(redis_client.ping())
4.8.3基本操作
LangChain中使用RedisVectorStore 初始化Redis向量存储。由于Redis需要相关配置因此LangChain提供了RedisConfig配置类供我们使用。如下所示:
from langchain_openai import OpenAIEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore
#嵌入模型
embeddings=OpenAIEmbeddings(model="text-embedding-3-large")
#配置Redis客户端
redis_url="redis://localhost:6379"
#config = 给 Redis 向量库的 “规则说明书”
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)
RedisVectorStore类其初始化参数如下:embeddings:用于存储的Embeddings实例。config:可选的RedisConfig对象。
RedisConfig配置类其参数如下:index_name:索引的名字。key_prefix:Redis Key的前缀。redis_url:Redis实例的URL。metadata_schema:元数据字段的schema
我们可以根据Index Name查询其下的所有的Index Fields,使用rvl命令行工具来检查索引。
添加文档:
我们可以使用add_documents方法,向向量库中去添加文档。这次我们可以给被分割的文档添加相关的元数据。如下所示:
from langchain_openai import OpenAIEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
#嵌入模型
embeddings=OpenAIEmbeddings(model="text-embedding-3-large")
#配置Redis客户端
redis_url="redis://localhost:6379"
#config = 给 Redis 向量库的 “规则说明书”
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)
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", chunk_size=200, chunk_overlap=50
)
# 加载⽂档
data = UnstructuredMarkdownLoader("./docs/markdown/Linux线程.md", category="QA").load()
# 分割⽂档
documents = text_splitter.split_documents(data)
# 为⽂档添加元数据
for i, doc in enumerate(documents, start=1):
doc.metadata["category"] = "QA"
doc.metadata["num"] = i
ids = vector_store.add_documents(documents=documents)
print(f"共编排了{len(ids)}个⽂档索引")
print(f"前3个⽂档的索引是:{ids[:3]}")
获取文档:
使用get_by_ids方法,通过索引列表获取对应的文档列表。如下所示:
ids="***"
doc3=vector_store.get_by_ids(ids)
print(f"{[doc.page_content for doc in doc3]}")
4.8.4向量搜索
1.相似性搜索,是哦那个similarity_search的方法实现。代码如下:
search_docs =vector_store .similarity_search(query="什么是线程?", k=2)
for doc in search_docs :
print("*" *30)
print(doc.page_content)
2.元数据过滤
在他们搜索之前都可以根据元数据进行过滤。示例如下:
from redisvl.query.filter import Tag
# 正确过滤:只搜 category = "QA" 的文档
filter_condition = Tag("category") == "QA"
# 带过滤的检索
scored_results = vector_store.similarity_search_with_score(
query="数据库表怎么设计的?",
k=2,
filter=filter_condition
)
4.8.5最大边际相关性检索
最大边际相关性是一种重新排序算法,它使用语义相似性为基础工具,从一个候选集中挑选出一组既能代表查询主题又彼此多样化的结果。
使用MMR搜索,需要使用max_marginal_relevance_search方法。代码如下:
from redisvl.query.filter import Tag,Num
category_is_qa=Tag("category")=="qa"
mmr_results=vector_store.max_marginal_relevance_search(
query="怎么设计的",
k=2,
fetch_k=10,
filter=filter_condition
)
for doc in mmr_results:
print(f"Content:{DOC.page_content[:100]...")
print(f"Metadata:{doc.metadata}")
5.检索器
5.1检索系统
检索系统是一个为了满足用户信息需求,从大规模非结构化的数据集合中,自动高效的查找排序并返回相关信息的计算机系统。
它的任务是:在正确的时间以正确的方式将信息传递给正确的人。
5.2检索器
检索器是检索系统中的一个核心组件,它接收来自用户接口的查询,检索出包含查询关键词的文档集合。
5.3使用向量数据库作为检索器
向量存储是索引和检索非结构化数据的一种强大而有效的方法,可以通过调用向量数据库的as_retriever方法,将向量存储用作检索器,在这里我们使用Redis向量存储。
from langchain_openai import OpenAIEmbeddings
from langchain_redis import RedisConfig,RedisVectorStore
embeddings=OpenAIEmbeddings(model="text-embedding-3-large")
redis_url="redis://localhost: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])
as_retriever方法也支持我们传递相关参数修改搜索结果。如:
search_type:设置相似算法
searcg_kwargs:
k:限制检索器返回的文档K的数量。
fetch_k:要传递给MMR算法的文档量。
要注意的是他虽然是Runnable对象,但其不提供任何流式处理,因为它本身通常是同步的,阻塞的操作。
另外除了使用as_retriever方法外,我们还可以自行创建一个检索器。
检索器特点:LangChain检索器是一个Runnable的对象;LangChain检索器输入为查询字符串,输出为文档列表。综上我们可以:
from langchain_core .runnables import chain
from typing import List
from langchain_core .documents import 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函数,且满足检索器输入输出的要求。在函数中,我们依旧使用向量数据库的相似性搜索方法,这样灵活性也更高想要进行元数据筛选也更方便。
6.RAG案例
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore
model=ChatOpenAI(model="gpt-4o-mini")
embeddings=OpenAIEmbeddings(model="text-embedding-3-large")
redis_url="redis://localhost:6379"
config=RedisConfig(
index_name="qa",
redis_url=redis_url,
metadata_schema=[
{"name":"category","type":"tag"},
{"name":"num","type":"numeric"},
],
)
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)
3018




被折叠的 条评论
为什么被折叠?



