这份 RAG 教程我从"为什么需要它"讲起,每个概念都用生活类比拆解,再从手写版(懂原理)到框架版(能上手),最后给完整实战。

RAG 完全教程(小白友好版)

一、先搞懂:RAG 到底是什么

RAG = Retrieval-Augmented Generation = 检索增强生成

拆成三个词理解:

  • 检索(Retrieval):先去资料库里"查资料"
  • 增强(Augmented):把查到的资料"塞给"AI
  • 生成(Generation):AI 基于资料"生成"答案

为什么需要 RAG?(用生活类比)

想象大模型是一个博学但健忘、还会瞎编的学霸

问题:大模型直接回答有三个毛病
├─ 不知道你公司内部的事(训练时没见过你的资料)
├─ 不知道最新消息(训练数据有截止日期)
└─ 不确定时会"一本正经地胡说"(幻觉)

RAG 的解决思路——开卷考试:

闭卷考试(纯大模型):
  你问 → AI 凭记忆答 → 可能记错/不知道/瞎编

开卷考试(RAG):
  你问 → 先翻书找到相关章节 → AI 看着书回答 → 准确、有依据

一句话:RAG 就是给 AI 配了一个"可以随时查阅的资料库",让它先查资料再回答,而不是凭记忆瞎答。

对照你的 Java 项目:RagAnswerAdvisor + pgvector 干的就是这个事。


二、RAG 完整流程全景图

RAG 分两个阶段,先理解这张图(最重要):

═══════ 阶段一:建库(离线,准备资料)═══════

你的文档              切成小块           变成向量          存入向量库
(PDF/Word/txt)  →   (chunking)   →   (embedding)  →   (vector store)
 一本厚书            撕成一页页          每页编个"坐标"      放进带索引的柜子


═══════ 阶段二:问答(在线,实时回答)═══════

用户提问         问题也变向量        去库里找最像的        资料+问题给AI        AI生成答案
"公司年假几天"  →  (embedding)   →   (检索 top-k)   →   (拼提示词)    →   (LLM生成)
                                    找到"年假政策"那几页    "根据以下资料回答"   "公司年假5天"

记住这个顺序加载 → 切分 → 向量化 → 存储 ||| 提问 → 检索 → 增强 → 生成

下面逐个拆解每一步。


三、核心概念逐个拆解

3.1 文档加载(Loading)

把各种格式的文件读成纯文本。

# 不同格式用不同加载器
from langchain_community.document_loaders import (
    TextLoader,           # txt
    PyPDFLoader,          # pdf
    Docx2txtLoader,       # word
    WebBaseLoader,        # 网页
)

# 加载 txt
loader = TextLoader("公司制度.txt", encoding="utf-8")
docs = loader.load()

print(docs[0].page_content)    # 文本内容
print(docs[0].metadata)        # 元数据(来源、页码等)

小白要点Document 对象有两部分——page_content(正文)和 metadata(出处信息,后面检索时用来标注"答案来自哪")。


3.2 文档切分(Chunking)——小白难点,重点讲

为什么要切?(这是 RAG 最关键的概念之一)

原因1:模型上下文有限
  一本300页的书全塞给AI?放不下,也太贵

原因2:检索要精准
  用户问"年假几天",你应该只给他"年假政策"那一段
  而不是把整本员工手册都给他

用类比理解:切分就像把一本书拆成一张张知识卡片,这样查的时候能精准抽出最相关的几张卡片。

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 每块最多500字符
    chunk_overlap=50,      # 相邻块重叠50字符
    separators=["\n\n", "\n", "。", "!", "?", " ", ""],  # 优先按这些断开
)

chunks = splitter.split_documents(docs)
print(f"切成了 {len(chunks)} 块")

两个关键参数(小白必懂):

① chunk_size(块大小)

太大(如2000):检索不精准,一块里混了好几个主题,还浪费token
太小(如100):上下文被切碎,一句话的意思可能断在两块里
经验值:300-800,中文偏小,英文偏大

② chunk_overlap(块重叠)

为什么要重叠?防止把完整意思切断

不重叠的问题:
  块1:...员工入职满一年,
  块2:可享受5天年假...      ← "享受5天年假"和"满一年"被切散了

重叠后:
  块1:...员工入职满一年,可享受
  块2:满一年,可享受5天年假...  ← 重叠部分保住了完整语义

经验值:chunk_size 的 10%-20%

类比:切香肠时每片留一点点连着,免得关键信息正好被切在两片中间。


3.3 Embedding 向量化——小白难点,重点讲

什么是向量(Embedding)?

这是 RAG 最玄但最核心的概念。用大白话讲:

向量 = 把一段文字变成一串数字(坐标)
      让"意思相近"的文字,数字也"相近"

用地图坐标类比(最好理解):

想象一张"语义地图",每个词/句子是地图上的一个点:

        猫 (1.2, 3.4)
        狗 (1.3, 3.5)        ← 猫和狗离得近(都是宠物)
                                    
                  汽车 (8.9, 0.2)   ← 汽车离得远(不相关)

"宠物"相关的词聚在一起,"交通"相关的词聚在另一边

实际上不是2维,而是几百上千维,但道理一样:相似的内容,向量距离近

from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    api_key="sk-xxx",
)

# 把文字变成向量
vector = embeddings.embed_query("猫喜欢吃鱼")
print(len(vector))      # 1536(1536个数字组成的坐标)
print(vector[:5])       # [0.012, -0.034, 0.056, ...]

为什么这玩意儿能用来检索?

用户问:"喵星人吃什么?"
→ 变成向量 (1.21, 3.41, ...)
→ 和库里"猫喜欢吃鱼"的向量 (1.20, 3.40, ...) 距离很近!
→ 即使没有一个字相同,也能匹配上(因为"喵星人"≈"猫")

这就是 RAG 厉害的地方:理解语义,不是简单关键词匹配

对照传统搜索

传统关键词搜索:搜"喵星人" → 找不到"猫"的文章(字面不匹配)
向量语义搜索:  搜"喵星人" → 能找到"猫"的文章(意思相近)

3.4 向量数据库(Vector Store)

存放向量,并能快速找出最相似的

from langchain_community.vectorstores import Chroma

# 把切好的块向量化并存入
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
)

常见向量库对比(小白选型):

向量库 特点 适合
Chroma 轻量,本地,零配置 学习/小项目
FAISS Facebook出品,快,本地 中等规模
pgvector PostgreSQL插件 已有PG数据库(你Java项目用这个)
Milvus 分布式,海量 大规模生产
Pinecone 云服务,免运维 不想自己搭

3.5 检索(Retrieval)

从向量库里找出和问题最相关的几块。

# 创建检索器
retriever = vectorstore.as_retriever(
    search_kwargs={"k": 4}     # 返回最相关的4块(top-k)
)

# 检索
results = retriever.invoke("公司年假有几天?")
for doc in results:
    print(doc.page_content)    # 最相关的4个文本块

k 怎么选(小白):

k太小(如1):可能漏掉相关信息
k太大(如20):塞太多无关内容,干扰AI,浪费token
经验值:3-5

3.6 生成(Generation)

把检索到的资料 + 用户问题,拼成提示词给 AI。

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o-mini", api_key="sk-xxx")

# 提示词模板(关键:告诉AI"根据资料回答")
prompt = ChatPromptTemplate.from_template("""
请根据以下参考资料回答用户问题。
如果资料中没有相关信息,就说"我不知道",不要编造。

参考资料:
{context}

用户问题:{question}

回答:
""")

# 拼接资料
context = "\n\n".join(doc.page_content for doc in results)

# 生成答案
chain = prompt | llm
answer = chain.invoke({"context": context, "question": "年假几天?"})
print(answer.content)

提示词里"没有就说不知道"这句很重要——这是抑制 AI 瞎编(幻觉)的关键。


四、从零手写一个最简 RAG(理解原理)

不用任何框架,用最朴素的方式实现,让你看清 RAG 的"骨架":

import numpy as np
from openai import OpenAI

client = OpenAI(api_key="sk-xxx")

# ========== 阶段一:建库 ==========

# 1. 准备知识(这里直接用文本,省略加载和切分)
documents = [
    "公司年假政策:入职满一年的员工享有5天年假。",
    "公司病假政策:员工每年享有10天带薪病假。",
    "公司报销流程:发票需在月底前提交给财务部。",
    "公司工作时间:早9点到晚6点,午休一小时。",
]

# 2. 把每个文档变成向量
def get_embedding(text):
    resp = client.embeddings.create(model="text-embedding-3-small", input=text)
    return resp.data[0].embedding

doc_vectors = [get_embedding(doc) for doc in documents]   # 提前算好所有文档的向量

# ========== 阶段二:问答 ==========

# 3. 计算相似度(余弦相似度:越接近1越相似)
def cosine_similarity(a, b):
    a, b = np.array(a), np.array(b)
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def rag_answer(question, top_k=2):
    # 4. 问题也变向量
    q_vector = get_embedding(question)

    # 5. 算问题和每个文档的相似度,排序取最高的几个
    similarities = [cosine_similarity(q_vector, dv) for dv in doc_vectors]
    top_indices = np.argsort(similarities)[::-1][:top_k]   # 取相似度最高的top_k个

    # 6. 取出最相关的文档
    context = "\n".join(documents[i] for i in top_indices)
    print(f"[检索到的资料]\n{context}\n")

    # 7. 拼提示词,让AI回答
    prompt = f"""根据以下资料回答问题,没有就说不知道。

资料:
{context}

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

    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

# 测试
print(rag_answer("年假有几天?"))
# [检索到的资料] 公司年假政策:入职满一年的员工享有5天年假。...
# 回答:入职满一年的员工享有5天年假。

这 40 行就是 RAG 的全部本质! 框架做的事一模一样,只是帮你处理了加载、切分、向量库索引等工程细节。


五、用 LangChain 实现(生产级,简洁)

理解原理后,实际项目用框架,几行搞定:

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 1. 加载 + 切分
docs = TextLoader("knowledge.txt", encoding="utf-8").load()
chunks = RecursiveCharacterTextSplitter(
    chunk_size=500, chunk_overlap=50
).split_documents(docs)

# 2. 向量化 + 入库
vectorstore = Chroma.from_documents(chunks, OpenAIEmbeddings(api_key="sk-xxx"))
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

# 3. 构建 RAG 链(LCEL 管道)
prompt = ChatPromptTemplate.from_template("""
根据以下资料回答问题,没有就说不知道。
资料:{context}
问题:{question}
""")

def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | ChatOpenAI(model="gpt-4o-mini", api_key="sk-xxx")
    | StrOutputParser()
)

# 4. 提问
print(rag_chain.invoke("公司年假几天?"))

六、RAG 进阶优化(从"能用"到"好用")

基础 RAG 效果一般,真实项目要靠这些技巧提升:

6.1 检索优化

① 混合检索(关键词 + 向量)

纯向量:理解语义好,但精确匹配差(如产品型号"X-200")
纯关键词:精确匹配好,但不懂语义
混合:两者结合,效果最好

LangChain 用 EnsembleRetriever 实现

② 重排(Rerank)

# 先粗检索20个,再用专门的重排模型精排出最相关的4个
# 类比:海选20人 → 精选4人
# 用 Cohere Rerank 或 BGE-Reranker

③ 查询改写(Query Rewriting)

用户问得口语化/模糊 → 先让LLM改写成更适合检索的query
"那个假期咋回事" → 改写成 → "公司年假政策是什么"

6.2 切分优化

# 按语义/结构切,而非死板按字数
# - Markdown 按标题切
# - 代码按函数切
# - 加上"父子分段":检索用小块(精准),喂给AI用大块(完整上下文)

6.3 加入元数据过滤

# 给每块打标签,检索时先过滤
# 对照你Java项目的 knowledge_tag!
retriever = vectorstore.as_retriever(
    search_kwargs={
        "k": 4,
        "filter": {"category": "HR政策"}    # 只在HR类文档里搜
    }
)

这正是你 Java 项目里 RagAnswerfilterExpression: "knowledge == '知识库名称'" 在做的事!

6.4 防幻觉

# 1. 提示词强调"只根据资料,没有就说不知道"
# 2. 让AI标注答案来源(引用哪段资料)
# 3. 返回检索到的原文,让用户可核对

七、完整实战:知识库问答系统

整合所有知识,做一个可复用的 RAG 类:

from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


class KnowledgeBase:
    """知识库问答系统(对应你Java项目的 RagService)"""

    def __init__(self, api_key: str):
        self.embeddings = OpenAIEmbeddings(api_key=api_key)
        self.llm = ChatOpenAI(model="gpt-4o-mini", api_key=api_key, temperature=0)
        self.vectorstore = None
        self.splitter = RecursiveCharacterTextSplitter(
            chunk_size=500, chunk_overlap=50
        )

    def add_documents(self, file_paths: list[str]):
        """添加文档建立知识库(阶段一:建库)"""
        all_chunks = []
        for path in file_paths:
            # 按扩展名选加载器
            if path.endswith(".pdf"):
                loader = PyPDFLoader(path)
            else:
                loader = TextLoader(path, encoding="utf-8")
            docs = loader.load()
            chunks = self.splitter.split_documents(docs)
            all_chunks.extend(chunks)

        # 入库
        if self.vectorstore is None:
            self.vectorstore = Chroma.from_documents(all_chunks, self.embeddings)
        else:
            self.vectorstore.add_documents(all_chunks)
        print(f"✅ 已添加 {len(all_chunks)} 个知识块")

    def ask(self, question: str, k: int = 4) -> dict:
        """提问(阶段二:问答)"""
        if self.vectorstore is None:
            return {"answer": "知识库为空,请先添加文档", "sources": []}

        # 1. 检索
        docs = self.vectorstore.similarity_search(question, k=k)
        context = "\n\n".join(d.page_content for d in docs)

        # 2. 生成
        prompt = ChatPromptTemplate.from_template("""
你是一个专业的知识库助手。请严格根据以下参考资料回答问题。
如果资料中没有相关信息,请回答"抱歉,知识库中没有相关信息",不要编造。

参考资料:
{context}

问题:{question}

回答:""")

        chain = prompt | self.llm | StrOutputParser()
        answer = chain.invoke({"context": context, "question": question})

        # 3. 返回答案 + 来源(可溯源,防幻觉)
        return {
            "answer": answer,
            "sources": [d.page_content[:50] + "..." for d in docs],
        }


# ========== 使用 ==========
kb = KnowledgeBase(api_key="sk-xxx")

# 建库
kb.add_documents(["公司制度.txt", "产品手册.pdf"])

# 问答
result = kb.ask("公司年假有几天?")
print("答案:", result["answer"])
print("来源:", result["sources"])

八、常见问题排查(小白避坑)

问题 原因 解决
检索不到相关内容 chunk 太大/太小,或 embedding 模型差 调 chunk_size,换更好的 embedding
AI 还是瞎编 提示词没强调"根据资料" 加"没有就说不知道"
答案不完整 k 太小,或切分把内容切断 增大 k,增加 overlap
中文效果差 用了英文 embedding 模型 换中文友好的(如 bge-zh、m3e)
精确词(型号)搜不到 纯向量语义检索的弱点 加混合检索(关键词+向量)
速度慢 每次都重新建库 向量库持久化,只建一次

九、RAG 知识地图总结

RAG = 给AI配资料库,先查再答

阶段一 建库(离线):
  加载 → 切分(chunk_size/overlap) → 向量化(embedding) → 存储(向量库)

阶段二 问答(在线):
  提问 → 向量化 → 检索(top-k) → 拼提示词 → LLM生成 → 答案

核心概念:
  Embedding = 文字变坐标,相似的靠得近
  Chunking  = 把文档切成知识卡片
  向量库     = 能快速找最相似的柜子
  检索       = 找最相关的k块
  生成       = 资料+问题给AI,强调"按资料答"

进阶优化:
  混合检索 / 重排 / 查询改写 / 元数据过滤 / 防幻觉
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐