到目前为止,我们已经学习了如何向模型发送消息、如何使用提示词模板,以及如何通过 Few-Shot 约束模型输出。但如果希望模型真正回答企业文档、项目资料、PDF、数据库中的内容,还需要掌握 RAG 相关组件。
一、输出解析器 Output Parser
模型原始返回的数据通常是 AIMessage。
python
result = model.invoke("介绍一下 LangChain")
print(result.content)
如果只需要获取文本内容,可以使用 StrOutputParser。
python
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
chain = model | parser
result = chain.invoke("介绍一下 LangChain")
print(result)
输出解析器的作用可以理解为:
python
模型原始输出
↓
解析器处理
↓
得到字符串、JSON、Pydantic 对象等结构化结果
1. StrOutputParser
最简单的解析器,用于提取模型文本内容。
python
from langchain_core.output_parsers import StrOutputParser
chain = model | StrOutputParser()
result = chain.invoke("什么是向量数据库?")
print(result)
2. JsonOutputParser
如果希望模型返回 JSON,可以使用 JsonOutputParser。
python
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
parser = JsonOutputParser()
prompt = PromptTemplate(
template="""
请从下面文本中提取姓名、年龄和职业。
文本:
{text}
{format_instructions}
""",
input_variables=["text"],
partial_variables={
"format_instructions": parser.get_format_instructions()
}
)
chain = prompt | model | parser
result = chain.invoke({
"text": "张三今年 25 岁,是一名 Python 后端开发工程师。"
})
print(result)
输出示例:
python
{
"name": "张三",
"age": 25,
"job": "Python 后端开发工程师"
}
3. Pydantic 输出解析
Pydantic 可以让输出类型更明确。
python
from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser
class Person(BaseModel):
name: str = Field(description="姓名")
age: int = Field(description="年龄")
job: str = Field(description="职业")
parser = PydanticOutputParser(pydantic_object=Person)
之后将 parser.get_format_instructions() 加入 Prompt,即可要求模型按指定结构返回。
二、什么是 RAG
RAG 的全称是 Retrieval-Augmented Generation,即检索增强生成。
RAG 主要解决三个问题:
- 模型知识可能已经过时;
- 模型不知道企业私有资料;
- 模型可能产生幻觉。
例如,企业有一份员工手册、产品说明书、项目文档。如果不使用 RAG,模型并不知道文档内容。如果使用 RAG,流程会变成:
用户问题
↓
在知识库中检索相关文档
↓
把相关文档放进 Prompt
↓
模型依据文档回答
RAG 的完整流程通常分为两部分。
1. 索引阶段
原始文档
↓
文档加载
↓
文本切分
↓
文本向量化
↓
写入向量数据库
2. 检索与生成阶段
用户问题
↓
问题向量化
↓
相似度搜索
↓
获取相关文本块
↓
将文本块与问题交给模型
↓
模型生成最终答案
三、Document:LangChain 中的文档对象
LangChain 中,文档通常使用 Document 对象表示。
python
from langchain_core.documents import Document
document = Document(
page_content="LangChain 是一个用于构建 LLM 应用的开发框架。",
metadata={
"source": "langchain_intro.md",
"category": "技术文档"
}
)
print(document.page_content)
print(document.metadata)
一个 Document 包含两部分:
| 属性 | 作用 |
|---|---|
page_content |
文档正文内容 |
metadata |
文档来源、页码、标题、分类等辅助信息 |
元数据非常重要。例如,最终回答时,我们可能需要告诉用户:
该回答来自《员工手册》第 12 页。
或者只检索某类资料:
只查询"产品文档"分类下的内容。
这些都依赖元数据。
四、文档加载器 Document Loader
文档加载器负责将外部资料读取为 Document 对象。
常见文档来源包括:
- TXT;
- Markdown;
- PDF;
- Word;
- Excel;
- HTML 网页;
- 数据库;
- Notion;
- 企业 Wiki。
1. 加载 TXT 文件
python
from langchain_community.document_loaders import TextLoader
loader = TextLoader(
"knowledge.txt",
encoding="utf-8"
)
documents = loader.load()
print(documents[0].page_content)
print(documents[0].metadata)
2. 加载 Markdown 文件
python
from langchain_community.document_loaders import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader(
"knowledge.md",
mode="single"
)
documents = loader.load()
print(documents[0].page_content)
mode="single" 通常表示将整个文件加载为一个 Document。有些加载器也支持按标题、段落等元素拆分文档。
3. 文档加载的注意点
加载文档后,不建议直接将整份文档传给模型。
原因包括:
- 文档可能过长;
- 超出上下文窗口;
- Token 成本过高;
- 无关内容影响回答质量。
因此,下一步通常是文本切分。
五、文本分割器 Text Splitter
文本切分器的职责是将长文档拆成多个小文本块。例如:
一份 100 页的产品说明书
↓
切成数百个可检索文本块
↓
每次只检索其中最相关的几块
1. 使用递归文本切分器
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=80
)
chunks = text_splitter.split_documents(documents)
print(len(chunks))
print(chunks[0].page_content)
参数说明:
| 参数 | 含义 |
|---|---|
chunk_size |
每个文本块的最大长度 |
chunk_overlap |
相邻文本块的重叠长度 |
2. 为什么需要 chunk_overlap
假设原始内容是:
Redis 是一个高性能内存数据库。
它支持字符串、列表、集合、有序集合等多种数据结构。
如果恰好在"数据库"和"它支持"之间切分,可能导致前后语义断裂。因此保留一部分重叠内容,可以减少切分造成的上下文丢失。
3. 如何选择切分大小
没有一个绝对通用的参数,需要根据资料类型调整。
| 文档类型 | 切分建议 |
|---|---|
| FAQ 问答 | 按问题和答案切分 |
| Markdown 文档 | 按标题层级切分 |
| 普通文章 | chunk_size=300~800 |
| 代码文件 | 按函数、类、模块切分 |
| PDF 资料 | 结合页码、标题、段落切分 |
一个好的文本块通常应该:
- 包含相对完整的语义;
- 不要过长;
- 不要只剩半句话;
- 能够独立回答一类小问题;
- 保留来源信息。
六、Embedding:文本向量化
计算机并不真正理解文字含义。Embedding 的作用是将文字转换为向量。
"Python 是一门编程语言"
↓
[0.012, -0.463, 0.842, ...]
这些数字本身没有直接可读性,但它们能表达文本之间的语义关系。例如:
Python 入门路线
Python 初学者如何学习
这两句话字面不同,但语义接近,因此它们的向量距离通常比较近。
1. 初始化 Embedding 模型
python
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small"
)
2. 向量化查询文本
python
vector = embeddings.embed_query(
"什么是向量数据库?"
)
print(len(vector))
3. 批量向量化文档
python
vectors = embeddings.embed_documents([
"LangChain 可以帮助开发者构建 LLM 应用。",
"RAG 可以让模型使用外部知识库。",
"Redis 可以作为缓存和向量数据库使用。"
])
print(len(vectors))
一般来说:
embed_documents():用于知识库文档入库;embed_query():用于用户问题检索。
七、Vector Store:向量数据库
向量数据库负责保存:
文本块
+ 文本向量
+ 元数据
并支持相似度搜索。
常见向量存储包括:
- Chroma;
- FAISS;
- Redis;
- Milvus;
- Pinecone;
- Qdrant。
1. 使用 Chroma 创建向量库
python
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small"
)
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
2. 相似度检索
python
results = vector_store.similarity_search(
query="LangChain 的作用是什么?",
k=3
)
for doc in results:
print(doc.page_content)
print(doc.metadata)
k=3 表示返回最相近的三段文档。
3. MMR 检索
普通相似度检索有时会返回内容高度重复的文档。MMR,即最大边际相关性,可以在"相关性"和"多样性"之间取得平衡。
python
results = vector_store.max_marginal_relevance_search(
query="如何使用 LangChain 构建知识库问答系统?",
k=3,
fetch_k=10
)
可以理解为:
先找出 10 条候选结果
↓
从中挑选 3 条既相关、又不重复的内容
MMR 在 RAG 场景中很实用,可以减少上下文内容重复。
八、Retriever:统一检索接口
Retriever 是 LangChain 对检索能力的统一抽象。
它不关心底层是:
- Chroma;
- Redis;
- Elasticsearch;
- SQL;
- Web 搜索;
- 企业内部 API。
只要能"根据问题返回相关文档",就可以封装为 Retriever。
1. 从向量数据库创建 Retriever
python
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={
"k": 3
}
)
调用检索器:
python
docs = retriever.invoke(
"LangChain 中 PromptTemplate 的作用是什么?"
)
for doc in docs:
print(doc.page_content)
2. 使用 MMR Retriever
python
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={
"k": 3,
"fetch_k": 10
}
)
3. Retriever 与模型的区别
| 组件 | 主要职责 |
|---|---|
| Chat Model | 根据上下文生成回答 |
| Embedding Model | 将文本转换为向量 |
| Vector Store | 保存向量并进行相似度搜索 |
| Retriever | 对外提供统一的检索接口 |
Retriever 本身通常只负责检索,不负责生成最终答案。
九、构建一个基础 RAG 链
前面所有组件组合起来,就可以构建一个基础 RAG 应用。
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_template("""
你是一名知识库问答助手。
请严格依据参考资料回答问题。
参考资料:
{context}
用户问题:
{question}
要求:
1. 回答简洁准确;
2. 如果资料中没有答案,请明确说明;
3. 不要编造资料中不存在的内容。
""")
定义文档格式化方法:
python
def format_docs(docs):
return "\n\n".join(
doc.page_content for doc in docs
)
构建链:
python
from langchain_core.runnables import RunnablePassthrough
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| prompt
| model
| StrOutputParser()
)
调用:
python
result = rag_chain.invoke(
"LangChain 中 Retriever 的作用是什么?"
)
print(result)
整体流程如下:
用户问题
↓
Retriever 检索相关文档
↓
format_docs 拼接检索结果
↓
Prompt 注入参考资料与问题
↓
Chat Model 生成最终回答
↓
StrOutputParser 提取文本结果
这就是一个最基础的 RAG 知识库问答流程。
十、总结
本文学习了构建 RAG 应用所需的核心组件:
- Output Parser:解析模型输出;
- Document:统一表示文档内容和元数据;
- Document Loader:加载 TXT、Markdown、PDF 等资料;
- Text Splitter:将长文档切分为小块;
- Embedding:将文本转换为语义向量;
- Vector Store:保存向量并进行相似度搜索;
- Retriever:对外提供统一的检索接口;
- RAG Chain:将检索结果交给模型生成答案。
至此,LangChain 的基础知识已经可以支撑一个简单的企业知识库问答系统。