Deep Read

LangChain - RAG

让 LLM 能"读"外部文档再回答——用户的私有数据大多不在大模型的训练集里,而是散落在外部文档、数据库里,LangChain 的数据连接组件负责把这些数据接进来:文档加载器、文档切分、文本嵌入、向量存储、检索器。

核心流程

  1. Document Loader:加载 PDF / 网页 / 数据库等
  2. Text Splitter:把长文本切成适当大小的 chunks
  3. Embedding:把 chunks 向量化
  4. Vector Store:存入向量数据库(Chroma、FAISS、Pinecone 等)
  5. Retriever:根据用户 query 检索最相关的 k 个 chunks
  6. 注入 Prompt:检索结果拼进 prompt → LLM 基于上下文回答
flowchart TD
    subgraph offline["离线:建立知识库"]
        D1["1、获取文档<br/>Docx2txtLoader"] --> D2["2、文档切分<br/>chunk_size=500, overlap=50"] --> D3["3、向量化入库<br/>DashScope Embedding → Chroma"]
    end

    subgraph online["在线:检索问答"]
        D4["4、检索<br/>retriever 按相似度取回文档块"] --> D5["5、拼接 Prompt<br/>{context} + {question}"] --> D6["6、LLM 生成<br/>基于上下文回答"]
    end

    D3 --> D4

一个完整例子:读取本地 Word 文档

对应 LangChain_RAG/RAG/main_01.pyllmllm_embedding 是提前配置好的模型客户端:

# 1. 获取文档:基于脚本所在目录拼绝对路径,避免运行目录不同导致找不到文件
docx_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "人事管理流程.docx")
documents = Docx2txtLoader(docx_path).load()

# 2. 文档切分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
split_doc = text_splitter.split_documents(documents)

# 3. 向量化 + 入库
vector_store = Chroma.from_documents(documents=split_doc, embedding=llm_embedding)

# 4. 检索器
retriever = vector_store.as_retriever()

message = """
 请使用提供的上下文回答下面的问题:
 {question}
 上下文:
 {context}
"""
template = ChatPromptTemplate.from_messages([("human", message)])

# 5+6. 拼接 Prompt -> LLM 生成
# question、context 并行接收同一个输入字符串:question 直传,context 由 retriever 检索得到
chain = {"question": RunnablePassthrough(), "context": retriever} | template | llm

resp = chain.invoke("晋升")
print(resp.content)

Chroma.from_documents 没传 persist_directory,向量库只存在内存里,每次跑脚本都要重新算一遍 embedding。文档不常变的话建议加上这个参数做持久化。

文档与文档加载

LangChain 支持 CSV、目录、HTML、JSON、Markdown、PDF 等格式,加载器的 load 方法把数据源转换成一个或多个 Document 对象(含文本及元数据)。之后不管是切分、嵌入还是检索,流转的都是 Document 对象或者它们的向量。

常用文本分割器:

  • CharacterTextSplitter:基于字符(默认 \n\n)切割,chunk_size 设大小、chunk_overlap 设重叠。
  • RecursiveCharacterTextSplitter:支持自然语言及多种编程语言(JavaScript、cpp、go、java、php、python 等)代码切割。
  • MarkdownHeaderTextSplitter:根据指定标题切割 Markdown 文档。
from langchain_core.documents import Document

documents = [
    Document(page_content="猫是柔软可爱的动物,但相对独立", metadata={"source": "常见动物宠物文档"}),
    Document(page_content="狗是人类很早开始的动物伴侣,具有团队能力", metadata={"source": "常见动物宠物文档"}),
    Document(page_content="金鱼是我们常常喂养的观赏动物之一,活泼灵动", metadata={"source": "鱼类宠物文档"}),
    Document(page_content="鹦鹉是猛禽,但能够模仿人类的语言", metadata={"source": "飞禽宠物文档"}),
    Document(page_content="兔子是小朋友比较喜欢的宠物,但是比较难喂养", metadata={"source": "常见动物宠物文档"}),
]

文本嵌入模型与向量数据库

Embeddings 类给各种嵌入模型(OpenAI、Cohere、HuggingFace、DashScope 等)提供统一接口,把文档和查询都转成向量,再靠向量空间距离找最相似的文本。它和 LLM 包装器、聊天模型包装器并称三大模型包装器

import os
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_chroma import Chroma
from langchain_core.runnables import RunnableLambda
from models import ALI_TONGYI_EMBEDDING_MODEL, ALI_TONGYI_API_KEY_OS_VAR_NAME

llm_embeddings = DashScopeEmbeddings(
    model=ALI_TONGYI_EMBEDDING_MODEL,
    dashscope_api_key=os.getenv(ALI_TONGYI_API_KEY_OS_VAR_NAME)
)

# 实例化向量空间
vector_store = Chroma.from_documents(documents=documents, embedding=llm_embeddings)

print(vector_store.similarity_search("狸花猫"))
# 按分数排序,分数越小越相似
print(vector_store.similarity_search_with_score("狸花猫"))

# 检索器:bind(k=1) 返回相似度最高的第一个
docs_find = RunnableLambda(vector_store.similarity_search).bind(k=1)
print(docs_find.batch(["狸花猫", "海豚"]))

检索器 (Retriever)

直接操作 Chroma、FAISS、Pinecone 这些向量库要分别了解它们的查询语法、连接方式。LangChain 用 VectorStoreRetriever 包了一层统一接口——向量库实例调用 as_retriever() 就拿到检索器,换底层向量库也不用改查询代码。

search_type 决定怎么从向量库里挑结果:

  • similarity(默认):直接按相似度取 top-k
  • mmr:Maximal Marginal Relevance,在相关性和结果多样性之间做权衡,避免几个结果都在说同一件事
  • similarity_score_threshold:只保留相似度分数超过阈值的结果,数量不固定
retriever = vector_store.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 2}
)

retriever.invoke("猫科动物")

# 换成阈值过滤
retriever = vector_store.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.5}
)