RAG检索增强生成实战教程:用Python搭建知识库问答系统

大模型很强,但它有一个绕不开的硬伤:它不知道你公司的内部知识。问它“我们公司的报销流程是什么”,它会一本正经地编造一个答案;问它“2026年6月最新的产品定价”,它的训练数据里根本没有。这种“一本正经地胡说八道”就是著名的幻觉(Hallucination)问题。

解决这个问题的主流方案就是 RAG(Retrieval-Augmented Generation,检索增强生成)。它的核心思路非常朴素:在让模型回答之前,先从你的知识库里检索出相关文档,把这些文档塞进提示词,让模型“看着资料答题”。本文将从原理到代码,带你完整搭建一个可用的知识库问答系统。

一、为什么需要 RAG

让模型掌握私有知识,理论上只有两条路:一是把知识“训练”进模型权重(微调),二是把知识“喂”给模型上下文(RAG)。两条路对比下来,RAG 几乎在所有维度上都更适合知识库场景:

维度微调RAG
知识更新需要重新训练,周期长更新文档即可,实时生效
成本训练 + 推理都很贵只需推理 + 向量检索
可溯源无法追溯答案来源可标注引用文档
准确性容易混淆新旧知识基于检索结果,幻觉少

所以 2026 年绝大多数企业知识库、客服机器人、文档助手,底层都是 RAG 架构。

二、RAG 的完整流程

一个标准的 RAG 系统分为两个阶段:离线索引在线问答

离线索引阶段

  1. 文档加载:从 PDF、Word、网页、数据库等来源读取原始文本。
  2. 文档切分(Chunking):把长文档切成几百字的小块,便于检索。
  3. 向量化(Embedding):用嵌入模型把每个文本块转成高维向量。
  4. 存入向量库:把向量和原文一起存进向量数据库。

在线问答阶段

  1. 问题向量化:用同一个嵌入模型把用户问题也转成向量。
  2. 向量检索:在向量库中找出与问题最相似的 Top-K 个文本块。
  3. 构造提示词:把检索到的文本块 + 用户问题拼成提示词。
  4. 生成回答:调用 LLM 基于检索资料生成答案。

理解了这个流程,下面的代码就是把它逐行实现出来。

三、环境准备

# 安装依赖:OpenAI SDK + 向量库 + 文本切分
pip install openai faiss-cpu numpy

# 设置 EnlyAI 统一接入的密钥与地址
export OPENAI_API_KEY="sk-your-enlyai-key"
export OPENAI_BASE_URL="https://enlyai.com/v1"

提示:通过 EnlyAI 统一接入,一个 key 即可调用 GPT-5.5、Claude Opus 4.8、Gemini 3.5 Pro 以及各类 Embedding 模型,无需在多个平台分别注册。

四、文档切分:影响检索质量的关键

切分(Chunking)看似简单,却是 RAG 效果好坏的最大变量。切得太大会引入噪声、检索不准;切得太小会破坏语义完整性。推荐使用递归字符切分:优先按段落、换行、句号切,保证语义边界。

import re

def recursive_split(text, chunk_size=500, chunk_overlap=80):
    """按段落 -> 句子 -> 字符递归切分,保留重叠避免语义断裂"""
    # 先按双换行切段落
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    chunks = []
    for para in paragraphs:
        # 段落过长再按句号切
        if len(para) > chunk_size:
            sentences = re.split(r'(?<=[。!?.!?])\s+', para)
            buf = ""
            for s in sentences:
                if len(buf) + len(s) <= chunk_size:
                    buf += s
                else:
                    if buf:
                        chunks.append(buf)
                    buf = s
            if buf:
                chunks.append(buf)
        else:
            chunks.append(para)
    return chunks

docs = """
EnlyAI 是一个 LLM API 聚合平台,支持 OpenAI、Claude、Gemini 等数十种模型。

API base URL 是 https://enlyai.com/v1,完全兼容 OpenAI API 格式。

新用户注册即送免费额度,可在控制台查看用量与账单。

支持流式输出、Function Calling、多模态等高级能力。
"""

chunks = recursive_split(docs)
print(f"切分出 {len(chunks)} 个文本块")

经验值:chunk_size 设 300-500 字符,chunk_overlap 设 10%-20%,能在大多数中文场景下取得平衡。

五、向量化:调用 Embedding API

切分完成后,需要把每个文本块转成向量。这里用 OpenAI SDK 调用 EnlyAI 提供的 Embedding 接口。

from openai import OpenAI
import numpy as np

client = OpenAI(
    api_key="sk-your-enlyai-key",
    base_url="https://enlyai.com/v1"
)

def get_embeddings(texts, model="text-embedding-3-small"):
    """批量获取文本向量"""
    resp = client.embeddings.create(model=model, input=texts)
    return [d.embedding for d in resp.data]

vectors = get_embeddings(chunks)
print(f"得到 {len(vectors)} 个向量,维度 {len(vectors[0])}")

Embedding 模型输出的通常是 1536 维或更高维度的浮点向量,语义相近的文本在向量空间里距离也近。这就是后续“相似度检索”的基础。

六、向量检索:用 FAISS 建索引

FAISS 是 Facebook 开源的高效向量检索库,适合中小型知识库(百万级以下)。我们把向量建成索引,就能用余弦相似度快速找最相关的文本块。

import faiss

dim = len(vectors[0])
index = faiss.IndexFlatIP(dim)  # 内积索引(向量归一化后等价于余弦相似度)

arr = np.array(vectors, dtype="float32")
faiss.normalize_L2(arr)  # 归一化,让内积 = 余弦相似度
index.add(arr)

def retrieve(query, k=3):
    """检索与 query 最相关的 k 个文本块"""
    q_vec = np.array(get_embeddings([query]), dtype="float32")
    faiss.normalize_L2(q_vec)
    scores, ids = index.search(q_vec, k)
    return [(chunks[i], float(scores[0][j])) for j, i in enumerate(ids[0])]

# 测试检索
for chunk, score in retrieve("EnlyAI 的 API 地址是什么?"):
    print(

            

总结

RAG 不是什么高深的技术,它的本质就是“检索 + 生成”两步走。但要把效果做到生产可用,关键在于细节:切分策略、Embedding 质量、检索召回率、提示词约束、重排序优化。本文给出的代码已经是一个可运行的最小闭环,你可以在此基础上替换真实文档、接入 Milvus、加入重排序,逐步演进到企业级系统。

而把 RAG 的模型调用统一交给 EnlyAI,你就能把精力集中在检索质量和业务逻辑上,不用在多平台密钥管理上浪费时间。

想快速搭建自己的知识库问答系统?

EnlyAI 提供 OpenAI 兼容的统一接口,一个 key 调用 GPT-5.5、Claude Opus 4.8、Gemini 3.5 Pro 及各类 Embedding 模型,注册即送免费额度,RAG 代码只需改一个 base_url 即可接入。

立即注册 EnlyAI →
="f">f"[{score:.3f}] {chunk}")

检索结果会按相似度从高到低返回。Top-K 一般取 3-5,太少容易漏掉关键信息,太多会撑爆上下文窗口并引入噪声。

七、生成回答:把检索结果喂给 LLM

最后一步是把检索到的文本块拼进提示词,让模型基于资料作答。关键是要明确告诉模型“只能依据资料回答,不知道就说不知道”,这是压制幻觉的核心。

def rag_answer(question, model="gpt-5.5"):
    # 1. 检索相关文档
    retrieved = retrieve(question, k=3)
    context = "\n\n".join([f"[资料{i+1}] {c}" for i, (c, _) in enumerate(retrieved)])

    # 2. 构造提示词
    prompt = f"""你是一个严谨的知识库助手。请只根据下方资料回答问题。
如果资料中没有答案,请直接回答"根据现有资料无法回答",不要编造。

{context}

问题:{question}
回答:"""

    # 3. 调用 LLM 生成
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        temperature=0.2  # 低温度保证答案稳定
    )
    return resp.choices[0].message.content

print(rag_answer("EnlyAI 支持哪些高级能力?"))

运行后,模型会基于检索到的资料回答“支持流式输出、Function Calling、多模态等高级能力”,而不是凭空编造。这就是 RAG 的威力。

八、进阶优化:让 RAG 效果翻倍

基础 RAG 跑通后,效果往往还达不到生产可用。以下几个优化方向能显著提升质量:

1. 混合检索(Hybrid Search)

纯向量检索对精确关键词(如产品型号、人名)不敏感。把向量检索 + BM25 关键词检索的结果做加权融合,能兼顾语义和精确匹配。

2. 重排序(Rerank)

先向量检索召回 Top-20,再用一个交叉编码器(Cross-Encoder)对这 20 条重新打分排序,取 Top-3。重排序模型精度远高于双塔向量,能大幅降低无关结果。

3. 查询改写(Query Rewriting)

用户提问往往口语化、信息不全。先用 LLM 把问题改写成更利于检索的形式(补全指代、拆解多轮问题),再检索。

4. 引用溯源

让模型在答案中标注引用了哪条资料,例如“根据[资料2]…”,既方便用户核实,也能在评估时自动核对答案是否有据可依。

九、生产环境最佳实践

  1. 向量库选型:百万级以下用 FAISS 即可;千万级以上、需要持久化和过滤的,选 Milvus 或 Pinecone。
  2. Embedding 模型一致性:索引和查询必须用同一个 Embedding 模型,换模型要全量重建索引。
  3. 增量更新:文档变更时只更新对应块,不要每次全量重建,用文档 ID 做幂等。
  4. 缓存热点查询:高频问题缓存答案,能省下大量 LLM 调用成本。
  5. 监控检索质量:记录每次检索的 Top-1 相似度分数,分数持续偏低说明知识库覆盖不足。
  6. 限制上下文长度:检索结果总长度控制在模型上下文的 30% 以内,留足生成空间。

十、用 EnlyAI 简化模型调用

RAG 系统里会用到两类模型:Embedding 模型和对话模型。如果分别去 OpenAI、Anthropic、Google 各自平台对接,密钥管理、计费、SDK 切换都很繁琐。通过 EnlyAI 统一接入,一个 key、一个 base_url 就能调用所有模型:

from openai import OpenAI

client = OpenAI(api_key="sk-your-enlyai-key", base_url="https://enlyai.com/v1")

# Embedding 用便宜的小模型
emb = client.embeddings.create(model="text-embedding-3-small", input=["测试文本"])

# 复杂问答用 GPT-5.5,简单问答用 GPT-5.4-mini 省钱
def smart_rag(question):
    # 先用便宜模型判断问题难度
    judge = client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[{"role": "user", "content": f"判断问题难度(简单/复杂):{question}"}]
    )
    model = "gpt-5.5" if "复杂" in judge.choices[0].message.content else "gpt-5.4-mini"
    return rag_answer(question, model=model)

统一接入的好处是:模型切换只改一个字符串,故障时