Ch13 RAG修订于 二〇二六年七月二日

13.2 RAG 系统架构

RAG 看起来很直观,但背后是一套工程系统。理解它的架构,你才能理解为什么”分块大小”、“检索策略”、“重排序”这些细节如此重要。

13.2.1 两条管线:离线索引 + 在线查询

RAG 系统由两条管线组成,一条在用户提问前运行(离线),一条在用户提问时运行(在线)。

离线索引管线(用户提问前):这条管线的目标是把你的文档库变成可检索的向量数据库。它不赶时间,可以慢慢跑,但必须跑得完整。

文档收集 → 文档解析 → 文本分块 → Embedding编码 → 存入向量数据库

每一步的细节:

  • 文档收集:从各种来源拉取文档——本地文件、网页、数据库、API、飞书文档、Notion、Confluence。你需要一个”连接器”(Connector)层,对接各种数据源。
  • 文档解析:把 PDF、Word、HTML、Markdown 等格式转成纯文本。这一步最容易被忽视,但解析质量直接影响后续所有环节。PDF 里的表格、图片、双栏排版,解析不好,文本就乱了。
  • 文本分块:把长文档切成小块。为什么?因为 Embedding 模型有输入长度限制(通常 512 token),而且小块检索更精准。分块策略是 RAG 性能的”地基”——分得不好,后面全白搭。详细策略见下一章。
  • Embedding 编码:把每个文本块转换成一个向量(一串数字)。语义相近的文本,向量也相近。这个向量就是”语义指纹”。
  • 存入向量数据库:把向量和原始文本一起存到向量数据库里。向量数据库支持”近似最近邻”(ANN)搜索,能在毫秒级从百万甚至十亿级别的向量中找到最相似的几个。

在线查询管线(用户提问时):这条管线要快,用户等不了。

用户提问 → Query处理 → Query Embedding → 向量检索 → 重排序 → Prompt拼接 → LLM生成 → 返回答案

每一步的细节:

  • Query 处理:用户的问题可能很口语化、很模糊、缺少关键信息。你可以做查询改写(把”那个东西怎么用”改写成”XX产品使用教程”)、查询扩展(生成多个相关查询,从不同角度检索)。
  • Query Embedding:把处理后的查询转成向量。
  • 向量检索:在向量数据库中搜索最相似的 Top-K 个文档块(K 通常取 10-50)。
  • 重排序:初检结果可能不够精准。用更强大的重排序模型(Cross-Encoder)对 Top-K 结果重新打分,选出最相关的 Top-N(N 通常取 3-10)。
  • Prompt 拼接:把检索到的文档块和用户问题按照模板拼成完整的 Prompt。模板里通常包含系统指令、检索到的文档内容、用户问题、以及格式要求(如”请标注引用来源”)。
  • LLM 生成:LLM 读取 Prompt,基于检索到的文档生成答案。
  • 返回答案:把答案和引用来源一起返回给用户。

13.2.2 为什么两条管线要分开

因为它们的性能要求完全不同。离线管线追求”完整”和”准确”——宁可慢,不能漏。在线管线追求”快”——用户期望秒级响应。把耗时的索引工作放在离线做,查询时只需要做快速的向量检索和 LLM 生成。

13.2.3 一个完整的 RAG 系统代码示例

下面是一个最小但完整的 RAG 系统,用 Python 伪代码展示核心流程。每一行都对应架构图中的一个环节:

# ===== 离线索引管线 =====

# 1. 文档收集 & 解析
documents = load_documents("knowledge_base/")  # 从文件夹加载所有文档
texts = [parse_document(doc) for doc in documents]  # PDF/Word/HTML → 纯文本

# 2. 文本分块
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,       # 每块最多 512 个 token
    chunk_overlap=50,     # 相邻块重叠 50 token,避免语义断裂
    separators=["\n\n", "\n", "。", ".", " ", ""]  # 优先在自然边界切分
)
chunks = splitter.split_text("\n\n".join(texts))

# 3. Embedding 编码
from openai import OpenAI
client = OpenAI()
embeddings = []
for chunk in chunks:
    resp = client.embeddings.create(
        model="text-embedding-3-small",  # OpenAI 的 Embedding 模型
        input=chunk
    )
    embeddings.append(resp.data[0].embedding)

# 4. 存入向量数据库
import chromadb
vector_db = chromadb.Client()
collection = vector_db.create_collection(name="my_knowledge_base")
for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
    collection.add(
        ids=[str(i)],
        embeddings=[emb],
        documents=[chunk],
        metadatas=[{"source": f"doc_{i}"}]
    )

# ===== 在线查询管线 =====

def rag_query(user_question: str, top_k: int = 10, top_n: int = 3) -> str:
    # 5. Query Embedding
    query_emb = client.embeddings.create(
        model="text-embedding-3-small",
        input=user_question
    ).data[0].embedding

    # 6. 向量检索
    results = collection.query(
        query_embeddings=[query_emb],
        n_results=top_k  # 先取 top_k 个候选
    )

    # 7. 重排序(这里用简单的 Cross-Encoder 示意)
    from sentence_transformers import CrossEncoder
    reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
    pairs = [[user_question, doc] for doc in results["documents"][0]]
    scores = reranker.predict(pairs)
    # 按分数重排,取 top_n
    ranked = sorted(
        zip(results["documents"][0], scores),
        key=lambda x: x[1], reverse=True
    )[:top_n]
    retrieved_docs = [doc for doc, score in ranked]

    # 8. Prompt 拼接
    context = "\n\n---\n\n".join(retrieved_docs)
    prompt = f"""你是一个专业的知识助手。请根据以下参考资料回答用户问题。

## 参考资料
{context}

## 用户问题
{user_question}

## 要求
1. 基于参考资料回答,不要编造信息
2. 如果参考资料不足以回答问题,请明确说明
3. 在答案中标注引用来源(如"[来源1]")"""

    # 9. LLM 生成
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.3,  # RAG 场景建议低温,减少幻觉
        stream=True        # 流式输出
    )

    # 10. 返回答案
    answer = ""
    for chunk in response:
        if chunk.choices[0].delta.content:
            answer += chunk.choices[0].delta.content
    return answer

这个例子虽然只有约 60 行代码,但它完整覆盖了 RAG 系统的全部核心环节。理解了每一行在做什么,你就理解了 RAG 的工程全貌。从这个小系统出发,你可以逐步优化每一个环节——换更好的分块策略、升级 Embedding 模型、加入混合检索、用专门的重排序服务——每一步优化都会带来可感知的质量提升。


← 上一节:13.1 为什么需要 RAG | 下一节:13.3 分块策略 —— RAG 的地基 →

关联5

知识图谱

局部图谱 · 点节点跳转