205 lines
7.6 KiB
Markdown
205 lines
7.6 KiB
Markdown
# 上下文门控器 (Context Gatekeeper)
|
||
|
||
轻量级上下文选择器,在同一会话中自动从历史对话里选出最小且相关的片段,减少话题污染和控制上下文长度。
|
||
|
||
## 特性
|
||
|
||
- 🚀 **纯 Python**,无需向量化模型依赖(无 embedding、reranker、分类器)
|
||
- 💻 **资源消耗极低**,依赖极少,普通的私有部署环境都能跑
|
||
- 🔍 **话题门控**,通过锚点 overlap + new_ratio 判断继续/切换,含指代词强制继承
|
||
- 📦 **稀疏召回**,BM25/IDF-overlap 评分,用户侧权重高于助手侧
|
||
- 🎯 **最小覆盖**,基于 IDF 加权集合覆盖的贪心选择
|
||
- ⚙️ **稳定约束区**,持久化用户偏好(语言/风格/禁用项)
|
||
|
||
## 核心流程
|
||
|
||
```
|
||
用户查询 q
|
||
↓
|
||
① 锚点提取(中文 2/3-gram、英文单词、代码标识符、版本号、引号短语)
|
||
↓
|
||
② 话题门控(overlap > 0.45 → 继续;overlap < 0.20 且 new_ratio > 0.70 → 切换;
|
||
有指代词 → 强制继续;中间地带默认继续)
|
||
↓
|
||
③ 稀疏召回(top-20,BM25/IDF-overlap + exact match + 新鲜度奖励)
|
||
↓ 话题切换时:内容词过滤(只保留包含 query 内容词的块)
|
||
④ 最小覆盖选择(gain = ΣIDF(t) / cost^α,贪心选择达到 85% 覆盖停止)
|
||
```
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
pip install -e .
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
```python
|
||
from src.gatekeeper import ContextGatekeeper
|
||
|
||
# 初始化,token 预算 4000
|
||
gate = ContextGatekeeper(token_budget=4000)
|
||
|
||
# 添加多轮对话
|
||
gate.add_turn("如何设计一个 Redis 分布式锁?",
|
||
"分布式锁需要满足互斥性、死锁避免、性能要求。")
|
||
gate.add_turn("锁的 TTL 设置多少合适?",
|
||
"TTL 取决于业务耗时,建议 3-5 倍 buffer,同时要续期机制。")
|
||
|
||
# 为当前查询选择上下文
|
||
selected = gate.select("锁的 TTL 设置多少合适?")
|
||
|
||
for item in selected:
|
||
print(f"轮次 {item['turn_id']}: {item['user']}")
|
||
print(f"助手: {item['assistant']}\n")
|
||
|
||
# 构建完整 prompt(可直接发给 LLM)
|
||
prompt = gate.build_prompt("锁的 TTL 设置多少合适?")
|
||
print(prompt)
|
||
```
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
context-gatekeeper/
|
||
├── src/
|
||
│ ├── anchor.py # 锚点提取(2/3-gram + IDF)
|
||
│ ├── block.py # Block 数据结构
|
||
│ ├── topic_gate.py # 话题门控(overlap + new_ratio + 指代词)
|
||
│ ├── sparse.py # 稀疏召回(BM25/IDF + exact + recency)
|
||
│ ├── selector.py # 最小覆盖选择(IDF加权贪心)
|
||
│ └── gatekeeper.py # 主模块(组合各子模块)+ 句级裁剪
|
||
├── test_100rounds_v2.py # 100轮4话题完整对照实验
|
||
└── README.md
|
||
```
|
||
|
||
## 运行测试
|
||
|
||
```bash
|
||
# 100轮4话题对照实验
|
||
python test_100rounds_v2.py
|
||
```
|
||
|
||
## 算法细节
|
||
|
||
### 锚点提取
|
||
|
||
从文本中提取有检索价值的关键词单元,支持:
|
||
|
||
- **中文**:2-gram 和 3-gram(如"分布式锁"、"跨进程通信")
|
||
- **英文**:单词形态
|
||
- **代码**:标识符、版本号(如 `v1.2.3`)
|
||
- **引号短语**:完整的技术术语
|
||
|
||
规则驱动,无需分词库,响应速度极快。
|
||
|
||
### 话题门控判断
|
||
|
||
```
|
||
overlap = Σ IDF(t) for t ∈ A(q)∩A(T) / Σ IDF(t) for t ∈ A(q)
|
||
new_ratio = Σ IDF(t) for t ∈ A(q)\A(T) / Σ IDF(t) for t ∈ A(q)
|
||
|
||
if overlap > 0.45: # 重叠度高,继续当前话题
|
||
continue
|
||
elif overlap < 0.20 and new_ratio > 0.70: # 新词占比高,切换话题
|
||
switch
|
||
elif has_deictic: # 有指代词,强制继承
|
||
continue
|
||
else:
|
||
continue # 中间地带默认继续,避免切断正在发展的思路
|
||
```
|
||
|
||
### 话题切换时的内容词过滤
|
||
|
||
话题切换时,用内容词(英文术语/代码标识符/长中文词)对候选块做硬过滤:
|
||
|
||
```python
|
||
# 从 query 中提取内容词(区分于通用 n-gram)
|
||
content_words = {
|
||
'redis', # 英文术语
|
||
'postgresql', 'explain', # 英文术语
|
||
'asyncio', 'gather', # 英文术语
|
||
'git', 'rebase', 'merge', # 英文术语
|
||
'v1.2.3', # 版本号
|
||
'惰性删除', # 长中文术语(>=4字符)
|
||
}
|
||
|
||
# 块必须包含至少一个内容词,否则被过滤掉
|
||
if topic_switched and content_words:
|
||
block_text = block.user_text + ' ' + block.assistant_text
|
||
if not any(cw in block_text.lower() for cw in content_words):
|
||
score = 0.0 # 硬过滤
|
||
```
|
||
|
||
为什么用内容词而不是 IDF 阈值:因为 IDF > 2.0 筛选出来的是稀有字符 n-gram(如"看执"、"行计"),这些是通用词,反而会桥接不同话题。内容词是显式的 topic 标识符,区分度高。
|
||
|
||
### 稀疏召回评分
|
||
|
||
```
|
||
score = 1.5·lex(u_b,q) + 0.7·lex(a_b,q) + 1.0·exact(b,q) + 0.2·recency(b)
|
||
```
|
||
|
||
- `lex(u_b,q)`:用户轮次与 Query 的 BM25/IDF 重叠(权重 1.5)
|
||
- `lex(a_b,q)`:助手轮次与 Query 的 BM25/IDF 重叠(权重 0.7,助手侧信息量通常更小)
|
||
- `exact(b,q)`:完全匹配奖励(精确命中关键词)
|
||
- `recency(b)`:新鲜度奖励,越近的轮次权重越高
|
||
|
||
取 top-20 进入下一步。
|
||
|
||
### 最小覆盖 gain
|
||
|
||
```
|
||
gain(b|S) = Σ IDF(t) for t ∈ cov(b)\covered(S) / cost(b)^α, α=0.8
|
||
|
||
覆盖率达到 85% 或 token 预算耗尽时停止。
|
||
```
|
||
|
||
**为什么用 IDF 加权**:高频词(如"数据"、"系统")区分度低,低频词(如"GeoHash"、"分布式锁")才是真正的语义锚点。用 IDF 加权确保选择的是真正有信息量的片段,而不是反复覆盖高频通用词。
|
||
|
||
### 句级裁剪
|
||
|
||
选中的 block 不一定整块都塞给 LLM。按句子分割后,只保留包含 query 锚点的句子,最多保留 3 句。助手侧即使不含锚点,也保留第一句作为上下文衔接。
|
||
|
||
## 100 轮 4 话题对照实验
|
||
|
||
**设置**:4 个话题(Redis、Python asyncio、PostgreSQL、Git),每话题 25 轮交替,总计 100 轮。Token 预算 4000。
|
||
|
||
**验证结果**:
|
||
|
||
| 验证 | 指标 | 结果 |
|
||
|------|------|------|
|
||
| 话题隔离 | 100轮后问Git,T1/T2/T3不应出现 | ✅ 无污染 |
|
||
| 召回完整性 | Git锚点覆盖 | ✅ 100% |
|
||
| Token节省 | 无门控 vs 有门控 | ✅ 97.7% 节省 |
|
||
| 交替无污染 | 5次交替查询,每次无跨话题召回 | ✅ 全部通过 |
|
||
| 完整召回 | Git/Redis窗口内召回率 | ✅ 100% |
|
||
|
||
**交替话题验证详情**(每轮问完立即动态跟踪 active_topic):
|
||
|
||
| 查询 | 目标话题 | 召回轮次 | 跨话题污染 | 结果 |
|
||
|------|---------|---------|-----------|------|
|
||
| EXPLAIN ANALYZE怎么看 | T3(PG) | [99] | 无 | ✅ |
|
||
| Git rebase/merge区别 | T4(Git) | [88,100,96,92] | 无 | ✅ |
|
||
| Redis惰性删除区别 | T1(Redis) | [89,93,97] | 无 | ✅ |
|
||
| asyncio.Task cancel | T2(asyncio) | [90,94,98] | 无 | ✅ |
|
||
| Git reset/revert场景 | T4(Git) | [88,100,96,92] | 无 | ✅ |
|
||
|
||
**结论**:在纯规则、轻量、资源受限约束下,上下文门控器实现了零跨话题污染,同时节省 97.7% token。
|
||
|
||
## 局限性与适用场景
|
||
|
||
**局限性:**
|
||
- 稀疏检索依赖词形匹配,语义相近但词形不同的情况容易漏召
|
||
- Token 估算为粗略估算(字符数×1.5),与实际有 2-3 倍误差
|
||
- "完整召回"与"最小覆盖"存在权衡:窗口内只选最相关的块,而非全部块
|
||
- 没有在 QuAC 这类标准学术数据集上做对照实验,无法跟 Attentive History 这类基于注意力机制的方法直接对比
|
||
|
||
**适用场景:**
|
||
- 资源受限的生产环境(边缘设备、私有部署)
|
||
- 对延迟敏感的实时对话
|
||
- 中等复杂度对话(10-100轮)
|
||
|
||
**不适用:**
|
||
- 需要精确语义匹配的场景(建议用向量检索)
|
||
- 极长对话(>100轮,IDF 全量更新有偏)
|