chore: update README with complete algorithm and 100-round 4-topic results
This commit is contained in:
114
README.md
114
README.md
@@ -22,7 +22,7 @@
|
||||
有指代词 → 强制继续;中间地带默认继续)
|
||||
↓
|
||||
③ 稀疏召回(top-20,BM25/IDF-overlap + exact match + 新鲜度奖励)
|
||||
↓
|
||||
↓ 话题切换时:内容词过滤(只保留包含 query 内容词的块)
|
||||
④ 最小覆盖选择(gain = ΣIDF(t) / cost^α,贪心选择达到 85% 覆盖停止)
|
||||
```
|
||||
|
||||
@@ -68,24 +68,16 @@ context-gatekeeper/
|
||||
│ ├── topic_gate.py # 话题门控(overlap + new_ratio + 指代词)
|
||||
│ ├── sparse.py # 稀疏召回(BM25/IDF + exact + recency)
|
||||
│ ├── selector.py # 最小覆盖选择(IDF加权贪心)
|
||||
│ └── gatekeeper.py # 主模块(组合各子模块)
|
||||
├── tests/
|
||||
│ ├── test_gatekeeper.py # 单元测试(9/9)
|
||||
│ └── test_full_evaluation.py # 完整评测
|
||||
├── evaluation_results.json # 评测结果(20轮对话)
|
||||
├── SUMMARY.md # 未完成灵感记录
|
||||
├── SPEC.md # 规格文档
|
||||
│ └── gatekeeper.py # 主模块(组合各子模块)+ 句级裁剪
|
||||
├── test_100rounds_v2.py # 100轮4话题完整对照实验
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 单元测试
|
||||
pytest tests/test_gatekeeper.py -v
|
||||
|
||||
# 对照实验(需要 SiliconFlow API key)
|
||||
python test_comparison.py
|
||||
# 100轮4话题对照实验
|
||||
python test_100rounds_v2.py
|
||||
```
|
||||
|
||||
## 算法细节
|
||||
@@ -117,6 +109,30 @@ 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 标识符,区分度高。
|
||||
|
||||
### 稀疏召回评分
|
||||
|
||||
```
|
||||
@@ -140,75 +156,49 @@ gain(b|S) = Σ IDF(t) for t ∈ cov(b)\covered(S) / cost(b)^α, α=0.8
|
||||
|
||||
**为什么用 IDF 加权**:高频词(如"数据"、"系统")区分度低,低频词(如"GeoHash"、"分布式锁")才是真正的语义锚点。用 IDF 加权确保选择的是真正有信息量的片段,而不是反复覆盖高频通用词。
|
||||
|
||||
## 对照实验(50轮对话)
|
||||
### 句级裁剪
|
||||
|
||||
使用 SiliconFlow Qwen/Qwen3-8B 模型,50轮对话(前35轮Redis,中间10轮Python,最后5轮Redis):
|
||||
选中的 block 不一定整块都塞给 LLM。按句子分割后,只保留包含 query 锚点的句子,最多保留 3 句。助手侧即使不含锚点,也保留第一句作为上下文衔接。
|
||||
|
||||
| 指标 | 无门控(完整50轮) | 有门控 |
|
||||
|------|-----------------|--------|
|
||||
| 召回范围 | 全部50轮 | 仅相关轮次 |
|
||||
| Token节省 | — | **96%** |
|
||||
## 100 轮 4 话题对照实验
|
||||
|
||||
有门控时 Query "Redis 的 GeoHash 用来做什么?" 仅召回轮次46(精确匹配),Python asyncio 轮次全部被过滤。
|
||||
**设置**:4 个话题(Redis、Python asyncio、PostgreSQL、Git),每话题 25 轮交替,总计 100 轮。Token 预算 4000。
|
||||
|
||||
完整伪代码:
|
||||
**验证结果**:
|
||||
|
||||
```
|
||||
function select(q, turns):
|
||||
# 1. 锚点提取
|
||||
anchors_q = extract_anchors(q)
|
||||
active_topic = get_active_topic()
|
||||
| 验证 | 指标 | 结果 |
|
||||
|------|------|------|
|
||||
| 话题隔离 | 100轮后问Git,T1/T2/T3不应出现 | ✅ 无污染 |
|
||||
| 召回完整性 | Git锚点覆盖 | ✅ 100% |
|
||||
| Token节省 | 无门控 vs 有门控 | ✅ 97.7% 节省 |
|
||||
| 交替无污染 | 5次交替查询,每次无跨话题召回 | ✅ 全部通过 |
|
||||
| 完整召回 | Git/Redis窗口内召回率 | ✅ 100% |
|
||||
|
||||
# 2. 话题门控
|
||||
overlap = compute_overlap(anchors_q, active_topic)
|
||||
new_ratio = compute_new_ratio(anchors_q, active_topic)
|
||||
**交替话题验证详情**(每轮问完立即动态跟踪 active_topic):
|
||||
|
||||
if overlap < 0.20 and new_ratio > 0.70:
|
||||
active_topic = create_new_topic(anchors_q) # 切换
|
||||
elif has_deictic(q):
|
||||
inherit_recent(2) # 指代词,强制继承最近2轮
|
||||
# 否则继续当前话题
|
||||
| 查询 | 目标话题 | 召回轮次 | 跨话题污染 | 结果 |
|
||||
|------|---------|---------|-----------|------|
|
||||
| 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] | 无 | ✅ |
|
||||
|
||||
# 3. 稀疏召回
|
||||
candidates = []
|
||||
for each turn i:
|
||||
score_i = 1.5 * bm25(user_i, q) + 0.7 * bm25(assistant_i, q) + \
|
||||
1.0 * exact_match(i, q) + 0.2 * recency(i)
|
||||
candidates.append((score_i, i))
|
||||
|
||||
top20 = top_k(candidates, k=20)
|
||||
|
||||
# 4. 最小覆盖贪心选择
|
||||
selected = []
|
||||
covered = empty_set()
|
||||
for each block b in top20 sorted by gain:
|
||||
new_anchors = extract_anchors(b) \ covered
|
||||
if len(new_anchors) == 0: continue
|
||||
gain_b = sum(IDF(t) for t in new_anchors) / cost(b)^0.8
|
||||
selected.append((gain_b, b))
|
||||
covered.update(new_anchors)
|
||||
if coverage(covered) >= 0.85: break
|
||||
|
||||
return selected
|
||||
```
|
||||
**结论**:在纯规则、轻量、资源受限约束下,上下文门控器实现了零跨话题污染,同时节省 97.7% token。
|
||||
|
||||
## 局限性与适用场景
|
||||
|
||||
**局限性:**
|
||||
- 稀疏检索依赖词形匹配,语义相近但词形不同的情况容易漏召
|
||||
- Token 估算为粗略估算(字符数×1.5),与实际有 2-3 倍误差
|
||||
- 最小粒度是整个 block,block 内部无句级裁剪,边界粗糙
|
||||
- "完整召回"与"最小覆盖"存在权衡:窗口内只选最相关的块,而非全部块
|
||||
- 没有在 QuAC 这类标准学术数据集上做对照实验,无法跟 Attentive History 这类基于注意力机制的方法直接对比
|
||||
|
||||
**适用场景:**
|
||||
- 资源受限的生产环境(边缘设备、私有部署)
|
||||
- 对延迟敏感的实时对话
|
||||
- 中等复杂度对话(10-50轮)
|
||||
- 中等复杂度对话(10-100轮)
|
||||
|
||||
**不适用:**
|
||||
- 需要精确语义匹配的场景(建议用向量检索)
|
||||
- 极长对话(>100轮,IDF 全量更新有偏)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Reference in New Issue
Block a user