这篇适合谁,读完能做到什么
适合想用自己的 PDF 做语义搜索,并打算以后接入大模型做问答的读者。读完你能把 PDF 切成文本块、为文本块建立向量索引,用相似度查询找到相关段落,并把查询功能包装成检索器。
语义检索的思路
语义检索把文本转换成数值向量,意思相近的文本在向量空间里距离更近。查询时,把问题也转换成向量,再用余弦相似度等指标找出最相关的文本[1]。本文的示例覆盖创建 Document、生成向量、读取并切分 PDF、写入向量库、相似度查询,以及把向量库包装成检索器[1]。
准备工作
- Python 用
pypdf读取 PDF,可以用pip install pypdf、conda install pypdf -c conda-forge或uv add pypdf安装。JavaScript 用pdf-parse,安装命令是npm i pdf-parse[1]。 - 示例 PDF 是一份 2023 年的 Nike 10-K 文件,需要提前下载,并把
file_path设为它的路径(绝对路径或相对于工作目录的相对路径)[1]。 - 追踪(可选):设置
LANGSMITH_TRACING和LANGSMITH_API_KEY两个环境变量,就能开始记录追踪[1]:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."第一步:Document 是什么
LangChain 用 Document 表示一段文本及其元数据。它有三个属性:page_content(文本内容)、metadata(任意字典)和可选的 id[1]。一个 Document 往往就是整份文件的一个片段,元数据可以记录来源和位置[1]。先用几条示例文本熟悉它的结构:
from langchain_core.documents import Document
documents = [
Document(
page_content="Dogs are great companions, known for their loyalty and friendliness.",
metadata={"source": "mammal-pets-doc"},
),
Document(
page_content="Cats are independent pets that often enjoy their own space.",
metadata={"source": "mammal-pets-doc"},
),
]第二步:读取 PDF 并切分
读取 PDF 时,示例中的辅助函数 load_pdf_pages 用 pypdf 逐页提取文字,每一页生成一个 Document,元数据里记录来源路径和页码[1]。代码注释说明这个辅助函数只用于演示,正式使用时可以换用专门的 PDF 文档加载器[1]:
import pypdf
from langchain_core.documents import Document
# Below is a minimal helper for demonstration purposes.
def load_pdf_pages(file_path: str) -> list[Document]:
reader = pypdf.PdfReader(file_path)
return [
Document(
page_content=page.extract_text() or "",
metadata={"source": file_path, "page": i},
)
for i, page in enumerate(reader.pages)
]
file_path = "../example_data/nke-10k-2023.pdf"
docs = load_pdf_pages(file_path)
print(len(docs))示例中这份 PDF 被读成了 107 页。整页文本对检索来说太粗,相关段落很容易被周围的文字稀释,所以还要再切分。RecursiveCharacterTextSplitter 会按换行等常见分隔符递归切分,直到每块接近目标长度。它是通用文本的推荐切分器[1]:
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)
print(len(all_splits))示例把 107 页切成了 516 个块。设置 add_start_index=True 后,每个块的元数据里会记录它在整份文件中的字符偏移量,即 start_index[1]。示例中的 chunk_size=1000 和 chunk_overlap=200 是切分的长度与重叠,换成其他文档时可以调整并对比结果。
第三步:生成向量并建立索引
embed_query 把一段文本转换成向量。示例中两段测试文本生成的向量长度相同,都是 1536[1]。接下来把切好的块写入向量库,代码用的是 vector_store.add_documents[1]:
ids = vector_store.add_documents(documents=all_splits)LangChain 的向量库集成很多。有些是托管服务,需要凭证;有些运行在独立的基础设施上;有些则是内存中的轻量实现[1]。大多数集成也支持连接已有的索引或集合,具体方式取决于所选的集成[1]。
第四步:相似度查询
用字符串查询最直接,similarity_search 返回一个 Document 列表。下面的例子用一个关于 Nike 美国分销中心数量的问题查询[1]:
results = vector_store.similarity_search(
"How many distribution centers does Nike have in the US?"
)
print(results[0])similarity_search_with_score 会同时返回分数。示例中的分数是一种距离,数值越小越相似;不同提供方的分数定义并不相同,使用前需要确认[1]。asimilarity_search 是异步版本;如果想先自己计算好向量,可以用 similarity_search_by_vector;想在相关性和多样性之间取得平衡,可以用最大边际相关性搜索(maximum marginal relevance)[1]。
第五步:包装成检索器
向量库对象不是 Runnable,不能直接使用标准的 invoke 和 batch 接口。办法是用 @chain 装饰一个函数,在函数里调用 similarity_search。示例中 k=1,对两个问题做批量查询[1]:
from typing import List
from langchain_core.documents import Document
from langchain_core.runnables import chain
@chain
def retriever(query: str) -> List[Document]:
return vector_store.similarity_search(query, k=1)
retriever.batch(
[
"How many distribution centers does Nike have in the US?",
"When was Nike incorporated?",
],
)这样得到的检索器可以接入检索增强生成(retrieval-augmented generation,RAG)或其他大模型流程[1]。
为什么要切分,又为什么保留位置
整页文本对检索来说太粗。切成小块后,每个块的内容更集中,检索出来的段落才更容易对上问题。示例中的 chunk_overlap=200 让相邻的块有一段重叠,减少一句话恰好被切在两块之间的情况。add_start_index=True 会记录每个块在整份文件中的起始位置,方便回到整份文件核对[1]。
检索结果怎么读
similarity_search 返回的是 Document 列表。每个元素都带有 page_content 和 metadata,所以可以直接看到命中的文字,以及它来自哪一页、从哪个字符位置开始[1]。similarity_search_with_score 多出的分数是距离,读它时要记住方向:数值越小,越相似[1]。
读完以后可以检查的几件事
- PDF 是否已经下载,
file_path指向的文件是否存在[1]。 - 切分后的块数是否合理,示例中 107 页被切成了 516 个块[1]。
- 检索结果的
metadata里是否有页码和起始位置,便于核对[1]。
检索器和问答的关系
检索器本身只负责找出与问题相关的段落。要做问答,还需要把这些段落和问题一起交给大模型,由模型组织答案,这就是检索增强生成(RAG)的基本思路。本文的示例到检索器为止,RAG 的完整做法可以在后续学习中再补充[1]。
常见问题
- 结果不理想:先看切分参数。示例使用
chunk_size=1000、chunk_overlap=200,并打开了add_start_index,换文档时可以逐项调整[1]。 - 分数看起来反了:示例中的分数是距离,越小越相似[1]。
- 加载器选择:示例的 PDF 辅助函数只用于演示,正式项目可以使用专门的 PDF 加载器,LangChain 的 PDF 文档加载器有
PyPDFLoader等实现和合作伙伴包[1]。
下一步
可以继续查阅 LangChain 支持的嵌入模型与向量库列表,或学习检索增强生成相关的内容。示例中还给出了基于检索器的最小 RAG 实现的思路[1]。
本文改编自 LangChain 开源文档中的 Build a semantic search engine with LangChain 页面[1],原文以 MIT 许可证发布。
来源
- 1Build a semantic search engine with LangChainLangChain docs (MIT)
引用本页: LangChain 搭知识库:文档加载、切分与检索. TellWorld, https://www.tellworld.net/zh/practice/practice-langchain-knowledge-base. 更新于 2026-11-14