Files
context-gatekeeper/README.md

205 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 上下文门控器 (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-20BM25/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轮后问GitT1/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 全量更新有偏)