# 上下文门控器 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` 常量