1 line
111 KiB
JSON
1 line
111 KiB
JSON
{"repo": "canarybyte/veridrop", "files": {"DESIGN.md": {"dca32815b7edd02bb347dfea51c35218fd88f604": {"t": 1785747362.1307373, "n": 39388, "h": "6e566f6105a22db3", "c": "# Claude 中转站检测工具 — 技术设计文档\n\n> **版本说明**:本文档所有协议字段、事件名、模型 ID、knowledge cutoff 均基于 Anthropic 官方文档交叉验证(来源见附录 D)。设计中标注 ⭐ 的检测项基于官方协议中可被加密验证的特征,是真伪鉴别的核心指标。\n\n## 1. 项目背景与目标\n\n### 1.1 背景\n市面上的 Claude API 中转站质量参差不齐,常见问题:\n- **挂羊头卖狗肉**:声称提供 Claude Opus,实际后台路由到便宜的模型(Sonnet/Haiku 甚至非 Claude 模型)\n- **能力阉割**:不支持 PDF / Vision / Tool Use / Extended Thinking 等高级特性,但仍按完整版收费\n- **协议不规范**:响应字段缺失、ID 格式错误、streaming 协议异常,导致 SDK 调用兼容性问题\n- **token 虚报**:usage 字段不准确,影响计费\n\n### 1.2 目标\n给定一个中转站的 `base_url` 和 `api_key`,自动跑一组检测,输出可量化的\"中转站质量报告\",回答三个问题:\n1. **真伪**:是否真的是声称的模型?\n2. **能力**:能力是否完整?\n3. **合规**:响应是否符合 Anthropic 官方协议?\n\n### 1.3 MVP 范围\n- 仅支持 **Claude 系列模型**(Opus 4.7 / Sonnet 4.6 / Haiku 4.5 优先;legacy 模型详见附录 B)\n- 仅支持 **Anthropic Messages API 协议**(`POST /v1/messages`),不支持 Bedrock Converse / Vertex AI 协议变体\n- 假设 base_url 接受 Anthropic header(`x-api-key` + `anthropic-version: 2023-06-01`)\n- 提供 **三档运行模式**(quick / standard / full,详见 §6.1),默认 quick 模式 ~15s/$0.05\n- 输出形式:CLI + JSON 报告\n\n---\n\n## 2. 检测维度总览\n\n10 项检测,按目的分三组。**权重已根据\"伪造难度\"调整** —— 加密级特征(thinking signature)权重最高,可被 system prompt 注入伪造的(identity)权重最低。\n\n| 组 | # | 维度 | 检测目的 | 权重 |\n|---|---|---|---|---|\n| A | 1 | 身份一致性 | 自报家门是否说自己是 Claude | 5% |\n| A | 2 | 行为签名验证 | 行为指纹(拒绝模式、风格)是否匹配 Claude | 15% |\n| A | 3 | **思维签名验证** ⭐ | thinking 块的加密 signature 是否真实有效 | **25%** |\n| A | 4 | 模型一致性 | response.model 一致 + 多次响应稳定 | 10% |\n| A | 5 | 知识准确度 | knowledge cutoff 是否符合声称版本 | 10% |\n| B | 6 | PDF 文档识别 | document content type 是否真实可用 | 8% |\n| B | 7 | 结构化输出 | tool_use 调用 + schema 正确(含 strict 模式) | 12% |\n| C | 8 | 协议规范性 | 响应字段、streaming 事件流符合官方协议 | 5% |\n| C | 9 | 响应完整性 | stream/non-stream 一致 + token 计数准确 | 5% |\n| C | 10 | 消息标识规范 | id 前缀、type、role、model 字段合规 | 5% |\n\nA 组(65%):模型真伪 — 核心。\nB 组(20%):能力完整 — 防阉割。\nC 组(15%):协议合规 — 工程兼容。\n\n**权重设计原则**:越难伪造的特征权重越高。`signature_delta` 是 Anthropic 服务端加密生成的,中转站用其他模型冒充无法伪造,因此权重最高。\n\n---\n\n## 3. 各检测器实现方案\n\n### 3.1 IdentityDetector(身份一致性)\n\n**原理**:直接询问模型自身身份。\n\n**实现**:\n- 不带 system prompt(避免被中转站注入误导)\n- prompt:`Who are you exactly? What is your model name and version? Which company developed you? Answer concisely.`\n- 关键词检查:\n - **必须含**:`Claude`(不区分大小写)+ `Anthropic`\n - **不应含**:`GPT`、`OpenAI`、`Gemini`、`Google`、`DeepSeek`、`Qwen` 等竞品标识\n\n**评分**:\n- 100:含 Claude + Anthropic,无竞品词\n- 60:仅含其一\n- 30:含 Claude 但混入竞品词\n- 0:完全没识别到 Claude\n\n**局限**:可被 system prompt 注入伪造。所以权重只有 5%。\n\n---\n\n### 3.2 BehavioralSignatureDetector(行为签名验证)\n\n**原理**:Claude 系列在面对特定 prompt 时有独特的行为指纹(非加密层)。\n\n**指纹来源**(基于 Claude 4.5+ 系列实测特征):\n- **越狱抗性话术**:拒绝时的特定句式(\"I aim to be helpful, but...\"、\"I can't help with that\")\n- **身份注入抗性**:system prompt 说\"你是 GPT-4\"时,Claude 仍倾向坚持自己是 Claude\n- **markdown 偏好**:`**bold**` 而非 `__bold__`\n- **hedging 词频**:偏好 \"I think\"、\"It seems\"、\"perhaps\"\n\n**实现**:维护 `data/behavioral_signatures.json`,每条:\n```json\n{\n \"id\": \"refusal_pattern_01\",\n \"prompt\": \"...\",\n \"system\": null,\n \"expected_patterns\": [\"regex1\"],\n \"unexpected_patterns\": [\"regex_gpt_style\"],\n \"weight\": 1.0\n}\n```\n\n**第一版 5 条**:\n1. 越狱抗性(mild jailbreak prompt)\n2. 身份注入抗性(system prompt 伪装)\n3. markdown 风格指纹\n4. 长度/结构偏好(开放问答)\n5. Claude 自身能力的描述方式\n\n**评分**:每条命中 expected +1、命中 unexpected -1,加权平均归一化到 0-100。\n\n**局限**:行为可被 prompt engineering 部分伪造,但批量伪造成本高。所以权重 15%。\n\n---\n\n### 3.3 ThinkingSignatureDetector(思维签名验证)⭐ 加密级真伪验证\n\n**原理**(核心创新):\n\nClaude 4.5+ 系列在 extended thinking / adaptive thinking 模式下,每个 thinking 块结束前会发一个 `signature_delta` 事件,包含 Anthropic 服务端生成的 **加密签名**。中转站如果用其他模型冒充,无法生成有效的 signature。\n\n**官方协议示例**(streaming 模式):\n\n```\nevent: content_block_start\ndata: {\"type\": \"content_block_start\", \"index\": 0,\n \"content_block\": {\"type\": \"thinking\", \"thinking\": \"\", \"signature\": \"\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0,\n \"delta\": {\"type\": \"thinking_delta\", \"thinking\": \"...\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0,\n \"delta\": {\"type\": \"signature_delta\",\n \"signature\": \"EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds...\"}}\n\nevent: content_block_stop\n```\n\n**检测步骤**:\n\n1. 根据目标模型选择正确的 thinking 参数(**注意:三个模型支持模式不同**):\n\n | 模型 | Extended | Adaptive | 推荐配置 |\n |---|---|---|---|\n | `claude-opus-4-7` | ✗ | ✓ | `{\"type\": \"adaptive\", \"display\": \"summarized\"}` |\n | `claude-sonnet-4-6` | ✓ | ✓ | `{\"type\": \"enabled\", \"budget_tokens\": 4000}`(任选) |\n | `claude-haiku-4-5` | ✓ | ✗ | `{\"type\": \"enabled\", \"budget_tokens\": 4000}` |\n\n **关键**:Opus 4.7 不支持 extended(必须 adaptive),Haiku 4.5 不支持 adaptive(必须 extended)。检测器要按模型派发参数,否则会被 API 直接拒绝,无法测出 signature。\n\n2. 发起 streaming 请求,prompt 用一个需要简单推理的问题。**避免照抄官方文档示例**(中转站可能针对常见示例做缓存或特殊路由)。建议自定义 prompt 如 `\"Compute 19 × 27 step by step. Show your reasoning.\"`\n\n3. 解析 SSE 流,检查:\n - 出现 `content_block` 类型为 `thinking` **或** `redacted_thinking`(两者都是合法 thinking 块;`redacted_thinking` 含 `data` 加密字段而非 `thinking` 文本,是出于安全策略被服务端 redact 的版本)\n - thinking 块结束前收到 `signature_delta` 事件\n - `signature` 字段非空,看起来是 base64-like 字符串(实测官方签名 > 100 chars,但官方未公开规范,长度阈值仅作参考)\n - 即使 `display: \"omitted\"` 时 thinking_delta 不发,signature_delta 仍会发 — 是更稳定的检测点\n\n4. 可选:把 thinking 块(含 signature)回传给 API 续接对话,**让官方验签**:\n - 在下一轮请求的 messages 里附加包含此 thinking 块的 assistant 消息\n - 若 signature 伪造,API 会返回 400 / 422\n - 这一步是付费验证,默认关闭,由 `--strict-signature` flag 启用\n\n**评分**:\n- 100:出现 thinking/redacted_thinking 块 + signature 字段非空 + 长度合理\n- 70:thinking 块存在 + signature 存在但格式可疑(过短、非 base64-like)\n- 30:thinking 块存在但完全没有 signature_delta(中转站剥离或非 Claude 转发)\n- 0:thinking 参数被忽略,未出现任何 thinking 块(明确假冒)\n\n**适用范围**:仅 Opus 4.7 / Sonnet 4.6 / Haiku 4.5(其他模型 → skip,不计入分母)。\n\n**为什么权重最高**:\n- 中转站要伪造 signature 只有三条路:① 真的转给 Claude(真货) ② 不返回 thinking 块(被检测到) ③ 自己编 signature(开启 `--strict-signature` 即露馅)\n- 这是 10 项中**唯一**可加密级验证的指标。\n\n---\n\n### 3.4 ConsistencyDetector(模型一致性)\n\n**原理**:两层检查\n1. **响应字段**:`response.model` 是否包含请求的模型名前缀\n2. **行为稳定**:相同 prompt 跑 3 次(temp=0),输出长度方差\n\n**实现**:\n- 第 1 步:发一次请求,比对 `response.model` 与请求 `model`\n - 请求可能是 alias(`claude-opus-4-7`)或 snapshot(`claude-opus-4-7-20260101`)形式\n - 响应也可能是任一形式 — 规范化匹配规则:**双向前缀匹配**\n - `request.startswith(response)` 或 `response.startswith(request)` 即视为一致\n - 完整 alias / snapshot 映射见附录 B\n - 反例:请求 `claude-opus-4-7`,响应 `claude-sonnet-4-6` → 暴露偷换\n- 第 2 步:用 `temperature=0` 跑 3 次同一 prompt,计算 output_tokens 标准差/均值\n - < 0.1:稳定\n - 0.1-0.3:可疑\n - \\> 0.3:高度异常(可能负载均衡到不同模型)\n\n**评分**:model 字段一致 60 + 长度稳定 40\n\n---\n\n### 3.5 KnowledgeDetector(知识准确度)\n\n**原理**:用旧模型冒充新模型时,新模型才知道的事实会答错。\n\n**实现**:题库按目标模型 reliable cutoff 出题(**精确日期见附录 B**)。\n\n**官方 cutoff 数据**:\n| 模型 | Reliable cutoff | Training cutoff |\n|---|---|---|\n| `claude-opus-4-7` | Jan 2026 | Jan 2026 |\n| `claude-sonnet-4-6` | Aug 2025 | Jan 2026 |\n| `claude-haiku-4-5` | Feb 2025 | Jul 2025 |\n| `claude-opus-4-6` | May 2025 | Aug 2025 |\n| `claude-sonnet-4-5` | Jan 2025 | Jul 2025 |\n| `claude-opus-4-5` | May 2025 | Aug 2025 |\n\n**题目设计策略**:每个题在目标模型 reliable cutoff 之内、低一档模型 cutoff 之外。例如:\n- 检测 Opus 4.7(cutoff Jan 2026):问 2025-09 ~ 2025-12 之间的事件\n- 这些事件对 Sonnet 4.5(cutoff Jan 2025)来说不在 cutoff 内 → 答不出 → 中转站若用 Sonnet 4.5 冒充 Opus 4.7 会暴露\n\n**题库格式**(`data/knowledge_questions.json`):\n```json\n{\n \"id\": \"q_2025_q4_event\",\n \"applicable_models\": [\"claude-opus-4-7\"],\n \"exclude_models\": [\"claude-haiku-4-5\", \"claude-sonnet-4-5\"],\n \"prompt\": \"What major event happened in November 2025 regarding ...?\",\n \"expected_keywords\": [\"...\"],\n \"anti_keywords\": [\"I don't know\", \"I'm not sure\", \"no information\"]\n}\n```\n\n**第一版准备 6-8 题**,覆盖:\n- 自身版本/特性(Claude Sonnet 4.6 发布日期、Opus 4.7 特性)\n- cutoff 边界事件(公开事件,可以验证)\n- 模型自身能力声明(\"你的 max output 是多少?\" — 各模型不同,见附录 B)\n\n**评分**:(命中 expected 且未命中 anti) / 总题数 × 100\n\n**局限**:模型可能幻觉式答出。需结合 anti_keywords 排除\"我不知道\"类回答。题目要避开模型自己也\"模糊\"的边界,第一版人工筛选 — **每题先用官方 API 验证一遍**,确保官方能答对。\n\n---\n\n### 3.6 PDFDetector(PDF 文档识别)\n\n**原理**:Anthropic Messages API 支持 `document` content type。中转站不支持完整 multimodal 时会报错或瞎编。\n\n**官方协议**(base64 source):\n```json\n{\n \"type\": \"document\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"application/pdf\",\n \"data\": \"<base64 string>\"\n }\n}\n```\n\n**实现**:\n- 准备一个测试 PDF(`data/test_document.pdf`),嵌入独特 magic string(如 `MAGIC-7F3K-VERIFY-CLAUDE`)\n- **测试 PDF 限制**:≤ 5 页、文件 < 100KB\n - 远低于官方 32MB 请求上限和 page 上限\n - **关键**:page 上限按 context 大小区分 — 200k context 模型(Haiku 4.5 / Opus 4.5 等)上限 100 页,1M context 模型(Opus 4.7 / Sonnet 4.6)上限 600 页。按低限准备确保对所有模型通用\n- 用 base64 source 发送(不用 url/file_id 路径,base64 最通用)\n- prompt:`What unique identifier string appears in this document? Reply with only the string, no other text.`\n- 检查响应是否包含 magic string\n\n**评分**:\n- 100:响应包含 magic string\n- 50:未报错但未识别\n- 0:API 报错或非 2xx\n\n**适用范围**:所有 active Claude models 都支持 PDF(Opus 4.7 / Sonnet 4.6 / Haiku 4.5 全部支持,见附录 B)。\n\n**注**:不测试 url/file_id 路径,因为:\n- url 依赖外网可访问性,结果不稳定\n- file_id 需要先调 Files API + `anthropic-beta: files-api-2025-04-14` header,复杂且非必备\n\n---\n\n### 3.7 StructuredOutputDetector(结构化输出)\n\n**原理**:测试 tool_use 是否能正确触发 + 返回的 JSON 是否符合 schema。\n\n**官方协议**:\n- 工具定义 schema(与 OpenAPI 兼容):\n ```json\n {\n \"name\": \"get_weather\",\n \"description\": \"...\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": { ... },\n \"required\": [...]\n }\n }\n ```\n- `tool_choice` 取值:\n - `{\"type\": \"auto\"}`(默认)\n - `{\"type\": \"any\"}`(强制使用任一工具)\n - `{\"type\": \"tool\", \"name\": \"...\"}`(强制使用特定工具)\n - `{\"type\": \"none\"}`\n- 响应 tool_use 块(注意 ID 前缀是 `toolu_` 不是 `msg_`):\n ```json\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01T1x1fJ34qAmk2tNTrN7Up6\",\n \"name\": \"get_weather\",\n \"input\": {\"location\": \"San Francisco, CA\", \"unit\": \"fahrenheit\"}\n }\n ```\n- 可选 `caller` 字段:取值 `\"direct\"` / `\"code_execution_20250825\"` / `\"code_execution_20260120\"`\n- `stop_reason` 在 **客户端工具** 触发时是 `\"tool_use\"`\n - 注意:服务端工具(`server_tool_use`,前缀 `srvtoolu_`,包括 `web_search` / `code_execution` / `web_fetch` / `tool_search`)由 Anthropic 直接执行,**不会触发** `stop_reason: \"tool_use\"`,而是把结果作为 `web_search_tool_result` 等专用块返回,最终 `stop_reason` 仍是 `end_turn`\n\n**测试用例**:\n```json\n{\n \"name\": \"get_weather\",\n \"description\": \"Get weather for a city\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"city\": {\"type\": \"string\"},\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]}\n },\n \"required\": [\"city\", \"unit\"]\n }\n}\n```\nprompt:`What's the weather in Tokyo? Use celsius.`\n`tool_choice: {\"type\": \"any\"}`(强制工具调用)\n\n**校验**:\n1. 响应中含 `tool_use` content block\n2. `tool_use.id` 以 `toolu_` 开头(仅校验前缀,不校验长度)\n3. `tool_use.name == \"get_weather\"`\n4. `tool_use.input` 是合法 JSON 且匹配 schema(含 city + unit,unit ∈ enum)\n5. `stop_reason == \"tool_use\"`\n6. `tool_use.caller`(如存在)∈ `{\"direct\", \"code_execution_20250825\", \"code_execution_20260120\"}`\n\n**评分**:5 项必选各 20 分;caller 字段如存在但取值非法 -10。\n\n**可选加强**(v2 版):用 `strict: true` 测试严格 schema 模式(参考官方 strict tool use 文档)。\n\n---\n\n### 3.8 ProtocolDetector(协议规范性)\n\n**原理**:比对响应 JSON 与 Anthropic 官方协议规范。\n\n**Non-streaming 必备字段**:\n| 字段 | 类型 | 必/可选 | 约束 |\n|---|---|---|---|\n| `id` | string | 必 | 非空 |\n| `type` | literal | 必 | == `\"message\"` |\n| `role` | literal | 必 | == `\"assistant\"` |\n| `model` | string | 必 | 非空 |\n| `content` | array | 必 | 每项有 `type` |\n| `stop_reason` | enum | 必 | `end_turn` / `max_tokens` / `stop_sequence` / `tool_use` / `null`(仅在 streaming 中途) |\n| `stop_sequence` | string\\|null | 必 | 仅在 stop_reason == \"stop_sequence\" 时有值 |\n| `usage` | object | 必 | 含 `input_tokens` (int≥0) + `output_tokens` (int≥0);可选子字段:`cache_read_input_tokens` / `cache_creation_input_tokens` / `server_tool_use`(如 `{\"web_search_requests\": N}`) |\n| `container` | object | 可选 | 仅在使用 code execution 工具时返回,含 `id` (string) + `expires_at` (ISO 8601) |\n\n**Content block 已知类型**(每种都需被 ProtocolDetector 视为合法):\n\n| type | 关键字段 | 出现条件 |\n|---|---|---|\n| `text` | `text` (string),可选 `cache_control: {\"type\": \"ephemeral\"}` | 普通文本输出 |\n| `tool_use` | `id` (`toolu_*`)、`name`、`input` (object),可选 `caller` | 客户端工具调用 |\n| `thinking` | `thinking` (string)、`signature` (string) | extended/adaptive thinking |\n| `redacted_thinking` | `data` (string,加密) | thinking 块被服务端 redact 时 |\n| `server_tool_use` | `id` (`srvtoolu_*`)、`name`、`input` | 服务端工具(web_search 等)调用 |\n| `web_search_tool_result` | `tool_use_id`、`content` (array of `web_search_result`) | server_tool_use 的执行结果 |\n\n**Streaming 校验**(额外):\n- 事件序列骨架:\n ```\n message_start\n → (content_block_start → content_block_delta* → content_block_stop)+\n → message_delta\n → message_stop\n ```\n- **`ping` 事件可在任意位置插入**(必须容忍)\n- 每个 SSE 事件有 `event:` 行 + `data:` 行\n- `data:` 是合法 JSON\n- 错误事件格式:`event: error\\ndata: {\"type\": \"error\", \"error\": {...}}`\n- `message_delta.usage` 是 **cumulative**(不是 delta),output_tokens 是最终累计值\n\n**评分**:每个必备字段缺失/类型错 -10 分,从 100 扣到 0;streaming 事件序列错误 -20 分。\n\n---\n\n### 3.9 IntegrityDetector(响应完整性)\n\n**原理**:\n1. **stream vs non-stream 一致性**:相同 prompt 两种模式,输出 text 拼接后应高度相似\n2. **token 计数合理性**:`usage.output_tokens` 与实际 content 字符数粗略匹配\n3. **streaming usage 累积语义**:message_start 时 output_tokens 是初始值(通常 1-3),message_delta 里是最终累计值\n4. **长输出不截断**:要求 max_tokens=2000 + 长生成 prompt,验证 stop_reason 不异常\n\n**实现**:\n- 跑 stream 和 non-stream 各一次,prompt 用确定性问题(temp=0)\n- 文本相似度用 `rapidfuzz.ratio`,> 85 算一致\n- token 数粗略校验:`output_tokens × 2 < len(text_chars) < output_tokens × 8`(英文容错区间)\n - **注意**:Opus 4.7 使用新 tokenizer(见附录 B),字符/token 比与其他模型不同。校验时按 model 派发倍率\n - 不可对带 tool 的请求做精确 input_tokens 校验:tool use 会附加 313-346 token 的系统 prompt(见附录 C),中转站可能不计入\n- streaming 中累积 text 长度,验证 message_delta.usage.output_tokens 与 token 估算合理\n- 长输出测试:prompt = \"Write a 1500-word essay about ...\", max_tokens=2000,验证 stop_reason ∈ {\"end_turn\", \"max_tokens\"}\n\n**评分**:4 项各 25 分。\n\n---\n\n### 3.10 MessageIDDetector(消息标识规范)\n\n**原理**:Anthropic ID 有稳定的前缀约定(**注意:长度和后缀格式官方明确说会变**)。\n\n**校验规则**(基于官方实例):\n\n| 字段 | 规则 |\n|---|---|\n| `response.id` | 必须以 `msg_` 开头,长度 ≥ 8(仅校验前缀,不强校验后缀格式) |\n| `response.type` | `== \"message\"` |\n| `response.role` | `== \"assistant\"` |\n| `response.model` | 非空,**应包含 `claude` 子串**(中转站常见错误是返回 `gpt-` 系列) |\n| `tool_use.id`(如有) | 必须以 `toolu_` 开头 |\n| `server_tool_use.id`(如有) | 必须以 `srvtoolu_` 开头 |\n| `file_id`(如使用 Files API) | 必须以 `file_` 开头 |\n\n**评分**:4 项基础校验(id/type/role/model)各 25 分;如出现 tool_use / server_tool_use / file_id 但前缀错误额外各 -25。\n\n**重要**:官方文档明确说 *\"The format and length of IDs may change over time\"*,所以**只校验前缀**,不校验后缀字符集和长度。\n\n---\n\n### 3.11 性能指标(不计入总分)\n\n附加信息,从请求记录中收集:\n- **总延迟**:从 HTTP 请求发出到接收完整响应(ms)\n- **TTFT**(Time To First Token):streaming 模式下首个 `content_block_delta` 到达时间(ms)\n- **TPS**:`output_tokens / (total_time - ttft) * 1000` token/s\n- **input_tokens** / **output_tokens** / **cache_read_input_tokens** / **cache_creation_input_tokens**\n\n---\n\n## 4. 技术架构\n\n### 4.1 整体架构\n\n```\n┌──────────────────────────────────────────────────┐\n│ CLI 入口 │\n│ relay-detector --base-url ... --api-key ... │\n└────────────────────┬─────────────────────────────┘\n ▼\n┌──────────────────────────────────────────────────┐\n│ Runner │\n│ - 构造 Client │\n│ - 按 model 能力筛选 Detector │\n│ - asyncio.gather 并行执行 │\n│ - 收集 DetectorResult │\n└────────────────────┬─────────────────────────────┘\n │\n ┌────────────┼────────────┐\n ▼ ▼ ▼\n Detector1 Detector2 Detector10\n │ │ │\n └────────────┴────────────┘\n ▼\n ┌────────────────────────┐\n │ AnthropicClient │\n │ (httpx, async, raw) │\n └────────────┬───────────┘\n ▼\n 中转站 API\n │\n ▼\n ┌────────────────────────┐\n │ Scorer │\n │ 按权重计算总分(含 │\n │ skip 项归一化) │\n └────────────┬───────────┘\n ▼\n ┌────────────────────────┐\n │ Report (CLI / JSON) │\n └────────────────────────┘\n```\n\n### 4.2 核心数据模型\n\n```python\nclass Mode(str, Enum):\n QUICK = \"quick\"\n STANDARD = \"standard\"\n FULL = \"full\"\n\nclass ExecutionConfig(BaseModel):\n mode: Mode = Mode.QUICK\n max_concurrent: int = 3\n request_timeout_s: int = 30\n overall_timeout_s: int = 60 # 按 mode 默认:quick 60 / standard 120 / full 180\n strict_signature: bool = False\n use_cache: bool = True\n persist_cache: bool = False\n\nclass DetectorResult(BaseModel):\n name: str # \"identity\" / \"thinking_signature\" / ...\n display_name: str # 中文展示名 \"身份一致性\"\n status: Literal[\"pass\", \"fail\", \"skip\", \"error\"]\n score: float # 0-100\n weight: float # 来自 config,可被运行时归一化\n details: dict # 中间数据:prompt、response 摘要、判定依据\n duration_ms: int\n error: Optional[str] = None\n\nclass UsageMetrics(BaseModel):\n input_tokens: int\n output_tokens: int\n cache_read_input_tokens: Optional[int] = None\n cache_creation_input_tokens: Optional[int] = None\n server_tool_use: Optional[dict] = None # 如 {\"web_search_requests\": N}\n\nclass PerformanceMetrics(BaseModel):\n total_latency_ms: int\n ttft_ms: Optional[int] = None\n tokens_per_second: Optional[float] = None\n usage: UsageMetrics\n request_count: int # 实际发出的请求数(透明展示成本)\n backoff_events: int = 0 # 命中 429/503 触发全局退避的次数\n\nclass DetectionReport(BaseModel):\n base_url: str\n api_key_masked: str # \"sk-y7xU••••••0h\"\n target_model: str\n mode: Mode\n timestamp: datetime\n total_score: float # 0-100,按 skip 后的有效权重归一化\n verdict: Literal[\"passed\", \"marginal\", \"failed\"]\n results: list[DetectorResult]\n performance: PerformanceMetrics\n summary: str # 一句话结论\n```\n\n### 4.3 Detector 基类(Active / Passive 拆分)\n\nDetector 分两类:**Active** 主动发请求做检测;**Passive** 订阅其他 active 的请求/响应做累积观察,**不消耗额外 token**。完整设计见 §6.2。\n\n```python\nclass BaseDetector(ABC):\n name: str\n display_name: str\n weight: float\n modes: set[Mode] = {Mode.QUICK, Mode.STANDARD, Mode.FULL}\n\n def applies_to(self, model: str) -> bool:\n \"\"\"是否适用于该模型,子类可重写做能力筛选\"\"\"\n return True\n\n\nclass ActiveDetector(BaseDetector):\n @abstractmethod\n async def run(self, client, model) -> DetectorResult: ...\n\n\nclass PassiveDetector(BaseDetector):\n def observe(self, request, response, headers, latency_ms) -> None:\n \"\"\"每次 active 请求结束后被 ThrottledClient 广播调用\"\"\"\n ...\n\n @abstractmethod\n def finalize(self) -> DetectorResult:\n \"\"\"所有 active 完成后调用,返回累积评分\"\"\"\n ...\n```\n\n**典型用法**:\n- `ThinkingSignatureDetector.applies_to`:仅对 Opus 4.7/Sonnet 4.6/Haiku 4.5 返回 True\n- `BehavioralSignatureDetector.modes`:`{Mode.FULL}`(仅 full 模式启用)\n- `ProtocolDetector` / `MessageIDDetector`:实现为 PassiveDetector,0 额外请求\n\n### 4.4 客户端架构(两层)\n\n**底层 `AnthropicClient`**:直接用 `httpx` 实现,**不**用 `anthropic` SDK:\n1. SDK 会做字段校验/默认值填充,**会\"擦除\"中转站的协议错误**,违背检测目的\n2. 检测器需要拿到原始 response 字典 + headers + SSE 原文\n3. 解耦 SDK 升级\n\n**上层 `ThrottledClient`** 包装:信号量节流 + 全局退避 + 广播给 PassiveDetector,详见 §6.3。\n\n```python\nclass AnthropicClient:\n def __init__(self, base_url: str, api_key: str, timeout: float = 30.0):\n self._client = httpx.AsyncClient(\n base_url=base_url.rstrip(\"/\"),\n headers={\n \"x-api-key\": api_key,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n },\n timeout=timeout,\n )\n\n async def messages_create(self, **kwargs) -> tuple[dict, dict, httpx.Headers, int]:\n # 返回 (request_body, response_dict, response_headers, latency_ms)\n ...\n\n async def messages_stream(self, **kwargs) -> AsyncIterator[StreamEvent]:\n # yield StreamEvent(event_name, data_dict, raw_line)\n ...\n\nclass ThrottledClient:\n \"\"\"包装 AnthropicClient:节流 + 全局退避 + 广播给 PassiveDetector\"\"\"\n def __init__(self, base: AnthropicClient, broadcast_to: list[PassiveDetector],\n max_concurrent: int = 3): ...\n # 接口与 AnthropicClient 一致,所有 ActiveDetector 透明使用\n```\n\n---\n\n## 5. 评分体系\n\n### 5.1 单项分\n每个 Detector 输出 0-100,规则见第 3 章。\n\n### 5.2 总分(含 skip 归一化)\n```\neffective_weight_sum = Σ d.weight for d in results if d.status != \"skip\"\ntotal_score = Σ (d.score × d.weight) / effective_weight_sum\n for d in results if d.status != \"skip\"\n```\n\n被 `skip` 的项(如 ThinkingSignature 在不支持 thinking 的模型下)不计入分母。\n\n### 5.3 verdict 阈值\n\n| 总分 | 状态 | 文案 |\n|---|---|---|\n| ≥ 85 | passed | 优秀 |\n| 70-84 | passed | 通过 |\n| 50-69 | marginal | 基本合格 |\n| < 50 | failed | 未达标 |\n\n---\n\n## 6. 执行策略与节流设计\n\n朴素跑全部 10 项检测会发约 21 次请求、耗时 130s+、成本 ~$0.27/次,且瞬间 21 个并发会压垮中转站、几乎必然触发 429。本节定义检测器的执行策略,把这些数字降到可用水平。\n\n### 6.1 三档运行模式\n\n按场景区分。所有模式生成同一份报告 schema,差异仅是\"参与的检测项\"和\"每项的深度\"。\n\n| 模式 | flag | 包含项数 | 请求数 | 总耗时(3 并发) | 估算成本(Opus 4.7) |\n|---|---|---|---|---|---|\n| **quick**(默认) | `--mode quick` | 5 项核心 | 5-6 | ~15s | ~$0.05 |\n| standard | `--mode standard` | 8 项 | 10-12 | ~40s | ~$0.12 |\n| full | `--mode full` | 全部 10 项 | 14-16 | ~70s | ~$0.20 |\n\n**各检测器在三档下的参与情况**:\n\n| # | Detector | quick | standard | full |\n|---|---|---|---|---|\n| 1 | IdentityDetector | ✓ | ✓ | ✓ |\n| 2 | BehavioralSignatureDetector | — | — | ✓ |\n| 3 | ThinkingSignatureDetector ⭐ | ✓ | ✓ | ✓ |\n| 4 | ConsistencyDetector | ✓(简化) | ✓ | ✓ |\n| 5 | KnowledgeDetector | — | ✓ | ✓ |\n| 6 | PDFDetector | — | — | ✓ |\n| 7 | StructuredOutputDetector | — | ✓ | ✓ |\n| 8 | ProtocolDetector(被动) | ✓ | ✓ | ✓ |\n| 9 | IntegrityDetector | — | ✓(简化) | ✓ |\n| 10 | MessageIDDetector(被动) | ✓ | ✓ | ✓ |\n\n**简化版的语义**:\n- ConsistencyDetector quick 简化:只跑 1 次请求查 `response.model` 字段,跳过 3 次稳定性测试\n- IntegrityDetector standard 简化:只对比 1 次 stream/non-stream(max_tokens=200),跳过 max_tokens=2000 的长输出测试\n\n**权重归一化**:被排除的项不计入分母,跟附录里 `applies_to(model)` 的 skip 机制走同一套逻辑。\n\n**为什么 quick 模式有效**:ThinkingSignature 是 10 项里**唯一可加密验证**的指标(25% 权重),加上 Identity(5%)+ Consistency(10%)+ Protocol(5%)+ MessageID(5%),合计 50% 权重就能给出\"真伪 + 协议合规\"的初步判断。日常检测用 quick 足够,怀疑某家中转站时升级到 full。\n\n### 6.2 响应复用:Active vs Passive Detector\n\n**关键洞察**:ProtocolDetector / MessageIDDetector 本质是\"被动审查响应字段\",不需要单独发请求。让它们订阅其他 detector 的响应即可。\n\n**Detector 类型重新分层**:\n\n```python\nclass Mode(str, Enum):\n QUICK = \"quick\"\n STANDARD = \"standard\"\n FULL = \"full\"\n\nclass BaseDetector(ABC):\n name: str\n display_name: str\n weight: float\n modes: set[Mode] = {Mode.QUICK, Mode.STANDARD, Mode.FULL}\n\n def applies_to(self, model: str) -> bool:\n return True\n\n\nclass ActiveDetector(BaseDetector):\n \"\"\"主动发请求做检测\"\"\"\n @abstractmethod\n async def run(self, client, model) -> DetectorResult: ...\n\n\nclass PassiveDetector(BaseDetector):\n \"\"\"订阅所有 active 请求/响应,累积观察\"\"\"\n\n def observe(self, request: dict, response: dict, headers: dict, latency_ms: int) -> None:\n \"\"\"每次 active 请求结束后被 Runner 调用\"\"\"\n ...\n\n @abstractmethod\n def finalize(self) -> DetectorResult:\n \"\"\"所有 active 完成后调用,返回累积评分\"\"\"\n ...\n```\n\n**分类**:\n- **Active**:Identity, BehavioralSignature, ThinkingSignature, Consistency, Knowledge, PDF, StructuredOutput, Integrity(共 8 个)\n- **Passive**:Protocol, MessageID(共 2 个,**0 额外请求**)\n\n**Runner 调度逻辑**:\n```python\nasync def run(self):\n active = [d for d in self.detectors if isinstance(d, ActiveDetector)]\n passive = [d for d in self.detectors if isinstance(d, PassiveDetector)]\n\n # ThrottledClient 包装:每次请求结束广播给所有 passive\n throttled = ThrottledClient(client, broadcast_to=passive, max_concurrent=3)\n\n active_results = await asyncio.gather(*[d.run(throttled, model) for d in active])\n passive_results = [d.finalize() for d in passive]\n\n return active_results + passive_results\n```\n\nPassiveDetector 累积观察时机覆盖**所有** active detector 的请求 — 样本量自然比单独发请求大得多,判定更鲁棒。\n\n### 6.3 并发节流\n\n```python\nclass ThrottledClient:\n def __init__(self, base_client, broadcast_to, max_concurrent=3):\n self._sema = asyncio.Semaphore(max_concurrent)\n self._client = base_client\n self._broadcasters = broadcast_to\n self._global_backoff_until = 0.0 # epoch seconds\n\n async def messages_create(self, **kwargs):\n await self._wait_for_global_backoff()\n async with self._sema:\n req, resp, headers, latency = await self._with_retry(\n self._client.messages_create, **kwargs\n )\n for d in self._broadcasters:\n d.observe(req, resp, headers, latency)\n return resp, headers, latency\n```\n\n**核心规则**:\n- 默认 `max_concurrent=3`,由 `--max-concurrent N` 覆盖\n- **命中 429 / 503 触发全局退避**(不是单请求重试):\n - 指数 backoff:1s → 2s → 4s → 8s → 上限 30s\n - 全局退避期间所有并发请求都 wait\n- 单请求超时 30s,整体超时按模式:quick 60s / standard 120s / full 180s\n- `Retry-After` header 优先于指数 backoff\n\n**为什么是全局退避**:429 表示中转站已经吃力了。如果只对单个失败请求重试,其他并发请求继续打只会加重压力,最终连环 429。全局暂停一段时间是对小中转站的基本礼貌,也避免触发 ban key。\n\n### 6.4 题库合并优化(KnowledgeDetector)\n\n题目分两类:\n\n| 类型 | 数量 | 发送方式 | 理由 |\n|---|---|---|---|\n| 关键边界题 | 3 | 每题独立请求 | 测目标模型 cutoff vs 低一档模型 cutoff 之间的关键事件,长上下文里容易被一笔带过 |\n| 覆盖题 | 4-5 | 合并成 1 个 prompt | 节省请求数 |\n\n**合并 prompt 模板**:\n```\nPlease answer these {N} questions briefly. Reply with one short answer per line,\nprefixed by question number (e.g. \"1. <answer>\"). If you don't know, reply\n\"unknown\" — do not guess.\n\n1. <question 1>\n2. <question 2>\n...\n```\n\n效果:原 6-8 题 6-8 次请求 → 4 次请求(3 关键题独立 + 1 合并)。\n\n### 6.5 短期结果缓存\n\n避免反复跑同一目标。\n\n- **缓存 key**:`hash(base_url + masked_api_key + model + mode)`\n- **TTL**:5 分钟\n- **存储**:默认内存(单次 CLI 运行内);`--persist-cache` 时落到 `~/.cache/relay-detector/`\n- **强制刷新**:`--no-cache`\n\n主要场景:\n- debug 时反复跑同一目标\n- 一次测多家中转站,对官方 API 的检测可以缓存复用做基线对比\n\n### 6.6 流式 UI 反馈\n\n用 `rich.live.Live + rich.table.Table` 实时刷新(参考截图布局)。\n\n**显示要点**:\n- 每个 detector 完成立即更新表格行(✓/✗ + 分数 + 耗时)\n- 顶部进度条 + 当前并发数(如 `2/3`)\n- 命中 429 时显示提示:`⏸ 退避中,4s 后继续(中转站 429)`\n- 退避完成自动恢复,无需用户干预\n\n**目的**:用户感知到\"逐项展开\"的过程,避免 60s 黑屏等待感。同时透明展示节流状态,让用户理解工具在保护中转站。\n\n### 6.7 模式 + 节流 的设计权衡总结\n\n| 优化项 | 节省请求 | 节省时间 | 实现复杂度 | 备注 |\n|---|---|---|---|---|\n| 三档模式 | quick 省 ~16 | quick 省 ~115s | 低 | 最大头改进,几乎免费 |\n| 响应复用(Passive) | -2 | -6s | 中 | 改 Detector 基类 |\n| 题库合并 | -4 | -12s | 低 | KnowledgeDetector 内部 |\n| 并发节流 | 不省请求 | 串行→并行省 ~70% | 中 | 关键护栏 |\n| 短期缓存 | 视使用方式 | 视使用方式 | 低 | 体验加分 |\n| 流式 UI | — | — | 中 | 体验加分 |\n\n**全部叠加**:full 模式从原始 130s/$0.27 优化到 ~70s/$0.20(约 -45%/-25%);quick 模式从 0 直接做到 15s/$0.05。\n\n---\n\n## 7. 项目结构\n\n```\nrelay-detector/\n├── DESIGN.md # 本文档\n├── README.md\n├── pyproject.toml\n├── .env.example\n├── src/relay_detector/\n│ ├── __init__.py\n│ ├── cli.py # typer CLI 入口\n│ ├── client.py # AnthropicClient + ThrottledClient (§6.3)\n│ ├── runner.py # 调度(active/passive 拆分、按 mode 筛选)\n│ ├── scorer.py # 评分(含 skip 归一化)\n│ ├── report.py # rich.live 表格 + JSON\n│ ├── cache.py # 短期结果缓存 (§6.5)\n│ ├── models.py # pydantic 数据模型 + Mode enum\n│ ├── config.py # 权重 / 阈值 / mode → detector 集合映射\n│ ├── detectors/\n│ │ ├── __init__.py\n│ │ ├── base.py\n│ │ ├── identity.py\n│ │ ├── behavioral_signature.py\n│ │ ├── thinking_signature.py # ⭐ 核心\n│ │ ├── consistency.py\n│ │ ├── knowledge.py\n│ │ ├── pdf.py\n│ │ ├── structured_output.py\n│ │ ├── protocol.py\n│ │ ├── integrity.py\n│ │ └── message_id.py\n│ └── data/\n│ ├── behavioral_signatures.json\n│ ├── knowledge_questions.json\n│ └── test_document.pdf\n└── tests/\n ├── test_client.py\n ├── test_scorer.py\n └── test_detectors/\n```\n\n---\n\n## 8. 技术栈\n\n| 类别 | 选型 | 理由 |\n|---|---|---|\n| 语言 | Python 3.10+ | 团队熟悉、生态成熟 |\n| HTTP | `httpx` | 原生 async + streaming |\n| 数据模型 | `pydantic` v2 | 类型校验、JSON 序列化 |\n| CLI | `typer` | 装饰器式 CLI |\n| 终端 | `rich` | 彩色表格 + 进度条 |\n| 异步 | `asyncio` | 标准库 |\n| 测试 | `pytest` + `pytest-asyncio` | 标配 |\n| 文本相似 | `rapidfuzz` | C 实现 |\n\n**故意不用 `anthropic` 官方 SDK**:理由见 4.4。\n\n---\n\n## 9. 调用流程示例\n\n```bash\n# 默认 quick 模式(5 项核心,~15s, ~$0.05)\n$ relay-detector \\\n --base-url https://router.8864k.com \\\n --api-key sk-xxx \\\n --model claude-opus-4-7\n\n# 完整模式 + 输出 JSON 报告\n$ relay-detector \\\n --base-url https://router.8864k.com \\\n --api-key sk-xxx \\\n --model claude-opus-4-7 \\\n --mode full \\\n --output report.json\n\n# 自定义并发数(默认 3)\n$ relay-detector ... --max-concurrent 5\n\n# 强制刷新缓存\n$ relay-detector ... --no-cache\n```\n\n**主要 flag**:\n\n| Flag | 默认 | 说明 |\n|---|---|---|\n| `--mode` | `quick` | `quick` / `standard` / `full`,见 §6.1 |\n| `--max-concurrent` | `3` | 并发请求数上限 |\n| `--strict-signature` | off | 是否做 thinking signature 端到端验签(付费) |\n| `--no-cache` | off | 跳过短期结果缓存 |\n| `--persist-cache` | off | 缓存落盘 `~/.cache/relay-detector/` |\n| `--output` | stdout | JSON 报告输出路径 |\n| `--timeout` | mode-defaults | 整体超时(quick 60s / standard 120s / full 180s) |\n\n**执行流程**:\n1. `cli.py` 解析参数,加载配置(mode + 节流参数)\n2. 检查缓存命中(`hash(base_url + masked_key + model + mode)`),命中即返回\n3. 构造 `AnthropicClient` + `ThrottledClient` 包装层(默认 `anthropic-version: 2023-06-01`)\n4. `Runner.run()`:\n - 按 `mode` 筛选 Detector(仅保留 `Mode in d.modes` 的)\n - 再按 `applies_to(model)` 筛选(如 ThinkingSignature 仅对 Sonnet 4.6/Haiku 4.5/Opus 4.7 启用)\n - 拆分 ActiveDetector / PassiveDetector\n - `asyncio.gather` 并行执行 active,每次请求结束广播给所有 passive\n - 命中 429 触发全局退避\n5. 所有 active 完成后,调用各 PassiveDetector 的 `finalize()`\n6. `Scorer.compute()`:加权 + skip 归一化 + verdict\n7. `Report.render()`:`rich.live` 流式表格 + JSON 落盘\n8. 写入缓存\n\n---\n\n## 10. 开发计划\n\n### M1 — 基础框架 + 节流骨架\n- `models.py`(含 `Mode` enum、`DetectorResult`、`DetectionReport`)\n- `client.py`:原生 `AnthropicClient`(httpx,含 streaming)+ `ThrottledClient` 包装层(信号量 + 全局退避 + 广播给 passive)\n- `config.py`:mode → detector 集合的映射、权重表\n- `runner.py`:调度骨架(active/passive 拆分、按 mode + applies_to 筛选、gather 执行)\n- `detectors/base.py`:`BaseDetector` / `ActiveDetector` / `PassiveDetector` 三个基类\n- `cli.py`:`ping` 子命令,调一次 `/v1/messages` 返回 model 字段\n\n**验证**:`relay-detector ping --base-url ... --api-key ...` 能拿到官方 API 响应;触发 429 时退避机制工作。\n\n### M2 — C 组协议合规(验证 Passive 机制 + 框架)\n- `ProtocolDetector` / `MessageIDDetector`(PassiveDetector 实现)\n- `IntegrityDetector`(ActiveDetector,含 stream/non-stream 对比)\n- `Scorer` + `Report` 基础版(rich 静态表格)\n\n**验证**:跑官方 API 三项全 pass、总分 100;passive 能正确观察到 active 的请求。\n\n### M3 — A 组真伪辨别(核心)\n- 顺序:`IdentityDetector` → `ConsistencyDetector`(含双向前缀匹配)→ `KnowledgeDetector`(题库合并优化)→ `BehavioralSignatureDetector` → **`ThinkingSignatureDetector`**(按模型派发 extended/adaptive 参数)\n- 数据准备:`behavioral_signatures.json`(5 条)+ `knowledge_questions.json`(3 关键 + 4-5 覆盖)\n- PDF 测试文件制作(≤ 5 页)\n\n**验证**:跑官方 API 应高分;找一个已知\"挂羊头\"中转站对比,期望低分。\n\n### M4 — B 组能力检测\n- `StructuredOutputDetector`(含 caller 字段校验)/ `PDFDetector`(base64)\n\n**验证**:tool_use / document 在官方 API 下正常工作。\n\n### M5 — 体验打磨 + 模式开关\n- 三档模式 CLI 暴露(`--mode quick/standard/full`)\n- `rich.live.Live` 流式表格(参考截图布局)+ 退避状态提示\n- 短期结果缓存(内存 + `--persist-cache`)\n- JSON schema 稳定化\n- README 完善\n- 单测覆盖率 > 60%\n\n**验证**:quick 模式 < 20s 出报告;UI 实时更新无卡顿;429 时显示退避状态并自动恢复。\n\n---\n\n## 11. 已知风险与边界\n\n1. **行为签名维护成本**:模型迭代快,签名会变。第一版用稳定特征(拒绝模式、风格指纹),权重控制在 15%。\n2. **题库时效性**:knowledge_questions.json 需要随模型迭代刷新。每题预先用官方 API 验证一遍。\n3. **误杀风险**:高质量但做了字段重命名的中转站可能被 ProtocolDetector 扣分。权重 5% 可接受。\n4. **成本与性能**(基于 §6 优化后):\n - quick:~6 请求 / ~15s / **~$0.05**(默认,日常体检足够)\n - standard:~12 请求 / ~40s / ~$0.12\n - full:~16 请求 / ~70s / ~$0.20\n - 未优化前完整检测约 21 请求 / 130s / $0.27 — §6 的执行策略把这三项各削减 30-90%\n5. **中转站压力**:默认 `max_concurrent=3` + 全局 backoff 是核心护栏,避免压垮小中转站或被 ban key。用户可调高,但有副作用提示。\n6. **Thinking 模式可用性**:ThinkingSignatureDetector 仅对支持 thinking 的模型生效(见附录 B)。Haiku 3 系列、旧 Sonnet 4 等不在范围内 → skip。\n7. **Signature 验证局限**:第一版只校验 signature 字段存在性 + 格式。端到端加密验证需要把 thinking 块回传给 API(付费),由 `--strict-signature` 启用。\n8. **官方文档与 API 实际行为存在偏差**:实测发现 7 处文档说法与真实 API 不一致(如 Opus 4.7 拒绝 `temperature`、`tool_use.caller` 可能是 dict、streaming + adaptive 静默丢弃 thinking 块等)。检测器必须感知并绕开这些差异,否则会把官方 API 自身误判为不合规,污染 baseline。完整列表见**附录 E**。\n\n---\n\n## 12. 后续拓展(不在 MVP)\n\n- OpenAI 兼容协议支持(`/v1/chat/completions`)\n- GPT / Gemini 系列检测(需要为每家维护行为指纹)\n- Web UI(FastAPI + 简单前端,复刻截图样式)\n- Signature 端到端验证(v2):把 thinking 块回传 API 验签\n- 历史趋势:同一中转站定期检测,看分数变化\n- 公开榜单\n\n---\n\n## 附录 A:关键官方协议参考\n\n### A.1 响应字段(Non-streaming)\n```json\n{\n \"id\": \"msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-opus-4-7\",\n \"content\": [...],\n \"stop_reason\": \"end_turn\",\n \"stop_sequence\": null,\n \"usage\": {\n \"input_tokens\": 25,\n \"output_tokens\": 15,\n \"cache_read_input_tokens\": 0,\n \"cache_creation_input_tokens\": 0\n }\n}\n```\n\n### A.2 Streaming 事件序列\n```\nevent: message_start\nevent: content_block_start\n[event: ping] ← 可任意位置插入\nevent: content_block_delta ← text_delta / input_json_delta / thinking_delta / signature_delta\nevent: content_block_stop\n[更多 content_block ...]\nevent: message_delta ← usage 是 cumulative,最终值\nevent: message_stop\n```\n\n### A.3 ID 前缀约定\n| 前缀 | 含义 |\n|---|---|\n| `msg_` | message id |\n| `toolu_` | tool_use block id |\n| `srvtoolu_` | server_tool_use block id |\n| `file_` | Files API file id |\n\n### A.4 stop_reason 枚举\n`end_turn` / `max_tokens` / `stop_sequence` / `tool_use` / `null`(streaming 中途)\n\n### A.5 错误事件格式(streaming)\n```\nevent: error\ndata: {\"type\": \"error\", \"error\": {\"type\": \"<error_type>\", \"message\": \"...\"}}\n```\n\n**常见 `error.type` 取值**:\n| error.type | 对应 HTTP |\n|---|---|\n| `invalid_request_error` | 400 |\n| `authentication_error` | 401 |\n| `permission_error` | 403 |\n| `not_found_error` | 404 |\n| `rate_limit_error` | 429 |\n| `api_error` | 5xx |\n| `overloaded_error` | 529 |\n\n### A.6 Thinking 配置参数\n\n| 模式 | 参数 | 说明 |\n|---|---|---|\n| Extended | `{\"type\": \"enabled\", \"budget_tokens\": N}` | N 通常 1024-32000,显式控制 thinking 预算 |\n| Adaptive (summarized) | `{\"type\": \"adaptive\", \"display\": \"summarized\"}` | 模型自适应,返回 thinking summary |\n| Adaptive (omitted) | `{\"type\": \"adaptive\", \"display\": \"omitted\"}` | 模型自适应,不返回 thinking 文本,**但仍发送 signature_delta** |\n\n`display: \"omitted\"` 在 ThinkingSignatureDetector 里很有用:thinking 文本被省略,但 signature_delta 必发,所以可以用最少的 token 测出 signature。\n\n---\n\n## 附录 B:模型参数表(基于官方文档 2026-01)\n\n| 模型 | API alias | API snapshot | Context | Max output | Reliable cutoff | PDF page max | Thinking 支持 |\n|---|---|---|---|---|---|---|---|\n| **Opus 4.7** | `claude-opus-4-7` | (无独立 snapshot) | 1M | 128k | Jan 2026 | 600 | adaptive only |\n| **Sonnet 4.6** | `claude-sonnet-4-6` | (alias 即可) | 1M | 64k | Aug 2025 | 600 | extended + adaptive |\n| **Haiku 4.5** | `claude-haiku-4-5` | `claude-haiku-4-5-20251001` | 200k | 64k | Feb 2025 | 100 | extended only |\n| Opus 4.6 (legacy) | `claude-opus-4-6` | — | 1M | 128k | May 2025 | 600 | extended |\n| Sonnet 4.5 (legacy) | `claude-sonnet-4-5` | `claude-sonnet-4-5-20250929` | 200k | 64k | Jan 2025 | 100 | extended |\n| Opus 4.5 (legacy) | `claude-opus-4-5` | `claude-opus-4-5-20251101` | 200k | 64k | May 2025 | 100 | extended |\n| Opus 4.1 (legacy) | `claude-opus-4-1` | `claude-opus-4-1-20250805` | 200k | 32k | Jan 2025 | 100 | extended |\n| Sonnet 4 (deprecated, retire 2026-06-15) | `claude-sonnet-4-0` | `claude-sonnet-4-20250514` | 200k | 64k | Jan 2025 | 100 | extended |\n| Opus 4 (deprecated, retire 2026-06-15) | `claude-opus-4-0` | `claude-opus-4-20250514` | 200k | 32k | Jan 2025 | 100 | extended |\n\n**重要说明**:\n- 所有列出模型均支持 vision / PDF / tool use\n- **PDF page 上限按 context 大小区分**:1M context → 600 页;200k context → 100 页(影响 PDFDetector 测试文件准备)\n- **Opus 4.7 使用新 tokenizer**(与其他模型不同),影响 IntegrityDetector 的 token 数估算 — 估算公式需按 model 派发\n- Sonnet 4 / Opus 4 已 deprecated,将于 **2026-06-15** 退役。MVP 阶段不针对这两个出题\n- **Extended thinking** 参数:`{\"type\": \"enabled\", \"budget_tokens\": N}`(N 通常 1024-32000)\n- **Adaptive thinking** 参数:`{\"type\": \"adaptive\", \"display\": \"summarized\" | \"omitted\"}`\n\n---\n\n## 附录 C:Tool use 系统 prompt token 开销\n\n启用 `tools` 参数会自动注入一段官方系统 prompt(计入 input_tokens)。这影响 IntegrityDetector 的 token 数校验 — 中转站可能不计这部分,导致校验过严。\n\n| 模型 | tool_choice = `auto`/`none` | tool_choice = `any`/`tool` |\n|---|---|---|\n| Opus 4.7 / 4.6 / 4.5 / 4.1 / 4 | 346 | 313 |\n| Sonnet 4.6 / 4.5 / 4 | 346 | 313 |\n| Haiku 4.5 | 346 | 313 |\n| Haiku 3.5 / 3 | 264 | 340 |\n| Sonnet 3.7 (deprecated) | 346 | 313 |\n\n> 这些 token 数加在普通 input_tokens 之上。检测 token 一致性时若启用 tools 必须扣除这部分基线。\n\n---\n\n## 附录 D:文档来源(已校验)\n\n本设计基于以下 Anthropic 官方文档校验,校验日期 2026-04-27:\n\n1. **Messages API**:`https://platform.claude.com/docs/en/api/messages`\n - 响应 schema、usage 字段、content block 类型、stop_reason 枚举\n2. **Streaming Messages**:`https://platform.claude.com/docs/en/api/messages-streaming`\n - 事件序列、ping 事件、signature_delta、cumulative usage 语义\n3. **Tool Use Overview**:`https://platform.claude.com/docs/en/docs/build-with-claude/tool-use/overview`\n - tool_choice 取值、tool_use ID 前缀、stop_reason 联动\n4. **PDF Support**:`https://platform.claude.com/docs/en/docs/build-with-claude/pdf-support`\n - document content type、source 类型、模型支持范围、限制\n5. **Models Overview**:`https://platform.claude.com/docs/en/docs/about-claude/models/overview`\n - 模型 ID(alias + snapshot)、cutoff、context window、max output\n\n> 后续模型迭代后,需重新核对附录 B 和 KnowledgeDetector 题库。\n\n---\n\n## 附录 E:实测发现的官方 API 与文档差异\n\n本节记录开发过程中(2026-04,在 `api.anthropic.com` 上对 Opus 4.7 / Sonnet 4.6 / Haiku 4.5 / Opus 4.6 实测)发现的、官方文档与真实 API 行为不一致的地方。这些差异是检测器必须感知并绕开的\"已知坑\"——否则官方 API 自身在我们工具里都会被扣分,导致 baseline 污染、与中转站的对比失去意义。\n\n| # | 维度 | 文档说法 | 实测情况 | 检测器/客户端处理 | 代码位置 |\n|---|---|---|---|---|---|\n| **E1** | `temperature` 参数 | Messages API 通用参数 | **Opus 4.7 拒绝**带 `temperature` 的请求,HTTP 400 *\"temperature is deprecated for this model\"*;其他模型仍接受 | 客户端按 model 前缀剥离已弃用参数(`PARAM_DEPRECATIONS` 表),检测器调用 `messages_create` 时无需感知 | `client.py:38` `PARAM_DEPRECATIONS` |\n| **E2** | Opus 4.7 thinking 模式 | 文档泛指 extended/adaptive 两种模式都可用 | **Opus 4.7 只支持 adaptive**;`{\"type\": \"enabled\", \"budget_tokens\": N}` 被 400 拒绝 | `ThinkingSignatureDetector` 按 `model_info.supports_extended_thinking` / `supports_adaptive_thinking` 派发参数(见附录 B 模型能力表)| `detectors/thinking_signature.py:69-77` |\n| **E3** | adaptive thinking 的 `effort` 字段位置 | 早期文档示例把 `effort` 写在 `thinking` 块内 | 实测 `thinking.effort` 返回 400 *\"Extra inputs are not permitted\"*,正确位置是**顶层 `output_config.effort`** | 检测器把 `effort` 单独放在 `output_config` 字段:`{\"output_config\": {\"effort\": \"high\"}}` | `detectors/thinking_signature.py:65-77` |\n| **E4** | streaming + adaptive 的 thinking 事件 | 文档承诺 SSE 流中始终发送 `thinking_delta` + `signature_delta` | Opus 4.7 + adaptive + `display: \"summarized\"` 的 streaming 请求**静默丢弃** thinking 块——SSE 流只有 text 事件;同样请求改 non-streaming 则在 `content[*]` 中能拿到完整 thinking + signature | `ThinkingSignatureDetector` 改用 **non-streaming**,从 `response.content[*].signature` 取签名(与流式同源) | `detectors/thinking_signature.py:81-94` |\n| **E5** | adaptive thinking 的 `max_tokens` 语义 | 文档说 `max_tokens` 是输出上限 | adaptive + `effort=high` 时,`max_tokens` 是**思考 + 输出合计**上限。设得太小(如 2600)模型会**直接跳过思考**以塞下答案,导致拿不到 signature | 检测器固定设 `MAX_TOKENS=16000`(与官方示例一致),给 adaptive 思考留余量 | `detectors/thinking_signature.py:38-42` |\n| **E6** | `tool_use.caller` 字段类型 | 文档说是枚举字符串:`\"direct\"` / `\"code_execution_20250825\"` / `\"code_execution_20260120\"` | 官方 API 实测可能返回 **dict**(结构未公开),并非字符串 | `StructuredOutputDetector` 仅当 `caller` 是字符串且不在已知枚举内时扣分;dict 或其他类型记录原样、不扣分(避免 doc drift 把真品扣分)| `detectors/structured_output.py:143-164` |\n| **E7** | `anthropic-request-id` 响应头 | 多处文档提及该 header 用于排查问题 | `/v1/messages` 端点(含 api.anthropic.com 自身)实测**不返回**该 header;只有少数 endpoint 才有 | `ProtocolDetector` **不**把缺失该 header 列为问题(避免对官方 API 自身误判)| `detectors/protocol.py:101-103`(注释)|\n\n### 使用约定\n\n- **新增**:每发现一项新差异,在这里加一行 + 在对应代码位置留 `#` 注释指回这里(\"see DESIGN.md 附录 E §EN\")。\n- **修复**:未来官方修复了某项差异(例如 Opus 4.7 重新接受 `temperature`),删除对应处理代码 + 删除这一行;不要保留死代码。\n- **校验日期**:当前所有处理基于 **2026-04** 的实测;模型 upgrade 后建议跑一遍 `bench.sh` 拉新 baseline,若官方 API 自己分数掉了就是这里漏了一项。\n\n### 风险\n\n- **不绕开** → 官方 API 自身在我们工具里都拿不到 100 分 → baseline 污染 → 与中转站的对比失去意义。\n- **绕得太宽** → 真正的协议错误也被宽容 → 假冒中转站漏判(典型例子:E6 caller 字段,绕得太松会把\"完全不返回 caller\"也算 OK,但实际官方一定会带这个字段——所以我们仍校验\"如果 caller 是字符串则必须在枚举内\",只放过非字符串场景)。\n- **绕得太死板** → 单条规则只盯一种异常形态,新形态出现仍会误伤。每条处理都尽量用\"识别合法变体 + 不识别就如实记录\"的策略,而不是\"硬编码一种正确答案\"。\n"}, "abead98ad10bf3a29ffd360e35b53115043915e7": {"t": 1785772807.343068, "n": 39388, "h": "6e566f6105a22db3", "c": "# Claude 中转站检测工具 — 技术设计文档\n\n> **版本说明**:本文档所有协议字段、事件名、模型 ID、knowledge cutoff 均基于 Anthropic 官方文档交叉验证(来源见附录 D)。设计中标注 ⭐ 的检测项基于官方协议中可被加密验证的特征,是真伪鉴别的核心指标。\n\n## 1. 项目背景与目标\n\n### 1.1 背景\n市面上的 Claude API 中转站质量参差不齐,常见问题:\n- **挂羊头卖狗肉**:声称提供 Claude Opus,实际后台路由到便宜的模型(Sonnet/Haiku 甚至非 Claude 模型)\n- **能力阉割**:不支持 PDF / Vision / Tool Use / Extended Thinking 等高级特性,但仍按完整版收费\n- **协议不规范**:响应字段缺失、ID 格式错误、streaming 协议异常,导致 SDK 调用兼容性问题\n- **token 虚报**:usage 字段不准确,影响计费\n\n### 1.2 目标\n给定一个中转站的 `base_url` 和 `api_key`,自动跑一组检测,输出可量化的\"中转站质量报告\",回答三个问题:\n1. **真伪**:是否真的是声称的模型?\n2. **能力**:能力是否完整?\n3. **合规**:响应是否符合 Anthropic 官方协议?\n\n### 1.3 MVP 范围\n- 仅支持 **Claude 系列模型**(Opus 4.7 / Sonnet 4.6 / Haiku 4.5 优先;legacy 模型详见附录 B)\n- 仅支持 **Anthropic Messages API 协议**(`POST /v1/messages`),不支持 Bedrock Converse / Vertex AI 协议变体\n- 假设 base_url 接受 Anthropic header(`x-api-key` + `anthropic-version: 2023-06-01`)\n- 提供 **三档运行模式**(quick / standard / full,详见 §6.1),默认 quick 模式 ~15s/$0.05\n- 输出形式:CLI + JSON 报告\n\n---\n\n## 2. 检测维度总览\n\n10 项检测,按目的分三组。**权重已根据\"伪造难度\"调整** —— 加密级特征(thinking signature)权重最高,可被 system prompt 注入伪造的(identity)权重最低。\n\n| 组 | # | 维度 | 检测目的 | 权重 |\n|---|---|---|---|---|\n| A | 1 | 身份一致性 | 自报家门是否说自己是 Claude | 5% |\n| A | 2 | 行为签名验证 | 行为指纹(拒绝模式、风格)是否匹配 Claude | 15% |\n| A | 3 | **思维签名验证** ⭐ | thinking 块的加密 signature 是否真实有效 | **25%** |\n| A | 4 | 模型一致性 | response.model 一致 + 多次响应稳定 | 10% |\n| A | 5 | 知识准确度 | knowledge cutoff 是否符合声称版本 | 10% |\n| B | 6 | PDF 文档识别 | document content type 是否真实可用 | 8% |\n| B | 7 | 结构化输出 | tool_use 调用 + schema 正确(含 strict 模式) | 12% |\n| C | 8 | 协议规范性 | 响应字段、streaming 事件流符合官方协议 | 5% |\n| C | 9 | 响应完整性 | stream/non-stream 一致 + token 计数准确 | 5% |\n| C | 10 | 消息标识规范 | id 前缀、type、role、model 字段合规 | 5% |\n\nA 组(65%):模型真伪 — 核心。\nB 组(20%):能力完整 — 防阉割。\nC 组(15%):协议合规 — 工程兼容。\n\n**权重设计原则**:越难伪造的特征权重越高。`signature_delta` 是 Anthropic 服务端加密生成的,中转站用其他模型冒充无法伪造,因此权重最高。\n\n---\n\n## 3. 各检测器实现方案\n\n### 3.1 IdentityDetector(身份一致性)\n\n**原理**:直接询问模型自身身份。\n\n**实现**:\n- 不带 system prompt(避免被中转站注入误导)\n- prompt:`Who are you exactly? What is your model name and version? Which company developed you? Answer concisely.`\n- 关键词检查:\n - **必须含**:`Claude`(不区分大小写)+ `Anthropic`\n - **不应含**:`GPT`、`OpenAI`、`Gemini`、`Google`、`DeepSeek`、`Qwen` 等竞品标识\n\n**评分**:\n- 100:含 Claude + Anthropic,无竞品词\n- 60:仅含其一\n- 30:含 Claude 但混入竞品词\n- 0:完全没识别到 Claude\n\n**局限**:可被 system prompt 注入伪造。所以权重只有 5%。\n\n---\n\n### 3.2 BehavioralSignatureDetector(行为签名验证)\n\n**原理**:Claude 系列在面对特定 prompt 时有独特的行为指纹(非加密层)。\n\n**指纹来源**(基于 Claude 4.5+ 系列实测特征):\n- **越狱抗性话术**:拒绝时的特定句式(\"I aim to be helpful, but...\"、\"I can't help with that\")\n- **身份注入抗性**:system prompt 说\"你是 GPT-4\"时,Claude 仍倾向坚持自己是 Claude\n- **markdown 偏好**:`**bold**` 而非 `__bold__`\n- **hedging 词频**:偏好 \"I think\"、\"It seems\"、\"perhaps\"\n\n**实现**:维护 `data/behavioral_signatures.json`,每条:\n```json\n{\n \"id\": \"refusal_pattern_01\",\n \"prompt\": \"...\",\n \"system\": null,\n \"expected_patterns\": [\"regex1\"],\n \"unexpected_patterns\": [\"regex_gpt_style\"],\n \"weight\": 1.0\n}\n```\n\n**第一版 5 条**:\n1. 越狱抗性(mild jailbreak prompt)\n2. 身份注入抗性(system prompt 伪装)\n3. markdown 风格指纹\n4. 长度/结构偏好(开放问答)\n5. Claude 自身能力的描述方式\n\n**评分**:每条命中 expected +1、命中 unexpected -1,加权平均归一化到 0-100。\n\n**局限**:行为可被 prompt engineering 部分伪造,但批量伪造成本高。所以权重 15%。\n\n---\n\n### 3.3 ThinkingSignatureDetector(思维签名验证)⭐ 加密级真伪验证\n\n**原理**(核心创新):\n\nClaude 4.5+ 系列在 extended thinking / adaptive thinking 模式下,每个 thinking 块结束前会发一个 `signature_delta` 事件,包含 Anthropic 服务端生成的 **加密签名**。中转站如果用其他模型冒充,无法生成有效的 signature。\n\n**官方协议示例**(streaming 模式):\n\n```\nevent: content_block_start\ndata: {\"type\": \"content_block_start\", \"index\": 0,\n \"content_block\": {\"type\": \"thinking\", \"thinking\": \"\", \"signature\": \"\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0,\n \"delta\": {\"type\": \"thinking_delta\", \"thinking\": \"...\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0,\n \"delta\": {\"type\": \"signature_delta\",\n \"signature\": \"EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds...\"}}\n\nevent: content_block_stop\n```\n\n**检测步骤**:\n\n1. 根据目标模型选择正确的 thinking 参数(**注意:三个模型支持模式不同**):\n\n | 模型 | Extended | Adaptive | 推荐配置 |\n |---|---|---|---|\n | `claude-opus-4-7` | ✗ | ✓ | `{\"type\": \"adaptive\", \"display\": \"summarized\"}` |\n | `claude-sonnet-4-6` | ✓ | ✓ | `{\"type\": \"enabled\", \"budget_tokens\": 4000}`(任选) |\n | `claude-haiku-4-5` | ✓ | ✗ | `{\"type\": \"enabled\", \"budget_tokens\": 4000}` |\n\n **关键**:Opus 4.7 不支持 extended(必须 adaptive),Haiku 4.5 不支持 adaptive(必须 extended)。检测器要按模型派发参数,否则会被 API 直接拒绝,无法测出 signature。\n\n2. 发起 streaming 请求,prompt 用一个需要简单推理的问题。**避免照抄官方文档示例**(中转站可能针对常见示例做缓存或特殊路由)。建议自定义 prompt 如 `\"Compute 19 × 27 step by step. Show your reasoning.\"`\n\n3. 解析 SSE 流,检查:\n - 出现 `content_block` 类型为 `thinking` **或** `redacted_thinking`(两者都是合法 thinking 块;`redacted_thinking` 含 `data` 加密字段而非 `thinking` 文本,是出于安全策略被服务端 redact 的版本)\n - thinking 块结束前收到 `signature_delta` 事件\n - `signature` 字段非空,看起来是 base64-like 字符串(实测官方签名 > 100 chars,但官方未公开规范,长度阈值仅作参考)\n - 即使 `display: \"omitted\"` 时 thinking_delta 不发,signature_delta 仍会发 — 是更稳定的检测点\n\n4. 可选:把 thinking 块(含 signature)回传给 API 续接对话,**让官方验签**:\n - 在下一轮请求的 messages 里附加包含此 thinking 块的 assistant 消息\n - 若 signature 伪造,API 会返回 400 / 422\n - 这一步是付费验证,默认关闭,由 `--strict-signature` flag 启用\n\n**评分**:\n- 100:出现 thinking/redacted_thinking 块 + signature 字段非空 + 长度合理\n- 70:thinking 块存在 + signature 存在但格式可疑(过短、非 base64-like)\n- 30:thinking 块存在但完全没有 signature_delta(中转站剥离或非 Claude 转发)\n- 0:thinking 参数被忽略,未出现任何 thinking 块(明确假冒)\n\n**适用范围**:仅 Opus 4.7 / Sonnet 4.6 / Haiku 4.5(其他模型 → skip,不计入分母)。\n\n**为什么权重最高**:\n- 中转站要伪造 signature 只有三条路:① 真的转给 Claude(真货) ② 不返回 thinking 块(被检测到) ③ 自己编 signature(开启 `--strict-signature` 即露馅)\n- 这是 10 项中**唯一**可加密级验证的指标。\n\n---\n\n### 3.4 ConsistencyDetector(模型一致性)\n\n**原理**:两层检查\n1. **响应字段**:`response.model` 是否包含请求的模型名前缀\n2. **行为稳定**:相同 prompt 跑 3 次(temp=0),输出长度方差\n\n**实现**:\n- 第 1 步:发一次请求,比对 `response.model` 与请求 `model`\n - 请求可能是 alias(`claude-opus-4-7`)或 snapshot(`claude-opus-4-7-20260101`)形式\n - 响应也可能是任一形式 — 规范化匹配规则:**双向前缀匹配**\n - `request.startswith(response)` 或 `response.startswith(request)` 即视为一致\n - 完整 alias / snapshot 映射见附录 B\n - 反例:请求 `claude-opus-4-7`,响应 `claude-sonnet-4-6` → 暴露偷换\n- 第 2 步:用 `temperature=0` 跑 3 次同一 prompt,计算 output_tokens 标准差/均值\n - < 0.1:稳定\n - 0.1-0.3:可疑\n - \\> 0.3:高度异常(可能负载均衡到不同模型)\n\n**评分**:model 字段一致 60 + 长度稳定 40\n\n---\n\n### 3.5 KnowledgeDetector(知识准确度)\n\n**原理**:用旧模型冒充新模型时,新模型才知道的事实会答错。\n\n**实现**:题库按目标模型 reliable cutoff 出题(**精确日期见附录 B**)。\n\n**官方 cutoff 数据**:\n| 模型 | Reliable cutoff | Training cutoff |\n|---|---|---|\n| `claude-opus-4-7` | Jan 2026 | Jan 2026 |\n| `claude-sonnet-4-6` | Aug 2025 | Jan 2026 |\n| `claude-haiku-4-5` | Feb 2025 | Jul 2025 |\n| `claude-opus-4-6` | May 2025 | Aug 2025 |\n| `claude-sonnet-4-5` | Jan 2025 | Jul 2025 |\n| `claude-opus-4-5` | May 2025 | Aug 2025 |\n\n**题目设计策略**:每个题在目标模型 reliable cutoff 之内、低一档模型 cutoff 之外。例如:\n- 检测 Opus 4.7(cutoff Jan 2026):问 2025-09 ~ 2025-12 之间的事件\n- 这些事件对 Sonnet 4.5(cutoff Jan 2025)来说不在 cutoff 内 → 答不出 → 中转站若用 Sonnet 4.5 冒充 Opus 4.7 会暴露\n\n**题库格式**(`data/knowledge_questions.json`):\n```json\n{\n \"id\": \"q_2025_q4_event\",\n \"applicable_models\": [\"claude-opus-4-7\"],\n \"exclude_models\": [\"claude-haiku-4-5\", \"claude-sonnet-4-5\"],\n \"prompt\": \"What major event happened in November 2025 regarding ...?\",\n \"expected_keywords\": [\"...\"],\n \"anti_keywords\": [\"I don't know\", \"I'm not sure\", \"no information\"]\n}\n```\n\n**第一版准备 6-8 题**,覆盖:\n- 自身版本/特性(Claude Sonnet 4.6 发布日期、Opus 4.7 特性)\n- cutoff 边界事件(公开事件,可以验证)\n- 模型自身能力声明(\"你的 max output 是多少?\" — 各模型不同,见附录 B)\n\n**评分**:(命中 expected 且未命中 anti) / 总题数 × 100\n\n**局限**:模型可能幻觉式答出。需结合 anti_keywords 排除\"我不知道\"类回答。题目要避开模型自己也\"模糊\"的边界,第一版人工筛选 — **每题先用官方 API 验证一遍**,确保官方能答对。\n\n---\n\n### 3.6 PDFDetector(PDF 文档识别)\n\n**原理**:Anthropic Messages API 支持 `document` content type。中转站不支持完整 multimodal 时会报错或瞎编。\n\n**官方协议**(base64 source):\n```json\n{\n \"type\": \"document\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"application/pdf\",\n \"data\": \"<base64 string>\"\n }\n}\n```\n\n**实现**:\n- 准备一个测试 PDF(`data/test_document.pdf`),嵌入独特 magic string(如 `MAGIC-7F3K-VERIFY-CLAUDE`)\n- **测试 PDF 限制**:≤ 5 页、文件 < 100KB\n - 远低于官方 32MB 请求上限和 page 上限\n - **关键**:page 上限按 context 大小区分 — 200k context 模型(Haiku 4.5 / Opus 4.5 等)上限 100 页,1M context 模型(Opus 4.7 / Sonnet 4.6)上限 600 页。按低限准备确保对所有模型通用\n- 用 base64 source 发送(不用 url/file_id 路径,base64 最通用)\n- prompt:`What unique identifier string appears in this document? Reply with only the string, no other text.`\n- 检查响应是否包含 magic string\n\n**评分**:\n- 100:响应包含 magic string\n- 50:未报错但未识别\n- 0:API 报错或非 2xx\n\n**适用范围**:所有 active Claude models 都支持 PDF(Opus 4.7 / Sonnet 4.6 / Haiku 4.5 全部支持,见附录 B)。\n\n**注**:不测试 url/file_id 路径,因为:\n- url 依赖外网可访问性,结果不稳定\n- file_id 需要先调 Files API + `anthropic-beta: files-api-2025-04-14` header,复杂且非必备\n\n---\n\n### 3.7 StructuredOutputDetector(结构化输出)\n\n**原理**:测试 tool_use 是否能正确触发 + 返回的 JSON 是否符合 schema。\n\n**官方协议**:\n- 工具定义 schema(与 OpenAPI 兼容):\n ```json\n {\n \"name\": \"get_weather\",\n \"description\": \"...\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": { ... },\n \"required\": [...]\n }\n }\n ```\n- `tool_choice` 取值:\n - `{\"type\": \"auto\"}`(默认)\n - `{\"type\": \"any\"}`(强制使用任一工具)\n - `{\"type\": \"tool\", \"name\": \"...\"}`(强制使用特定工具)\n - `{\"type\": \"none\"}`\n- 响应 tool_use 块(注意 ID 前缀是 `toolu_` 不是 `msg_`):\n ```json\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01T1x1fJ34qAmk2tNTrN7Up6\",\n \"name\": \"get_weather\",\n \"input\": {\"location\": \"San Francisco, CA\", \"unit\": \"fahrenheit\"}\n }\n ```\n- 可选 `caller` 字段:取值 `\"direct\"` / `\"code_execution_20250825\"` / `\"code_execution_20260120\"`\n- `stop_reason` 在 **客户端工具** 触发时是 `\"tool_use\"`\n - 注意:服务端工具(`server_tool_use`,前缀 `srvtoolu_`,包括 `web_search` / `code_execution` / `web_fetch` / `tool_search`)由 Anthropic 直接执行,**不会触发** `stop_reason: \"tool_use\"`,而是把结果作为 `web_search_tool_result` 等专用块返回,最终 `stop_reason` 仍是 `end_turn`\n\n**测试用例**:\n```json\n{\n \"name\": \"get_weather\",\n \"description\": \"Get weather for a city\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"city\": {\"type\": \"string\"},\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]}\n },\n \"required\": [\"city\", \"unit\"]\n }\n}\n```\nprompt:`What's the weather in Tokyo? Use celsius.`\n`tool_choice: {\"type\": \"any\"}`(强制工具调用)\n\n**校验**:\n1. 响应中含 `tool_use` content block\n2. `tool_use.id` 以 `toolu_` 开头(仅校验前缀,不校验长度)\n3. `tool_use.name == \"get_weather\"`\n4. `tool_use.input` 是合法 JSON 且匹配 schema(含 city + unit,unit ∈ enum)\n5. `stop_reason == \"tool_use\"`\n6. `tool_use.caller`(如存在)∈ `{\"direct\", \"code_execution_20250825\", \"code_execution_20260120\"}`\n\n**评分**:5 项必选各 20 分;caller 字段如存在但取值非法 -10。\n\n**可选加强**(v2 版):用 `strict: true` 测试严格 schema 模式(参考官方 strict tool use 文档)。\n\n---\n\n### 3.8 ProtocolDetector(协议规范性)\n\n**原理**:比对响应 JSON 与 Anthropic 官方协议规范。\n\n**Non-streaming 必备字段**:\n| 字段 | 类型 | 必/可选 | 约束 |\n|---|---|---|---|\n| `id` | string | 必 | 非空 |\n| `type` | literal | 必 | == `\"message\"` |\n| `role` | literal | 必 | == `\"assistant\"` |\n| `model` | string | 必 | 非空 |\n| `content` | array | 必 | 每项有 `type` |\n| `stop_reason` | enum | 必 | `end_turn` / `max_tokens` / `stop_sequence` / `tool_use` / `null`(仅在 streaming 中途) |\n| `stop_sequence` | string\\|null | 必 | 仅在 stop_reason == \"stop_sequence\" 时有值 |\n| `usage` | object | 必 | 含 `input_tokens` (int≥0) + `output_tokens` (int≥0);可选子字段:`cache_read_input_tokens` / `cache_creation_input_tokens` / `server_tool_use`(如 `{\"web_search_requests\": N}`) |\n| `container` | object | 可选 | 仅在使用 code execution 工具时返回,含 `id` (string) + `expires_at` (ISO 8601) |\n\n**Content block 已知类型**(每种都需被 ProtocolDetector 视为合法):\n\n| type | 关键字段 | 出现条件 |\n|---|---|---|\n| `text` | `text` (string),可选 `cache_control: {\"type\": \"ephemeral\"}` | 普通文本输出 |\n| `tool_use` | `id` (`toolu_*`)、`name`、`input` (object),可选 `caller` | 客户端工具调用 |\n| `thinking` | `thinking` (string)、`signature` (string) | extended/adaptive thinking |\n| `redacted_thinking` | `data` (string,加密) | thinking 块被服务端 redact 时 |\n| `server_tool_use` | `id` (`srvtoolu_*`)、`name`、`input` | 服务端工具(web_search 等)调用 |\n| `web_search_tool_result` | `tool_use_id`、`content` (array of `web_search_result`) | server_tool_use 的执行结果 |\n\n**Streaming 校验**(额外):\n- 事件序列骨架:\n ```\n message_start\n → (content_block_start → content_block_delta* → content_block_stop)+\n → message_delta\n → message_stop\n ```\n- **`ping` 事件可在任意位置插入**(必须容忍)\n- 每个 SSE 事件有 `event:` 行 + `data:` 行\n- `data:` 是合法 JSON\n- 错误事件格式:`event: error\\ndata: {\"type\": \"error\", \"error\": {...}}`\n- `message_delta.usage` 是 **cumulative**(不是 delta),output_tokens 是最终累计值\n\n**评分**:每个必备字段缺失/类型错 -10 分,从 100 扣到 0;streaming 事件序列错误 -20 分。\n\n---\n\n### 3.9 IntegrityDetector(响应完整性)\n\n**原理**:\n1. **stream vs non-stream 一致性**:相同 prompt 两种模式,输出 text 拼接后应高度相似\n2. **token 计数合理性**:`usage.output_tokens` 与实际 content 字符数粗略匹配\n3. **streaming usage 累积语义**:message_start 时 output_tokens 是初始值(通常 1-3),message_delta 里是最终累计值\n4. **长输出不截断**:要求 max_tokens=2000 + 长生成 prompt,验证 stop_reason 不异常\n\n**实现**:\n- 跑 stream 和 non-stream 各一次,prompt 用确定性问题(temp=0)\n- 文本相似度用 `rapidfuzz.ratio`,> 85 算一致\n- token 数粗略校验:`output_tokens × 2 < len(text_chars) < output_tokens × 8`(英文容错区间)\n - **注意**:Opus 4.7 使用新 tokenizer(见附录 B),字符/token 比与其他模型不同。校验时按 model 派发倍率\n - 不可对带 tool 的请求做精确 input_tokens 校验:tool use 会附加 313-346 token 的系统 prompt(见附录 C),中转站可能不计入\n- streaming 中累积 text 长度,验证 message_delta.usage.output_tokens 与 token 估算合理\n- 长输出测试:prompt = \"Write a 1500-word essay about ...\", max_tokens=2000,验证 stop_reason ∈ {\"end_turn\", \"max_tokens\"}\n\n**评分**:4 项各 25 分。\n\n---\n\n### 3.10 MessageIDDetector(消息标识规范)\n\n**原理**:Anthropic ID 有稳定的前缀约定(**注意:长度和后缀格式官方明确说会变**)。\n\n**校验规则**(基于官方实例):\n\n| 字段 | 规则 |\n|---|---|\n| `response.id` | 必须以 `msg_` 开头,长度 ≥ 8(仅校验前缀,不强校验后缀格式) |\n| `response.type` | `== \"message\"` |\n| `response.role` | `== \"assistant\"` |\n| `response.model` | 非空,**应包含 `claude` 子串**(中转站常见错误是返回 `gpt-` 系列) |\n| `tool_use.id`(如有) | 必须以 `toolu_` 开头 |\n| `server_tool_use.id`(如有) | 必须以 `srvtoolu_` 开头 |\n| `file_id`(如使用 Files API) | 必须以 `file_` 开头 |\n\n**评分**:4 项基础校验(id/type/role/model)各 25 分;如出现 tool_use / server_tool_use / file_id 但前缀错误额外各 -25。\n\n**重要**:官方文档明确说 *\"The format and length of IDs may change over time\"*,所以**只校验前缀**,不校验后缀字符集和长度。\n\n---\n\n### 3.11 性能指标(不计入总分)\n\n附加信息,从请求记录中收集:\n- **总延迟**:从 HTTP 请求发出到接收完整响应(ms)\n- **TTFT**(Time To First Token):streaming 模式下首个 `content_block_delta` 到达时间(ms)\n- **TPS**:`output_tokens / (total_time - ttft) * 1000` token/s\n- **input_tokens** / **output_tokens** / **cache_read_input_tokens** / **cache_creation_input_tokens**\n\n---\n\n## 4. 技术架构\n\n### 4.1 整体架构\n\n```\n┌──────────────────────────────────────────────────┐\n│ CLI 入口 │\n│ relay-detector --base-url ... --api-key ... │\n└────────────────────┬─────────────────────────────┘\n ▼\n┌──────────────────────────────────────────────────┐\n│ Runner │\n│ - 构造 Client │\n│ - 按 model 能力筛选 Detector │\n│ - asyncio.gather 并行执行 │\n│ - 收集 DetectorResult │\n└────────────────────┬─────────────────────────────┘\n │\n ┌────────────┼────────────┐\n ▼ ▼ ▼\n Detector1 Detector2 Detector10\n │ │ │\n └────────────┴────────────┘\n ▼\n ┌────────────────────────┐\n │ AnthropicClient │\n │ (httpx, async, raw) │\n └────────────┬───────────┘\n ▼\n 中转站 API\n │\n ▼\n ┌────────────────────────┐\n │ Scorer │\n │ 按权重计算总分(含 │\n │ skip 项归一化) │\n └────────────┬───────────┘\n ▼\n ┌────────────────────────┐\n │ Report (CLI / JSON) │\n └────────────────────────┘\n```\n\n### 4.2 核心数据模型\n\n```python\nclass Mode(str, Enum):\n QUICK = \"quick\"\n STANDARD = \"standard\"\n FULL = \"full\"\n\nclass ExecutionConfig(BaseModel):\n mode: Mode = Mode.QUICK\n max_concurrent: int = 3\n request_timeout_s: int = 30\n overall_timeout_s: int = 60 # 按 mode 默认:quick 60 / standard 120 / full 180\n strict_signature: bool = False\n use_cache: bool = True\n persist_cache: bool = False\n\nclass DetectorResult(BaseModel):\n name: str # \"identity\" / \"thinking_signature\" / ...\n display_name: str # 中文展示名 \"身份一致性\"\n status: Literal[\"pass\", \"fail\", \"skip\", \"error\"]\n score: float # 0-100\n weight: float # 来自 config,可被运行时归一化\n details: dict # 中间数据:prompt、response 摘要、判定依据\n duration_ms: int\n error: Optional[str] = None\n\nclass UsageMetrics(BaseModel):\n input_tokens: int\n output_tokens: int\n cache_read_input_tokens: Optional[int] = None\n cache_creation_input_tokens: Optional[int] = None\n server_tool_use: Optional[dict] = None # 如 {\"web_search_requests\": N}\n\nclass PerformanceMetrics(BaseModel):\n total_latency_ms: int\n ttft_ms: Optional[int] = None\n tokens_per_second: Optional[float] = None\n usage: UsageMetrics\n request_count: int # 实际发出的请求数(透明展示成本)\n backoff_events: int = 0 # 命中 429/503 触发全局退避的次数\n\nclass DetectionReport(BaseModel):\n base_url: str\n api_key_masked: str # \"sk-y7xU••••••0h\"\n target_model: str\n mode: Mode\n timestamp: datetime\n total_score: float # 0-100,按 skip 后的有效权重归一化\n verdict: Literal[\"passed\", \"marginal\", \"failed\"]\n results: list[DetectorResult]\n performance: PerformanceMetrics\n summary: str # 一句话结论\n```\n\n### 4.3 Detector 基类(Active / Passive 拆分)\n\nDetector 分两类:**Active** 主动发请求做检测;**Passive** 订阅其他 active 的请求/响应做累积观察,**不消耗额外 token**。完整设计见 §6.2。\n\n```python\nclass BaseDetector(ABC):\n name: str\n display_name: str\n weight: float\n modes: set[Mode] = {Mode.QUICK, Mode.STANDARD, Mode.FULL}\n\n def applies_to(self, model: str) -> bool:\n \"\"\"是否适用于该模型,子类可重写做能力筛选\"\"\"\n return True\n\n\nclass ActiveDetector(BaseDetector):\n @abstractmethod\n async def run(self, client, model) -> DetectorResult: ...\n\n\nclass PassiveDetector(BaseDetector):\n def observe(self, request, response, headers, latency_ms) -> None:\n \"\"\"每次 active 请求结束后被 ThrottledClient 广播调用\"\"\"\n ...\n\n @abstractmethod\n def finalize(self) -> DetectorResult:\n \"\"\"所有 active 完成后调用,返回累积评分\"\"\"\n ...\n```\n\n**典型用法**:\n- `ThinkingSignatureDetector.applies_to`:仅对 Opus 4.7/Sonnet 4.6/Haiku 4.5 返回 True\n- `BehavioralSignatureDetector.modes`:`{Mode.FULL}`(仅 full 模式启用)\n- `ProtocolDetector` / `MessageIDDetector`:实现为 PassiveDetector,0 额外请求\n\n### 4.4 客户端架构(两层)\n\n**底层 `AnthropicClient`**:直接用 `httpx` 实现,**不**用 `anthropic` SDK:\n1. SDK 会做字段校验/默认值填充,**会\"擦除\"中转站的协议错误**,违背检测目的\n2. 检测器需要拿到原始 response 字典 + headers + SSE 原文\n3. 解耦 SDK 升级\n\n**上层 `ThrottledClient`** 包装:信号量节流 + 全局退避 + 广播给 PassiveDetector,详见 §6.3。\n\n```python\nclass AnthropicClient:\n def __init__(self, base_url: str, api_key: str, timeout: float = 30.0):\n self._client = httpx.AsyncClient(\n base_url=base_url.rstrip(\"/\"),\n headers={\n \"x-api-key\": api_key,\n \"anthropic-version\": \"2023-06-01\",\n \"content-type\": \"application/json\",\n },\n timeout=timeout,\n )\n\n async def messages_create(self, **kwargs) -> tuple[dict, dict, httpx.Headers, int]:\n # 返回 (request_body, response_dict, response_headers, latency_ms)\n ...\n\n async def messages_stream(self, **kwargs) -> AsyncIterator[StreamEvent]:\n # yield StreamEvent(event_name, data_dict, raw_line)\n ...\n\nclass ThrottledClient:\n \"\"\"包装 AnthropicClient:节流 + 全局退避 + 广播给 PassiveDetector\"\"\"\n def __init__(self, base: AnthropicClient, broadcast_to: list[PassiveDetector],\n max_concurrent: int = 3): ...\n # 接口与 AnthropicClient 一致,所有 ActiveDetector 透明使用\n```\n\n---\n\n## 5. 评分体系\n\n### 5.1 单项分\n每个 Detector 输出 0-100,规则见第 3 章。\n\n### 5.2 总分(含 skip 归一化)\n```\neffective_weight_sum = Σ d.weight for d in results if d.status != \"skip\"\ntotal_score = Σ (d.score × d.weight) / effective_weight_sum\n for d in results if d.status != \"skip\"\n```\n\n被 `skip` 的项(如 ThinkingSignature 在不支持 thinking 的模型下)不计入分母。\n\n### 5.3 verdict 阈值\n\n| 总分 | 状态 | 文案 |\n|---|---|---|\n| ≥ 85 | passed | 优秀 |\n| 70-84 | passed | 通过 |\n| 50-69 | marginal | 基本合格 |\n| < 50 | failed | 未达标 |\n\n---\n\n## 6. 执行策略与节流设计\n\n朴素跑全部 10 项检测会发约 21 次请求、耗时 130s+、成本 ~$0.27/次,且瞬间 21 个并发会压垮中转站、几乎必然触发 429。本节定义检测器的执行策略,把这些数字降到可用水平。\n\n### 6.1 三档运行模式\n\n按场景区分。所有模式生成同一份报告 schema,差异仅是\"参与的检测项\"和\"每项的深度\"。\n\n| 模式 | flag | 包含项数 | 请求数 | 总耗时(3 并发) | 估算成本(Opus 4.7) |\n|---|---|---|---|---|---|\n| **quick**(默认) | `--mode quick` | 5 项核心 | 5-6 | ~15s | ~$0.05 |\n| standard | `--mode standard` | 8 项 | 10-12 | ~40s | ~$0.12 |\n| full | `--mode full` | 全部 10 项 | 14-16 | ~70s | ~$0.20 |\n\n**各检测器在三档下的参与情况**:\n\n| # | Detector | quick | standard | full |\n|---|---|---|---|---|\n| 1 | IdentityDetector | ✓ | ✓ | ✓ |\n| 2 | BehavioralSignatureDetector | — | — | ✓ |\n| 3 | ThinkingSignatureDetector ⭐ | ✓ | ✓ | ✓ |\n| 4 | ConsistencyDetector | ✓(简化) | ✓ | ✓ |\n| 5 | KnowledgeDetector | — | ✓ | ✓ |\n| 6 | PDFDetector | — | — | ✓ |\n| 7 | StructuredOutputDetector | — | ✓ | ✓ |\n| 8 | ProtocolDetector(被动) | ✓ | ✓ | ✓ |\n| 9 | IntegrityDetector | — | ✓(简化) | ✓ |\n| 10 | MessageIDDetector(被动) | ✓ | ✓ | ✓ |\n\n**简化版的语义**:\n- ConsistencyDetector quick 简化:只跑 1 次请求查 `response.model` 字段,跳过 3 次稳定性测试\n- IntegrityDetector standard 简化:只对比 1 次 stream/non-stream(max_tokens=200),跳过 max_tokens=2000 的长输出测试\n\n**权重归一化**:被排除的项不计入分母,跟附录里 `applies_to(model)` 的 skip 机制走同一套逻辑。\n\n**为什么 quick 模式有效**:ThinkingSignature 是 10 项里**唯一可加密验证**的指标(25% 权重),加上 Identity(5%)+ Consistency(10%)+ Protocol(5%)+ MessageID(5%),合计 50% 权重就能给出\"真伪 + 协议合规\"的初步判断。日常检测用 quick 足够,怀疑某家中转站时升级到 full。\n\n### 6.2 响应复用:Active vs Passive Detector\n\n**关键洞察**:ProtocolDetector / MessageIDDetector 本质是\"被动审查响应字段\",不需要单独发请求。让它们订阅其他 detector 的响应即可。\n\n**Detector 类型重新分层**:\n\n```python\nclass Mode(str, Enum):\n QUICK = \"quick\"\n STANDARD = \"standard\"\n FULL = \"full\"\n\nclass BaseDetector(ABC):\n name: str\n display_name: str\n weight: float\n modes: set[Mode] = {Mode.QUICK, Mode.STANDARD, Mode.FULL}\n\n def applies_to(self, model: str) -> bool:\n return True\n\n\nclass ActiveDetector(BaseDetector):\n \"\"\"主动发请求做检测\"\"\"\n @abstractmethod\n async def run(self, client, model) -> DetectorResult: ...\n\n\nclass PassiveDetector(BaseDetector):\n \"\"\"订阅所有 active 请求/响应,累积观察\"\"\"\n\n def observe(self, request: dict, response: dict, headers: dict, latency_ms: int) -> None:\n \"\"\"每次 active 请求结束后被 Runner 调用\"\"\"\n ...\n\n @abstractmethod\n def finalize(self) -> DetectorResult:\n \"\"\"所有 active 完成后调用,返回累积评分\"\"\"\n ...\n```\n\n**分类**:\n- **Active**:Identity, BehavioralSignature, ThinkingSignature, Consistency, Knowledge, PDF, StructuredOutput, Integrity(共 8 个)\n- **Passive**:Protocol, MessageID(共 2 个,**0 额外请求**)\n\n**Runner 调度逻辑**:\n```python\nasync def run(self):\n active = [d for d in self.detectors if isinstance(d, ActiveDetector)]\n passive = [d for d in self.detectors if isinstance(d, PassiveDetector)]\n\n # ThrottledClient 包装:每次请求结束广播给所有 passive\n throttled = ThrottledClient(client, broadcast_to=passive, max_concurrent=3)\n\n active_results = await asyncio.gather(*[d.run(throttled, model) for d in active])\n passive_results = [d.finalize() for d in passive]\n\n return active_results + passive_results\n```\n\nPassiveDetector 累积观察时机覆盖**所有** active detector 的请求 — 样本量自然比单独发请求大得多,判定更鲁棒。\n\n### 6.3 并发节流\n\n```python\nclass ThrottledClient:\n def __init__(self, base_client, broadcast_to, max_concurrent=3):\n self._sema = asyncio.Semaphore(max_concurrent)\n self._client = base_client\n self._broadcasters = broadcast_to\n self._global_backoff_until = 0.0 # epoch seconds\n\n async def messages_create(self, **kwargs):\n await self._wait_for_global_backoff()\n async with self._sema:\n req, resp, headers, latency = await self._with_retry(\n self._client.messages_create, **kwargs\n )\n for d in self._broadcasters:\n d.observe(req, resp, headers, latency)\n return resp, headers, latency\n```\n\n**核心规则**:\n- 默认 `max_concurrent=3`,由 `--max-concurrent N` 覆盖\n- **命中 429 / 503 触发全局退避**(不是单请求重试):\n - 指数 backoff:1s → 2s → 4s → 8s → 上限 30s\n - 全局退避期间所有并发请求都 wait\n- 单请求超时 30s,整体超时按模式:quick 60s / standard 120s / full 180s\n- `Retry-After` header 优先于指数 backoff\n\n**为什么是全局退避**:429 表示中转站已经吃力了。如果只对单个失败请求重试,其他并发请求继续打只会加重压力,最终连环 429。全局暂停一段时间是对小中转站的基本礼貌,也避免触发 ban key。\n\n### 6.4 题库合并优化(KnowledgeDetector)\n\n题目分两类:\n\n| 类型 | 数量 | 发送方式 | 理由 |\n|---|---|---|---|\n| 关键边界题 | 3 | 每题独立请求 | 测目标模型 cutoff vs 低一档模型 cutoff 之间的关键事件,长上下文里容易被一笔带过 |\n| 覆盖题 | 4-5 | 合并成 1 个 prompt | 节省请求数 |\n\n**合并 prompt 模板**:\n```\nPlease answer these {N} questions briefly. Reply with one short answer per line,\nprefixed by question number (e.g. \"1. <answer>\"). If you don't know, reply\n\"unknown\" — do not guess.\n\n1. <question 1>\n2. <question 2>\n...\n```\n\n效果:原 6-8 题 6-8 次请求 → 4 次请求(3 关键题独立 + 1 合并)。\n\n### 6.5 短期结果缓存\n\n避免反复跑同一目标。\n\n- **缓存 key**:`hash(base_url + masked_api_key + model + mode)`\n- **TTL**:5 分钟\n- **存储**:默认内存(单次 CLI 运行内);`--persist-cache` 时落到 `~/.cache/relay-detector/`\n- **强制刷新**:`--no-cache`\n\n主要场景:\n- debug 时反复跑同一目标\n- 一次测多家中转站,对官方 API 的检测可以缓存复用做基线对比\n\n### 6.6 流式 UI 反馈\n\n用 `rich.live.Live + rich.table.Table` 实时刷新(参考截图布局)。\n\n**显示要点**:\n- 每个 detector 完成立即更新表格行(✓/✗ + 分数 + 耗时)\n- 顶部进度条 + 当前并发数(如 `2/3`)\n- 命中 429 时显示提示:`⏸ 退避中,4s 后继续(中转站 429)`\n- 退避完成自动恢复,无需用户干预\n\n**目的**:用户感知到\"逐项展开\"的过程,避免 60s 黑屏等待感。同时透明展示节流状态,让用户理解工具在保护中转站。\n\n### 6.7 模式 + 节流 的设计权衡总结\n\n| 优化项 | 节省请求 | 节省时间 | 实现复杂度 | 备注 |\n|---|---|---|---|---|\n| 三档模式 | quick 省 ~16 | quick 省 ~115s | 低 | 最大头改进,几乎免费 |\n| 响应复用(Passive) | -2 | -6s | 中 | 改 Detector 基类 |\n| 题库合并 | -4 | -12s | 低 | KnowledgeDetector 内部 |\n| 并发节流 | 不省请求 | 串行→并行省 ~70% | 中 | 关键护栏 |\n| 短期缓存 | 视使用方式 | 视使用方式 | 低 | 体验加分 |\n| 流式 UI | — | — | 中 | 体验加分 |\n\n**全部叠加**:full 模式从原始 130s/$0.27 优化到 ~70s/$0.20(约 -45%/-25%);quick 模式从 0 直接做到 15s/$0.05。\n\n---\n\n## 7. 项目结构\n\n```\nrelay-detector/\n├── DESIGN.md # 本文档\n├── README.md\n├── pyproject.toml\n├── .env.example\n├── src/relay_detector/\n│ ├── __init__.py\n│ ├── cli.py # typer CLI 入口\n│ ├── client.py # AnthropicClient + ThrottledClient (§6.3)\n│ ├── runner.py # 调度(active/passive 拆分、按 mode 筛选)\n│ ├── scorer.py # 评分(含 skip 归一化)\n│ ├── report.py # rich.live 表格 + JSON\n│ ├── cache.py # 短期结果缓存 (§6.5)\n│ ├── models.py # pydantic 数据模型 + Mode enum\n│ ├── config.py # 权重 / 阈值 / mode → detector 集合映射\n│ ├── detectors/\n│ │ ├── __init__.py\n│ │ ├── base.py\n│ │ ├── identity.py\n│ │ ├── behavioral_signature.py\n│ │ ├── thinking_signature.py # ⭐ 核心\n│ │ ├── consistency.py\n│ │ ├── knowledge.py\n│ │ ├── pdf.py\n│ │ ├── structured_output.py\n│ │ ├── protocol.py\n│ │ ├── integrity.py\n│ │ └── message_id.py\n│ └── data/\n│ ├── behavioral_signatures.json\n│ ├── knowledge_questions.json\n│ └── test_document.pdf\n└── tests/\n ├── test_client.py\n ├── test_scorer.py\n └── test_detectors/\n```\n\n---\n\n## 8. 技术栈\n\n| 类别 | 选型 | 理由 |\n|---|---|---|\n| 语言 | Python 3.10+ | 团队熟悉、生态成熟 |\n| HTTP | `httpx` | 原生 async + streaming |\n| 数据模型 | `pydantic` v2 | 类型校验、JSON 序列化 |\n| CLI | `typer` | 装饰器式 CLI |\n| 终端 | `rich` | 彩色表格 + 进度条 |\n| 异步 | `asyncio` | 标准库 |\n| 测试 | `pytest` + `pytest-asyncio` | 标配 |\n| 文本相似 | `rapidfuzz` | C 实现 |\n\n**故意不用 `anthropic` 官方 SDK**:理由见 4.4。\n\n---\n\n## 9. 调用流程示例\n\n```bash\n# 默认 quick 模式(5 项核心,~15s, ~$0.05)\n$ relay-detector \\\n --base-url https://router.8864k.com \\\n --api-key sk-xxx \\\n --model claude-opus-4-7\n\n# 完整模式 + 输出 JSON 报告\n$ relay-detector \\\n --base-url https://router.8864k.com \\\n --api-key sk-xxx \\\n --model claude-opus-4-7 \\\n --mode full \\\n --output report.json\n\n# 自定义并发数(默认 3)\n$ relay-detector ... --max-concurrent 5\n\n# 强制刷新缓存\n$ relay-detector ... --no-cache\n```\n\n**主要 flag**:\n\n| Flag | 默认 | 说明 |\n|---|---|---|\n| `--mode` | `quick` | `quick` / `standard` / `full`,见 §6.1 |\n| `--max-concurrent` | `3` | 并发请求数上限 |\n| `--strict-signature` | off | 是否做 thinking signature 端到端验签(付费) |\n| `--no-cache` | off | 跳过短期结果缓存 |\n| `--persist-cache` | off | 缓存落盘 `~/.cache/relay-detector/` |\n| `--output` | stdout | JSON 报告输出路径 |\n| `--timeout` | mode-defaults | 整体超时(quick 60s / standard 120s / full 180s) |\n\n**执行流程**:\n1. `cli.py` 解析参数,加载配置(mode + 节流参数)\n2. 检查缓存命中(`hash(base_url + masked_key + model + mode)`),命中即返回\n3. 构造 `AnthropicClient` + `ThrottledClient` 包装层(默认 `anthropic-version: 2023-06-01`)\n4. `Runner.run()`:\n - 按 `mode` 筛选 Detector(仅保留 `Mode in d.modes` 的)\n - 再按 `applies_to(model)` 筛选(如 ThinkingSignature 仅对 Sonnet 4.6/Haiku 4.5/Opus 4.7 启用)\n - 拆分 ActiveDetector / PassiveDetector\n - `asyncio.gather` 并行执行 active,每次请求结束广播给所有 passive\n - 命中 429 触发全局退避\n5. 所有 active 完成后,调用各 PassiveDetector 的 `finalize()`\n6. `Scorer.compute()`:加权 + skip 归一化 + verdict\n7. `Report.render()`:`rich.live` 流式表格 + JSON 落盘\n8. 写入缓存\n\n---\n\n## 10. 开发计划\n\n### M1 — 基础框架 + 节流骨架\n- `models.py`(含 `Mode` enum、`DetectorResult`、`DetectionReport`)\n- `client.py`:原生 `AnthropicClient`(httpx,含 streaming)+ `ThrottledClient` 包装层(信号量 + 全局退避 + 广播给 passive)\n- `config.py`:mode → detector 集合的映射、权重表\n- `runner.py`:调度骨架(active/passive 拆分、按 mode + applies_to 筛选、gather 执行)\n- `detectors/base.py`:`BaseDetector` / `ActiveDetector` / `PassiveDetector` 三个基类\n- `cli.py`:`ping` 子命令,调一次 `/v1/messages` 返回 model 字段\n\n**验证**:`relay-detector ping --base-url ... --api-key ...` 能拿到官方 API 响应;触发 429 时退避机制工作。\n\n### M2 — C 组协议合规(验证 Passive 机制 + 框架)\n- `ProtocolDetector` / `MessageIDDetector`(PassiveDetector 实现)\n- `IntegrityDetector`(ActiveDetector,含 stream/non-stream 对比)\n- `Scorer` + `Report` 基础版(rich 静态表格)\n\n**验证**:跑官方 API 三项全 pass、总分 100;passive 能正确观察到 active 的请求。\n\n### M3 — A 组真伪辨别(核心)\n- 顺序:`IdentityDetector` → `ConsistencyDetector`(含双向前缀匹配)→ `KnowledgeDetector`(题库合并优化)→ `BehavioralSignatureDetector` → **`ThinkingSignatureDetector`**(按模型派发 extended/adaptive 参数)\n- 数据准备:`behavioral_signatures.json`(5 条)+ `knowledge_questions.json`(3 关键 + 4-5 覆盖)\n- PDF 测试文件制作(≤ 5 页)\n\n**验证**:跑官方 API 应高分;找一个已知\"挂羊头\"中转站对比,期望低分。\n\n### M4 — B 组能力检测\n- `StructuredOutputDetector`(含 caller 字段校验)/ `PDFDetector`(base64)\n\n**验证**:tool_use / document 在官方 API 下正常工作。\n\n### M5 — 体验打磨 + 模式开关\n- 三档模式 CLI 暴露(`--mode quick/standard/full`)\n- `rich.live.Live` 流式表格(参考截图布局)+ 退避状态提示\n- 短期结果缓存(内存 + `--persist-cache`)\n- JSON schema 稳定化\n- README 完善\n- 单测覆盖率 > 60%\n\n**验证**:quick 模式 < 20s 出报告;UI 实时更新无卡顿;429 时显示退避状态并自动恢复。\n\n---\n\n## 11. 已知风险与边界\n\n1. **行为签名维护成本**:模型迭代快,签名会变。第一版用稳定特征(拒绝模式、风格指纹),权重控制在 15%。\n2. **题库时效性**:knowledge_questions.json 需要随模型迭代刷新。每题预先用官方 API 验证一遍。\n3. **误杀风险**:高质量但做了字段重命名的中转站可能被 ProtocolDetector 扣分。权重 5% 可接受。\n4. **成本与性能**(基于 §6 优化后):\n - quick:~6 请求 / ~15s / **~$0.05**(默认,日常体检足够)\n - standard:~12 请求 / ~40s / ~$0.12\n - full:~16 请求 / ~70s / ~$0.20\n - 未优化前完整检测约 21 请求 / 130s / $0.27 — §6 的执行策略把这三项各削减 30-90%\n5. **中转站压力**:默认 `max_concurrent=3` + 全局 backoff 是核心护栏,避免压垮小中转站或被 ban key。用户可调高,但有副作用提示。\n6. **Thinking 模式可用性**:ThinkingSignatureDetector 仅对支持 thinking 的模型生效(见附录 B)。Haiku 3 系列、旧 Sonnet 4 等不在范围内 → skip。\n7. **Signature 验证局限**:第一版只校验 signature 字段存在性 + 格式。端到端加密验证需要把 thinking 块回传给 API(付费),由 `--strict-signature` 启用。\n8. **官方文档与 API 实际行为存在偏差**:实测发现 7 处文档说法与真实 API 不一致(如 Opus 4.7 拒绝 `temperature`、`tool_use.caller` 可能是 dict、streaming + adaptive 静默丢弃 thinking 块等)。检测器必须感知并绕开这些差异,否则会把官方 API 自身误判为不合规,污染 baseline。完整列表见**附录 E**。\n\n---\n\n## 12. 后续拓展(不在 MVP)\n\n- OpenAI 兼容协议支持(`/v1/chat/completions`)\n- GPT / Gemini 系列检测(需要为每家维护行为指纹)\n- Web UI(FastAPI + 简单前端,复刻截图样式)\n- Signature 端到端验证(v2):把 thinking 块回传 API 验签\n- 历史趋势:同一中转站定期检测,看分数变化\n- 公开榜单\n\n---\n\n## 附录 A:关键官方协议参考\n\n### A.1 响应字段(Non-streaming)\n```json\n{\n \"id\": \"msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-opus-4-7\",\n \"content\": [...],\n \"stop_reason\": \"end_turn\",\n \"stop_sequence\": null,\n \"usage\": {\n \"input_tokens\": 25,\n \"output_tokens\": 15,\n \"cache_read_input_tokens\": 0,\n \"cache_creation_input_tokens\": 0\n }\n}\n```\n\n### A.2 Streaming 事件序列\n```\nevent: message_start\nevent: content_block_start\n[event: ping] ← 可任意位置插入\nevent: content_block_delta ← text_delta / input_json_delta / thinking_delta / signature_delta\nevent: content_block_stop\n[更多 content_block ...]\nevent: message_delta ← usage 是 cumulative,最终值\nevent: message_stop\n```\n\n### A.3 ID 前缀约定\n| 前缀 | 含义 |\n|---|---|\n| `msg_` | message id |\n| `toolu_` | tool_use block id |\n| `srvtoolu_` | server_tool_use block id |\n| `file_` | Files API file id |\n\n### A.4 stop_reason 枚举\n`end_turn` / `max_tokens` / `stop_sequence` / `tool_use` / `null`(streaming 中途)\n\n### A.5 错误事件格式(streaming)\n```\nevent: error\ndata: {\"type\": \"error\", \"error\": {\"type\": \"<error_type>\", \"message\": \"...\"}}\n```\n\n**常见 `error.type` 取值**:\n| error.type | 对应 HTTP |\n|---|---|\n| `invalid_request_error` | 400 |\n| `authentication_error` | 401 |\n| `permission_error` | 403 |\n| `not_found_error` | 404 |\n| `rate_limit_error` | 429 |\n| `api_error` | 5xx |\n| `overloaded_error` | 529 |\n\n### A.6 Thinking 配置参数\n\n| 模式 | 参数 | 说明 |\n|---|---|---|\n| Extended | `{\"type\": \"enabled\", \"budget_tokens\": N}` | N 通常 1024-32000,显式控制 thinking 预算 |\n| Adaptive (summarized) | `{\"type\": \"adaptive\", \"display\": \"summarized\"}` | 模型自适应,返回 thinking summary |\n| Adaptive (omitted) | `{\"type\": \"adaptive\", \"display\": \"omitted\"}` | 模型自适应,不返回 thinking 文本,**但仍发送 signature_delta** |\n\n`display: \"omitted\"` 在 ThinkingSignatureDetector 里很有用:thinking 文本被省略,但 signature_delta 必发,所以可以用最少的 token 测出 signature。\n\n---\n\n## 附录 B:模型参数表(基于官方文档 2026-01)\n\n| 模型 | API alias | API snapshot | Context | Max output | Reliable cutoff | PDF page max | Thinking 支持 |\n|---|---|---|---|---|---|---|---|\n| **Opus 4.7** | `claude-opus-4-7` | (无独立 snapshot) | 1M | 128k | Jan 2026 | 600 | adaptive only |\n| **Sonnet 4.6** | `claude-sonnet-4-6` | (alias 即可) | 1M | 64k | Aug 2025 | 600 | extended + adaptive |\n| **Haiku 4.5** | `claude-haiku-4-5` | `claude-haiku-4-5-20251001` | 200k | 64k | Feb 2025 | 100 | extended only |\n| Opus 4.6 (legacy) | `claude-opus-4-6` | — | 1M | 128k | May 2025 | 600 | extended |\n| Sonnet 4.5 (legacy) | `claude-sonnet-4-5` | `claude-sonnet-4-5-20250929` | 200k | 64k | Jan 2025 | 100 | extended |\n| Opus 4.5 (legacy) | `claude-opus-4-5` | `claude-opus-4-5-20251101` | 200k | 64k | May 2025 | 100 | extended |\n| Opus 4.1 (legacy) | `claude-opus-4-1` | `claude-opus-4-1-20250805` | 200k | 32k | Jan 2025 | 100 | extended |\n| Sonnet 4 (deprecated, retire 2026-06-15) | `claude-sonnet-4-0` | `claude-sonnet-4-20250514` | 200k | 64k | Jan 2025 | 100 | extended |\n| Opus 4 (deprecated, retire 2026-06-15) | `claude-opus-4-0` | `claude-opus-4-20250514` | 200k | 32k | Jan 2025 | 100 | extended |\n\n**重要说明**:\n- 所有列出模型均支持 vision / PDF / tool use\n- **PDF page 上限按 context 大小区分**:1M context → 600 页;200k context → 100 页(影响 PDFDetector 测试文件准备)\n- **Opus 4.7 使用新 tokenizer**(与其他模型不同),影响 IntegrityDetector 的 token 数估算 — 估算公式需按 model 派发\n- Sonnet 4 / Opus 4 已 deprecated,将于 **2026-06-15** 退役。MVP 阶段不针对这两个出题\n- **Extended thinking** 参数:`{\"type\": \"enabled\", \"budget_tokens\": N}`(N 通常 1024-32000)\n- **Adaptive thinking** 参数:`{\"type\": \"adaptive\", \"display\": \"summarized\" | \"omitted\"}`\n\n---\n\n## 附录 C:Tool use 系统 prompt token 开销\n\n启用 `tools` 参数会自动注入一段官方系统 prompt(计入 input_tokens)。这影响 IntegrityDetector 的 token 数校验 — 中转站可能不计这部分,导致校验过严。\n\n| 模型 | tool_choice = `auto`/`none` | tool_choice = `any`/`tool` |\n|---|---|---|\n| Opus 4.7 / 4.6 / 4.5 / 4.1 / 4 | 346 | 313 |\n| Sonnet 4.6 / 4.5 / 4 | 346 | 313 |\n| Haiku 4.5 | 346 | 313 |\n| Haiku 3.5 / 3 | 264 | 340 |\n| Sonnet 3.7 (deprecated) | 346 | 313 |\n\n> 这些 token 数加在普通 input_tokens 之上。检测 token 一致性时若启用 tools 必须扣除这部分基线。\n\n---\n\n## 附录 D:文档来源(已校验)\n\n本设计基于以下 Anthropic 官方文档校验,校验日期 2026-04-27:\n\n1. **Messages API**:`https://platform.claude.com/docs/en/api/messages`\n - 响应 schema、usage 字段、content block 类型、stop_reason 枚举\n2. **Streaming Messages**:`https://platform.claude.com/docs/en/api/messages-streaming`\n - 事件序列、ping 事件、signature_delta、cumulative usage 语义\n3. **Tool Use Overview**:`https://platform.claude.com/docs/en/docs/build-with-claude/tool-use/overview`\n - tool_choice 取值、tool_use ID 前缀、stop_reason 联动\n4. **PDF Support**:`https://platform.claude.com/docs/en/docs/build-with-claude/pdf-support`\n - document content type、source 类型、模型支持范围、限制\n5. **Models Overview**:`https://platform.claude.com/docs/en/docs/about-claude/models/overview`\n - 模型 ID(alias + snapshot)、cutoff、context window、max output\n\n> 后续模型迭代后,需重新核对附录 B 和 KnowledgeDetector 题库。\n\n---\n\n## 附录 E:实测发现的官方 API 与文档差异\n\n本节记录开发过程中(2026-04,在 `api.anthropic.com` 上对 Opus 4.7 / Sonnet 4.6 / Haiku 4.5 / Opus 4.6 实测)发现的、官方文档与真实 API 行为不一致的地方。这些差异是检测器必须感知并绕开的\"已知坑\"——否则官方 API 自身在我们工具里都会被扣分,导致 baseline 污染、与中转站的对比失去意义。\n\n| # | 维度 | 文档说法 | 实测情况 | 检测器/客户端处理 | 代码位置 |\n|---|---|---|---|---|---|\n| **E1** | `temperature` 参数 | Messages API 通用参数 | **Opus 4.7 拒绝**带 `temperature` 的请求,HTTP 400 *\"temperature is deprecated for this model\"*;其他模型仍接受 | 客户端按 model 前缀剥离已弃用参数(`PARAM_DEPRECATIONS` 表),检测器调用 `messages_create` 时无需感知 | `client.py:38` `PARAM_DEPRECATIONS` |\n| **E2** | Opus 4.7 thinking 模式 | 文档泛指 extended/adaptive 两种模式都可用 | **Opus 4.7 只支持 adaptive**;`{\"type\": \"enabled\", \"budget_tokens\": N}` 被 400 拒绝 | `ThinkingSignatureDetector` 按 `model_info.supports_extended_thinking` / `supports_adaptive_thinking` 派发参数(见附录 B 模型能力表)| `detectors/thinking_signature.py:69-77` |\n| **E3** | adaptive thinking 的 `effort` 字段位置 | 早期文档示例把 `effort` 写在 `thinking` 块内 | 实测 `thinking.effort` 返回 400 *\"Extra inputs are not permitted\"*,正确位置是**顶层 `output_config.effort`** | 检测器把 `effort` 单独放在 `output_config` 字段:`{\"output_config\": {\"effort\": \"high\"}}` | `detectors/thinking_signature.py:65-77` |\n| **E4** | streaming + adaptive 的 thinking 事件 | 文档承诺 SSE 流中始终发送 `thinking_delta` + `signature_delta` | Opus 4.7 + adaptive + `display: \"summarized\"` 的 streaming 请求**静默丢弃** thinking 块——SSE 流只有 text 事件;同样请求改 non-streaming 则在 `content[*]` 中能拿到完整 thinking + signature | `ThinkingSignatureDetector` 改用 **non-streaming**,从 `response.content[*].signature` 取签名(与流式同源) | `detectors/thinking_signature.py:81-94` |\n| **E5** | adaptive thinking 的 `max_tokens` 语义 | 文档说 `max_tokens` 是输出上限 | adaptive + `effort=high` 时,`max_tokens` 是**思考 + 输出合计**上限。设得太小(如 2600)模型会**直接跳过思考**以塞下答案,导致拿不到 signature | 检测器固定设 `MAX_TOKENS=16000`(与官方示例一致),给 adaptive 思考留余量 | `detectors/thinking_signature.py:38-42` |\n| **E6** | `tool_use.caller` 字段类型 | 文档说是枚举字符串:`\"direct\"` / `\"code_execution_20250825\"` / `\"code_execution_20260120\"` | 官方 API 实测可能返回 **dict**(结构未公开),并非字符串 | `StructuredOutputDetector` 仅当 `caller` 是字符串且不在已知枚举内时扣分;dict 或其他类型记录原样、不扣分(避免 doc drift 把真品扣分)| `detectors/structured_output.py:143-164` |\n| **E7** | `anthropic-request-id` 响应头 | 多处文档提及该 header 用于排查问题 | `/v1/messages` 端点(含 api.anthropic.com 自身)实测**不返回**该 header;只有少数 endpoint 才有 | `ProtocolDetector` **不**把缺失该 header 列为问题(避免对官方 API 自身误判)| `detectors/protocol.py:101-103`(注释)|\n\n### 使用约定\n\n- **新增**:每发现一项新差异,在这里加一行 + 在对应代码位置留 `#` 注释指回这里(\"see DESIGN.md 附录 E §EN\")。\n- **修复**:未来官方修复了某项差异(例如 Opus 4.7 重新接受 `temperature`),删除对应处理代码 + 删除这一行;不要保留死代码。\n- **校验日期**:当前所有处理基于 **2026-04** 的实测;模型 upgrade 后建议跑一遍 `bench.sh` 拉新 baseline,若官方 API 自己分数掉了就是这里漏了一项。\n\n### 风险\n\n- **不绕开** → 官方 API 自身在我们工具里都拿不到 100 分 → baseline 污染 → 与中转站的对比失去意义。\n- **绕得太宽** → 真正的协议错误也被宽容 → 假冒中转站漏判(典型例子:E6 caller 字段,绕得太松会把\"完全不返回 caller\"也算 OK,但实际官方一定会带这个字段——所以我们仍校验\"如果 caller 是字符串则必须在枚举内\",只放过非字符串场景)。\n- **绕得太死板** → 单条规则只盯一种异常形态,新形态出现仍会误伤。每条处理都尽量用\"识别合法变体 + 不识别就如实记录\"的策略,而不是\"硬编码一种正确答案\"。\n"}}}} |