|
LLM 两大局限决定了 RAG 的存在价值:
1. **知识不是实时的**:训练周期长、更新昂贵,不可能实时训练
2. **知识缺针对性**:不知道你私有的领域/业务知识
RAG = Retrieval-Augmented Generation(检索增强生成):先检索少量相关片段,再交给 LLM 组织答案——**开卷考试**。
|
|
| 维度 | RAG(开卷) | 微调(闭卷背书) |
|------|------------|----------------|
| 知识更新 | 换文档即时生效 | 需重训 |
| 成本 | 低(检索+prompt) | 高(训练算力) |
| 可溯源 | 可给引用出处 | 黑盒 |
| 幻觉 | 有拒答兜底空间 | 依赖训练质量 |
| 适用 | 知识频繁变化/私有文档 | 风格/格式/稳定性要求 |
|
|
核心术语表:
| 术语 | 含义 |
|------|------|
| Embeddings | 文本→定长浮点向量,语义相近则向量相近 |
| Chunking | 文档切块,决定检索粒度 |
| Vector DB | 存向量并支持 ANN 近邻检索 |
| Retriever | 检索器抽象(统一接口换后端) |
| Top-K | 召回片段数(经验 3~5) |
| Rerank | 对召回结果精排(CrossEncoder) |
| 倒排索引 | 词→文档列表(关键词检索的底座,BM25) |
|
|
**离线建索引 4 步 + 在线检索生成 5 步**:
```text
离线:加载文档 → 切块(chunk) → 向量化(embed) → 入库(vector store)
在线:Query → (改写) → 向量化 → 检索TopK → (重排/融合) → 拼 Prompt → LLM → 答案(+引用)
```
LangChain 十行链路:
```python
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
docs = TextLoader("llama2.pdf").load()
chunks = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20).split_documents(docs)
db = FAISS.from_documents(chunks, OpenAIEmbeddings())
retriever = db.as_retriever(search_kwargs={"k": 3})
```
|
|
chunk_size / overlap 权衡:
- 经验值:chunk_size 200~1000 字符;overlap 取 10%~20%
- 太小:语义被切碎,检索到的片段不足以回答
- 太大:噪声多、TopK 塞不下、embedding 稀释语义
- FAISS 构造三选一:`from_documents(docs, emb)` / `from_texts(texts, emb, metadatas, ids)` / `from_embeddings(pairs, emb)`
**Retriever 抽象的价值**:统一接口(`invoke(query)`)、可换后端、可组合(多路/重排)、异步与框架兼容——不要在业务代码里直接持有 FAISS 对象。
|
|
提示词模板五要素 + 占位符填充:
```python
prompt_template = """
你是一个专业的AI问答助手。
任务:根据下述给定的文档信息回答用户问题。 # 角色+任务
重要约束:完全基于给定信息回答;信息不足时直接回复"无法回答此问题"。 # 约束+拒答
参考文档:
__INFO__ # 信息注入
用户问题:__QUERY__
你的回答: # 输出要求(需要引用时给示例:[来源:price_01])
"""
def build_prompt(template, **kwargs):
prompt = template
for k, v in kwargs.items():
v = '\n'.join(v) if isinstance(v, list) else str(v)
prompt = prompt.replace(f"__{k.upper()}__", v)
return prompt
```
|
|
相似度计算:
$$\cos\theta = \frac{A \cdot B}{\|A\|\|B\|} \quad\text{(越大越相似)}$$
```python
from numpy import dot
from numpy.linalg import norm
def cos_sim(a, b): return dot(a, b) / (norm(a) * norm(b))
```
BM25(关键词检索经典打分,ES 默认思想):词频饱和(k1)+ 文档长度归一(b),无上界、同查询内可比分;弱点是"词汇鸿沟"——同义改写检不到,正是向量检索的切入点。
|
|
| 索引 | 原理 | 特点 |
|------|------|------|
| Flat | 暴力全扫 | 精确,小库可用 |
| IVF | 先聚类分桶,查近邻桶 | 牺牲少量召回换速度 |
| PQ | 向量分段量化压缩 | 省内存,精度略降 |
| HNSW | 多层跳表式图 | 高召回低延迟,主流 |
|
|
选型速查:
- 学习/Demo:**Chroma**(嵌入式,5 行起步)
- 生产亿级/国内事实标准:**Milvus**(docker-compose 部署)
- 已有 PG 栈:**pgvector**(`CREATE EXTENSION vector`,`<=>` 余弦距离 + hnsw 索引)
- 极致性能:**FAISS**(F1 发动机,0.19ms/2000QPS 级)
- 轻量二进制部署:**Qdrant**(v1.19 支持快照备份、payload 过滤)
- ES 8.x 也可 `dense_vector` + `_knn_search` 做向量
经验:混合检索(关键词+向量)几乎是生产标配;"像不像"用向量,"属不属于"(元数据过滤)用结构化条件。
|
|
FAISS 关键认知:
```python
import faiss, numpy as np
index = faiss.IndexFlatL2(dim)
index.add(np.array(embeddings, dtype='float32'))
faiss.write_index(index, "x.index")
loaded = faiss.read_index("x.index")
dis, ind = loaded.search(query_vec, k) # 返回(距离, 下标)
```
- **index 只存向量不存文本**——原文映射靠 LangChain 的 docstore(`db.save_local / load_local(allow_dangerous_deserialization=True)`)
- 得分解读:`similarity_search_with_score` 是 L2 距离(越小越好);`similarity_search_with_relevance_scores` 归一到 0~1(越大越好)
- 期望答案可能只排第 3——严格场景把 Top3 都送 LLM
|
|
pgvector:
```sql
CREATE EXTENSION vector;
CREATE TABLE docs (id bigserial PRIMARY KEY, content text,
embedding vector(1536));
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
-- 余弦距离检索
SELECT content FROM docs ORDER BY embedding <=> '[...]' LIMIT 3;
```
Milvus(MilvusClient):
```python
client.create_collection("rag", dimension=1536)
client.insert("rag", [{"id": i, "vector": v, "text": t} for ...])
res = client.search("rag", data=[query_vec], limit=3)
```
|
|
**混合检索 + RRF 融合**:
$$\text{RRF}(d) = \sum_{r \in \text{routes}} \frac{1}{k + \text{rank}_r(d)} \quad (k=60)$$
```python
def rrf_merge(list_kw, list_vec, k=60, top_n=5):
scores = {}
for lst in (list_kw, list_vec):
for rank, doc in enumerate(lst):
scores[doc] = scores.get(doc, 0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)[:top_n]
```
**坑**:BM25 零分结果会污染 RRF——融合前先过滤零分项(实测"打断"这类词双路命中、"插嘴"仅向量命中,过滤后准确率显著提升)。
|
|
- **两阶段**:Embedding 双塔召回(快而粗)→ CrossEncoder 交叉精排(慢而准),如 `cross-encoder/ms-marco-MiniLM-L-6-v2` / bge-reranker
- **查询改写**:RAG-Fusion(多视角改写+融合)、MultiQueryRetriever、HyDE(先让 LLM 生成假设答案再检索)
- **分块进阶**:父子块 small-to-big(小块检索、大块送 LLM)
|
|
RAGAS 四指标 + 检索指标:
| 指标 | 含义 |
|------|------|
| faithfulness | 答案是否忠于检索内容(防幻觉) |
| answer_relevancy | 答案与问题的相关性 |
| context_precision | 检索内容的精准度 |
| context_recall | 应检内容覆盖率 |
| Recall@K / MRR / NDCG | 检索排序质量 |
工程上先建 golden 测试集(直答/同义改写/库外拒答三类考点),再跑自动评估。
|
|
十大陷阱(Checklist 精选):
1. 中文切分必须传 `length_function=len`(默认按字符数会错)
2. 建库与查询必须用**同一个 embedding 模型**(换模型=全量重建;ada-002 与 text-embedding-3 向量空间不互通)
3. Top-K 不是越大越好(3~5 起步)
4. 必须有拒答兜底,防止硬编
5. 检索无结果直接返回"无法回答",不要强行生成
6. 引用格式要给具体示例(`[来源:price_01]`),否则模型自由发挥
|
|
向量 RAG 五大痛点:多跳推理弱(A→B→C 串不起来)、全局摘要弱、关系型问题差、片段间关联丢失、无法解释推理路径。
GraphRAG 思路:实体-关系图 + 向量双索引,检索沿图走 k-hop 子图。
|
|
离线构图 9 步(MVP 路线):
```text
文档切块 → LLM抽取实体/关系/属性 → 实体归一(同义词合并)
→ 建图(Neo4j) → 实体/关系分别向量化 → 双索引入库(图+向量)
```
常见坑:切块破坏跨段关系、实体归一不全导致图碎片化、子图爆炸(限制 k-hop=2 起步)。
|
|
在线 7 步:查询理解 → 种子节点定位 → k-hop 子图扩展 → 证据打包 → 生成(带路径引用)→ 自检 → 回退向量检索。
评估指标:Faithfulness(事实一致)、Coverage(覆盖)、Path Explainability(路径可解释)、Citation Rate(引用率)、Latency。
选型:简单事实问答用向量 RAG;多跳/关系/全局摘要类问题上 GraphRAG。
|
MTEB 向量模型评测
斯坦福 IR Book