diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..492056a --- /dev/null +++ b/DESIGN.md @@ -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_.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` 常量