· 王思腾 · 大模型工程 · 9 分钟阅读

动手实战:从零搭一个 RAG 知识库问答系统(含代码与生产化清单)

原理篇讲了 RAG 是什么,这篇带你亲手搭一个能跑的。从文档切分、向量化入库,到检索、重排、拼上下文、生成答案,给出最小可运行代码;然后把 Demo 推向生产:混合检索、Rerank、查询改写、相似度兜底、引用溯源、评测集和增量更新,附一份逐项可落地的调优清单。

原理篇讲了 RAG 是什么,这篇带你亲手搭一个能跑的。从文档切分、向量化入库,到检索、重排、拼上下文、生成答案,给出最小可运行代码;然后把 Demo 推向生产:混合检索、Rerank、查询改写、相似度兜底、引用溯源、评测集和增量更新,附一份逐项可落地的调优清单。

RAG 原理(见检索增强生成篇)讲的是“为什么”和“链路长什么样”,这篇我们动手把它搭出来。目标是:丢进去一批文档,就能对着它们提问、拿到带来源的答案。先用最小代码跑通,再一步步把它加固到能上生产。

示例用 Python + LangChain + Chroma(本地向量库,零依赖起步),模型用 OpenAI 兼容接口——换成国内模型(通义、DeepSeek、智谱等)只需改 embedding 和 chat 的接入。

一、整体链路

flowchart TB
    subgraph Off["离线 · 建库"]
        D0["原始文档"] --> D1["加载 + 清洗"] --> D2["切分 Chunking"] --> D3["Embedding 向量化"] --> DB[("向量库")]
    end
    subgraph On["在线 · 问答"]
        Q["用户提问"] --> Q1["查询改写"] --> Q2["向量+关键词混合检索"] --> RR["Rerank 重排"]
        RR --> CTX["拼接上下文"] --> LLM["LLM 生成"] --> A["带引用的答案"]
    end
    DB --> Q2

离线把文档变成向量存起来,在线把问题向量化、检索、重排、塞给模型。我们先做最朴素的版本,再逐层加东西。

二、准备依赖

pip install langchain langchain-openai langchain-community chromadb

设好 API Key(用国内模型就换成对应的 base_url 和 key):

export OPENAI_API_KEY=sk-xxx

三、离线:文档切分与入库

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import TextLoader, DirectoryLoader

# 1. 加载文档(支持整个目录、多种格式)
docs = DirectoryLoader("./docs", glob="**/*.txt", loader_cls=TextLoader).load()

# 2. 切分:中文文档 chunk 300~600 字,重叠 10%~20%
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=80,
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""],
)
chunks = splitter.split_documents(docs)

# 3. 向量化并入库
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",
    collection_metadata={"hnsw:space": "cosine"},  # 余弦相似度
)
print(f"入库 {len(chunks)} 个片段")

切分是 RAG 效果的第一关,几个要点:

  • 中文用递归切分器并显式给出中文标点分隔符,默认按英文换行切会把中文段落切碎;
  • chunk_size 中文取 300~600 字、chunk_overlap 取块大小的 10%~20%,重叠是为了避免一句话被拦腰截断、上下文丢失;
  • 保留元数据:每个 chunk 带上来源文件、页码、标题,后面做引用溯源和权限过滤全靠它。

四、在线:检索 + 生成

from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA

llm = ChatOpenAI(model="gpt-4o", temperature=0)

qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vectorstore.as_retriever(search_kwargs={"k": 4}),
    return_source_documents=True,
)

result = qa.invoke({"query": "产品支持哪些退款方式?"})
print(result["result"])
for doc in result["source_documents"]:
    print("来源:", doc.metadata.get("source"))

chain_type="stuff" 是把检索到的片段全塞进一次调用,片段少时最简单。temperature=0 让答案更确定。到这一步,一个能用的知识库问答就跑通了。

五、从 Demo 到生产:逐项加固

跑通只值 10 分,剩下 90 分在检索质量和工程化上。

1. 混合检索(Hybrid Search)

纯向量检索擅长语义,但对产品型号、错误码、人名这类精确专有名词反而容易漏。叠加一路 BM25 关键词检索,再融合结果:

flowchart LR
    Q["查询"] --> V["向量检索<br/>语义相似 Top-20"]
    Q --> K["BM25 关键词<br/>精确匹配 Top-20"]
    V --> F["融合 RRF"] --> RR["Rerank"]
    K --> F

2. Rerank 重排

向量检索是“双塔”近似,快但粗;用交叉编码器(Cross-Encoder,如 bge-reranker)把召回的 Top-20 逐一和问题精细打分,取 Top-4 给 LLM。这一步通常是单点收益最大的优化。

3. 查询改写

多轮对话里“它支持退款吗”里的“它”没法直接检索。先让模型把问题改写成独立、检索友好的表述(“XX 产品是否支持退款”),再去检索。

4. 相似度兜底,拒绝硬答

设相似度阈值,低于阈值说明库里没有,直接回答“知识库中未找到相关信息”,别让模型编:

你只能根据下面【参考资料】回答。若资料中没有相关内容,
请直接回复“知识库中未找到相关信息”,不要根据常识编造。
【参考资料】{context}
【问题】{question}

5. 引用溯源

答案里标注来自哪个文档/页码,既方便用户核对,也让错误可定位。

6. 增量更新

知识库会变。设计成按文档 ID 做 upsert:文档更新就删掉旧 chunk、重建该文档的片段,而不是全库重建;可以用文件 hash 判断是否需要重新 embedding。

六、评测:没有数字就没有优化

RAG 最容易犯的错是“凭感觉调”。务必建一个评测集:

  • 准备 50~100 条真实问答对(问题 + 标准答案/应命中的文档);
  • 拆两个指标分别看:召回率(该找到的片段有没有检索到)和答案正确率(生成有没有答对);
  • 召回不行就调切分、检索、Rerank;召回对了但答错,才去调 Prompt 和模型。
flowchart LR
    E["评测集问答对"] --> R["跑检索"] --> M1{"召回命中?"}
    R --> G["跑生成"] --> M2{"答案正确?"}
    M1 -- 否 --> T1["调切分/混合检索/Rerank"]
    M2 -- 否 --> T2["调 Prompt/模型/上下文"]

把“检索问题”和“生成问题”分开定位,能省掉大量无效调参。

七、常见坑

  • 垃圾进垃圾出:PDF 表格、图片文字、页眉页脚抽取错乱,会把脏东西喂给模型。文档清洗和解析往往比换模型更重要;
  • 切分太碎或太大:太碎语义不全,太大稀释重点、还费 token,按上面经验值起步再按评测调;
  • 只建不评:没有评测集,所有“优化”都是心理安慰;
  • 忽视更新:文档过期后模型还在一本正经地回答旧政策,必须有增量同步;
  • 权限没隔离:多租户场景要在检索阶段按元数据过滤,不能把别人的文档检索出来。

小结

  1. 最小 RAG 四步:切分 → 向量化入库 → 检索 → 拼上下文生成,几十行代码能跑通;
  2. 切分是第一关:中文显式指定标点分隔,chunk 300~600 字、重叠 10%~20%,带好元数据;
  3. 生产化收益排序通常是:Rerank ≈ 混合检索 > 查询改写 > 兜底拒答 > 引用溯源;
  4. 一定要相似度兜底,允许模型说“不知道”,这是挡幻觉的关键;
  5. 建评测集,把召回问题和生成问题分开定位;
  6. 文档清洗、增量更新、权限过滤这些“笨功夫”,才是 RAG 真正的护城河。

RAG 的上限不在模型,而在数据处理、检索质量和评测体系——模型谁都能调,差距全在这些不起眼的地方。

本文由 AI 辅助创作,所有内容仅供交流与参考。
分享:
返回博客
大模型应用架构进阶:从 Prompt、工作流、RAG 到 Agent 的选型与演进

大模型应用架构进阶:从 Prompt、工作流、RAG 到 Agent 的选型与演进

“Prompt、RAG、Agent 三选一”是个过于粗糙的说法。真实系统是一条从“全确定”到“全自主”的连续光谱,RAG 本质是给模型接知识的能力,Agent 也分固定编排和自主循环。这篇往深里聊:工作流模式、RAG 的三个深浅层级、什么时候才该让模型自主决策,以及成本、延迟和失败模式这些入门文章很少算的账。

Spring AI 介绍与实践:用 Java 生态的方式接入大模型

Spring AI 介绍与实践:用 Java 生态的方式接入大模型

Java 团队想接大模型,不必硬套 Python 的 LangChain。Spring AI 把 ChatClient、Prompt 模板、结构化输出、RAG(向量库)、函数调用、对话记忆这些能力做成了 Spring 风格的抽象和 Starter,配几行 yml、写几行链式调用就能跑。这篇从核心抽象讲起,带你用 Spring Boot 一步步实现聊天、结构化映射、RAG 知识库和工具调用,并聊聊生产里要注意的事。

向量数据库原理:海量 Embedding 如何被毫秒检索

向量数据库原理:海量 Embedding 如何被毫秒检索

RAG、语义搜索、推荐都依赖“给一个向量,找出最相似的 N 个向量”。百万到十亿级向量下,暴力比对不现实。本文从向量相似度讲起,聊 ANN 近似最近邻索引(IVF、HNSW、PQ 量化)、元数据过滤与混合检索,以及 Milvus、pgvector、Qdrant 该怎么选。

RAG 原理详解:让大模型用上私有知识的检索增强生成

RAG 原理详解:让大模型用上私有知识的检索增强生成

大模型知识冻结、会幻觉、不知道企业内部数据。RAG(检索增强生成)通过“先检索、再生成”把外部知识注入上下文,是当前落地最广的大模型应用范式。本文带你走通 RAG 的完整链路:分块、嵌入、检索与重排,以及工程上最常踩的坑。