← 返回博客
2026-06-18 11:00:00

从零搭建你的第一个 RAG 应用:手把手教程 + 深度理解

从零搭建你的第一个 RAG 应用:手把手教程 + 深度理解

2026-06-18 11:00:00 · 标签:RAG、LangChain、OpenAI、GPT、教程、AI工程

阅读指南

这篇文章分为两部分:

你可以先跟着上篇动手,再读下篇;也可以直接跳到感兴趣的部分。


上篇:手把手教程

前提条件

开始之前,确保你的环境满足以下条件:

没有 OpenAI API Key? 也可以用开源模型替代——参见《从零搭建你的第一个 RAG 应用(开源模型篇):零成本、全本地、可商用》。

第 1 步:安装依赖

打开终端,创建项目文件夹并安装所需库:

mkdir my-rag-app && cd my-rag-app
pip install langchain langchain-community langchain-openai faiss-cpu pypdf

#或者国内镜像
pip install langchain langchain-community langchain-openai faiss-cpu pypdf -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn

其他可选镜像:https://mirrors.aliyun.com/pypi/simple/、https://pypi.mirrors.ustc.edu.cn/simple/

装了什么?

作用
langchainRAG 流程的核心框架
langchain-community社区贡献的文档加载器、向量库集成
langchain-openaiOpenAI 的 LLM 和 Embedding 接口
faiss-cpuMeta 开源的向量检索库(CPU 版,免配)
pypdf读取 PDF 文件

第 2 步:准备你的文档

在项目文件夹里新建一个 data/ 目录,把你想"问"的文档丢进去:

mkdir data
# 把你的 PDF、MD、TXT 文件复制到 data/ 里

比如你可以放一份《员工手册》PDF、几篇技术博客的 Markdown 文件,或者产品的 API 文档。先放 2-3 份文件就好,跑通了再加更多。


第 3 步:加载文档 → 切成小块

这是 RAG 的第一步:把原始文件读进来,切成合适大小的"文本块"(chunk)。

为什么不能直接把整份 PDF 丢给 AI?因为大模型的上下文窗口有限,而且把整本书塞进去,它反而抓不住重点。所以我们要先把文档切成几百字的小块,每次只挑最相关的几块送给 AI。

新建 main.py,写入:

import os
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# ========== 第 3 步:加载与分割文档 ==========

def load_and_split(data_dir="./data"):
    """把 data/ 里的所有文档读进来,切成小块"""

    # --- 3.1 加载 PDF ---
    pdf_loader = DirectoryLoader(
        data_dir,
        glob="**/*.pdf",
        loader_cls=PyPDFLoader
    )
    documents = pdf_loader.load()

    # --- 3.2 加载 TXT 和 Markdown ---
    text_loader = DirectoryLoader(
        data_dir,
        glob="**/*.{txt,md}",
        loader_cls=TextLoader
    )
    documents += text_loader.load()

    if not documents:
        raise FileNotFoundError(f"data/ 目录下没找到任何文档,请放入 PDF/TXT/MD 文件。")

    # --- 3.3 分割 ---
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=600,        # 每块最多 600 个字符
        chunk_overlap=100,     # 相邻两块重叠 100 个字符
        separators=["\n\n", "\n", "。", ".", " ", ""]
    )
    chunks = splitter.split_documents(documents)

    print(f"加载了 {len(documents)} 个文档,切成 {len(chunks)} 个文本块")
    # 打印第一个块看看长什么样
    print(f"\n第一个文本块预览:\n{chunks[0].page_content[:300]}...")
    return chunks


if __name__ == "__main__":
    load_and_split()

跑一下看看效果:

python main.py

你应该看到类似这样的输出:

加载了 3 个文档,切成 47 个文本块
第一个文本块预览:
第3章 请假制度
员工每年享有5天带薪年假...
chunk_size 设多大? - 中文文档:400-800 字比较合适 - 英文文档:256-512 tokens - 太小的块语义不完整,太大的块检索不精确 chunk_overlap 为什么重要? 假设关键信息正好卡在第 300 和第 301 个字之间——如果没有重叠,这句话会被切成两半,AI 永远看不到完整的。100 个字符的重叠像个"安全网",确保关键信息至少完整出现在一个块里。

第 4 步:把文本块变成"向量",存进向量库

这是 RAG 最核心的概念,我们用最直觉的方式理解它:

什么是向量? 把一句话变成一个 1000 多个数字的数组。两句话意思越接近,它们在数学空间里的距离就越近。

什么是向量库? 一个专门存"向量 + 原文"的数据库,可以根据"距离远近"快速找到最相似的内容。

流程是这样的:

你问:"年假有几天?"
    ↓ Embedding 模型把它变成向量 [0.03, -0.12, 0.47, ...]
    ↓ 向量库在所有文本块中找距离最近的
    ↓ 找到:"员工每年享有5天带薪年假..."
    ↓ 把这句话和你的问题一起发给 AI
    ↓ AI 回答:"根据员工手册,每年享有5天带薪年假。"

main.py 中追加:

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

# ========== 第 4 步:构建向量库 ==========

def build_index(chunks, persist_dir="./vector_store"):
    """把文本块转为向量,存入 FAISS 向量库"""

    # 使用 OpenAI 的 Embedding 模型
    embeddings = OpenAIEmbeddings(
        model="text-embedding-3-small",  # 性价比最高,1024维
        openai_api_key=os.getenv("OPENAI_API_KEY")  # 从环境变量读取
    )

    # 逐块计算向量,构建索引
    vector_store = FAISS.from_documents(chunks, embeddings)

    # 保存到磁盘(下次就不用重新计算了)
    vector_store.save_local(persist_dir)
    print(f"向量库已保存到 {persist_dir}/")
    return vector_store
为什么要 save_local Embedding 计算要调 API,如果你的文档很多,每次都重新计算既慢又花钱。保存后下次直接加载就行。

配置 API Key:

在终端里设置环境变量(每次开新终端都要设一次):

# Mac / Linux
export OPENAI_API_KEY="sk-你的key"

# Windows PowerShell
$env:OPENAI_API_KEY="sk-你的key"

# Windows CMD
set OPENAI_API_KEY=sk-你的key

第 5 步:实现最基础的问答

现在把"检索"和"生成"串起来。追加到 main.py

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# ========== 第 5 步:问答 ==========

def ask(question, vector_store):
    """根据向量库中的文档回答问题"""

    llm = ChatOpenAI(
        model="gpt-4o-mini",
        temperature=0,
        openai_api_key=os.getenv("OPENAI_API_KEY")
    )

    # 检索最相关的 4 个文本块
    docs = vector_store.similarity_search(question, k=4)

    # 拼接成上下文
    context = "\n\n---\n\n".join(
        f"[来源: {d.metadata.get('source', '未知')}]\n{d.page_content}"
        for d in docs
    )

    # 构造提示词
    prompt = ChatPromptTemplate.from_template("""
你是一个知识助手。请根据以下参考资料回答问题。
如果资料不足以回答,请诚实地说"根据现有资料无法确定"。

参考资料:
{context}

问题:{question}

回答:
""".strip())

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

    # 打印结果
    print(f"\n检索到 {len(docs)} 个相关片段:")
    for i, d in enumerate(docs):
        print(f"  [{i+1}] {d.metadata.get('source', '?')}: {d.page_content[:80]}...")

    print(f"\n回答:\n{answer}")
    return answer

第 6 步:跑起来!

把前面几步串成一个完整的交互程序。修改 main.py 最后的 if __name__ == "__main__":

if __name__ == "__main__":
    import os

    if not os.getenv("OPENAI_API_KEY"):
        print("请先设置 OPENAI_API_KEY 环境变量")
        print("   Mac/Linux: export OPENAI_API_KEY='sk-...'")
        print("   Windows:   $env:OPENAI_API_KEY='sk-...'")
        exit(1)

    # 如果已有向量库就直接加载,否则先构建
    if os.path.exists("./vector_store/index.faiss"):
        print("加载已有向量库...")
        embeddings = OpenAIEmbeddings(
            model="text-embedding-3-small",
            openai_api_key=os.getenv("OPENAI_API_KEY")
        )
        vector_store = FAISS.load_local(
            "./vector_store", embeddings,
            allow_dangerous_deserialization=True
        )
    else:
        print("首次运行,构建向量库...")
        chunks = load_and_split("./data")
        vector_store = build_index(chunks)

    # 交互式问答
    print("\n" + "="*50)
    print("  你的文档问答助手已就绪!")
    print("  输入问题开始对话,输入 q 退出")
    print("="*50 + "\n")

    while True:
        question = input("你的问题:").strip()
        if question.lower() == "q":
            print("再见!")
            break
        if not question:
            continue
        ask(question, vector_store)
        print("\n" + "-"*50)

完整运行:

python main.py

你会进入一个交互式对话界面:

首次运行,构建向量库...
加载了 3 个文档,切成 47 个文本块
向量库已保存到 ./vector_store/

==================================================
  你的文档问答助手已就绪!
  输入问题开始对话,输入 q 退出
==================================================

你的问题:年假有几天?

检索到 4 个相关片段:
  [1] 员工手册.pdf: 第3章 请假制度。员工每年享有5天带薪年假,工作满1年后可申请...
  [2] 考勤制度.pdf: 年假申请需提前3个工作日提交OA审批...
  ...

回答:
根据《员工手册》的规定,员工每年享有5天带薪年假,工作满1年后可申请。
申请需要提前3个工作日通过OA系统提交。

你的问题:q
再见!

恭喜! 🎉 你刚刚从零搭建了一个能"读懂"你的文档、回答你的问题的 AI 助手。这就是 RAG。


第 6.5 步:理解你刚刚做了什么(上篇小结)

停下来想一下整个流程,这对后面深入理解很重要:

你的文档(PDF/MD/TXT)
    ↓ [加载] 读入程序
    ↓ [分割] 切成几百字的小块
    ↓ [向量化] 每块变成一个 1000+ 维的数学向量
    ↓ [存储] 向量 + 原文存入 FAISS 向量库
    ════════════════════════ 以上是"准备阶段",只做一次
    ↓
你的问题
    ↓ [向量化] 问题也变成向量
    ↓ [检索] 在向量库中找距离最近的 K 个文本块
    ↓ [拼接] 把文本块 + 问题组合成提示词
    ↓ [生成] 发给 GPT/Claude 生成回答
    ════════════════════════ 以上是"问答阶段",每次提问都走一遍

你的程序做了两件聪明的事:

  1. 检索(Retrieval):不是全文关键词匹配(Ctrl+F),而是在"语义空间"里找——"年假"和"带薪休假"在向量空间里距离很近,即使字面上完全不同。
  2. 增强生成(Augmented Generation):AI 不需要"记住"你的文档,而是在你提问的瞬间动态检索最相关的内容,作为参考资料喂给它。

这就是 RAG 这五个字母的全部秘密。


下篇:深度理解与进阶

上篇我们跑通了一个最朴素的 RAG。它已经能用了,但你很快会发现问题:

下面我们逐一解决这些问题,探讨 RAG 每个环节的深度设计。


一、文档分割:RAG 中最被低估的环节

为什么分割策略决定了下限?

想象你在图书馆找书。如果每本书都被撕成随机大小的碎片,即使图书管理员(向量检索)再厉害,也很难帮你找到完整的信息。分割策略就是决定"每一片多大""从哪里撕开"的问题。

三种分割方式,什么时候用哪种

方式 1:按字符数分割(最常用、最基础)

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=600,
    chunk_overlap=100,
    separators=["\n\n", "\n", "。", ".", " ", ""]
)

Recursive 的精妙之处在于 separators优先级:它先尝试在段落处断开(\n\n),如果段落太长才退而求其次在句子处断开(),实在不行才在空格处断。这样能最大程度保持语义完整。

经验值

方式 2:按文档结构分割(有标题时优先用)

from langchain.text_splitter import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ("#", "h1"),
    ("##", "h2"),
    ("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
docs = splitter.split_text(markdown_content)
# 每个块会带上它的标题层级作为 metadata
# 比如 doc.metadata = {"h1": "第三章", "h2": "请假制度"}

这有个巨大的好处:检索时你可以利用 metadata 做过滤——"只要第三章的内容"、"只要关于请假制度的部分"。这是让 RAG 从玩具走向可用的关键一步。

最佳实践:先用 MarkdownHeaderTextSplitter 按结构分割,再对过长的块用 RecursiveCharacterTextSplitter 二次分割。

方式 3:语义分割(最智能但最慢)

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

splitter = SemanticChunker(
    OpenAIEmbeddings(),
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=90
)

原理:计算相邻句子的向量相似度。当相似度突然大幅下降时——说明话题切换了——就在那里断开。比固定长度更合理,但代价是分割阶段就要调 Embedding API。

简单粗暴的选择指南

你的文档推荐策略
纯文本、没什么结构RecursiveCharacterTextSplitter
Markdown、有标题层级MarkdownHeaderTextSplitter + Recursive 二次分割
话题跳跃大、预算充足SemanticChunker
混合格式按文档类型分别处理

二、Embedding:从"搜关键词"到"搜意思"

直观理解

2026 年主流 Embedding 模型对比

模型维度最佳场景成本
OpenAI text-embedding-3-small512/1536性价比首选,中英文都够用$0.02/1M tokens
OpenAI text-embedding-3-large256/1024/3072精度要求高时用$0.13/1M tokens
BGE-M3(开源·BAAI)1024中文场景最优,支持稀疏+稠密混合自部署
Cohere embed-v41024压缩率高,延迟敏感场景有免费额度
Jina jina-embeddings-v31024长文本(8192 token 输入)有免费额度

选型逻辑(不用记,需要时回来查)

  1. 先看语言:中文为主 → BGE-M3 或 text-embedding-3-large;英文为主 → 都可以
  2. 再看规模:< 1 万条 → 随便选,差别不大;> 10 万条 → 关注维度和存储成本
  3. 最后看预算:想省钱 → text-embedding-3-small 或自部署 BGE-M3
维度高一定更好吗? 不。1024 维在绝大多数场景下已经够用。768 → 1024 的收益递减,1024 → 3072 更是边际效应。更高维度 = 更多存储 + 更慢检索,要权衡。

三、检索策略:从"找到"到"找对"

朴素检索用的是 similarity_search——找向量空间中距离最近的 K 个。这在工作中有几个典型问题:

问题 1:检索结果冗余——4 个片段其实是同一段话

场景:你的文档里,"年假政策"在 5 个地方用不同措辞重复了 5 遍。朴素检索返回的 4 个片段全是关于年假的——第 5 名可能是"病假政策",但根本没被选上。

解法:MMR(最大边际相关性)

retriever = vector_store.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 4,          # 最终返回 4 个
        "fetch_k": 20,   # 先拉 20 个候选,再从中挑 4 个多样化的
        "lambda_mult": 0.6  # 0=最多样化, 1=最相关, 0.5-0.7 平衡
    }
)

MMR 做了件聪明的事:它不只挑"最相关的",还尽量避免挑"跟前一个太像的"。结果就是你会同时拿到"年假政策""病假政策""事假政策"——覆盖更全面。

问题 2:用户的查询自带条件——"张三写的支付设计文档"

场景:用户说"找张三写的支付模块设计文档"。朴素检索只会用整个句子做向量搜索,"张三"和"支付模块"这些结构化信息被淹没在语义噪声里。

解法:自查询检索(Self-Querying)

from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo

metadata_field_info = [
    AttributeInfo(name="author", description="文档作者", type="string"),
    AttributeInfo(name="date", description="文档日期", type="string"),
    AttributeInfo(name="category", description="文档分类", type="string"),
]

retriever = SelfQueryRetriever.from_llm(
    llm, vector_store, "文档库",
    metadata_field_info
)

# 用户输入:"张三写的支付模块设计文档"
# LLM 自动拆解:
#   - 结构化过滤:author = "张三", category = "支付模块"
#   - 语义搜索:"设计文档"
# 两个条件同时生效

这是实际业务中最常用的高级特性——用户查询几乎总是"XX 写的关于 XX 的 XX"这种格式。

问题 3:同一个问题需要从多个角度检索

场景:用户问"怎么提升数据库性能"。你只做一次检索,可能只命中"索引优化"相关的内容,漏掉了"查询改写""连接池配置""缓存策略"这些同样相关的方面。

解法:多查询检索(Multi-Query)

from langchain.retrievers.multi_query import MultiQueryRetriever

retriever = MultiQueryRetriever.from_llm(
    retriever=base_retriever,
    llm=llm
)

# 原始查询:"怎么提升数据库性能"
# LLM 自动生成变体:
#   1. "数据库性能优化方法有哪些"
#   2. "SQL慢查询的排查和解决方案"
#   3. "数据库索引优化的最佳实践"
# 三次检索结果合并去重 → 覆盖更全面

变体数量控制在 3-5 个——太少起不到多角度效果,太多增加延迟且引入噪声。


四、高级范式:当朴素 RAG 不够用

当你把上面的改进都加上了,系统在大多数情况下表现不错。但还有几个"刁钻"场景:

HyDE:问题太短,检索不到

问题:用户输入"那个转账失败"——只有 6 个字。向量搜索用这 6 个字去匹配几百字的文档片段,距离不可靠。

HyDE 的思路很反直觉:先让 LLM 编一个答案,用这个编出来的答案去做检索。

# 第一步:让 LLM 编一个假设性答案
# 用户:"那个转账失败"
# → LLM 编:"转账失败通常是以下原因:1. 账户余额不足 2. 风控规则拦截
#    3. 银行通道异常 4. 收款账户信息错误。排查步骤:先查余额,再查日志..."

# 第二步:用这个充满"专业词汇"的长答案去检索
# → 轻松命中"转账失败排查指南""风控规则配置""银行通道监控"等文档

为什么有效?因为 HyDE 把短查询中缺失的"领域词汇"补全了。用户可能不知道"风控拦截"这个词,但 LLM 生成的假设性回答里自然会有。

hyde_prompt = ChatPromptTemplate.from_template("""
请根据以下问题,写一段简短的科普性回答。
即使不确定,也给出你的最佳猜测,尽量使用专业术语。

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

# 用假设性回答的向量去检索
hypo_text = (hyde_prompt | llm | StrOutputParser()).invoke({"question": question})
docs = vector_store.similarity_search(hypo_text, k=5)

CRAG:检索回来的文档真的有用吗?

朴素 RAG 默认"检索结果一定相关"。但向量检索有时候会抽风——明明不相关的文档因为措辞巧合被排在前面。

CRAG(Corrective RAG)在检索后加了一个自检环节

class RelevanceCheck(BaseModel):
    relevant: bool
    reason: str

def crag_retrieve(question, vector_store, llm):
    # 1. 检索
    docs = vector_store.similarity_search(question, k=5)

    # 2. 逐条检查:这篇文档真的相关吗?
    verified = []
    for doc in docs:
        check = llm.with_structured_output(RelevanceCheck).invoke(
            f"这篇文档与问题相关吗?\n问题:{question}\n文档:{doc.page_content[:300]}"
        )
        if check.relevant:
            verified.append(doc)

    # 3. 如果相关文档太少,向外求助(Web 搜索)
    if len(verified) < 2:
        print("本地文档不足,触发 Web 搜索...")
        web_docs = web_search(question)  # 用 Tavily 或类似 API
        verified.extend(web_docs)

    return verified

CRAG 的核心哲学:不要假设检索结果一定是对的。验证、筛选、补救——三步闭环。

混合检索:稠密 + 稀疏,两条腿走路

向量检索(稠密)擅长语义匹配,但遇到精确查找就拉胯——搜"订单号 20240618-XXXX"时向量检索不懂这是 ID 精确匹配。

关键词检索(稀疏,如 BM25)恰好相反——精确匹配 ID、日期、代码片段是强项,但没有语义理解能力。

2026 年生产环境的标配:两者结合

from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever

# 稀疏检索:关键词匹配
bm25_retriever = BM25Retriever.from_documents(all_docs)

# 稠密检索:语义匹配
dense_retriever = vector_store.as_retriever(search_kwargs={"k": 5})

# 集成:两个结果加权融合
ensemble = EnsembleRetriever(
    retrievers=[dense_retriever, bm25_retriever],
    weights=[0.7, 0.3]  # 语义为主,关键词为辅
)

五、生产化考量:Demo 和产品的距离

延迟优化

一个典型的 RAG 请求要走:查询改写 → 向量检索 → 相关性判断 → LLM 生成。每一步都有延迟。

优化点具体做法节省
查询改写用小模型gpt-4o-mini 替代 gpt-4o~500ms
Embedding 批量计算多条文档一起 Embedding50-80%
向量索引预加载服务启动时加载到内存~200ms
流式输出Token 级 Streaming体感大幅提升
多路检索并行稠密/稀疏同时跑40-60%

成本控制:分层模型策略

# 一个生产级 RAG 的模型分工
rewrite_llm  = ChatOpenAI(model="gpt-4o-mini")   # 查询改写 → 便宜
judge_llm    = ChatOpenAI(model="gpt-4o-mini")   # 相关性判断 → 便宜
answer_llm   = ChatOpenAI(model="gpt-4o")        # 最终回答 → 质量优先
# Embedding
embeddings   = OpenAIEmbeddings(model="text-embedding-3-small")  # 够用就好

经验法则:检索侧的 LLM 调用全用 mini 模型,只给最终生成用大模型。成本下降 60-80%,回答质量几乎不变。

缓存

相同的问题会反复出现(尤其在客服场景)。缓存两次 LLM 调用和 Embedding 计算会省不少钱:

from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache

set_llm_cache(InMemoryCache())  # 进程内缓存
# 生产环境用 Redis:同一问题跨进程共享缓存

通常有 15-30% 的缓存命中率——意味着每省下 3 次 API 调用就有 1 次。

评估:不要盲目调参

没有评估的优化是盲目的。RAGAS 是目前最成熟的 RAG 评估框架:

from ragas import evaluate
from ragas.metrics import (
    faithfulness,         # 回答是否忠于检索到的文档(有没有胡编)
    answer_relevancy,     # 回答是否切题
    context_precision,    # 检索到的文档是否精确相关
    context_recall,       # 检索是否找到了所有相关信息
)

result = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)

四个指标各盯一个环节:


六、什么时候不该用 RAG

RAG 不是银弹。以下场景请三思:

场景为什么不适合替代方案
"总结我所有文档的核心观点"RAG 每次只看 K 个片段,无法把握全局Map-Reduce 或长上下文模型
"对比所有产品的价格"精确的结构化查询SQL 直接查数据库
实时性要求 < 500ms多层 LLM 调用延迟不可控预计算 + 缓存,或不用 RAG
知识更新极度频繁(秒级)向量库重建跟不上直接 Fine-tune 或只用搜索

七、2026 年的前沿方向


总结

这篇文章从零走到生产级,核心就三条:

  1. 找到真正相关的内容——靠分割策略、Embedding 选型、混合检索和查询改写。这决定了系统的召回率(该找的都找到了吗)。
  2. 不让模型被噪声带偏——靠 MMR 去重、上下文压缩、相关性自检。这决定了系统的精确率(找到的都有用吗)。
  3. 平衡成本与延迟——靠分层模型、缓存和并行架构。这决定了系统能不能上线

朴素 RAG 只是一个起点。从 Multi-Query 到 CRAG,从混合检索到 Agentic RAG,每一步改进都在缩小"原型"和"产品"的差距。

最好的 RAG 系统,是用户感受不到"检索"存在的系统——他们只是在提问,然后得到准确的回答。

希望这篇指南让你的 RAG 之路少些弯路。如果只有一句话带走,那就是:先跑通最简单的版本,然后针对你实际遇到的问题逐步加组件——而不是一开始就把所有高级范式堆上去。


参考资料 - LangChain 官方文档:https://python.langchain.com/docs - RAGAS 评估框架:https://docs.ragas.io - Gao, Y. et al. "Retrieval-Augmented Generation for Large Language Models: A Survey" - FAISS: https://github.com/facebookresearch/faiss