重排序

💡 重排(Rerank)是 RAG 检索链路的最后一道质量关口:先用便宜的方式把候选面铺宽,再用贵的方式精挑细选。本文面向开发者,讲清楚它”为什么存在、有哪些做法、怎么配参数、怎么评测、有哪些坑”。

一、为什么需要重排:两阶段检索架构

RAG 的检索环节普遍采用召回(Recall)→ 重排(Rerank)的两阶段架构。召回阶段用向量检索或 BM25 从海量文档中快速取回 Top 50200 个候选,追求的是召回率——别把正确答案漏掉;但快是有代价的,召回结果里必然混入大量”语义相近但不真正回答问题”的噪音。重排阶段则用更强的模型对这批候选精算相关性,选出 Top 510 送入 LLM 上下文,追求的是精确率——喂给模型的每一段都要值得占上下文窗口。

1
2
3
4
flowchart LR
Q["用户 Query"] --> R["召回 Recall · Top 50~200"]
R --> RR["重排 Rerank · Top 5~10"]
RR --> L["LLM 基于高相关上下文生成答案"]
Read More

向量化

向量化(Embedding)是 RAG(Retrieval-Augmented Generation,检索增强生成)系统中最底层的一环:它把文本变成高维浮点向量,让”语义是否相近”变成”距离是否够近”,从而可以像查数据一样查语义。检索是 RAG 的地基,向量化是检索的地基——模型再强,检索拿不到相关上下文,回答就只能靠编。

开发人员需要掌握的知识,可以归结为下面六个方面。先看全景:离线阶段,文档经过切分、向量化后写入向量库;在线阶段,用户查询用同一个模型向量化,经 ANN 召回、与关键词召回融合、精排后,把最相关的片段交给 LLM 生成回答。

1
2
3
4
5
6
7
8
9
10
11
flowchart LR
subgraph OFFLINE["离线索引链路"]
D["原始文档"] --> C["结构感知切分 chunk+overlap"] --> DE["Embedding 模型"] --> IDX[("向量库 HNSW / IVF")]
end
subgraph ONLINE["在线查询链路"]
Q["用户查询"] --> QE["同一 Embedding 模型向量化"] --> ANN["向量召回 ANN top 50~100"]
Q --> KW["BM25 关键词召回 top 50~100"]
ANN --> F["RRF / 加权融合"] --> RR["Reranker 精排"] --> T["top 3~5 片段"] --> LLM["LLM 生成回答"]
KW --> F
end
IDX -.-> ANN

一、Embedding 模型:选型与一致性

Embedding 模型本质是一个双塔(bi-encoder)编码器:把任意文本独立编码成固定维度的向量,让语义相近的文本在向量空间里彼此靠近。它不做生成,只负责”把语义压进一个点”。工程上要记住的第一条纪律是:查询和文档必须用同一个模型、同一个版本向量化。不同模型的向量空间互不相通,维度恰好一样也不行;模型升级后必须全量重建索引,常见做法是新旧双库并行,灰度切换后下线旧库。

Read More

上下文压缩策略

一、先看清问题:压缩不是”删掉旧消息”

模型的上下文窗口是一个硬上限,而 agent 的历史增长速度和聊天机器人完全不是一个量级——读一个文件几千 token,跑一次测试几万行输出,几十轮工具调用之后窗口就见底了。所以任何要长时间干活的 agent,迟早都必须回答一个问题:历史装不下了怎么办。

第一版方案几乎都是滑动窗口:只保留最近 N 条消息,更老的直接丢掉。它在聊天场景里勉强可用,在 agent 场景里会立刻出事。因为 agent 的历史不是等价的——最开头那条”帮我把登录模块重构成 XX 结构,注意不要改动数据库”往往是整个任务里信息密度最高的一句,而它恰好是最先被滑出窗口的。丢掉它之后模型会开始偏离目标、重复已经否决过的方案、违反早就说过的约束。更糟的是,如果窗口边界正好落在一次工具调用和它的结果之间,你会发给模型一段结构上非法的历史。

正确的思路是折叠而不是丢弃:把较早的一整段对话交给模型,让它压成一份结构化的摘要,再用这一条摘要替代原来那一整段。信息密度高的内容被保留下来,只是形态从”完整过程”变成了”结论与状态”。下面这张图是一套成熟压缩策略的完整决策流,本文余下部分就是逐个解释图里的每个决定为什么必须这样做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
flowchart TD
A["请求发出前\n(主动预判)"] --> M{"估算历史用量\n≥ 触发阈值?"}
B["模型返回报错\n(被动兜底)"] --> C{"是上下文超长\n这一类错误?"}
C -->|否| Z1["不介入\n交给其他错误处理"]
C -->|是| D{"本轮兜底次数\n未超上限?"}
D -->|否| Z1
M -->|否| Z2["无需压缩\n放行本次请求"]
M -->|是| P
D -->|是| P["先做零成本截断\n(可选,不调模型)"]
P --> R["选择可压缩范围\n保留近期 + 不切开配对"]
R -->|无安全范围| Z2
R --> T["进入压缩事务"]
subgraph TX ["压缩事务(开始标记先写、结束标记最后写)"]
T --> S1["写入开始标记\n取得压缩锁"]
S1 --> S2["重放原对话 + 摘要指令\n一次模型调用"]
S2 --> S3["写入摘要记录\n含被遮蔽的范围"]
S3 --> S4["写入替换操作\n用摘要遮蔽旧段"]
S4 --> S5["写入结束标记\n释放压缩锁"]
end
S5 --> G{"兜底路径:\n持久进展计数器\n推进了?"}
G -->|是| RETRY["允许重试\n用新历史再发一次"]
G -->|否| Z3["保留原始报错\n禁止重试"]
Read More