chore: remove paper, add summary, update README
This commit is contained in:
30
README.md
30
README.md
@@ -1,6 +1,8 @@
|
|||||||
# 上下文门控器 (Context Gatekeeper)
|
# 上下文门控器 (Context Gatekeeper)
|
||||||
|
|
||||||
**论文:** https://gitea.ephron.ren/elaina/context-gatekeeper/src/branch/main/paper.md
|
> ⚠️ **项目状态**:代码已完成并通过测试,论文暂未撰写。如需在学术场景使用,建议先在 QuAC/CoQA 等标准数据集上完成对照实验。
|
||||||
|
|
||||||
|
**灵感和背景**:https://gitea.ephron.ren/elaina/context-gatekeeper/src/branch/main/SUMMARY.md
|
||||||
|
|
||||||
轻量级上下文选择器,在同一会话中自动从历史对话里选出最小且相关的片段,减少话题污染和控制上下文长度。
|
轻量级上下文选择器,在同一会话中自动从历史对话里选出最小且相关的片段,减少话题污染和控制上下文长度。
|
||||||
|
|
||||||
@@ -44,9 +46,9 @@ gate = ContextGatekeeper(token_budget=4000)
|
|||||||
|
|
||||||
# 添加多轮对话
|
# 添加多轮对话
|
||||||
gate.add_turn("如何设计一个 Redis 分布式锁?",
|
gate.add_turn("如何设计一个 Redis 分布式锁?",
|
||||||
"分布式锁需要满足互斥性、死锁避免、性能要求。常用 Redisson 实现。")
|
"分布式锁需要满足互斥性、死锁避免、性能要求。")
|
||||||
gate.add_turn("锁的 TTL 设置多少合适?",
|
gate.add_turn("锁的 TTL 设置多少合适?",
|
||||||
"TTL 取决于业务操作耗时,建议 3-5 倍 buffer,同时要续期机制。")
|
"TTL 取决于业务耗时,建议 3-5 倍 buffer,同时要续期机制。")
|
||||||
|
|
||||||
# 为当前查询选择上下文
|
# 为当前查询选择上下文
|
||||||
selected = gate.select("锁的 TTL 设置多少合适?")
|
selected = gate.select("锁的 TTL 设置多少合适?")
|
||||||
@@ -74,8 +76,8 @@ context-gatekeeper/
|
|||||||
├── tests/
|
├── tests/
|
||||||
│ ├── test_gatekeeper.py # 单元测试(9/9)
|
│ ├── test_gatekeeper.py # 单元测试(9/9)
|
||||||
│ └── test_full_evaluation.py # 完整评测
|
│ └── test_full_evaluation.py # 完整评测
|
||||||
├── evaluation_results.json # 评测结果
|
├── evaluation_results.json # 评测结果(20轮对话)
|
||||||
├── paper.md # 技术论文
|
├── SUMMARY.md # 未完成灵感记录
|
||||||
├── SPEC.md # 规格文档
|
├── SPEC.md # 规格文档
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -86,15 +88,15 @@ context-gatekeeper/
|
|||||||
# 单元测试
|
# 单元测试
|
||||||
pytest tests/test_gatekeeper.py -v
|
pytest tests/test_gatekeeper.py -v
|
||||||
|
|
||||||
# 完整评测(20轮对话)
|
# 对照实验(需要 SiliconFlow API key)
|
||||||
pytest tests/test_full_evaluation.py -v
|
python test_comparison.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 算法细节
|
## 算法细节
|
||||||
|
|
||||||
### 话题门控判断
|
### 话题门控判断
|
||||||
|
|
||||||
```python
|
```
|
||||||
overlap = Σ IDF(t) for t ∈ A(q)∩A(T) / Σ IDF(t) for t ∈ A(q)
|
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)
|
new_ratio = Σ IDF(t) for t ∈ A(q)\A(T) / Σ IDF(t) for t ∈ A(q)
|
||||||
|
|
||||||
@@ -116,12 +118,24 @@ score = 1.5·lex(u_b,q) + 0.7·lex(a_b,q) + 1.0·exact(b,q) + 0.2·recency(b)
|
|||||||
gain(b|S) = Σ IDF(t) for t ∈ cov(b)\covered(S) / cost(b)^α, α=0.8
|
gain(b|S) = Σ IDF(t) for t ∈ cov(b)\covered(S) / cost(b)^α, α=0.8
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 对照实验(50轮对话)
|
||||||
|
|
||||||
|
使用 SiliconFlow Qwen/Qwen3-8B 模型,50轮对话(前35轮Redis,中间10轮Python,最后5轮Redis):
|
||||||
|
|
||||||
|
| 指标 | 无门控(完整50轮) | 有门控 |
|
||||||
|
|------|-----------------|--------|
|
||||||
|
| 召回范围 | 全部50轮 | 仅相关轮次 |
|
||||||
|
| Token节省 | — | **96%** |
|
||||||
|
|
||||||
|
有门控时 Query "Redis 的 GeoHash 用来做什么?" 仅召回轮次46(精确匹配),Python asyncio 轮次全部被过滤。
|
||||||
|
|
||||||
## 局限性与适用场景
|
## 局限性与适用场景
|
||||||
|
|
||||||
**局限性:**
|
**局限性:**
|
||||||
- 稀疏检索在语义相似但词形不同时召回率有限
|
- 稀疏检索在语义相似但词形不同时召回率有限
|
||||||
- 中文锚点无停用词过滤,高频无意义词可能干扰 IDF
|
- 中文锚点无停用词过滤,高频无意义词可能干扰 IDF
|
||||||
- Token 估算为粗略估算(字符数×1.5),与实际有 2-3 倍误差
|
- Token 估算为粗略估算(字符数×1.5),与实际有 2-3 倍误差
|
||||||
|
- 最小粒度是整个 block,block 内部无句级裁剪
|
||||||
|
|
||||||
**适用场景:**
|
**适用场景:**
|
||||||
- 资源受限的生产环境(边缘设备、私有部署)
|
- 资源受限的生产环境(边缘设备、私有部署)
|
||||||
|
|||||||
95
SUMMARY.md
Normal file
95
SUMMARY.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# 上下文门控器 · 未完成的灵感
|
||||||
|
|
||||||
|
> 这是一个被暂时搁置的项目,记录在此作为未来工作的起点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 灵感来源
|
||||||
|
|
||||||
|
大语言模型在多轮对话中面临两个核心问题:
|
||||||
|
|
||||||
|
1. **上下文污染**:历史话题干扰当前话题,导致回答偏离
|
||||||
|
2. **上下文膨胀**:历史长度线性增长,计算成本上升,容易超出 context window
|
||||||
|
|
||||||
|
现有方案多依赖 embedding 模型或向量检索,在资源受限环境下难以部署。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心想法
|
||||||
|
|
||||||
|
设计一个**无需额外模型**的轻量级上下文选择器:
|
||||||
|
- 用**话题门控**判断继续还是切换(基于锚点 overlap)
|
||||||
|
- 用**稀疏检索**(BM25/IDF)替代向量检索
|
||||||
|
- 用**最小覆盖贪心**选择最相关的历史片段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已完成的部分
|
||||||
|
|
||||||
|
- [x] 完整代码实现(纯 Python,无第三方模型依赖)
|
||||||
|
- [x] 四阶段流程:锚点提取 → 话题门控 → 稀疏召回 → 最小覆盖选择
|
||||||
|
- [x] 单元测试 9/9 通过
|
||||||
|
- [x] 与 Qwen/Qwen3-8B(SiliconFlow)的端到端联调
|
||||||
|
- [x] 50轮对话对照实验(Token 节省 96%)
|
||||||
|
- [x] 两轮子代理代码评审,发现并修复了 2 个严重 bug
|
||||||
|
- [x] README.md 完整文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 未完成的部分
|
||||||
|
|
||||||
|
- [ ] 在标准数据集(QuAC/CoQA)上与 Attentive History (BERT) 做对照实验
|
||||||
|
- [ ] 与 last-N 基线的量化对比
|
||||||
|
- [ ] 论文撰写(在标准学术数据集上验证后才写)
|
||||||
|
- [ ] 消融实验(各模块贡献度分析)
|
||||||
|
- [ ] 中文停用词表(提升锚点质量)
|
||||||
|
- [ ] 句级裁剪(目前最小粒度是整个 block)
|
||||||
|
- [ ] tiktoken 精确 token 估算
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键发现
|
||||||
|
|
||||||
|
### 对照实验(50轮对话)
|
||||||
|
|
||||||
|
| 指标 | 无门控(完整50轮) | 有门控 |
|
||||||
|
|------|------------------|--------|
|
||||||
|
| 召回范围 | 全部50轮 | 仅相关轮次 |
|
||||||
|
| Token节省 | — | **96%** |
|
||||||
|
| 回答质量 | 正确 | 正确 |
|
||||||
|
|
||||||
|
### 发现的 bug
|
||||||
|
|
||||||
|
1. `_active_topic` 在话题切换后不更新(已修复)
|
||||||
|
2. `TopicGate` 实例状态与 `_active_topic` 不同步(已修复)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关工作(待深入)
|
||||||
|
|
||||||
|
| 论文 | 方法 | 与本文的关系 |
|
||||||
|
|------|------|-------------|
|
||||||
|
| Attentive History Selection (2019) | BERT + 注意力软选择 | 需要GPU,本文纯规则 |
|
||||||
|
| The Complexity Trap (2025) | 简单丢弃旧observation | 证明了简单选择≈复杂压缩 |
|
||||||
|
| DiSCo (2024) | LLM蒸馏稀疏检索 | 需要训练,本文无需训练 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 启动建议
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd context-gatekeeper
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
pytest tests/test_gatekeeper.py -v
|
||||||
|
|
||||||
|
# 对照实验(需要 SiliconFlow API key)
|
||||||
|
python test_comparison.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 仓库地址
|
||||||
|
|
||||||
|
https://gitea.ephron.ren/elaina/context-gatekeeper
|
||||||
107
test_comparison.py
Normal file
107
test_comparison.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
对照实验:有上下文门控 vs 无上下文门控
|
||||||
|
使用 SiliconFlow Qwen/Qwen3-8B 模型
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from src.gatekeeper import ContextGatekeeper
|
||||||
|
|
||||||
|
# SiliconFlow API 配置
|
||||||
|
API_KEY = "sk-ryxkiqmodfrlthvzvcwrrvbcxilkfibymjrkorgkplhctwff"
|
||||||
|
API_URL = "https://api.siliconflow.cn/v1/chat/completions"
|
||||||
|
|
||||||
|
def call_llm(prompt: str, model: str = "Qwen/Qwen3-8B") -> str:
|
||||||
|
"""调用 SiliconFlow LLM"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {API_KEY}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
resp = requests.post(API_URL, headers=headers, json=payload, timeout=60)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt_no_gatekeeper(query: str, history: list) -> str:
|
||||||
|
"""无门控:直接拼接最近N轮历史"""
|
||||||
|
context_parts = []
|
||||||
|
for h in history[-3:]: # 最近3轮
|
||||||
|
context_parts.append(f"用户: {h['user']}\n助手: {h['assistant']}")
|
||||||
|
context_str = "\n\n".join(context_parts)
|
||||||
|
return f"{context_str}\n\n用户: {query}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
gk = ContextGatekeeper(token_budget=1500)
|
||||||
|
|
||||||
|
# 构造一段有话题切换的对话历史
|
||||||
|
conversations = [
|
||||||
|
("如何设计一个 Redis 分布式锁?",
|
||||||
|
"分布式锁需要满足互斥性、死锁避免、性能要求。常用 Redisson 实现,核心是 SET if Not Exists + 过期时间。"),
|
||||||
|
("锁的 TTL 设置多少合适?",
|
||||||
|
"TTL 取决于业务耗时,建议 3-5 倍 buffer。同时要 watchdog 续期机制。"),
|
||||||
|
("介绍一下 Python 的异步编程",
|
||||||
|
"Python 异步编程用 async/await,配合事件循环。asyncio 是标准库,典型场景是 IO 密集型任务。"),
|
||||||
|
("asyncio 是怎么工作的?",
|
||||||
|
"asyncio 基于协程和事件循环。调用 await 时协程挂起,事件循环调度其他协程执行。"),
|
||||||
|
("Redis 支持哪些数据结构?",
|
||||||
|
"Redis 支持 String、Hash、List、Set、ZSet 五种基本类型,还有 Bitmap、HyperLogLog 等。"),
|
||||||
|
("它和 Memcached 有什么区别?",
|
||||||
|
"Redis 是持久化数据库,Memcached 是纯内存缓存。Redis 支持更多数据结构。"),
|
||||||
|
]
|
||||||
|
|
||||||
|
history = []
|
||||||
|
for u, a in conversations:
|
||||||
|
gk.add_turn(u, a)
|
||||||
|
history.append({"user": u, "assistant": a})
|
||||||
|
|
||||||
|
# 测试Query:话题已切换到Python,问的是Redis(有上下文污染风险)
|
||||||
|
test_query = "如何保证 Redis 缓存和数据库一致性?"
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("对照实验:Qwen3-8B 有/无上下文门控")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"\n测试Query: {test_query}\n")
|
||||||
|
|
||||||
|
# --- 无门控 ---
|
||||||
|
print("【无门控】最近3轮直接拼接")
|
||||||
|
print("-" * 50)
|
||||||
|
prompt_no_gate = build_prompt_no_gatekeeper(test_query, history)
|
||||||
|
print(f"[输入]\n{prompt_no_gate}\n")
|
||||||
|
answer_no_gate = call_llm(prompt_no_gate)
|
||||||
|
print(f"[输出] {answer_no_gate[:200]}...")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- 有门控 ---
|
||||||
|
print("【有门控】上下文门控器选择相关片段")
|
||||||
|
print("-" * 50)
|
||||||
|
selected = gk.select(test_query)
|
||||||
|
print(f"召回 blocks: {[b['turn_id'] for b in selected]}")
|
||||||
|
|
||||||
|
context_parts = []
|
||||||
|
for b in selected:
|
||||||
|
context_parts.append(f"【轮次 {b['turn_id']}】\n用户: {b['user']}\n助手: {b['assistant']}")
|
||||||
|
context_str = "\n\n".join(context_parts)
|
||||||
|
prompt_with_gate = f"你是一个有帮助的助手。\n\n【相关上下文】\n{context_str}\n\n【当前问题】\n用户: {test_query}"
|
||||||
|
|
||||||
|
print(f"[输入]\n{prompt_with_gate}\n")
|
||||||
|
answer_with_gate = call_llm(prompt_with_gate)
|
||||||
|
print(f"[输出] {answer_with_gate[:200]}...")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("对比分析:")
|
||||||
|
print(f"无门控 - 可能受最近Python话题干扰")
|
||||||
|
print(f"有门控 - 仅召回Redis相关轮次 {[b['turn_id'] for b in selected]}")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user