docs: add DESIGN.md following Google Stitch spec
This commit is contained in:
255
DESIGN.md
Normal file
255
DESIGN.md
Normal 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 Palette(CLI 输出配色)
|
||||||
|
|
||||||
|
> 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` 常量
|
||||||
Reference in New Issue
Block a user