外观
从零搭建 RAG 应用
读十篇 RAG 教程,不如完整跑通一条 RAG 流水线。本文带你用纯 Python 从零搭一套可运行的个人知识库问答系统:没有魔法,每一行代码都知道在干什么。跑通之后,你会真正理解RAG的每一步在解决什么问题。
**检索增强生成(Retrieval-Augmented Generation, RAG)**是一种让大语言模型"先查资料、再回答问题"的架构:把私有文档切成片段、向量化后建库,回答问题时先检索出最相关的片段,连同问题一起交给 LLM 生成答案。它解决的痛点是 LLM 不知道你的私有资料、又喜欢一本正经地编造——见大语言模型里的"幻觉"讨论。
为什么值得亲手搭一遍?因为 RAG 的每个环节都有独立的权衡:切多大会丢语义、检索不到怎么办、检索到但答不对怎么办。框架(LangChain、LlamaIndex)把这套流程封装成了"几行代码",但对初学者来说,被封装的部分恰恰是坑最多的地方。先手写一遍,再用框架,你会发现自己能读得懂框架的报错和默认行为。
完整项目分为五个步骤,先看整体地图:
个人文档(.md / .txt / PDF)
│
▼
① 文档加载与切分 ────── 回答"知识怎么变成可检索的片段"
▼
② Embedding 与建库 ─── 回答"片段怎么变成可比较的向量"
▼
③ 检索(向量 + BM25)── 回答"怎么把最相关的片段找回来"
▼
④ Prompt 组装与生成 ── 回答"怎么让 LLM 只依据资料作答"
▼
⑤ 完整可运行 Demo ──── 端到端跑通 + 调优 + 评估前置知识
本文假设你已有基础:RAG 是什么(RAG 核心概念)、向量和语义检索的直觉(向量数据库与语义检索)、怎么和 LLM 打交道(提示词工程)。缺哪块先回去补,再来跑代码。想先看 RAG 在生产里的样子,可以读Perplexity 与 AI 搜索。
一、技术选型:先定四个组件
RAG 由四个可替换的组件拼成。选型原则一句话:能本地跑的先本地跑,能少装服务的少装服务。原型阶段选"免费 + 简单",上线阶段再按规模替换。
| 组件 | 选项 | 特点 | 适用场景 |
|---|---|---|---|
| Embedding 模型 | BGE(BAAI/bge-small-zh-v1.5、bge-m3) | 中文/多语言效果好,本地可跑,MIT 协议 | 中文知识库首选 |
| OpenAI text-embedding-3-small / large | API 调用,无需本地算力,多语言 | 已有 OpenAI API、追求省事 | |
| M3E / Jina Embeddings / Cohere | 各有侧重(中文、多语言、长文本) | 按评测数据挑 | |
| 向量库 | FAISS | 进程内索引,秒级建库,无服务进程 | 单机原型、中小规模(百万级以下) |
| Chroma | Python 内嵌、接口友好、支持持久化 | 原型快速迭代、教学演示 | |
| pgvector | PostgreSQL 扩展,与业务数据同库 | 已有 PG 的团队、要事务与权限 | |
| LLM | OpenAI GPT 系列 | 效果稳、省心 | 线上服务、预算充足 |
| Ollama 本地模型(qwen2.5、llama3) | 免费、私密、可控 | 数据不出内网、离线场景 | |
| DeepSeek / 通义 / 豆包等国产 API | 中文好、价格低 | 中文场景、成本敏感 | |
| 框架 | 纯手写(本文) | 每步透明、方便调优与教学 | 学习、需要精细控制 |
| LlamaIndex | 围绕"文档→知识库→问答"封装好 | 快速搭知识库问答 | |
| LangChain | 生态大、工具链全 | 复杂链路、多模型集成 |
三个选型相关的判断:
- Embedding 模型决定检索质量的上限。它把你的文本映射成向量,相似文档的向量才离得近。中文场景用 BGE 系(如
BAAI/bge-small-zh-v1.5,384 维、约 400MB,普通笔记本可跑);词面重合度高、想更省资源时再考虑轻量模型。模型清单与最新榜单见模型与榜单速查。 - 向量库先别纠结。原型阶段 FAISS 就够,它是检索层的"字典实现"——查得准、速度快、不引入运维负担。等数据到千万级、需要过滤/更新/权限时再迁移到 pgvector 或专门服务。
- LLM 与 Embedding 可以来自不同厂商。Embedding 用 BGE 本地算、生成用 GPT 或本地 Ollama,完全没问题,两者互不依赖。
关于框架的实话
框架(LangChain / LlamaIndex)本身不是坑,用框架当"黑盒"才是坑。本文先手写,是因为 RAG 的调优点全在细节里:切分边界怎么找、检索结果怎么融合、prompt 怎么写。这些在框架里是"默认参数",你看不见它们,也就不知道坏结果从哪来。跑通本文后,再用框架,你会更快上手——因为你知道它替你做了什么。
二、环境搭建
bash
# 创建虚拟环境并激活(永远不要把依赖装进系统 Python)
python -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows
# 核心依赖
pip install sentence-transformers faiss-cpu numpy openai jieba rank-bm25
# 可选:用 Chroma 替代 FAISS 时
pip install chromadb版本要求:Python 3.10+,sentence-transformers 3.x,faiss-cpu 1.7+(CPU 版够用)。
首次运行会下载 Embedding 模型(约 400MB),可先做冒烟测试:
python
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
v = model.encode(["你好,世界"], normalize_embeddings=True)
print(v.shape) # (1, 384),模型就绪三、分步实现
步骤 1:文档加载与切分(chunking)
切分(chunking)是把长文档切成检索单元。切分是 RAG 第一个、也往往是最重要的调优点:片段太小 → 单片段信息不足、检索命中但答案不全;片段太大 → 一个片段混进多个主题、向量被稀释,还浪费上下文窗口。常见策略:
| 策略 | 做法 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 固定窗口 + 重叠 | 按 N 字符切,相邻重叠 M 字符 | 实现最简单、最稳 | 可能切断句子/语义 | 通用基线,先跑这个 |
| 段落 / 标题切分 | 按 \n\n、标题层级切 | 保持语义完整 | 长段落仍超窗 | 结构清晰的 Markdown 文档 |
| 句子聚合 | 切句后按语义/长度聚合成块 | 边界语义完整 | 依赖切句质量 | 问答对、网页正文 |
| 递归字符切分 | 多级分隔符从大到小回退 | 兼顾结构与长度 | 仍需调 size 与 overlap | LangChain 用户 |
一句话判断
先用"固定窗口 500 字符 + 重叠 50"跑通,再按评估结果调。不要在项目第一天就追求花哨的切分策略——大多数坏结果来自更后面的环节。
先加载文档:
python
from pathlib import Path
def load_texts(directory: str = "./kb") -> list[dict]:
"""加载目录下所有 .txt / .md 文件,返回 [{"source": 路径, "text": 全文}]。"""
docs = []
for fp in sorted(Path(directory).glob("*")):
if fp.suffix.lower() not in {".txt", ".md"}:
continue
docs.append({"source": str(fp), "text": fp.read_text(encoding="utf-8")})
return docs再写切分函数。要点:固定窗口为主,落刀时尽量往"段落、句号"等自然边界上靠,并且让相邻片段有重叠,避免一个句子被拦腰截断:
python
def split_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""固定窗口切分 + 自然边界回退 + 相邻重叠。
overlap 必须小于 chunk_size,否则会死循环。
"""
assert overlap < chunk_size, "overlap 必须小于 chunk_size"
if len(text) <= chunk_size:
return [text]
chunks, start = [], 0
while start < len(text):
end = min(start + chunk_size, len(text))
if end < len(text):
# 在窗口后半段找最后一个自然边界(优先级:段落 > 换行 > 句号)
for sep in ("\n\n", "\n", "。", ";"):
pos = text.rfind(sep, start + chunk_size // 2, end)
if pos != -1:
end = pos + len(sep)
break
chunks.append(text[start:end])
start = end - overlap
return chunks
# 组织成统一的文档结构
def build_chunks(docs: list[dict], chunk_size=500, overlap=50) -> list[dict]:
"""返回 [{"source", "chunk_index", "text"}],后续建库与检索都用这份列表。"""
result = []
for doc in docs:
for i, chunk in enumerate(split_text(doc["text"], chunk_size, overlap)):
result.append({"source": doc["source"], "chunk_index": i, "text": chunk})
return result步骤 2:Embedding 与建库
Embedding 把每个片段变成固定维度的向量;语义相近的文本,向量方向也相近。这里用 BGE 中文模型做归一化编码(归一化后,余弦相似度等于向量内积,FAISS 用内积索引即可):
python
import json
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 384 维,中文/多语言
chunks = build_chunks(load_texts())
texts = [c["text"] for c in chunks]
# normalize_embeddings=True:向量单位化,使内积等价于余弦相似度
vecs = model.encode(texts, normalize_embeddings=True)
print("片段数:", len(texts), "向量维度:", vecs.shape) # 如 (N, 384)
np.save("kb_vecs.npy", vecs)
# 元数据(片段文本 + 来源)单独落盘,向量与文本一一对应
json.dump(chunks, open("kb_meta.json", "w", encoding="utf-8"), ensure_ascii=False)建 FAISS 索引。原型阶段用 IndexFlatIP(暴力内积检索,精确但检索量为线性);数据量上来后再换 IndexIVFFlat(聚类粗筛)或 HNSW 图索引:
python
import faiss
dim = vecs.shape[1]
index = faiss.IndexFlatIP(dim) # 内积索引(向量已归一化)
index.add(vecs.astype("float32")) # 必须 float32
faiss.write_index(index, "kb.index") # 落盘,之后加载即用
print("索引大小:", index.ntotal)向量数据库在这条链路里的位置
你可能会问:这跟向量数据库有什么关系?FAISS 只是"进程内索引",没有服务、没有网络接口、没有 CRUD。当你有多个客户端同时查询、要支持增删改和权限控制时,才需要真正的向量数据库(Chroma、pgvector 或云服务)。原型用 FAISS,工程化再上数据库——别让架构负担拖慢你验证想法。
步骤 3:检索:向量 + BM25 混合
检索的目标是"召回最相关的片段"。只靠向量召回有一个已知盲区:词面重合度高但语义不同的查询——例如查"苹果公司财报"时,"苹果"可能把大量水果相关文档顶上来。因此成熟的做法是向量 + BM25 混合检索,再用 RRF(Reciprocal Rank Fusion)融合排序。
向量召回:
python
def vector_search(query: str, k: int = 5) -> list[int]:
"""返回 top-k 片段在 chunks 里的下标。"""
qv = model.encode([query], normalize_embeddings=True)
scores, ids = index.search(qv.astype("float32"), k)
return [int(i) for i in ids[0]]BM25 是词频/逆文档频率的经典稀疏检索,对精确关键词非常敏锐(中文先分词):
python
import jieba
from rank_bm25 import BM25Okapi
tokenized = [list(jieba.cut(t)) for t in texts]
bm25 = BM25Okapi(tokenized)
def bm25_search(query: str, k: int = 5) -> list[int]:
scores = bm25.get_scores(list(jieba.cut(query)))
return scores.argsort()[::-1][:k].tolist()RRF 融合:不看分数绝对值,只看"排名",把两条检索结果的排序信息合并(超参数 60 是 RRF 论文的常规常数):
python
def rrf_fuse(rank_lists: list[list[int]], k: int = 60, top_n: int = 5) -> list[int]:
"""Reciprocal Rank Fusion:对每份排序列表按 1/(k+rank) 计分后相加。"""
scores: dict[int, float] = {}
for lst in rank_lists:
for rank, doc_id in enumerate(lst):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)[:top_n]
def hybrid_search(query: str, top_n: int = 5) -> list[dict]:
vec_ids = vector_search(query, 10) # 各自先多召回,再融合
bm25_ids = bm25_search(query, 10)
fused = rrf_fuse([vec_ids, bm25_ids], top_n=top_n)
return [chunks[i] for i in fused]一句话判断
混合检索几乎总是优于单一检索。BM25 保精确关键词,向量保语义相关,RRF 融合让两边互补。这是从"demo"迈向"能用"的第一个免费提升。
步骤 4:生成:Prompt 组装
检索到的片段要"灌进"prompt。组装遵循三条纪律(详见提示词工程):
- 限定知识来源:只依据给定资料回答,资料没有就明说"不知道",这是对抗幻觉的第一道闸。
- 带上来源:给每个片段编号并注明出处文件,让答案"可溯源"。
- 控制上下文体积:每个片段截断到合理长度,防止超窗口被截断、或无关文字稀释注意力。
python
def build_prompt(query: str, hits: list[dict], max_chars: int = 800) -> str:
context = "\n\n".join(
f"[片段 {i + 1}](来源:{h['source']})\n{h['text'][:max_chars]}"
for i, h in enumerate(hits)
)
return (
"你是一名个人知识库助手。请只依据下面提供的资料回答问题;"
"如果资料中没有答案,请直接回答『知识库中没有相关信息』,不要编造。\n\n"
f"### 资料\n{context}\n\n### 问题\n{query}\n\n### 回答"
)
def generate(query: str, hits: list[dict]) -> str:
prompt = build_prompt(query, hits)
resp = client.chat.completions.create(
model=LLM_MODEL, messages=[{"role": "user", "content": prompt}],
temperature=0.3, # 知识问答场景温度压低,减少自由发挥
)
return resp.choices[0].message.contenttemperature=0.3 是知识问答的常用设置——你不需要模型"有创意",需要它"忠实"。回答风格控制的更多技巧见提示词实战手册。
步骤 5:完整可运行 Demo
把前四步拼成一个自包含脚本 rag_demo.py。用法:把个人文档放进 kb/ 目录,首次运行加 --rebuild 建索引,之后直接提问:
python
"""
rag_demo.py —— 个人知识库问答(纯 Python 最小实现)
用法:
python rag_demo.py --rebuild # 首次:建索引
python rag_demo.py "什么是向量数据库?" # 提问
依赖:pip install sentence-transformers faiss-cpu numpy openai jieba rank-bm25
"""
import json
import sys
from pathlib import Path
import faiss
import jieba
import numpy as np
from openai import OpenAI
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
# ── 配置区 ──────────────────────────────────────────────
KB_DIR = Path("./kb") # 把你的 .md / .txt 放这里
EMBED_MODEL = "BAAI/bge-small-zh-v1.5" # 本地 Embedding 模型
LLM_MODEL = "gpt-4o-mini" # 生成模型(Ollama 本地见文末)
CHUNK_SIZE, CHUNK_OVERLAP = 500, 50
TOP_N = 4
# ─────────────────────────────────────────────────────────
client = OpenAI() # 需要 OPENAI_API_KEY 环境变量
model = SentenceTransformer(EMBED_MODEL)
def load_texts() -> list[dict]:
docs = []
for fp in sorted(KB_DIR.glob("*")):
if fp.suffix.lower() in {".txt", ".md"}:
docs.append({"source": str(fp), "text": fp.read_text(encoding="utf-8")})
return docs
def split_text(text: str, chunk_size=CHUNK_SIZE, overlap=CHUNK_OVERLAP) -> list[str]:
assert overlap < chunk_size
if len(text) <= chunk_size:
return [text]
chunks, start = [], 0
while start < len(text):
end = min(start + chunk_size, len(text))
if end < len(text):
for sep in ("\n\n", "\n", "。", ";"):
pos = text.rfind(sep, start + chunk_size // 2, end)
if pos != -1:
end = pos + len(sep)
break
chunks.append(text[start:end])
start = end - overlap
return chunks
def build_index():
"""切分 → Embedding → FAISS 建库 → 落盘(元数据 + 向量 + 索引)。"""
chunks = [
{"source": d["source"], "chunk_index": i, "text": c}
for d in load_texts()
for i, c in enumerate(split_text(d["text"]))
]
vecs = model.encode([c["text"] for c in chunks], normalize_embeddings=True)
index = faiss.IndexFlatIP(vecs.shape[1])
index.add(vecs.astype("float32"))
faiss.write_index(index, "kb.index")
np.save("kb_vecs.npy", vecs)
json.dump(chunks, open("kb_meta.json", "w", encoding="utf-8"), ensure_ascii=False)
print(f"索引完成:{len(chunks)} 个片段")
def load_index():
global chunks, index, bm25
chunks = json.load(open("kb_meta.json", encoding="utf-8"))
index = faiss.read_index("kb.index")
vecs = np.load("kb_vecs.npy")
bm25 = BM25Okapi([list(jieba.cut(c["text"])) for c in chunks])
def vector_search(q: str, k: int) -> list[int]:
qv = model.encode([q], normalize_embeddings=True)
_, ids = index.search(qv.astype("float32"), k)
return [int(i) for i in ids[0]]
def bm25_search(q: str, k: int) -> list[int]:
scores = bm25.get_scores(list(jieba.cut(q)))
return scores.argsort()[::-1][:k].tolist()
def hybrid_search(q: str, top_n: int = TOP_N) -> list[dict]:
scores: dict[int, float] = {}
for lst in (vector_search(q, 10), bm25_search(q, 10)):
for rank, did in enumerate(lst):
scores[did] = scores.get(did, 0.0) + 1.0 / (60 + rank)
return [chunks[i] for i in sorted(scores, key=scores.get, reverse=True)[:top_n]]
def ask(query: str) -> str:
hits = hybrid_search(query)
context = "\n\n".join(
f"[片段 {i + 1}](来源:{h['source']})\n{h['text'][:800]}"
for i, h in enumerate(hits)
)
prompt = (
"你是个人知识库助手。只依据下面的资料回答问题;资料中没有答案时,"
"直接回答『知识库中没有相关信息』,不要编造。\n\n"
f"### 资料\n{context}\n\n### 问题\n{query}\n\n### 回答"
)
resp = client.chat.completions.create(
model=LLM_MODEL, messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)
answer = resp.choices[0].message.content
# 引用溯源:把命中的片段附在答案后面,供人工核验
refs = "\n".join(f"- {h['source']}" for h in hits)
return f"{answer}\n\n**参考来源**:\n{refs}"
if __name__ == "__main__":
if "--rebuild" in sys.argv:
build_index()
load_index()
query = sys.argv[-1]
if query in {"--rebuild", "rag_demo.py"}:
print("请用:python rag_demo.py \"你的问题\"")
else:
print(ask(query))Ollama 本地版:把 client 和 LLM_MODEL 换成下面两行,其余代码不动,即可全离线运行:
python
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
LLM_MODEL = "qwen2.5:7b" # 任选支持工具调用的本地模型,先 ollama pull qwen2.5跑通后你应该能看到:答案带上参考来源、且能指出"知识库中没有相关信息"——这正是 RAG 相对裸 LLM 的两个核心改进。
四、关键调优点:从"能跑"到"好用"
RAG 的问题几乎都出在"检索没找对"或"生成没用好",按下面的顺序调,收益递减:
| 调优点 | 怎么调 | 经验取值 | 判断信号 |
|---|---|---|---|
| chunk 大小 | 做实验:256 / 500 / 800 / 1200 各跑一遍评估 | 300~800 字符起步 | 答案缺一半→太小;答案跑题→太大 |
| overlap | 跟随 chunk 同步调 | chunk 的 10%~20% | 句子被截断→加大 |
| top-k | 增大检索数量观察 | 3~8(融合前各召回 10) | 命中但答不全→加大;上下文太乱→减小 |
| 重排(rerank) | 向量召回 top 50 → cross-encoder 重排 → 取前 5 | 必做(效果显著) | 答案总在"第二正确"的位置 |
| 引用溯源 | 返回片段编号与出处文件 | 回答后附来源 | 用户无法验证答案→必须加 |
重排是性价比最高的一步。向量检索是"近似匹配",cross-encoder 重排器(如 BAAI/bge-reranker-v2-m3)则把"查询+候选片段"整体过一遍模型打分,精度高但慢,所以"先粗召回、再精重排"是标准姿势:
python
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
cands = vector_search(query, 50) # 先粗召回
pairs = [(query, chunks[i]["text"]) for i in cands]
scores = reranker.predict(pairs)
top = [cands[i] for i in scores.argsort()[::-1][:TOP_N]] # 再精排序五、评估:别凭"感觉好用"
RAG 分两段评估:检索段(找没找对)与生成段(答没答对)。没有评估,你永远不知道调 chunk 是变好还是变坏——这是新手和工程化的分水岭。
检索段指标(需要人工标注"每个问题对应哪几个正确片段",或从文档结构自动生成):
| 指标 | 公式 | 含义 |
|---|---|---|
| Recall@k | 命中的正确片段数 / 全部正确片段数 | 前 k 个结果里,找回了多少正确的 |
| MRR | 第一个正确结果排名的倒数,取均值 | "第一个就对"的顺畅程度 |
| Hit@k | 前 k 个里是否出现至少一个正确片段 | 够不够用 |
生成段指标:忠实度(faithfulness,答案是否可被检索片段支撑)与答案相关性(answer relevance,答没答到点上),常用 LLM-as-judge 让另一个模型打分——具体搭建流程见搭建一套 LLM 评估,指标体系的完整理论见LLM 评估与基准。
评估的纪律
- 先建评估集再调参:收集 30~100 条"问题 + 正确片段 + 参考答案",没有评估集的所有调优都是自我安慰。
- 检索与生成分开测:生成再好,检索召回为 0 也白搭;先单独优化检索指标,再上生成。
- 每改一个变量跑一遍:同时改 chunk 和 top-k 和重排,出了好结果也不知道是哪个的功劳。
六、常见坑
| 坑 | 症状 | 根因 | 对策 |
|---|---|---|---|
| 切分切断语义 | 答案缺一半、前后不搭 | 固定窗口正好把段落/句子劈开 | 加 overlap、向自然边界回退、按段落切 |
| 检索不到 → 幻觉 | 模型一本正经编造 | 相关片段没召回,模型只能"圆场" | 混合检索、加大召回、rerank,prompt 明令"不知道就直说" |
| 未过滤元数据 | 检索常命中页脚/版权/目录 | 页脚文字混进片段并被索引 | 清洗阶段剔除样板文本、加过滤规则 |
| embedding 未归一化 | 相似度分数不可比 | 余弦相似度与内积混用 | 统一 normalize_embeddings=True |
| 上下文被截断 | 答案答到一半、或忽略后半资料 | 片段数 × 片段长超过窗口 | 截断单片段、控制 top-k、按需取长上下文模型 |
| 索引不更新 | 新文档问不到 | 改完文档忘了重建索引 | 把建索引纳入文档更新流程(CI/定时任务) |
| 只测一两个问题 | "感觉"很好,上线就崩 | 样本太少、恰好都命中 | 建 30+ 条评估集,量化指标 |
| 检索命中但答错 | 资料明明有,答案却错 | prompt 未约束、或片段里混入干扰 | 检查 prompt 约束、检查片段纯度、调 rerank |
更多通用工程反模式见常见陷阱与反模式。
七、进阶路线
跑通并评估过基础 RAG 之后,按需进阶:
- GraphRAG:多跳与全局问答。基础 RAG 对"跨多个片段的关系型问题"(如"哪几篇文档都提到了同一件事")力不从心。把文档抽成实体—关系知识图谱再检索,可支撑多跳推理——原理见知识图谱与知识注入。
- Agentic RAG:把检索变成工具。让 Agent 自主决定"何时检索、检索几次、是否追问澄清、是否改写查询"。多轮澄清能显著改善模糊问题,查询改写能缓解"问法跟文档写法不一致"——这正是从零开发一个 Agent里讲的工具调用循环在 RAG 上的应用,概念基础见Agent 核心概念。
- 流式与缓存。生成端做 SSE 流式输出改善体验;对高频重复问题做语义缓存(相似问题直接命中缓存答案),省 token 也省延迟——见推理优化与量化和部署与推理优化实战。
- Embedding 微调。领域术语多、通用 Embedding 区分不开时,可用领域数据微调 Embedding 模型,见微调与 PEFT。
收尾判断
一套"合格"的 RAG:评估集上有稳定指标、回答带引用、资料没有时能诚实说不知道。做到这三点,它就已经强于大部分"裸 LLM + 知识库文件"的伪 RAG 了。术语不懂的随时查术语表,全景图见什么是 AI 热门概念。
八、延伸阅读
- RAG 核心概念 —— 本文的完整理论版:RAG 的分类、流程、缺陷与变体
- 向量数据库与语义检索 —— 向量索引原理、相似度度量、数据库选型
- 提示词工程 —— prompt 组装、上下文约束、few-shot 技巧
- 提示词实战手册 —— 本文 prompt 写法背后的系统方法论
- LLM 评估与基准 —— 检索与生成指标的理论基础
- 搭建一套 LLM 评估 —— 把第五节落地成可跑的评估管道
- 从零开发一个 Agent —— Agentic RAG 的工具调用实现
- 知识图谱与知识注入 —— GraphRAG 进阶的理论入口
- Perplexity 与 AI 搜索 —— 生产级 RAG 产品的完整解剖
- 常见陷阱与反模式 —— 第六节的 25 项完整版
- 模型与榜单速查 —— Embedding 与 LLM 的最新选型数据
- 总体架构解剖 —— 从本文的小项目放大到生产系统
参考资料
- Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks(RAG 原论文,arXiv 2005.11401) —— 提出 RAG 架构的原始论文,必读
- Retrieval-Augmented Generation for Large Language Models: A Survey(arXiv 2312.10997) —— 2024 年的 RAG 系统综述,含切分、检索、生成全链路技术盘点
- sentence-transformers 文档 —— BGE 等 Embedding 模型的官方使用说明
- BAAI/bge 系列模型(Hugging Face) —— 中文 Embedding 模型的下载与用法
- FAISS 官方文档 —— 向量索引的索引类型选择与调优指南
- Chroma 官方文档 —— Python 内嵌向量数据库的上手文档
- pgvector 官方文档 —— PostgreSQL 向量扩展
- rank-bm25(PyPI) —— BM25 稀疏检索实现
- OpenAI Embeddings 文档 —— 云端 Embedding API
- Ollama 官网 —— 本地 LLM 的安装与模型清单