docs: add DESIGN.md following Google Stitch spec

This commit is contained in:
Elaina
2026-04-22 19:33:01 +08:00
parent d18a521f9c
commit 2064eb7bdf

255
DESIGN.md Normal file
View File

@@ -0,0 +1,255 @@
# 上下文门控器 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` 常量