Files
context-gatekeeper/DESIGN.md

256 lines
8.5 KiB
Markdown
Raw 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.
# 上下文门控器 DESIGN.md
> 本文件描述 context-gatekeeper 的视觉语言、算法哲学与实现规范,供 AI 编码 Agent 在开发/重构时参考。
> 遵循 Google Stitch [DESIGN.md 规范](https://stitch.withgoogle.com/docs/design-md/overview/)。
---
## 1. Visual Theme and Atmosphere
**工具型命令行项目,不是面向终端用户的可视化产品。**
核心氛围是**克制与精确**——每个设计决策都有算法层面的理由,不做视觉上的"美化"。
代码即文档。输出即证据。接口即契约。
---
## 2. Design Language
### 审美取向
- **风格**工程师工具engineer-tool aesthetic。类比 `htop``tcpdump`、Shellcheck
- **信息密度优先**:宁可多给一行调试输出,也不要静默失败
- **零装饰原则**:没有 emoji logo、没有渐变色、没有动画。结构即美学
### 排版规范
- 代码:等宽字体(`'JetBrains Mono'`, `'Fira Code'`, `monospace`
- 表格ASCII 风格 Pipe Table算法伪代码用 Markdown code block
- 日志输出:单行简短,带时间戳或轮次编号
### 命名哲学
- **变量名即类型注释**:不用注释解释的东西,变量名就要说清楚
-`anchor_overlap_ratio`, `new_token_ratio`
-`score`, `val`, `tmp`
- **函数名即行为描述**`extract_anchors()` → 提取锚点,`gate_topic()` → 判断话题切换
- **模块名即职责边界**`anchor.py` 专司锚点提取,`topic_gate.py` 专司门控判断
---
## 3. Color PaletteCLI 输出配色)
> CLI 输出不使用颜色时最安全。本节定义配色规范,供未来添加 `--color` 输出时参考。
| Role | Token | Value | Usage |
|------|-------|-------|-------|
| 正常输出 | `--text-normal` | `#FFFFFF` | 日志、返回值 |
| 高亮/标题 | `--text-highlight` | `#5EEAD4` | 锚点命中、话题切换提示 |
| 警告 | `--text-warn` | `#FBBF24` | Token 超限、召回率低 |
| 错误 | `--text-error` | `#F87171` | 过滤异常、API 错误 |
| Debug | `--text-debug` | `#6B7280` | 内部状态、IDF 值 |
| 强调 | `--text-accent` | `#A78BFA` | 核心指标(覆盖增益) |
---
## 4. Algorithm Specification
> 本项目的核心是算法,不是 UI。实现必须严格遵循算法规范不得随意改参数。
### 4.1 锚点提取Anchor Extraction
**职责**:从文本中提取有检索区分度的词单元。
**提取规则**(优先级从高到低):
1. **英文术语**:完整单词,小写化(`redis``asyncio``postgresql`
2. **代码标识符**:变量名/函数名,提取连续字母数字串(`v1.2.3``v1`
3. **中文 n-gram**2-gram + 3-gram`分布式锁``跨进程通信`
4. **引号短语**:双引号/单引号内的完整短语
**IDF 计算**
- 语料库:全局所有已处理的对话块
- 平滑公式:`log((N + 1) / (df + 1)) + 1`
- 低于 `1.5` 的 n-gram 不参与锚点计算(过高的 IDF 通常是噪声)
**输出格式**
```python
# anchor.py
anchors: dict[str, float] # {词单元: IDF值}
```
### 4.2 话题门控Topic Gate
**职责**:判断当前 query 是否属于新话题。
**决策树**
```
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)
overlap > 0.45 → continue继续当前话题
overlap < 0.20 and new_ratio > 0.70 → switch话题切换
has_deictic(q) → continue指代词强制继续
else → continue中间地带默认继续
```
**指代词检测**(必须硬编码,不得依赖规则引擎):
```python
DEICTIC_PATTERNS = [
r'^这个', r'^那个', r'^它', r'^这', r'^那',
r'^继续', r'^然后呢', r'^还有呢',
]
```
### 4.3 话题切换时的内容词过滤
**触发条件**topic_gate 判定为 switch。
**内容词识别**
```python
CONTENT_WORD_MIN_LEN = 4 # 中文词 >= 4字符
CONTENT_WORD_PATTERNS = [
r'\b[a-z]{4,}\b', # 英文术语 >= 4字母
r'\bv?\d+\.\d+\.\d+\b', # 版本号
]
```
**过滤逻辑**
- 候选块必须包含至少一个内容词,否则 `score = 0.0`(硬过滤)
- 不走 IDF 阈值过滤IDF > 2.0 筛选出的是稀有字符 n-gram不是话题标识符
### 4.4 稀疏召回Sparse Recall
**评分公式**
```
score(b, q) = 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)` | 用户轮次 BM25/IDF 与 Query 重叠度 | 1.5 |
| `lex(a_b,q)` | 助手轮次 BM25/IDF 与 Query 重叠度 | 0.7 |
| `exact(b,q)` | 完全匹配奖励(块含 Query 所有锚点) | 1.0 |
| `recency(b)` | 新鲜度奖励:`1 - (current_turn - b.turn_id) / window` | 0.2 |
**召回数量上限**top-20超出部分不参与后续选择。
### 4.5 最小覆盖选择Minimum Coverage Selection
**贪心选择**:每次选 `gain = Σ IDF(t) for t ∈ cov(b)\covered(S) / cost(b)^α`,直到:
- 覆盖率达到 85%,或
- Token 预算耗尽
**参数**`α = 0.8`(不得改动)
**Token 估算**`len(text) * 1.5`(与实际有 2-3 倍误差,保守估算)
### 4.6 句级裁剪Sentence-Level Pruning
**输入**:选中的 block可能含多句话
**规则**
1.`. ``\n` 分割为句子
2. 保留含 Query 锚点的句子,最多 3 句
3. 助手侧即使不含锚点,也保留第一句(保证上下文衔接)
4. 裁剪后的块 token 估算独立重新计算
---
## 5. API 接口规范
### `ContextGatekeeper(token_budget: int)`
初始化。`token_budget` 为单次 prompt 的最大 token 数(不含系统 prompt
### `gate.add_turn(user_text: str, assistant_text: str) -> int`
添加一轮对话。返回 `turn_id`(从 0 开始)。
### `gate.select(query: str) -> list[dict]`
返回选中的上下文块列表。每项格式:
```python
{
'turn_id': int,
'user': str, # 裁剪后的用户轮次文本
'assistant': str, # 裁剪后的助手轮次文本
'score': float, # 该块的最终得分
'covered_ids': list[int], # 该块新增覆盖的锚点 turn_id
}
```
### `gate.build_prompt(query: str) -> str`
返回可直接发给 LLM 的完整 prompt 字符串(不含系统 prompt
---
## 6. 模块边界Do's and Don'ts
### ✅ 允许
-`src/` 外层添加新模块(如 `benchmark.py`
- 扩展 `anchor.py` 支持新的 n-gram 规则
- 添加新的话题切换检测模式(需同时更新 `DEICTIC_PATTERNS`
- 调整 `token_budget` 数值
### ❌ 禁止
-`anchor.py` 里做话题门控判断(违反单一职责)
-`selector.py` 里修改 `α` 参数(该参数经过实验验证)
-`BM25/IDF` 替换为 embedding/reranker资源受限前提的核心约束
- 在 CLI 输出中硬编码颜色Unix 管道场景下颜色会变成乱码)
---
## 7. Error Handling
| 场景 | 行为 |
|------|------|
| `add_turn()` 时 token 估算 > budget | 记录 warning不 abort |
| `select()` 时无候选块 | 返回空 list |
| 锚点提取为空 | fallback取 query 前 20 字符作为锚点 |
| 句级裁剪后块为空 | 跳过该块,继续选下一个 |
---
## 8. 测试规范
### 单元测试
- 每个模块独立的 `test_<module>.py`
- 锚点提取:输入固定文本,输出 IDF 值在已知范围内
- 话题门控用预制的query, history, expected_decision三元组覆盖所有分支
### 集成测试
- `test_100rounds_v2.py`4 话题 × 25 轮交替,验证零跨话题污染
- 必须覆盖话题切换、内容词过滤、Token 预算耗尽、指代词强制继续
### 回归基准
- 每次修改后必须通过 `test_100rounds_v2.py`
- 对话轮次超过 100 时需重新评估 IDF 偏倚问题
---
## 9. Agent Prompt Guide给 AI 助手的参考)
### 快速参考
| 概念 | 值 |
|------|-----|
| 锚点权重 | 用户侧 1.5 / 助手侧 0.7 |
| 覆盖停止阈值 | 85% |
| 贪心参数 α | 0.8 |
| 召回上限 | top-20 |
| Token 估算系数 | ×1.5 |
### 常见任务提示词
**"我想加一个 BM25 以外的召回策略"** →
→ 检查 `sparse.py``lex()` 函数,不要改动 `score` 公式的权重
**"某个话题老被误过滤"** →
→ 先查话题切换时的 `content_word` 过滤逻辑,确认过滤条件是否过严
**"我想试试不同的 overlap 阈值"** →
→ 修改 `topic_gate.py``OVERLAP_CONTINUE_THRESHOLD`,然后跑 `test_100rounds_v2.py` 验证零污染
**"Token 预算想从 4000 改成 8000"** →
→ 改 `ContextGatekeeper(token_budget=8000)`,检查 `test_100rounds_v2.py` 中对应的 `BUDGET` 常量