LangChain 第9章 RAG 与项目实战:文档切分、嵌入、Milvus 与客服知识库

**摘要:**本文系统梳理 LangChain 第 9 章 RAG 与项目实战的核心内容,涵盖文档加载、切分、嵌入、Milvus 向量检索、rerank 重排、混合检索与多查询扩展,并给出客服知识库毕业项目的端到端实现骨架与常见坑位调优方案。

目录:

LangChain 第9章 RAG 与项目实战:文档切分、嵌入、Milvus 与客服知识库

一句话总结:RAG = 检索 + 增强 + 生成,把文档加载、切分、嵌入后存入 Milvus,查询时检索相关片段喂给模型;最后 4 集的客服知识库是能写进简历的毕业项目。

📎 参考资料一:李沐深度学习191集课程全解析(模块拆解、学习路径)

李沐深度学习191集课程全解析:模块拆解、学习路径-CSDN博客

📎 参考资料二:吴恩达《面向开发者的提示词工程》

吴恩达《面向开发者的提示词工程》-CSDN博客

📎 参考资料三:吴恩达 MCP 教程(Model Context Protocol)

吴恩达 MCP 教程(Model Context Protocol)(一)-CSDN博客

📎 参考资料四:多模态大模型教程学习笔记(ViT · CLIP · SAM · GLIP · Stable Diffusion)

多模态大模型教程学习笔记 --- ViT · CLIP · SAM · GLIP · Stable Diffusion

说明:本章本地仅保留 111--113 集视频目录、无字幕文件(102--110、114--120 未导出),以下内容据课程集目录与 LangChain 1.x 官方体系整理,具体代码以课件/视频为准。

本章可视化图谱

📎 知识图谱在线链接(点击打开 / 一键复制 / 分享他人):

https://mcnj5s80gkfh.doubaoapps.com/app/app_17fqgvd5bmx

一、本章定位

第 102--120 集是课程的落地篇。RAG(Retrieval-Augmented Generation,检索增强生成)让模型"开卷考试":答案不依赖参数记忆,而来自检索回来的资料,从而解决知识更新、私有数据问答并显著降低幻觉。学完本章应能独立交付一个客服知识库智能体。

二、RAG 全流程

索引侧(离线)

  1. 多格式语料:PDF、Word、Markdown、网页、FAQ、历史工单等。

  2. Loader 文档加载器:把不同格式读成统一文本(102--110 集区间讲各类加载器)。

  3. TextSplitter 切分(111):按 chunk_size 与 overlap 把长文档切成块。

  4. Embedding 嵌入(112):初始化嵌入模型,把每个文档块转成向量。

  5. 入向量库(113):用 docker 部署 Milvus,建立集合并写入向量。

查询侧(在线)

  1. 把用户提问 embed 成向量;

  2. 在向量库做相似度检索,取 TopK 最相关文档块(可选 rerank 重排);

  3. 把检索到的资料作为上下文拼进提示词;

  4. 模型基于资料生成答案,可附出处,减少幻觉。

三、三个关键组件(111--113)

  • TextSplitter(111):递归字符、字符、token、语义等多种切分器;chunk 太小丢上下文、太大稀释相关性,需配合 overlap 调参,文件解析质量直接决定上限。

  • 嵌入模型(112):向量是文本的语义坐标,语义相近的文本向量距离也近;embed_documents 批量向量化文档,embed_query 向量化单条查询。

  • 向量数据库(113):课程主线用 docker 部署 Milvus(collection、相似度索引);轻量可选 Chroma / FAISS,复用 PostgreSQL 生态可选 pgvector。

四、客服知识库毕业项目(117--120)

  1. 语料准备:整理产品手册、常见问题、历史客服工单。

  2. 切分入库:加载 → 切分 → 嵌入 → 写入 Milvus。

  3. 检索链:问题向量化、TopK 检索,必要时加 rerank 提升精度。

  4. 结合 Agent:把检索封装成 Agent 的一个工具,还可串联查询订单、退换货等外部能力。

  5. 评估上线:检验检索命中率与回答质量,形成可写进简历的端到端项目。

五、难点与调优

  • 检索质量是天花板:关注文件解析、切分粒度、检索算法、混合检索(关键词 + 向量)与 rerank(高精度场景用、高实时场景慎用)。

  • RAG 与 Agent 的关系:RAG 可独立成问答链,也可作为 Agent 的长期记忆或一个检索工具;缺语料补 RAG,需要对接外部系统则用工具调用。

  • 幻觉抑制:要求模型基于检索资料作答、给不出就明说,并保留来源引用。

六、知识点速查

主题 要点
RAG 三段 Retrieval 检索 + Augmented 增强 + Generation 生成
索引侧 Loader → TextSplitter → Embedding → 向量库
查询侧 问题向量化 → TopK 检索(+rerank) → 拼 Prompt → 生成
切分 chunk_size + overlap,多种 Splitter(111)
嵌入 embed_documents / embed_query,向量即语义坐标(112)
向量库 Milvus(docker,113)/ Chroma / FAISS / pgvector
毕业项目 117--120 客服知识库,端到端可写进简历

深度拓展:本章完整知识体系与实战进阶

深度拓展一:核心原理与源码解读

RAG(Retrieval-Augmented Generation,检索增强生成)要解决的核心问题是:LLM 的知识冻结在训练截止日,而且它不知道你公司的私有文档 。你不能为了让模型回答"我们公司的退款政策是什么"就去微调一次模型------成本太高、更新太慢。RAG 的思路是:把你的文档提前切成小块、算好向量存进向量库;用户提问时,先从向量库里捞出和问题最相关的几块,拼进 prompt,再让 LLM 基于这些材料回答。这样模型不需要"记住"所有知识,它只需要"看着材料答题"。

一条完整的 RAG 链路长这样,记住这 7 步,后面所有优化都是在改其中某一步:

  1. 加载(Load):用 Loader 把 PDF、Word、HTML、CSV 读成 Document 对象(每页/每文件一个 Document,带 metadata)。

  2. 切分(Split / Chunk):把长 Document 切成几百 token 一段的小 chunk,控制每段语义完整。

  3. 嵌入(Embedding):用 embedding 模型把每个 chunk 文本算成一个向量(比如 1024 维浮点数组)。

  4. 入库(Store):把 (chunk_text, vector, metadata) 三元组写进向量库(Milvus、Chroma、PGVector)。

  5. 检索(Retrieve):用户提问时,把问题也算成向量,在向量库里找最相似的 top-k 个 chunk。

  6. 拼 Prompt(Augment):把捞回来的 chunk 拼进一个"请基于以下资料回答问题"的模板里。

  7. 生成(Generate):把拼好的 prompt 送给 LLM,产出最终答案。

这 7 步可以用一张图概括:

阶段 输入 输出 LangChain 对应组件
加载 .pdf / .html / .csv 文件 ListDocument PyPDFLoader、WebBaseLoader、CSVLoader
切分 ListDocument Listchunk Document RecursiveCharacterTextSplitter
嵌入 chunk 文本 Listfloat 向量 OpenAIEmbeddings、HuggingFaceBgeEmbeddings
入库 chunk + 向量 + metadata 向量库 collection Milvus.from_documents()
检索 用户问题文本 top-k 相关 chunk vector_store.as_retriever()
拼 Prompt 问题 + chunks 完整 prompt ChatPromptTemplate
生成 prompt 答案文本 ChatOpenAI.invoke()

从源码角度看,LangChain 的 RAG 链路其实就是一串 LCEL(LangChain Expression Language)管道:

python 复制代码
# 最简 RAG 管道(LCEL 风格,理解原理用)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "你是客服助手,只能根据下面的资料回答。如果资料里没有,就说不知道。\n\n资料:{context}"),
("human", "{question}"),
])
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
调用 rag_chain.invoke("退款政策是什么") 时:
1. retriever 先把问题算成向量、去 Milvus 捞 top-k
2. format_docs 把捞回的 Document 拼成一段文本
3. prompt 模板把 context 和 question 填进去
4. llm 生成答案

理解这条管道后,你就知道"优化 RAG"到底在优化哪里:切得不好 → 改 Splitter;检索不准 → 改 Embedding 或加 rerank;生成胡编 → 改 prompt 或换更强的模型。每个 bad case 都能定位到具体环节。

深度拓展二:代码实战------从最小可用到生产级

示例 1:文档加载与切分

python 复制代码
from langchain_community.document_loaders import PyPDFLoader, TextLoader, CSVLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
1. 各种 Loader
pdf_docs = PyPDFLoader("product_manual.pdf").load()  # 一页一个 Document
txt_docs = TextLoader("faq.txt", encoding="utf-8").load()
csv_docs = CSVLoader("orders.csv", metadata_columns=["order_id"]).load()
web_docs = WebBaseLoader("https://example.com/help").load()
2. 切分器:最常用的递归字符切分器
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,  # 每块大约 500 个字符(中文约 250~300 字)
chunk_overlap=50,  # 相邻块重叠 50 字符,防止把一句话切断
separators=["\n\n", "\n", "。", ";", ",", " ", ""],  # 优先在段落/句号处切
)
chunks = splitter.split_documents(pdf_docs + txt_docs)
print("切完后 chunk 数量:", len(chunks))
print("第一个 chunk 内容:", chunks[0].page_content[:200])
print("metadata:", chunks[0].metadata)  # 会带 source、page 等信息

chunk_size 不是越大越好:太大 → 一块里塞太多主题,检索时不精准;太小 → 语义被切断,模型看不懂。一般中文文档 300~500 字一块,overlap 取 chunk_size 的 10%~15%。

示例 2:嵌入 + 写入 Milvus

python 复制代码
from langchain_openai import OpenAIEmbeddings
from langchain_milvus import Milvus
1. 初始化嵌入模型(OpenAI 版)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
2. 本地起一个 Milvus(Docker 版),或者用 Zilliz Cloud
uri 可以是本地文件("./milvus_demo.db")或远程地址 "http://localhost:19530"
vector_store = Milvus.from_documents(
documents=chunks,
embedding=embeddings,
collection_name="customer_service",
connection_args={"uri": "./milvus_demo.db"},
index_params={"index_type": "AUTOINDEX", "metric_type": "COSINE"},
)
print("写入完成,collection 里文档数:", vector_store.col.num_entities)

如果你想用本地开源 embedding 模型(比如 BGE,不需要 OpenAI key):

python 复制代码
from langchain_huggingface import HuggingFaceEmbeddings
本地加载 bge-small-zh-v1.5,适合中文
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True},  # 归一化后用 COSINE 更稳
)

示例 3:相似度检索

python 复制代码
# 直接相似度搜索:捞最相似的 top-k
query = "退款需要几天到账?"
results = vector_store.similarity_search(query, k=4)
for i, doc in enumerate(results):
    print(f"--- 第 {i+1} 条 ---")
    print(doc.page_content[:150])
    print("来源:", doc.metadata.get("source"))
带分数的搜索:返回 (doc, score) 元组
scored = vector_store.similarity_search_with_score(query, k=4)
for doc, score in scored:
print(f"score={score:.4f} 内容={doc.page_content[:80]}")

示例 4:用 retriever 作为工具接入 create_agent

这是本章最重要的实战:把向量检索包装成一个 tool,让 Agent 自己决定什么时候去查知识库。

python 复制代码
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
把上面建好的 vector_store 变成一个 retriever
retriever = vector_store.as_retriever(search_kwargs={"k": 4})
@tool
def search_knowledge_base(query: str) -> str:
"""在客服知识库里搜索相关资料。用户问到退款、物流、账户、发票等问题时调用。
Args:
query: 要搜索的问题关键词
"""
docs = retriever.invoke(query)
if not docs:
return "知识库里没有找到相关内容。"
# 把捞回的文档拼成一段文本返回给模型
return "\n\n---\n\n".join(
f"[来源:{d.metadata.get('source')}]\n{d.page_content}" for d in docs
)
组装 Agent
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_agent(
model=model,
tools=[search_knowledge_base],
checkpointer=InMemorySaver(),
system_prompt="你是一个专业的客服助手。回答用户问题前,先用 search_knowledge_base 查知识库,"
"如果知识库里有答案就基于它回答;如果查不到,就老实说'这个问题我需要转人工'。",
)
跑一轮
result = agent.invoke(
{"messages": [HumanMessage(content="我昨天申请的退款,什么时候能到账?")]},
config={"configurable": {"thread_id": "cust-001"}},
)
print(result["messages"][-1].content)

这段代码跑起来你会看到:模型先自己决定调用 search_knowledge_base("退款到账时间"),拿到知识库内容后,再组织成自然语言回复。这就是"RAG + Agent"的标准姿势------检索不是写死在管道里的,而是模型按需调用的工具。

示例 5:检索优化------MMR 和相似度阈值

python 复制代码
# MMR(最大边际相关性):既相关、又尽量不重复,适合结果去重
retriever_mmr = vector_store.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 4, "fetch_k": 20, "lambda_mult": 0.5},
)
带相似度阈值:分数低于阈值的直接不要,避免硬凑
retriever_thresh = vector_store.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.75},
)

示例 6:客服知识库端到端项目骨架

python 复制代码
# ===== ingest.py:文档入库脚本(离线跑一次)=====
from langchain_community.document_loaders import DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_milvus import Milvus
def ingest_docs(doc_dir: str, collection_name: str):
# 1. 加载目录下所有 .md / .pdf
loader = DirectoryLoader(doc_dir, glob="**/*.{md,pdf}", show_progress=True)
docs = loader.load()
# 2. 切分
chunks = RecursiveCharacterTextSplitter(
    chunk_size=400,
    chunk_overlap=60,
    separators=["\n## ", "\n### ", "\n\n", "\n", "。", " "],
).split_documents(docs)
3. 嵌入 + 写 Milvus
emb = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
Milvus.from_documents(
chunks,
emb,
collection_name=collection_name,
connection_args={"uri": "http://localhost:19530"},
drop_old=False,  # True = 重建 collection,跑全量入库时用
)
print(f"入库完成:{len(chunks)} 个 chunk -> {collection_name}")
===== serve.py:在线查询 Agent =====
就是示例 4 那段 create_agent,把 retriever 指向同一个 collection
区别只是:vector_store 这次用 Milvus(collection_name=..., connection_args=...) 连已有库,
而不是 from_documents 新建。

深度拓展三:常见坑与调试技巧

下面是 10 个 RAG 项目里最常见的 bad case。

**坑 1:检索结果和问题完全不相关。**现象:问"退款政策",捞回来的全是"物流时效"。原因:embedding 模型和你的领域不匹配(比如用英文 embedding 模型搜中文),或者 chunk 切得太大一块里混了好几个主题。解决:换中文 embedding 模型(bge-small-zh),缩小 chunk_size,检查切分点。

**坑 2:chunk 把一句话切两半。**现象:检索回来的 chunk 开头是"...天内可以",结尾是"在 7",拼起来才是"7 天内可以"。原因:chunk_overlap 太小,或者 separators 没配对中文标点。解决:加大 overlap 到 15%~20%,separators 里加上中文句号、分号。

**坑 3:嵌入维度不匹配。**现象:新建 collection 用了 1024 维的 embedding,后来换了个 768 维的模型,插入时报错。原因:Milvus collection 的向量维度在建表时就定死了,不能改。解决:换模型就要新建 collection,或者用支持动态字段的版本。

**坑 4:检索到了正确文档,但模型还是答非所问。**现象:知识库明明写了"7 天无理由退款",模型回答"15 天"。原因:prompt 里没有强约束"只能基于资料回答",或者模型本身的先验知识压过了检索内容。解决:system prompt 里明确写"只根据下面的资料回答,资料里没有就说不知道",并把资料放在 prompt 靠前位置。

**坑 5:top-k 设太大,context 塞爆 token。**现象:k=10,每个 chunk 500 字,一次塞 5000 字进 prompt,很快超 context window。原因:贪多。解决:k 一般取 3~5,配合 rerank 把最相关的留下。

**坑 6:PDF 表格切分成乱码。**现象:PDF 里的价格表被按行切碎,每行单独成一个 chunk,语义完全碎了。原因:普通字符切分器不认识表格结构。解决:用专门的 PDF 表格解析器(比如 PyMuPDF 的表格抽取、Unstructured),或者把表格转成 Markdown 表格再切。

**坑 7:metadata 没带 source,无法溯源。**现象:模型回答了一个数字,你想核对是哪份文档说的,对不上。原因:入库时没把文件名、页码写进 metadata。解决:Loader 默认会带 source,切分时 metadata 会自动继承,不要 split_documents 之后手动覆盖 metadata。

**坑 8:Milvus 连接串写错。**现象:本地跑没问题,上服务器连不上。原因:本地用的是 "./milvus_demo.db"(单机文件模式),服务器上要连 Milvus 服务 "http://host:19530"。解决:环境变量区分开发/生产连接串。

**坑 9:每次用户提问都全量重算 embedding。**现象:文档更新一次就要等 10 分钟入库。原因:每次都重新对所有 chunk 算 embedding,没有增量更新。解决:记录每个文档的版本号/mtime,只对新增或修改过的文档做嵌入,老 chunk 按 ID 增量插入。

**坑 10:多义词检索不准。**现象:问"苹果手机坏了怎么办",捞回来一堆"苹果种植"文档。原因:纯向量检索理解不了语境。解决:加一层 query 改写(用 LLM 把用户问题改写成更明确的检索词),或者加 rerank 模型(bge-reranker)对初筛结果重排。

深度拓展四:最佳实践与性能优化

  • chunk_size 按文档类型调:FAQ 问答对,一个问答一块;产品手册,按章节切;代码文档,按函数切。别用一个参数套所有文档。

  • embedding 模型选中文场景专用:中文优先 BGE 系列(bge-small-zh / bge-base-zh),英文用 OpenAI text-embedding-3。跨语种任务用 multilingual-e5。

  • 加 rerank 层:初检 top-20,用 bge-reranker 重排取 top-4。比单纯靠向量相似度准很多,成本只多一次小模型推理。

  • query 改写:用户口语化提问"我想退那个东西",先让 LLM 改写成"退款政策 退货流程",再拿去检索。

  • 混合检索(Hybrid Search):向量检索 + 关键词检索(BM25)两路并行,结果合并。专有名词、型号、错误码这种关键词,纯向量检索经常漏。

  • metadata 过滤:给 chunk 打上 category(退款/物流/账户)、doc_type(政策/FAQ)等标签,检索时按用户意图过滤,缩小范围。

  • 缓存 embedding 结果:同样的 chunk 文本不要重复算 embedding,把 (text_hash, vector) 存进一张小表。

  • 评估要自动化:准备 50~100 条标准问答对(question, expected_doc_ids),每次改 pipeline 都跑一遍命中率(recall@k)和答案忠实度(faithfulness),别靠人工感觉。

  • 不要把 RAG 和微调混为一谈:RAG 适合"知识频繁更新、需要溯源"的场景;微调适合"固定风格、固定格式"的任务。客服知识库这种天天更新政策的,用 RAG 就对了。

  • prompt 里强制引用来源:让模型回答时标注"根据《退款政策》第 3 条",既提升可信度,也方便用户核验。

深度拓展五:面试高频考点

Q1:什么是 RAG?它解决了 LLM 的什么问题?

A:RAG 是检索增强生成。它在 LLM 生成答案前,先从外部知识库检索相关文档拼进 prompt。解决三个问题:(1) LLM 知识截止,不知道新信息;(2) LLM 不知道你的私有数据;(3) LLM 会幻觉,给它材料可以约束它"看着材料答"。

Q2:RAG 的完整链路有哪些步骤?

A:加载 → 切分 → 嵌入 → 入库 → 检索 → 拼 prompt → 生成。离线做前四步(文档处理),在线做后三步(查询处理)。

Q3:chunk_size 和 chunk_overlap 怎么选?

A:chunk_size 控制每块语义完整性,太大不精准、太切碎语义。中文一般 300~500 字;overlap 取 chunk_size 的 10%~15%,防止把一句话切两半。要按文档类型调,FAQ 一块就是一个问答对,代码文档按函数切。

Q4:为什么要用 RecursiveCharacterTextSplitter?

A:它按优先级尝试分隔符(段落 > 换行 > 句号 > 逗号 > 空格),尽量在自然边界切分,而不是硬按字符数切。比纯按长度切保留更好的语义。

Q5:MMR 是什么?比纯相似度好在哪?

A:MMR(最大边际相关性)在"和问题相关"和"结果之间不重复"之间做权衡。纯相似度可能捞回来 4 条几乎一样的文档,MMR 会尽量给你覆盖面更广的 top-k。

Q6:embedding 模型为什么要归一化?

A:归一化后向量都是单位长度,用 COSINE 距离和用 IP(内积)等价,数值更稳定。Milvus 里 metric_type 选 COSINE 时,最好对 embedding 做 normalize。

Q7:怎么评估 RAG 系统好不好?

A:两个层面:(1) 检索层------召回率 recall@k(标准答案在不在 top-k 里)、命中率;(2) 生成层------忠实度 faithfulness(答案是不是真的来自检索文档,有没有瞎编)、答案相关性。可以用 RAGAS 等自动化评估框架。

Q8:rerank 模型和 embedding 模型有什么区别?

A:embedding 是双塔模型(query 和 doc 分别编码,算余弦相似度),快但粗;rerank 是交叉编码(query 和 doc 拼在一起送进模型直接打分),准但慢。工程上用 embedding 初筛 top-20,再用 rerank 精排取 top-4,平衡速度和精度。

Q9:RAG 和微调怎么选?

A:RAG 适合知识频繁更新、需要溯源、文档量大的场景(客服、法律、医疗);微调适合风格固定、格式固定、知识稳定的场景(写特定风格的文案、分类任务)。生产里经常两者结合:用 RAG 给知识,用微调教模型风格。

Q10:为什么要把 retriever 包成 tool 接进 Agent?

A:直接把 retriever 写死在管道里,不管用户问什么都检索一次,浪费 token 也可能检索出无关内容。包成 tool 后,模型自己判断"这个问题需要查知识库吗?查什么关键词?",多轮对话里也能在合适时机调用,更灵活。

Q11:文档更新了,向量库怎么同步?

A:按文档 ID 做增量:新文档直接 insert;修改过的文档先按 source/path 删旧 chunk,再 insert 新 chunk;删除的文档按 metadata.source 批量 delete。不要每次全量重建。

Q12:什么是混合检索?为什么需要它?

A:向量检索擅长语义相似,但对专有名词、型号、错误码这种"必须字面匹配"的词经常失灵;BM25 关键词检索正好相反。混合检索两路并行、结果融合,兼顾语义和关键词。

深度拓展六:课后练习与思考

练习 1:找一份你自己的 PDF(比如课程讲义),用 PyPDFLoader 加载,分别用 chunk_size=200、500、1000 切分,对比 chunk 数量和内容完整性。体会 chunk_size 对语义的影响。思路提示:重点看切分点是不是落在句子中间,overlap 调成不同值对比。

练习 2:用 Milvus 建一个 collection,写入 10 条 FAQ,然后用 similarity_search 搜一个问题,打印 top-3 的分数。故意写一个和所有 FAQ 都不相关的问题,看返回的分数是不是明显更低。思路提示:用 similarity_search_with_score 看分数分布,设定一个阈值过滤掉低分结果。

练习 3:把 retriever 包成 tool 接进 create_agent,写一个"法律条文助手":用户问"合同违约怎么赔",Agent 自动检索法律条文、引用原文回答。思路提示:参考示例 4,system prompt 里要求模型必须引用条文编号。

练习 4:给上面的 RAG 加 MMR 检索和相似度阈值两种模式,写个小脚本对比三种检索方式(纯相似度 / MMR / 阈值过滤)在 5 个测试问题上的结果差异。思路提示:用 as_retriever(search_type=...) 切换,把每次捞回的文档数和内容打印出来对比。

练习 5:思考一个问题------如果你的知识库有 10 万篇文档,用户每次提问都全库检索会很慢。你会怎么设计分层检索?思路提示:先按 category 粗筛到几十篇,再在小范围内向量检索;或者先用 embedding 建粗索引,再用 rerank 精排。

练习 6:写一个 ingest 脚本,支持"增量更新":记录每个文件的 mtime,只处理新增或修改过的文件,老文件跳过。思路提示:维护一张 (file_path, mtime, milvus_pk) 的映射表,更新时先按 pk 删旧 chunk 再插新的。

练习 7:尝试加一层 query 改写------用户输入很口语化时,先用一个小 LLM 把问题改写成更适合检索的关键词,再送去向量检索。对比改写前后的召回效果。思路提示:改写用 gpt-4o-mini 就够了,prompt 写"把下面用户问题改写成 3-5 个检索关键词"。

深度拓展补充:进阶案例与 FAQ

补充一:RAG 质量评估体系

做 RAG 系统最怕"看起来能用,实际答不准"。上线前必须建立量化评估体系,而不是靠人肉试几个问题感觉良好就上线。下面是一套可落地的评估流程:

**第一步:建评估集。**找业务方要 50~100 个真实用户问题,每个问题标注三件事:(1) 标准答案要点;(2) 正确答案应该引用哪些文档的哪些段落(ground_truth_doc_ids);(3) 问题类型(事实型 / 推理型 / 闲聊)。这个评估集就是你的"考试卷",每次改 pipeline 都拿它跑一遍。

**第二步:检索层指标。**核心看 Recall@k(标准答案文档在不在 top-k 里)和 MRR(正确文档排第几)。如果 Recall@5 低于 80%,说明检索环节有问题,先别折腾 prompt,去调切分和 embedding。常见改进方向:换更强的 embedding 模型、加 hybrid search、加 rerank。

**第三步:生成层指标。**检索到了正确文档,但模型还是答错?这时看 Faithfulness(答案里每句话能不能在检索文档里找到依据)和 Answer Relevancy(答案是不是答了用户的问题,有没有跑题)。Faithfulness 低说明 prompt 约束不够或模型爱幻觉,Answer Relevancy 低说明检索到的文档和问题不匹配。

**第四步:回归测试。**每次改 chunk_size、换 embedding 模型、加 rerank,都跑一遍评估集,和上次的分数对比。分数掉了就回滚,别凭感觉上生产。

补充二:rerank 层实战

向量检索的本质是"双塔"------query 和 doc 分别编码成向量再算余弦,速度快但理解不深。Rerank 是"交叉编码",把 query 和 doc 拼在一起送进一个小模型直接打相关性分,准但慢。工程上的经典组合:

python 复制代码
# 两阶段检索:向量初筛 top-20,rerank 精排 top-4
from langchain_milvus import Milvus
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.runnables import chain
emb = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vs = Milvus(embedding_function=emb, collection_name="kb", connection_args={"uri": "http://localhost:19530"})
第一阶段:向量检索,捞 20 条候选
retriever_fast = vs.as_retriever(search_kwargs={"k": 20})
第二阶段:用 bge-reranker 对 20 条重排
from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-base", use_fp16=True)
@chain
def retrieve_with_rerank(query: str):
candidates = retriever_fast.invoke(query)
# 对每条候选算 (query, doc) 的相关性分
pairs = [[query, doc.page_content] for doc in candidates]
scores = reranker.compute_score(pairs)
# 按分数排序,取 top-4
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [doc for doc, score in ranked[:4]]
用这个 retrieve_with_rerank 替换原来的 retriever

Rerank 加进去后,recall 通常能从 70% 提升到 90% 以上,代价是每次查询多一次小模型推理(几十毫秒)。这是性价比最高的 RAG 优化手段,没有之一。

补充三:混合检索(Hybrid Search)配置

纯向量检索对错误码、型号、专有名词这种"必须字面匹配"的词很吃力。比如问"ERR_CONNECTION_RESET 怎么解决",向量检索可能捞回一堆讲网络原理的文档,但捞不到那个专门讲这个错误码的 FAQ。混合检索就是把 BM25 关键词检索和向量检索结果合并:

python 复制代码
# Milvus 2.4+ 支持混合检索:同时跑稠密向量和稀疏向量(BM25)
# 这里用 LangChain 的 Retriever 组合来示意:
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
BM25 retriever:在内存里建倒排索引
bm25 = BM25Retriever.from_documents(all_chunks)
bm25.k = 4
向量 retriever
vector_retriever = vs.as_retriever(search_kwargs={"k": 4})
集成:两路结果加权融合
ensemble = EnsembleRetriever(
retrievers=[bm25, vector_retriever],
weights=[0.4, 0.6],  # 向量权重高一点,BM25 辅助关键词
)
之后用 ensemble 当 retriever 接进 Agent 即可

补充四:多查询(Multi-Query)扩展

用户一句话可能有多种问法。"退款多久到账"和"钱什么时候退回来"语义一样但字面不同。Multi-Query 的思路是:用 LLM 把用户问题改写成 3~5 个不同说法,每个说法都去检索一次,结果合并去重:

python 复制代码
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
让 LLM 生成多个查询版本
prompt = ChatPromptTemplate.from_template(
"把下面用户问题改写成 3 个不同说法的检索查询,每行一个,不要编号:\n{q}"
)
llm = ChatOpenAI(model="gpt-4o-mini")
query_gen = prompt | llm | StrOutputParser()
def multi_query_retrieve(question: str):
queries = [q.strip() for q in query_gen.invoke({"q": question}).split("\n") if q.strip()]
all_docs = []
for q in queries:
all_docs.extend(vector_store.similarity_search(q, k=3))
# 按内容去重
seen, unique = set(), []
for d in all_docs:
key = d.page_content[:100]
if key not in seen:
seen.add(key)
unique.append(d)
return unique[:6]

补充五:PDF 表格和代码块的切分坑

普通 RecursiveCharacterTextSplitter 对纯文本效果好,但遇到 PDF 里的表格、代码块就抓瞎。展开讲几个特殊场景:

  • PDF 表格:价格表、参数表被按行切碎后,每一行单独成一个 chunk,检索回来用户看到"价格:299"但不知道是什么产品。解决:用 PyMuPDF 或 pdfplumber 专门抽表格,转成 Markdown 表格("| 产品 | 价格 |\n|---|---|\n| A | 299 |"),整个表格作为一个 chunk。

  • 代码块:代码里的函数被按字符切断后,检索回来半段代码没法读。解决:用 MarkdownHeaderTextSplitter 先按 ``` 围栏代码块切,代码块整体不拆。

  • HTML 文档:直接把 HTML 标签一起嵌入,噪音很大。先用 HTMLTextSplitter 去掉 nav、footer、script,只留正文 main 区域。

  • CSV/Excel:每行作为一个 chunk,metadata 带上列名。适合"查某条订单"这种精确查询。

补充六:高频 FAQ

**FAQ 1:embedding 模型输出维度越高越好吗?**不一定。维度越高表达力越强,但存储和计算成本也越高。text-embedding-3-small 是 1536 维,text-embedding-3-large 是 3072 维,但很多场景 small 就够了。先从小模型试,不够再换大的。

**FAQ 2:为什么我的检索结果分数都很低?**大概率是 embedding 没归一化,或者 metric_type 选成了 L2 但向量没归一化。检查 embedding 阶段有没有 normalize_embeddings=True,Milvus collection 的 metric_type 和 embedding 是否匹配。

**FAQ 3:知识库更新了,要不要重新建 collection?**不要。用 Milvus 的 delete + insert 做增量:按 metadata.source 删掉旧文档的 chunk,插入新切的 chunk。只有换了 embedding 模型(维度变了)才必须重建。

**FAQ 4:RAG 回答里出现"根据资料......"但资料里根本没有怎么办?**这是幻觉。在 prompt 里加硬约束:"如果资料中没有答案,直接说'抱歉,知识库里没有相关内容,请转人工',不要编造。"同时在生成后加一道校验:用另一个 LLM 判断答案里的每句话能不能在检索文档里找到依据,找不到就打回。

**FAQ 5:chunk_overlap 设成 0 行不行?**技术上能跑,但边界处的句子会被切断。比如 chunk A 结尾是"退款",chunk B 开头是"政策是 7 天",用户问"退款政策"时只捞回了 chunk B,就丢了"退款"两个字。overlap 的意义就是让相邻块有重叠上下文。

**FAQ 6:Milvus 本地模式和服务器模式怎么选?**本地模式(uri="./xxx.db")是嵌入式的单文件,零依赖适合 demo;生产必须用 Docker 起 Milvus 服务(19530 端口),支持并发、持久化、分片。

**FAQ 7:怎么让 RAG 回答时带上引用来源?**拼 prompt 时把每个 chunk 的 metadata.source 标在前面,比如"文档1: 退款政策.pdf 内容......",然后 system prompt 要求"回答时必须标注引用了哪份文档"。模型会自动照着做。

**FAQ 8:用户问的问题和文档用词完全不一样怎么办?**这就是为什么需要 query 改写和 hybrid search。用户说"我想退钱",文档里写的是"退款流程",纯字面匹配(BM25)会漏,但向量检索能捞到。两者结合最稳。

深度拓展再补充:项目实战细节与答疑

客服知识库项目完整部署清单

把本章的客服知识库项目从代码到上线的完整链路列一遍,方便你照着搭:

  1. 准备文档:把产品手册、FAQ、退款政策、物流说明整理成 .md 文件,放在 docs/ 目录。每个文档开头加一级标题作为文档名。

  2. 起 Milvus:docker run -d --name milvus -p 19530:19530 milvusdb/milvus:latest standalone。

  3. 跑 ingest:执行 ingest.py,把 docs/ 下所有文档切分、嵌入、写入 customer_service collection。跑完用 Milvus Attu 工具看一眼向量数对不对。

  4. 起 Agent 服务:用 FastAPI 包一层 create_agent,接收前端请求,传 thread_id,返回答案。

  5. 接前端:聊天窗口组件,用户发消息 → 调后端 /chat 接口 → 流式返回 SSE。

  6. 加监控:记录每次查询的 question、检索到的 doc_ids、答案、耗时,存一张 logs 表。用户反馈"答错了"时能立刻回溯。

  7. 建评估集:业务方整理 50 条标准问答,每周跑一次回归测试。

切分策略深度对比

不同切分器的适用场景再展开讲一下,别再只会用 RecursiveCharacterTextSplitter:

切分器 适用文档 特点
RecursiveCharacterTextSplitter 普通文本、文章、手册 按段落/句号递归切,最通用
MarkdownHeaderTextSplitter Markdown 文档 按 # / ## / ### 标题切,保留层级
HTMLHeaderTextSplitter 网页抓取内容 按 h1/h2/h3 切,去掉导航噪音
PythonCodeTextSplitter 代码文档 按函数/类定义切,代码块不拆
TokenTextSplitter 按 token 数精确控制 直接用 tiktoken 数 token,适合严格控制 context

生产里经常是组合拳:先用 MarkdownHeaderTextSplitter 按章节粗切,再对每个章节用 RecursiveCharacterTextSplitter 细切。这样既保留了章节语义,又控制了 chunk 大小。

细节答疑集锦

**Q:为什么 OpenAIEmbeddings 和 HuggingFaceEmbeddings 不能混用?**它们输出的向量维度和语义空间都不一样。同一个 collection 里所有 chunk 必须用同一个 embedding 模型算,不然相似度计算毫无意义。换模型 = 重建 collection。

**Q:chunk 里要不要带文档标题?**强烈建议带。切分时把章节标题拼到 chunk 开头,比如"【退款政策】7 天无理由退货......"。这样检索回来的 chunk 自带上下文,模型更容易理解,embedding 也因为有标题而更准。

**Q:用户问闲聊("你好""今天天气怎么样")也要走 RAG 吗?**不要。在 Agent 前面加一个意图分类:闲聊直接回复,问题型才走 search_knowledge_base。否则闲聊也去查知识库,既浪费又答非所问。

**Q:Milvus 的 index_type 选 AUTOINDEX 还是 HNSW?**AUTOINDEX 是 Milvus 2.3+ 的默认选项,自动选最优索引,适合大多数场景。HNSW 是手动指定,适合你对召回率和延迟有精细要求时调参(M、efConstruction)。

**Q:检索结果怎么缓存?**高频问题(比如"营业时间""退款电话")可以在 Redis 里缓存 question → docs 映射,TTL 一天。同样的问题直接返回缓存,不用每次算 embedding 加查 Milvus。

**Q:怎么处理多语言知识库?**用 multilingual-e5 这种多语言 embedding 模型,或者中文文档用中文 embedding、英文文档用英文 embedding,检索时按语言分 collection。混在一个 collection 里效果会打折扣。

**Q:RAG 系统的延迟主要花在哪?**排序:(1) LLM 生成答案,几百毫秒到几秒;(2) embedding 编码 query,几十毫秒;(3) Milvus 向量检索,几毫秒到几十毫秒;(4) rerank,几十到几百毫秒。想优化延迟,先砍 LLM(换小模型、流式输出),再砍 rerank。

**Q:文档里有图片,RAG 能处理吗?**纯文本 RAG 不行。图片里的信息要先用多模态模型(GPT-4V、Qwen-VL)把图片内容描述成文本,再把描述嵌入入库。这叫"多模态 RAG",是进阶话题。

深度拓展收尾:本章知识图谱速记

最后用一段话把本章所有知识点串成一张思维导图,方便你复习时一眼回忆全貌:

RAG 全链路 = 加载 Loader (PyPDF/Text/CSV/Web)→ 切分 Splitter (RecursiveCharacterTextSplitter,chunk_size 300~500、overlap 10%~15%)→ 嵌入 Embedding (OpenAI 或本地 BGE,中文场景优先 bge-small-zh)→ 入库 VectorStore (Milvus collection,COSINE 距离)→ 检索 Retriever (向量相似度 top-k,加 MMR / 阈值过滤)→ 拼 prompt (system 强约束"只基于资料回答")→ 生成 LLM 。进阶优化三板斧:rerank (bge-reranker 精排)、hybrid search (向量 + BM25 关键词)、query 改写(LLM 把口语问题改成检索词)。把 retriever 包成 @tool 接进 create_agent,就是一个会自己查知识库的客服助手。上线前必须建评估集,盯 recall@k 和 faithfulness 两个指标,每次改 pipeline 都回归测试。

记住一句话:RAG 的本质是"让模型看着材料答题",而不是"让模型背下所有知识"。切分、嵌入、检索、重排,每一步都是为了把"最相关的那几块材料"精准送到模型眼前。

相关推荐
水如烟1 小时前
孤能子视角:EIS术语梳理·术层+吐纳——操作语法与收束:从投射到呼吸节律
人工智能
偷吃米饭2 小时前
图片特征怎么描述?用华为云码道开发 OpenCV ORB 特征提取工具
人工智能·opencv·华为云
risc1234562 小时前
万变不离其宗
人工智能·低代码·云原生
科技看点说2 小时前
SAP B1如何支撑高科技与精密制造?项目、物料、委外与成本管理解析
大数据·人工智能
skywalk81632 小时前
FreeBSD系统在Deepseek-harness中配置使用其它几家的模型,比如hy3 hunyuan-2 glm5.3-flash等
人工智能·freebsd·deepseek·harness
马六六i2 小时前
目前知名的分布式Ai数智共享商城哪家好
人工智能·分布式
愈努力俞幸运2 小时前
Oauth登录
人工智能
无障碍研究院2 小时前
2026TADC|韩冰、王永兴:解锁座舱无障碍量产落地新范式
人工智能·智能汽车·无障碍
闭包不眠2 小时前
手机拍的证件照怎么压缩到20k?3种方法(2026)
图像处理·人工智能·计算机视觉·智能手机·html