04 · RAG 全流程:从切分到引用溯源
英文标题:04 · RAG 全流程:从切分到引用溯源
RAG 是整套系统里最容易被低估的一环。 很多人以为它就是"切分 + embedding + top-k",但真正决定效果的是那些细节。 一、先看整体链路 二、① 切分:结构优先,而不是按字数硬切 为
8.9 KB
中文
本篇对应代码:
app/rag/loader.py、embedding.py、vectorstore.py、retriever.py
RAG 是整套系统里最容易被低估 的一环。 很多人以为它就是"切分 + embedding + top-k",但真正决定效果的是那些细节。
一、先看整体链路
Markdown 文档
│ ① 结构优先切分(按标题 → 递归按句 → 保留重叠)
▼
Chunks(带 title / heading / source 元数据)
│ ② Embedding(openai 语义向量 / hashing 本地向量)
▼
向量库(memory / pgvector / chroma)
│
用户提问 ──┬─▶ ③ 向量召回 top20 ──┐
└─▶ ③ BM25 召回 top20 ─┴─▶ ④ RRF 融合 ─▶ ⑤ 重排 ─▶ ⑥ 阈值兜底 ─▶ top5
│
▼
⑦ 拼上下文(带 [编号])──▶ LLM
│
▼
⑧ 答案带引用 ──▶ 前端可展开原文
二、① 切分:结构优先,而不是按字数硬切
def split_markdown(doc, chunk_size=480, overlap=80):
为什么这么做 :
- 按字数硬切会把"定义"和"例外"拆开 → 模型只看到半句 → 答错
- 标题路径(
成本管理制度 > 预算与超支管控)有两个用途:重排加权 和 答案引用展示 - 递归切分保证了"优先在自然边界断开"
参数:chunk_size=480, overlap=80。 实测 300~600 区别不大,制度类文档段落短,480 能装下完整条款。
三、② Embedding:两套实现,一键切换
| 实现 | 适用 | 说明 |
|---|---|---|
OpenAIEmbedder |
生产 | text-embedding-3-small / 通义 v3,1024 维 |
HashingEmbedder |
离线/演示 | 零依赖,纯本地计算 |
HashingEmbedder 的原理 (面试可以讲):
toks = tokenize(text)
w = 1.0 + math.log(count)
h = md5(token); idx = h % dim; sign = ±1
vec[idx] += sign * w
质量不如语义向量,但全链路是真的 :切分、入库、混合召回、RRF、重排、阈值全部执行。 (这是"无 Key 也能演示"的关键一环)
四、③ 混合召回:为什么不能只用向量
这是我实测最有感的一点:
- 用户问 "P0 缺陷多久修复" ——
P0这种编号在语义向量里几乎没有区分度 - 用户问 "超支 20% " —— 数字是关键词的强项、向量的弱项
- 用户问 "太湖网关 鉴权" —— 专有名词向量容易漂移
所以双路召回:
vec_hits = store.search(qvec, candidate_k)
kw_scores = bm25.scores(query)
BM25 我手写了简化版(避免强依赖),公式:
score = Σ idf(t) * (f * (k1+1)) / (f + k1 * (1 - b + b * dl/avgdl))
五、④ RRF 融合:为什么不用加权求和
for rank, hit in enumerate(vec_hits): score[hit] += 1 / (60 + rank + 1)
for rank, hit in enumerate(kw_hits): score[hit] += 1 / (60 + rank + 1)
理由 :向量分是 01 的余弦,BM25 分可能是 0 20+, 加权求和需要调参且随语料漂移;RRF 只看排名,零参数、鲁棒性好 。
六、⑤ 重排:成本与效果的平衡
class HeuristicReranker:
score = 0.6 * vector_score + 0.3 * 词覆盖度 + 0.1 * 标题命中
class LLMReranker:
成本对比 (面试可讲):
| 方案 | 每次额外调用 | 适用 |
|---|---|---|
| 启发式 | 0 | 默认,性价比最高 |
| bge-reranker(本地) | 0(GPU/CPU 推理) | 生产推荐 |
| LLM 打分 | top-20 = 20 次 | 效果最好但太贵 |
七、⑥ 低分兜底:抑制幻觉的关键
if docs[0].vector_score < threshold and docs[0].keyword_score <= 0:
return []
这是整个 RAG 里我最想强调的一点。
检索不到时如果硬塞一段不相关的上下文,模型会"看着它编"—— 这是企业场景幻觉的最大来源 。返回空 → Agent 明确回复"知识库未覆盖,建议人工确认", 用户体验反而更好,因为可信 。
八、⑦⑧ 引用溯源:企业用户只信可溯源答案
拼上下文时给每个 chunk 编号:
[1] 来源:缺陷分级与处理规范 · 缺陷等级定义
P0 | 核心功能不可用、资损… | 15 分钟 | 4 小时
Prompt 里强制要求"句末标注[编号]",前端把编号渲染成可展开的原文卡片。
为什么值得做 :在企业里,一个答案能不能被采用, 往往不取决于它多聪明,而取决于用户能不能验证它 。 这一点比提高 2 个百分点的准确率更能决定项目能否上线。
九、向量库:三种实现同一个接口
class BaseVectorStore:
def add(self, chunks, vectors): ...
def search(self, vector, top_k) -> List[Tuple[Chunk, float]]: ...
| 实现 | 适用 | 说明 |
|---|---|---|
InMemoryVectorStore |
Demo | numpy 余弦,可落盘 JSON + npy |
PgVectorStore |
生产推荐 | 和业务库同实例,事务一致,HNSW 索引 |
ChromaVectorStore |
轻量独立部署 |
pgvector 建表:
CREATE TABLE eap_chunks (
id TEXT PRIMARY KEY, doc_id TEXT, text TEXT,
metadata JSONB, embedding vector(1024)
);
CREATE INDEX ON eap_chunks USING hnsw (embedding vector_cosine_ops);
选 pgvector 的理由 :不用再维护一套中间件,运维成本低; 数据量到千万级再考虑 Milvus。
十、效果演示
GET /api/knowledge/search?query=P0 缺陷多久修复
[1] 缺陷分级与处理规范 · 缺陷等级定义 score=0.42 vector=0.31 bm25=8.7
[2] 缺陷分级与处理规范 · 处理流程 score=0.35 vector=0.35 bm25=2.1
[3] 告警推送与值班SOP · Agent自动推送约束 score=0.21 vector=0.28 bm25=0.0
前端"知识库/RAG"面板能直接看到三个分数,调参时非常直观。
English / 原文
本篇对应代码:
app/rag/loader.py、embedding.py、vectorstore.py、retriever.py
RAG 是整套系统里最容易被低估 的一环。 很多人以为它就是"切分 + embedding + top-k",但真正决定效果的是那些细节。
一、先看整体链路
Markdown 文档
│ ① 结构优先切分(按标题 → 递归按句 → 保留重叠)
▼
Chunks(带 title / heading / source 元数据)
│ ② Embedding(openai 语义向量 / hashing 本地向量)
▼
向量库(memory / pgvector / chroma)
│
用户提问 ──┬─▶ ③ 向量召回 top20 ──┐
└─▶ ③ BM25 召回 top20 ─┴─▶ ④ RRF 融合 ─▶ ⑤ 重排 ─▶ ⑥ 阈值兜底 ─▶ top5
│
▼
⑦ 拼上下文(带 [编号])──▶ LLM
│
▼
⑧ 答案带引用 ──▶ 前端可展开原文
二、① 切分:结构优先,而不是按字数硬切
def split_markdown(doc, chunk_size=480, overlap=80):
为什么这么做 :
- 按字数硬切会把"定义"和"例外"拆开 → 模型只看到半句 → 答错
- 标题路径(
成本管理制度 > 预算与超支管控)有两个用途:重排加权 和 答案引用展示 - 递归切分保证了"优先在自然边界断开"
参数:chunk_size=480, overlap=80。 实测 300~600 区别不大,制度类文档段落短,480 能装下完整条款。
三、② Embedding:两套实现,一键切换
| 实现 | 适用 | 说明 |
|---|---|---|
OpenAIEmbedder |
生产 | text-embedding-3-small / 通义 v3,1024 维 |
HashingEmbedder |
离线/演示 | 零依赖,纯本地计算 |
HashingEmbedder 的原理 (面试可以讲):
toks = tokenize(text)
w = 1.0 + math.log(count)
h = md5(token); idx = h % dim; sign = ±1
vec[idx] += sign * w
质量不如语义向量,但全链路是真的 :切分、入库、混合召回、RRF、重排、阈值全部执行。 (这是"无 Key 也能演示"的关键一环)
四、③ 混合召回:为什么不能只用向量
这是我实测最有感的一点:
- 用户问 "P0 缺陷多久修复" ——
P0这种编号在语义向量里几乎没有区分度 - 用户问 "超支 20% " —— 数字是关键词的强项、向量的弱项
- 用户问 "太湖网关 鉴权" —— 专有名词向量容易漂移
所以双路召回:
vec_hits = store.search(qvec, candidate_k)
kw_scores = bm25.scores(query)
BM25 我手写了简化版(避免强依赖),公式:
score = Σ idf(t) * (f * (k1+1)) / (f + k1 * (1 - b + b * dl/avgdl))
五、④ RRF 融合:为什么不用加权求和
for rank, hit in enumerate(vec_hits): score[hit] += 1 / (60 + rank + 1)
for rank, hit in enumerate(kw_hits): score[hit] += 1 / (60 + rank + 1)
理由 :向量分是 01 的余弦,BM25 分可能是 0 20+, 加权求和需要调参且随语料漂移;RRF 只看排名,零参数、鲁棒性好 。
六、⑤ 重排:成本与效果的平衡
class HeuristicReranker:
score = 0.6 * vector_score + 0.3 * 词覆盖度 + 0.1 * 标题命中
class LLMReranker:
成本对比 (面试可讲):
| 方案 | 每次额外调用 | 适用 |
|---|---|---|
| 启发式 | 0 | 默认,性价比最高 |
| bge-reranker(本地) | 0(GPU/CPU 推理) | 生产推荐 |
| LLM 打分 | top-20 = 20 次 | 效果最好但太贵 |
七、⑥ 低分兜底:抑制幻觉的关键
if docs[0].vector_score < threshold and docs[0].keyword_score <= 0:
return []
这是整个 RAG 里我最想强调的一点。
检索不到时如果硬塞一段不相关的上下文,模型会"看着它编"—— 这是企业场景幻觉的最大来源 。返回空 → Agent 明确回复"知识库未覆盖,建议人工确认", 用户体验反而更好,因为可信 。
八、⑦⑧ 引用溯源:企业用户只信可溯源答案
拼上下文时给每个 chunk 编号:
[1] 来源:缺陷分级与处理规范 · 缺陷等级定义
P0 | 核心功能不可用、资损… | 15 分钟 | 4 小时
Prompt 里强制要求"句末标注[编号]",前端把编号渲染成可展开的原文卡片。
为什么值得做 :在企业里,一个答案能不能被采用, 往往不取决于它多聪明,而取决于用户能不能验证它 。 这一点比提高 2 个百分点的准确率更能决定项目能否上线。
九、向量库:三种实现同一个接口
class BaseVectorStore:
def add(self, chunks, vectors): ...
def search(self, vector, top_k) -> List[Tuple[Chunk, float]]: ...
| 实现 | 适用 | 说明 |
|---|---|---|
InMemoryVectorStore |
Demo | numpy 余弦,可落盘 JSON + npy |
PgVectorStore |
生产推荐 | 和业务库同实例,事务一致,HNSW 索引 |
ChromaVectorStore |
轻量独立部署 |
pgvector 建表:
CREATE TABLE eap_chunks (
id TEXT PRIMARY KEY, doc_id TEXT, text TEXT,
metadata JSONB, embedding vector(1024)
);
CREATE INDEX ON eap_chunks USING hnsw (embedding vector_cosine_ops);
选 pgvector 的理由 :不用再维护一套中间件,运维成本低; 数据量到千万级再考虑 Milvus。
十、效果演示
GET /api/knowledge/search?query=P0 缺陷多久修复
[1] 缺陷分级与处理规范 · 缺陷等级定义 score=0.42 vector=0.31 bm25=8.7
[2] 缺陷分级与处理规范 · 处理流程 score=0.35 vector=0.35 bm25=2.1
[3] 告警推送与值班SOP · Agent自动推送约束 score=0.21 vector=0.28 bm25=0.0
前端"知识库/RAG"面板能直接看到三个分数,调参时非常直观。