RAG:把你的数据交给模型
LLM 的知识冻结在训练截止;你的文档每周都在变。检索增强生成(RAG)把它解冻:查询时抓取你数据的相关切片、粘进提示词、让模型凭证据带引用作答。模型做推理;索引做记忆。
流水线,一个完整算例
拿一本 12,000 词元的维护手册、从头到尾走一遍:
- 分块。 先按标题切(小节完整存活)、再把超过 ~400 词元的小节二次切、保留 ~60 词元重叠(10–15%)。合理的块尺寸在 200–800 词元。结果:大约 35 块。
- 嵌入。 每块变成一个向量——bge-m3 每块发 1,024 个浮点。CPU 上整本手册花几秒。
- 索引。 把向量 + 文本 + 元数据(源文件、标题路径)存进向量库。
- 检索。 嵌入问题、按余弦相似度抓前 k = 5 块(4–10 是清醒范围)。
- 生成。 组装提示词:指令、带 ID 标签的 5 块、问题。
按语义边界分块胜过固定窗口。与表头行分离的表、或在第 4 步被切断的编号流程、无论嵌入器多好都是不可检索的垃圾。
两个立刻回本的动作:把标题路径(manual › §4 › Bolts)存为元数据以便精确过滤和引用、并原样嵌入用户键入的查询——“清理”式预处理通常让检索更糟而不是更好。
嵌入:多语种的硬要求
嵌入器决定“相似”是什么意思。对韩语和韩英混合语料、选择很窄——仅英语的嵌入器对韩语查询无声失败、返回貌似合理但错误的块:
| 模型 | 维度 | 备注 |
|---|---|---|
| bge-m3 | 1024 | 多语种、强韩语——安全的本地默认 |
| nomic-embed-text-v1.5 | 768 | 扎实英语、长上下文 |
| all-MiniLM-L6-v2 | 384 | 快、以英语为中心、韩语弱 |
嵌入模型很小(0.1–0.6B 参数);一颗 CPU 核嚼几千块——CPU 推理。语料嵌入一次、查询在提问时嵌入。每个索引保持嵌入器冻结:中途换模型让向量来自不同空间、相似度分数变成噪声。
向量库
| 库 | 何时 |
|---|---|
| sqlite-vec | 单用户、最多 ~100 万向量、零运维——它是一个文件 |
| ChromaDB | 带元数据过滤的本地优先原型 |
| Qdrant | 生产:过滤、混合搜索、规模 |
三者都本地跑。从 sqlite-vec 开始;真撞到墙再搬、不要提前。
组装提示词
只根据下面的摘录回答。每条主张都引用为 [n]。
如果摘录不含答案,回答“文档中没有”。
[1] (manual §4.2) Torque the M8 bolt to 12 Nm after lubrication...
[2] (manual §7.1) Do not re-use stretch bolts...
Question: M8 螺栓的扭矩规格是什么?
引用让用户核实而不是信任。“not in the docs” 的出口要紧:没有它、模型宁可发明答案也不愿破坏格式。
混合搜索与重排序
纯向量搜索漏掉精确字符串——错误码、函数名、零件号——关键词搜索一击即中。混合 = BM25 + 向量、用倒数排名融合合并;它近乎免费、且可靠地胜过任一单独使用。
要质量、加重排序器:检索 20–50 个候选、用交叉编码器重排序回前 5(bge-reranker 家族——与 bge-m3 同样的多语种故事)。重排序器把块和查询一起读、单向量嵌入器做不到。覆盖多数需求的便宜配方:混合检索 25、重排到 5。
分诊:检索失手还是生成失手?
调任何东西之前、先回答一个问题:正确的块被检索到了吗?
- 打印失败查询的前 k 块、带分数。
- 如果承载答案的块不在列表里、那是检索问题——修分块、嵌入、或加大 k。
- 如果它在列表里但答案是错的、那是生成问题——更大模型、更好提示词、重排把干扰项挤出去。
这一个测试省下数小时调错半条流水线的时间。
失败模式
| 症状 | 原因 | 修法 |
|---|---|---|
| 检索到正确文档、答案错误 | 前 k 里有干扰项 | 重排;缩小 k |
| 答案在文档里却从不浮现 | 前 k 饥饿或坏的边界 | 加大 k;按标题分块 |
| 自信但过时的答案 | 索引比文档老 | 变更时重建索引 |
| 韩语查询漏掉英语文档 | 单语嵌入 | bge-m3 |
| 引用指向错误的块 | 块丢了来源元数据 | 每块存来源 + 标题 |
测量、不要凭感觉
保留一个 20–50 条已知答案的真实问题的黄金集。分开记两个数:检索命中率(正确块在前 k 里吗?)和答案正确性。块尺寸、嵌入器、或 k 每次改动后跟踪它们。没有黄金集的 RAG 工作是占星术。
何时改用微调
微调改变行为——格式、风格、术语。它是加事实的坏办法:模型不能引用它被调过的内容、新文档意味着新的训练轮。经验法则:事实 → RAG、行为 → LoRA、两者都只在提示词用尽之后:微调。
本地栈,从小到大
Open WebUI 内置文档上传和混合检索——从它开始、你可能永远不用写流水线代码。RAG 诚实的核心是 ~50 行;写一遍、你就知道框架们在干什么:
chunks = chunk_by_header(read("manual.md"), max_tokens=400, overlap=60)
index = [(embed(c), c) for c in chunks] # 一次性完成
def answer(question):
hits = topk_by_cosine(embed(question), index, k=5)
ctx = "\n\n".join("[{}] {}".format(i, c) for i, (v, c) in enumerate(hits))
prompt = TEMPLATE.replace("{ctx}", ctx).replace("{q}", question)
return chat(prompt) # 小型本地模型足矣
需要权限、定时刷新或评估 harness 时再够 LlamaIndex 或 LangChain——不为上面的循环。对韩语文档具体而言、多语种嵌入(bge-m3)不是可选项、基于标题的分块应当像尊重 markdown ## 标题一样尊重韩语文档结构(장/절 标题)。