04 · RAG 全流程:从切分到引用溯源

本篇对应代码:app/rag/loader.pyembedding.pyvectorstore.pyretriever.py

RAG 是整套系统里最容易被低估的一环。 很多人以为它就是"切分 + embedding + top-k",但真正决定效果的是那些细节。

一、先看整体链路

bash 复制代码
Markdown 文档
   │  ① 结构优先切分(按标题 → 递归按句 → 保留重叠)
   ▼
Chunks(带 title / heading / source 元数据)
   │  ② Embedding(openai 语义向量 / hashing 本地向量)
   ▼
向量库(memory / pgvector / chroma)
   │
用户提问 ──┬─▶ ③ 向量召回 top20 ──┐
           └─▶ ③ BM25 召回 top20 ─┴─▶ ④ RRF 融合 ─▶ ⑤ 重排 ─▶ ⑥ 阈值兜底 ─▶ top5
                                                                              │
                                                                              ▼
                                                          ⑦ 拼上下文(带 [编号])──▶ LLM
                                                                              │
                                                                              ▼
                                                          ⑧ 答案带引用 ──▶ 前端可展开原文

二、① 切分:结构优先,而不是按字数硬切

python 复制代码
def split_markdown(doc, chunk_size=480, overlap=80):
    # 1) 按 Markdown 标题(#/##/###)切块,记录标题路径
    # 2) 超长块再递归切:\n\n → \n → 。 → ;→ ,
    # 3) 相邻块保留 overlap

为什么这么做

  • 按字数硬切会把"定义"和"例外"拆开 → 模型只看到半句 → 答错
  • 标题路径(成本管理制度 > 预算与超支管控)有两个用途:重排加权答案引用展示
  • 递归切分保证了"优先在自然边界断开"

参数:chunk_size=480, overlap=80。 实测 300~600 区别不大,制度类文档段落短,480 能装下完整条款。

三、② Embedding:两套实现,一键切换

实现 适用 说明
OpenAIEmbedder 生产 text-embedding-3-small / 通义 v3,1024 维
HashingEmbedder 离线/演示 零依赖,纯本地计算

HashingEmbedder 的原理(面试可以讲):

python 复制代码
# 中文:单字 + 相邻双字(bigram);英文:按词
toks = tokenize(text)
# sublinear tf:1 + log(tf),避免长文本权重爆炸
w = 1.0 + math.log(count)
# 有符号哈希投影(hashing trick)
h = md5(token); idx = h % dim; sign = ±1
vec[idx] += sign * w
# L2 归一化 → 点积即余弦

质量不如语义向量,但全链路是真的 :切分、入库、混合召回、RRF、重排、阈值全部执行。 (这是"无 Key 也能演示"的关键一环)

四、③ 混合召回:为什么不能只用向量

这是我实测最有感的一点:

  • 用户问 "P0 缺陷多久修复" ------ P0 这种编号在语义向量里几乎没有区分度
  • 用户问 "超支 20%" ------ 数字是关键词的强项、向量的弱项
  • 用户问 "太湖网关 鉴权" ------ 专有名词向量容易漂移

所以双路召回:

python 复制代码
vec_hits  = store.search(qvec, candidate_k)   # 向量语义召回
kw_scores = bm25.scores(query)                # BM25 关键词召回

BM25 我手写了简化版(避免强依赖),公式:

scss 复制代码
score = Σ idf(t) * (f * (k1+1)) / (f + k1 * (1 - b + b * dl/avgdl))

五、④ RRF 融合:为什么不用加权求和

python 复制代码
# Reciprocal Rank Fusion
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 只看排名,零参数、鲁棒性好

六、⑤ 重排:成本与效果的平衡

python 复制代码
class HeuristicReranker:   # 默认,零调用成本
    score = 0.6 * vector_score + 0.3 * 词覆盖度 + 0.1 * 标题命中

class LLMReranker:         # RERANK_BACKEND=llm 时启用
    # 让模型对候选打 0~10 分

成本对比(面试可讲):

方案 每次额外调用 适用
启发式 0 默认,性价比最高
bge-reranker(本地) 0(GPU/CPU 推理) 生产推荐
LLM 打分 top-20 = 20 次 效果最好但太贵

七、⑥ 低分兜底:抑制幻觉的关键

python 复制代码
if docs[0].vector_score < threshold and docs[0].keyword_score <= 0:
    return []   # 明确告诉 Agent:知识库没覆盖

这是整个 RAG 里我最想强调的一点。

检索不到时如果硬塞一段不相关的上下文,模型会"看着它编"------ 这是企业场景幻觉的最大来源 。返回空 → Agent 明确回复"知识库未覆盖,建议人工确认", 用户体验反而更好,因为可信

八、⑦⑧ 引用溯源:企业用户只信可溯源答案

拼上下文时给每个 chunk 编号:

csharp 复制代码
[1] 来源:缺陷分级与处理规范 · 缺陷等级定义
P0 | 核心功能不可用、资损... | 15 分钟 | 4 小时

Prompt 里强制要求"句末标注 [编号]",前端把编号渲染成可展开的原文卡片。

为什么值得做 :在企业里,一个答案能不能被采用, 往往不取决于它多聪明,而取决于用户能不能验证它。 这一点比提高 2 个百分点的准确率更能决定项目能否上线。

九、向量库:三种实现同一个接口

python 复制代码
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 建表:

sql 复制代码
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。

十、效果演示

ini 复制代码
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"面板能直接看到三个分数,调参时非常直观。


下一篇:05 · Agent 编排:LangGraph 状态图与降级内核

相关推荐
再吃一根胡萝卜1 小时前
11 · 如何把项目写进简历与面试
面试
再吃一根胡萝卜7 小时前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜7 小时前
03 · 后端:FastAPI 与分层架构
面试
Interview Aid11212 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
ocean210316 小时前
2025-2026年AI提效与实践大厂面试高频问题
人工智能·面试·职场和发展·提示词工程·ai提效
程序员梅雨17 小时前
Linux & Shell 实用干货
linux·运维·服务器·后端·面试·php
YonyouHRSaaS19 小时前
AI视频面试系统定义、功能作用、品牌推荐、选择攻略
人工智能·面试·职场和发展·ai面试·视频面试·ai视频面试
小鱼爱吃草灬灬20 小时前
实时面试辅助排查清单:音频来源、问题输入与上下文
面试·职场和发展·音视频
颜挺锐1 天前
如何轻松通过性能测试面试之第九篇:RPO、RTO是什么意思
面试·职场和发展