RAG检索增强

影响回答质量的因素:

  • 文档是否被正确加载
  • 文本是否被合理切块
  • Embedding 是否稳定
  • 检索是否召回了真正相关的片段
  • Prompt 是否把上下文用对了
方案 本质 优势 局限
直接问模型 不接外部知识,直接生成 上手最快 容易不知道私有 / 最新知识,幻觉较多
RAG 先检索资料,再生成 更新快、改动小、可追溯 依赖文档质量、分块策略、检索效果
微调 调整模型参数 能改变回答风格、任务习惯 成本高、更新慢,不适合高频改知识
RAG + 微调 外部知识 + 参数适配 兼顾知识与表达 成本和系统复杂度更高

 

缺点:

  • 响应时延更高:每次问答前都要多做一次检索,有时还会经过过滤、重排等步骤。
  • Token 消耗更高:检索结果会进入 Prompt,召回内容越多,送给模型的上下文越长。
  • 效果依赖链路质量:文档质量、切块策略、Embedding 质量、检索效果、Prompt 约束方式,都会影响最终回答。

管道式RAG 

  先检索,再生成。适合:企业知识库问答、文档问答、规范 / 手册 / FAQ 检索增强

 Agent式RAG

   模型自己决定要不要检索、什么时候检索、检索几次

LangChain组件

组件 作用 常见类 / 说明
Document LangChain 中统一的文档对象 由 page_content + metadata 组成
文档加载器 从 TXT、PDF、Word、Markdown、JSON、CSV 等读入文档 TextLoaderPyPDFLoaderUnstructuredWordDocumentLoader 等
文本分割器 把长文档切成较小片段 RecursiveCharacterTextSplitter 最常见
嵌入模型 把文本片段转成向量 本项目常见 DashScopeEmbeddings
向量数据库 存储向量并支持相似检索 Redis / RedisStack、Chroma、FAISS 等
检索器 用户提问时从向量库召回相关片段 常见由 vector_store.as_retriever() 得到
提示词模板 把检索结果和用户问题组织成 Prompt PromptTemplateChatPromptTemplate
聊天模型 基于上下文生成最终答案 ChatOpenAIinit_chat_model() 等

两种常见入库方式

方法 适合场景 手里通常有什么数据 常见理解
from_documents 已经完成“加载 + 分割”之后的一步入库 Document 列表 更像“把文档片段整批建库”
add_texts 已经有向量库实例,需要持续追加内容 字符串列表 + 可选 metadata 更像“往现有索引里追加文本”
  • 文档流驱动Loader -> Splitter -> List[Document] -> from_documents(...)(典型 RAG 建索引)
  • 纯文本流驱动texts + metadata -> add_texts(...)(接口落库、增量追加、脱离 Loader 的实验)

相似性检索

from langchain_redis import RedisConfig, RedisVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
import os
from dotenv import load_dotenv

load_dotenv()

# 1. 嵌入模型
embeddingsModel = DashScopeEmbeddings(
    model="text-embedding-v3",
    dashscope_api_key=os.getenv("aliQwen-api")
)
# 2. 连接已有索引
vector_store = redisVectorStore(
    embeddingsModel,
    config=RedisConfig(index_name="newsgroups",redis_url="redis://localhost:26379")
)
# 3. 查询文本 → 向量化 → 在库中做相似度检索;这里取前 3 条结果
query = "我喜欢什么手机"
results = vector_store.similarity_search_with_score(query, k = 3)
print("=== 查询结果 ===")
for i,(doc,score) in enumerate(results, 1):
    # 这里把“距离”近似换算成“相似度”只是为了展示更直观;工程里请以具体返回定义为准
    similarity = 1 - score
    print(f"结果 {i}:")
    print(f"内容: {doc.page_content}")
    print(f"元数据: {doc.metadata}")
    print(f"相似度: {similarity:.4f}")
    

文档加载器

  • load():一次性加载全部文档
  • lazy_load():按需流式加载,适合大文件或大批量数据

为什么加载后要统一成 Document

Document 是 LangChain 在 RAG 里的基础数据结构,它通常有两个核心字段:

  • page_content:正文内容
  • metadata:来源、页码、文件名、分类等附加信息

如何选择加载器

  • TXT 用 TextLoader
  • PDF 用 PyPDFLoader
  • Word 用 UnstructuredWordDocumentLoader 或 Docx2txtLoader
  • Markdown 用 UnstructuredMarkdownLoader
  • JSON 用 JSONLoader
  • CSV 用 CSVLoader

 

加载各种文件

Text

# TEXT
from langchain_community.document_loaders import TextLoader

file_path = "assets/sample.txt"
encoding = "utf-8"

# load() 为 BaseLoader 统一接口,返回 List[Document]'
docs = TextLoader(file_path,encoding).load()
print(docs)
#[Document(metadata={'source': 'assets/sample.txt'}, page_content='LangChain 是一个用于构建基于大语言模型(LLM)应用的开发框架,旨在帮助开发者更高效地集成、管理和增强大语言模型的能力,....')]

PDF

# pip install langchain_community pypdf
from langchain_community.document_loaders import PyPDFLoader

docs = PyPDFLoader(
    file_path="assets/sample.pdf",
    extraction_mode="plain",  # plain 纯文本;layout 按版面
).load()

print(docs)

word

# pip install langchain_community unstructured[docx] python-docx
from langchain_community.document_loaders import UnstructuredWordDocumentLoader

docs = UnstructuredWordDocumentLoader(
    file_path="assets/alibaba-more.docx",
    mode="single",  # single 整篇一个 Document;elements 按元素切分
).load()

print(docs)

markDown

# pip install langchain_community unstructured[md]
from langchain_community.document_loaders import UnstructuredMarkdownLoader

docs = UnstructuredMarkdownLoader(
    file_path="assets/sample.md",
    mode="elements",  # single 整篇;elements 按元素切分
).load()

print(docs)

JSON

# pip install jq langchain_community
from langchain_community.document_loaders import JSONLoader

docs = JSONLoader(
    file_path="assets/sample.json",
    jq_schema=".",  # 提取所有字段
    text_content=False,  # 是否按字符串处理内容
).load()

print(docs)

cvs

# pip install langchain_community
from langchain_community.document_loaders.csv_loader import CSVLoader

# 方式一:不指定列 → 整行(所有列)拼成一条字符串作为 page_content,metadata 通常只有 source 等
docs_all = CSVLoader(file_path="assets/sample.csv").load()
print("=== 方式一:整行作为 page_content ===")
print(
    "page_content 示例:",
    (
        docs_all[0].page_content[:80] + "..."
        if len(docs_all[0].page_content) > 80
        else docs_all[0].page_content
    ),
)
print("metadata 示例:", docs_all[0].metadata, "\n")

# 方式二:指定 content_columns 与 metadata_columns → 正文只取 content 列,title/author 进 metadata,便于检索时按作者/标题过滤
docs_split = CSVLoader(
    file_path="assets/sample.csv",
    metadata_columns=["title", "author"],
    content_columns=["content"],
).load()
print("=== 方式二:content 列作为正文,title/author 进 metadata ===")
print("page_content 示例:", docs_split[0].page_content)
print("metadata 示例:", docs_split[0].metadata)

文本分割器

文档加载之后,通常还不能直接拿去建 RAG。原因很简单:原始文档经常太长

会带来两个现实问题:

  • 检索效果差:整篇文档太大,向量表达会过于粗糙,难以精确定位到真正相关的小段内容。
  • 生成成本高:就算检索回来整篇文档,也很可能塞不进模型上下文,或者把大量无关内容一并送给模型。

所以,RAG 中几乎都会有“切块(chunking)”这一步。

LangChain 官方也明确建议:面对通用文本时,RecursiveCharacterTextSplitter 往往是最推荐的入门分割器。它会尽量优先保留较大的语义单位,例如段落、句子;如果某一段还太长,再继续往更小层级切。

为什么一定要切块

可以把切块理解成“把大文档拆成多个更容易被检索的小段”。这样做有几个直接好处:

  • 更容易召回真正相关的片段
  • 更容易控制每次送给模型的上下文长度
  • 更容易降低无关内容干扰
  • 更适合做来源标注和精确引用

在真实项目里,切块策略会直接影响 RAG 的最终效果。很多时候不是模型不行,而是:

  • 块切得太大,相关信息被淹没
  • 块切得太小,语义被切碎
  • 没有重叠,导致一句话被拦腰截断

所以,分割策略本身就是 RAG 质量的重要一环。

常见分割器与场景

分割器 作用
RecursiveCharacterTextSplitter 通用首选,优先保持较大语义单位,必要时递归切得更细
CharacterTextSplitter 按指定分隔符切,简单直接
MarkdownHeaderTextSplitter  按 Markdown 标题层级切分
HTMLHeaderTextSplitter 按 HTML 标题结构切分
TokenTextSplitter 按 token 数控制块大小,更贴近模型上下文限制
语义切分(Semantic Chunking) 按语义变化切分,尽量让相关内容保留在同一块中,但成本更高、实现更复杂
代码类分割器 按函数、类、逻辑块切分代码文本

 

RecursiveCharacterTextSplitter参数

参数 含义 实践理解
chunk_size 单块最大长度  块太大不利于检索,块太小又容易语义碎片化
chunk_overlap 相邻块重叠长度 防止句子、语义被截断,常见取块大小的 10%~20%
length_function 长度计算方式 默认常用 len,即按字符数;也可按 token 数
separators 优先切分分隔符 决定先按段落、换行、句号还是更细粒度去切

 

image

 

纯文本分割

#     RecursiveCharacterTextSplitter 按字符递归切,尽量保持语义完整,是通用文本场景里最常见的入门分割器。
#    chunk_size:单块最大长度(按 length_function 计算,默认 len 即字符数);chunk_overlap:相邻块重叠字符数,常用 size 的 10%~20%。
#    split_text(content):把字符串切成字符串列表;create_documents(texts):把字符串列表转成 Document 列表(或直接用 split_documents 处理 Document)。
#    重叠部分会重复出现在相邻块中,总字符数会大于原文,这不是 bug,而是为了减少「半句话被截断」的问题
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1. 待分割的原文
content = (
    "大模型RAG(检索增强生成)是一种结合生成模型与外部知识检索的技术,通过从大规模文档或数据库中检索相关信息,"
    "辅助生成模型以提升回答的准确性和相关性。其核心流程包括用户输入查询、系统检索相关知识、"
    "生成模型基于检索结果生成内容,并输出最终答案。RAG的优势在于能够弥补生成模型的知识盲区,"
    "提供更准确、实时和可解释的输出,广泛应用于问答系统、内容生成、客服、教育和企业领域。"
    "然而,其也面临依赖高质量知识库、可能的响应延迟、较高的维护成本以及数据隐私等挑战。"
)
# 2. 分割器:块大小 100 字符,重叠 30 字符,长度按 len(字符数)计算
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=30,
    length_function=len
)
# 3. 先切成字符串列表 .split_text()
splitter_texts = text_splitter.split_text(content)

# 4. 再转成 Document 列表(便于后续与向量库、检索器对接) ..create_documents()
splitter_documents = text_spliter.create_documents(splitter_texts)

print(f"原始文本大小:{len(content)}")
print(f"分割文档数量:{len(splitter_documents)}")

for splitter_document in splitter_documents:
    print(
        f"文档片段大小:{len(splitter_document.page_content)},文档内容:{splitter_document.page_content}"
    )

 

分割document对象(先加载在分割)

# pip install langchain-unstructured(部分环境加载本地文件还需 python-magic-bin)
from langchain_text_spltters import RecursiveCharacterTextSplitter
from langchain_unstructured import UnstructuredLoader

# 1. 加载文档得到 Document 列表
loader = UnstructuredLoader("rag.txt")
documents = loader.load()

# 2. 同一套分割参数:块 100 字符,重叠 30
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size="100",
    chunk_overlap="30",
    length_function=len
)
# 3. 直接对 Document 列表分割,返回更小的 Document 列表(不能改用 split_text:入参是 Document 列表,且需保留 metadata)
splitter_documents = text_splitter.split_documents(documents)

print(f"分割文档数量:{len(splitter_documents)}")
for splitter_document in splitter_documents:
    print(f"文档片段:{splitter_document.page_content}")
    print(
        f"文档片段大小:{len(splitter_document.page_content)}, 文档元数据:{splitter_document.metadata}"
    )

 

  • split_text():字符串 -> 字符串列表
  • create_documents():字符串列表 -> Document 列表
  • split_documents()Document 列表 -> 更小的 Document 列表

其中在真实 RAG 项目里,最常见的往往是最后一种,也就是:

loader.load() -> split_documents() -> embeddings -> vector store

案例

from langchain_core.prompt import PromptTemplate
from langchain.chat_model import init_chat_model
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import Docx2txtLoader
from langchain_core.runnables import RunnablePassthrough
from langchain_community.vectorstores import Redis
from langchain_classic.text_splitter import CharacterTextSplitter
from langchain_community.embeddings import DashScopeEmbeddings

load_dotenv()

# 大模型
llm = init_chat_model(
    model="qwen-plus",
    model_provider="openai",
    api_key=os.getenv("aliQwen-api"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

# 提示词模板:{context} 由检索器填充,{question} 由用户输入填充;最终会生成一段字符串 Prompt 再交给聊天模型
prompt_template = """
    请使用以下提供的文本内容来回答问题。仅使用提供的文本信息,
    如果文本中没有相关信息,请回答"抱歉,提供的文本中没有这个信息"。

    文本内容:
    {context}

    问题:{question}

    回答:
    "
"""
prompt = PromptTemplate(
    template= prompt_template,
    input_variables= ["context", "question"]
)

# 嵌入模型:用于文档与查询的向量化
embeddings = DashScopeEmbeddings(
    model="text-embedding-v3",
    dashscope_api_key= os.getenv("aliQwen-api")
)

# 加载docx
loader = Docx2txtLoader("alibabba-java.docx")
documents = loader.load()

# 2. 分割(此处用 CharacterTextSplitter 便于快速跑通;真实项目里更常见的通用首选是 RecursiveCharacterTextSplitter)
text_splitter = CharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=0,
    length_function=len
)
texts = text_splitter.split_document(documents)
print(f"文档个数:{len(texts)}")

# 3. 向量化并写入 Redis,建立索引(必须用分割后的 texts,否则整篇文档作为一块)
vector_store = Redis.from_documents(
    documents = texts,
    embedding = embeddings,
    redis_url = "redis://localhost:26379",
    index_name = "my_index3"
)

# 4. 检索器:按相似度取前 k 条作为 context
retriever = vector_store.as_retriever(search_kwargs={"k": 2})

# 5. LCEL 链:输入 question → context 由 retriever 查得,question 直通 → 拼 prompt → 调 llm
rag_chain = {"context": retriever,"question": RunnablePassthrough()} | prompt | llm

# 6. 提问并打印答案
question = "langchain学习路线是什么"
result = rag_chain.invoke(question)
print("\n=== 有外挂知识库(RAG:从 alibaba-java.docx 检索)===")
print("回答:", result.content)

# 7. 对比演示:同一问题但「无外挂知识库」(context 为空,不查向量库,模拟未挂载文档)
no_rag_chain = (
    {
        "context": lambda _: "(未提供相关文档,模拟无外挂知识库)",
        "question": RunnablePassthrough(),
    },
    | prompt | llm
)

result_no_rag = no_rag_chain_invoke(question)
print("回答:", result_no_rag.content)

实际流程:

  1. 用 Docx2txtLoader 加载 alibaba-java.docx
  2. 用 CharacterTextSplitter 把文档切成块
  3. 用 DashScopeEmbeddings 把片段向量化
  4. 用 Redis 向量库 存储这些片段
  5. 用 as_retriever() 生成检索器
  6. 用 PromptTemplate 组织 context + question
  7. 用 聊天模型 基于检索结果生成最终答案

值得注意:

  1. 综合案例用的是 CharacterTextSplitter
    这能帮助你快速跑通流程;但在通用文本场景里,实际项目中更常见的首选仍然是 RecursiveCharacterTextSplitter

  2. Prompt 里明确写了“如果文本中没有相关信息,请直接说明”
    这是 RAG 里很重要的一个工程习惯。因为检索不是百分百准确,Prompt 应该引导模型“基于上下文回答”,而不是脱离上下文自行发挥。

  3. 脚本做了“有 RAG / 无 RAG”的对比演示
    这一点非常适合教学,也非常适合项目早期验证价值。只有做对比,你才更容易判断:问题究竟出在模型本身,还是出在检索链路。

  4. Redis 只是这个案例里的向量存储后端
    RAG 的本质不是绑定 Redis,而是“检索增强生成”这条流程。以后你换成 Chroma、FAISS、Milvus、PgVector,整体思路并不会变。

 

posted @ 2026-05-08 09:48  幻影之舞  阅读(33)  评论(0)    收藏  举报