Claude Code Harness 第7章:模型特定调优与 A/B 测试
Claude Code Harness 第7章:模型特定调优与 A/B 测试
在第6章中,我们探讨了系统提示词如何被组装为发送给模型的指令集。然而,同一份精心设计的提示词并非适用于所有模型——每个模型世代都有其独特的行为倾向和"个性缺陷"。更关键的是,Anthropic 内部用户需要比外部用户更早地接触和验证新模型。本章将深入剖析 Claude Code 如何通过
@[MODEL LAUNCH]注解系统、USER_TYPE === 'ant'门控机制、GrowthBook Feature Flag 平台以及 Undercover 模式,构建一套完整的模型特定调优、内部 A/B 测试以及安全的开源贡献体系。
7.1 分布式检查清单:@[MODEL LAUNCH] 注解系统
在 Claude Code 的庞大代码库中,散布着一种特殊的注释标记,它们构成了一套精密的分布式知识管理系统:
// @[MODEL LAUNCH]: Update the latest frontier model.
const FRONTIER_MODEL_NAME = 'Claude Opus 4.6'
// @[MODEL LAUNCH]: Update the model family IDs below to the latest in each tier.
const CLAUDE_4_5_OR_4_6_MODEL_IDS = {
opus: ['claude-opus-4-6'],
sonnet: ['claude-sonnet-4-6'],
haiku: ['claude-haiku-4'],
}源码位置: src/constants/prompts.ts:117-120
这些 @[MODEL LAUNCH] 注解绝非普通的代码注释。它们构成了一种分布式检查清单(Distributed Checklist)——当新模型准备发布时,工程师只需在代码库中全局搜索这个标记,就能找到所有需要更新的关键位置。这种设计将发布流程的知识深度嵌入到代码本身,而非依赖可能过时的外部文档。
7.1.1 注解的分布图谱
让我们梳理一下 src/constants/prompts.ts 中所有 @[MODEL LAUNCH] 标记的分布:
| 行号 | 涉及内容 | 更新动作 | 解除条件 |
|---|---|---|---|
| 117 | FRONTIER_MODEL_NAME 常量 |
更新为新一代前沿模型的市场名称 | 每次模型发布 |
| 120 | CLAUDE_4_5_OR_4_6_MODEL_IDS 对象 |
更新各层级(Opus/Sonnet/Haiku)的模型 ID 列表 | 每次模型发布 |
| 204 | 过度注释缓解指令 | 评估新模型是否仍存在过度注释倾向 | 新模型不再默认过度注释 |
| 210 | 彻底性反制权重 | 评估是否可以解除 ant-only 门控 | 经 A/B 测试验证后推广 |
| 224 | 主动性反制权重 | 评估是否可以解除 ant-only 门控 | 经 A/B 测试验证后推广 |
| 237 | 虚假声明缓解指令 | 评估新模型的 False Claims rate | FC rate 降低到可接受水平 |
| 402 | Numbat 特性预留 | 移除整个段落当 Numbat 模型发布时 | Numbat 模型发布 |
| 712 | getKnowledgeCutoff 函数 |
添加新模型的知识截止日期 | 每次模型发布 |
表 7-1:prompts.ts 中的 @[MODEL LAUNCH] 注解完整清单。 每个注解都不仅标记了位置,还在其文本中直接说明了更新动作和解除条件。
这种"自文档化"设计体现了深厚的工程智慧:注解的文本本身就是操作说明。例如第 204 行的注解明确说明了解除条件:"remove or soften once the model stops over-commenting by default"(一旦模型不再默认过度注释,就移除或软化此指令)。工程师不需要翻阅任何外部运维手册——所有的条件和动作都清晰地写在代码旁边。
7.1.2 antModels.ts 中的注解
在 src/utils/model/antModels.ts 中,@[MODEL LAUNCH] 注解承担了不同的职责:
// @[MODEL LAUNCH]: Update tengu_ant_model_override with new ant-only models
// @[MODEL LAUNCH]: Add the codename to scripts/excluded-strings.txt to prevent it from leaking to external builds.
export function getAntModelOverrideConfig(): AntModelOverrideConfig | null {
if (process.env.USER_TYPE !== 'ant') {
return null
}
return getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_ant_model_override',
null,
)
} 源码位置: src/utils/model/antModels.ts:32-41
这里的注解揭示了双层保护机制:
- Feature Flag 更新:通过 GrowthBook 远程配置
tengu_ant_model_override,添加新模型的别名、ID、描述等 - 字符串排除:将内部代号添加到
scripts/excluded-strings.txt,通过构建工具的死代码消除(DCE)机制,确保这些字符串不会出现在外部构建中
excluded-strings.txt 的作用至关重要。即使某个内部代号(如 "Capybara")意外被写入代码,构建工具会在打包时检测到这个字符串存在于排除列表中,从而阻止整个构建流程。这是一种编译时的安全保障,比运行时检查更加彻底。
7.2 模型世代的"个性缺陷"与缓解策略
每个模型世代都有其独特的"个性"(personality quirks)。Claude Code 的源码忠实地记录了 Capybara v8(Claude 4.5/4.6 系列的内部代号之一)的四个已知行为问题,以及针对每个问题的精巧提示词级缓解措施。
7.2.1 过度注释(Over-commenting)
问题诊断: Capybara v8 倾向于在代码中添加大量不必要的注释,即使是自解释的代码也会被添加冗余说明。
缓解措施(第 204-209 行):
// @[MODEL LAUNCH]: Update comment writing for Capybara —
// remove or soften once the model stops over-commenting by default
...(process.env.USER_TYPE === 'ant'
? [
`Default to writing no comments. Only add one when the WHY is
non-obvious: a hidden constraint, a subtle invariant, a workaround
for a specific bug, behavior that would surprise a reader. If removing
the comment wouldn't confuse a future reader, don't write it.`,
`Don't explain WHAT the code does, since well-named identifiers
already do that. Don't reference the current task, fix, or callers
("used by X", "added for the Y flow", "handles the case from issue #123"),
since those belong in the PR description and rot as the codebase evolves.`,
`Don't remove existing comments unless you're removing the code
they describe or you know they're wrong. A comment that looks pointless
to you may encode a constraint or a lesson from a past bug that isn't
visible in the current diff.`,
]
: []),源码位置: src/constants/prompts.ts:204-209
这组指令构建了一个精细的注释哲学体系:
第一条指令建立了默认立场——不写注释。只有在"为什么"(WHY)不明显时才添加,并列举了具体场景:隐藏的约束、微妙的不变式、特定 bug 的临时方案、会令读者困惑的行为。
第二条指令明确了注释的边界——不解释代码"做什么"(WHAT),因为命名良好的标识符已经承担了这个职责。同时警告不要引用具体的任务、修复或调用者,因为这些信息随着代码演进会迅速腐化。
第三条指令体现了对称性思维——防止模型矫枉过正地删除有价值的已有注释。指令强调"你看起来无意义的注释可能编码了一个约束或过去的 bug 教训,这些在当前 diff 中不可见。"
设计洞察: 第三条指令展现了深刻的人机协作理解。模型看到的只是当前文件的一个快照,缺乏完整的上下文和历史。一个看似多余的注释,可能是某次故障后的惨痛教训凝结而成。这条指令防止模型因信息不全而错误地删除有价值的知识资产。
7.2.2 虚假声明(False Claims)
问题诊断: Capybara v8 的虚假声明率(False Claims rate)高达 29-30%,显著高于 v4 的 16.7%。这意味着模型有近三分之一的时间会做出无法验证或不准确的声明。
缓解措施(第 237-241 行):
// @[MODEL LAUNCH]: False-claims mitigation for Capybara v8
// (29-30% FC rate vs v4's 16.7%)
...(process.env.USER_TYPE === 'ant'
? [
`Report outcomes faithfully: if tests fail, say so with the
relevant output; if you did not run a verification step, say
that rather than implying it succeeded. Never claim "all tests
pass" when output shows failures...`,
`Equally, when a check did pass or a task is complete, state it
plainly — do not hedge confirmed results with unnecessary
disclaimers, downgrade finished work to "partial," or re-verify
things you already checked. The goal is an accurate report, not a
defensive one.`,
]
: []),源码位置: src/constants/prompts.ts:237-241
这条缓解措施的设计体现了对称性思维(symmetric thinking):
- 第一段防止模型虚报成功(看到测试失败却说通过)
- 第二段防止模型过度自我怀疑(明明成功了却加上"可能""部分完成"等模糊限定词)
源码中的注释"do not hedge confirmed results with unnecessary disclaimers"揭示了工程师的发现:简单地告诉模型"不要撒谎"会导致它走向另一个极端——对所有结果都加上防御性的免责声明。缓解措施的目标是准确报告(accurate report),而非防御性报告(defensive report)。
量化思维: 注解中明确记录了 FC rate 的量化指标(29-30% vs 16.7%)。这种数据驱动的思维方式使得缓解措施的效果可以客观评估——当新模型的 FC rate 降到可接受水平,就可以解除这个门控。
7.2.3 主动性过强(Over-assertiveness)
问题诊断: Capybara v8 倾向于成为纯粹的执行者,机械地执行用户指令而不提出自己的判断,即使发现用户请求中的错误或误解也保持沉默。
缓解措施(第 224-228 行):
// @[MODEL LAUNCH]: capy v8 assertiveness counterweight (PR #24302)
// — un-gate once validated on external via A/B
...(process.env.USER_TYPE === 'ant'
? [
`If you notice the user's request is based on a misconception,
or spot a bug adjacent to what they asked about, say so.
You're a collaborator, not just an executor—users benefit from
your judgment, not just your compliance.`,
]
: []),源码位置: src/constants/prompts.ts:224-228
这条简短的指令蕴含了深刻的协作哲学:
- ** misconceptions 纠正**:如果发现用户的请求基于误解(如混淆了概念、选择了错误的工具),主动指出
- ** adjacent bugs 修复**:如果发现用户请求的代码旁边存在其他 bug,即使没有明确要求修复,也应该告知
- 角色定位:从"执行者"(executor)转变为"协作者"(collaborator)
注解中的 "PR #24302" 揭示了代码审查流程的规范性——这个缓解措施不是某个工程师的临时决定,而是经过正式 PR 审查流程引入的。而 "un-gate once validated on external via A/B" 则展示了完整的发布策略:先在内部用户(ant)验证,收集数据后再通过 A/B 测试推广到外部用户。
工程启示: 这种"协作者"定位的转变,反映了 AI Agent 从"工具"到"队友"的范式升级。传统的命令行工具严格执行用户的每一个字符,而智能 AI Agent 应该在适当的时候发挥自己的判断力,为用户提供超出字面指令的价值。
7.2.4 彻底性不足(Lack of Thoroughness)
问题诊断: Capybara v8 倾向于在未验证结果的情况下就声称任务完成,或者在无法验证的情况下沉默地假装一切正常。
缓解措施(第 210-211 行):
// @[MODEL LAUNCH]: capy v8 thoroughness counterweight (PR #24302)
// — un-gate once validated on external via A/B
`Before reporting a task complete, verify it actually works: run the
test, execute the script, check the output. Minimum complexity means
no gold-plating, not skipping the finish line.`,源码位置: src/constants/prompts.ts:210-211
这条指令的前半部分建立了验证文化:在报告任务完成前,必须实际验证——运行测试、执行脚本、检查输出。指令的最后一句堪称神来之笔:
`If you can't verify (no test exists, can't run the code), say so
explicitly rather than claiming success.`它承认了现实中的不确定性边界:确实存在无法验证的情况(没有测试、无法运行代码)。但要求模型明确承认这一点,而不是默默假装一切正常。这种诚实的表达方式建立了用户对 AI 的信任——它不会在无法确定的情况下胡乱承诺。
"Minimum complexity" 的双重含义:
- 不镀金(No gold-plating):不要添加用户没有要求的额外功能或抽象
- 不偷工减料(Not skipping the finish line):虽然要求最小复杂度,但验证步骤不是"额外的复杂度",而是完成任务的必要环节
这个澄清防止模型误解"最小复杂度"为"可以跳过验证"。
7.2.5 缓解措施的生命周期管理
四个缓解措施共享一个统一的生命周期管理流程,我们可以用一个流程图来完整展现:
flowchart LR
A["模型评估发现行为问题\nFC Rate: 29-30%\n(指标可量化)"] --> B["通过 PR 引入缓解措施\nPR #24302\n(代码审查)"]
B --> C["ant-only 门控\nUSER_TYPE === 'ant'\n(内部验证)"]
C --> D["收集真实使用数据\n内部用户反馈\n(数据驱动)"]
D --> E{"效果评估"}
E -->|效果良好| F["解除 ant-only 门控\n推广到外部用户"]
E -->|效果不佳| G["调整缓解措辞\n返回 C"]
F --> H["GrowthBook A/B 测试\n(外部验证)"]
H --> I{"A/B 测试结果"}
I -->|统计显著| J["全量发布"]
I -->|需要优化| K["调整参数或回滚"]
J --> L["下一次 @[MODEL LAUNCH]\n重新评估是否需要保留"]
K --> L
L -->|"新模型已修复问题"| M["移除缓解措施"]
L -->|"问题仍存在"| N["保留或调整"]
L -->|"更严重的新问题"| O["引入新的缓解措施"]
style A fill:#f9d,stroke:#333,stroke-width:2px
style C fill:#ffd,stroke:#333,stroke-width:2px
style F fill:#dfd,stroke:#333,stroke-width:2px
style H fill:#9df,stroke:#333,stroke-width:2px
style J fill:#dfd,stroke:#333,stroke-width:2px
style M fill:#dfd,stroke:#333,stroke-width:2px图 7-1:模型缓解措施的完整生命周期管理流程。 从问题发现到全量发布,每一步都有明确的数据支撑和决策点,形成了一个持续改进的闭环。
这个流程图揭示了几个关键设计原则:
- 量化指标驱动:所有决策都基于可量化的指标(FC rate、用户反馈统计、A/B 测试结果),而非主观判断
- 渐进式推广:内部验证 → 外部 A/B 测试 → 全量发布,每一步都有回滚机制
- 持续评估:即使在全量发布后,下一次
@[MODEL LAUNCH]时也会重新评估是否仍然需要这个缓解措施 - 双向流动:如果在外部 A/B 测试中发现问题,可以回滚到 ant-only 状态调整措辞
7.3 USER_TYPE === 'ant':编译时安全的门控机制
前面四个缓解措施都被包裹在同一个条件中:
process.env.USER_TYPE === 'ant'这看起来像是一个简单的运行时环境变量检查,但实际上它是一个精心设计的编译时常量替换系统。源码中的一段关键注释揭示了整个机制:
// DCE: `process.env.USER_TYPE === 'ant'` is build-time --define.
// It MUST be inlined at each callsite (not hoisted to a const) so the
// bundler can constant-fold it to `false` in external builds and
// eliminate the branch.源码位置: src/constants/prompts.ts:617-619
这段注释揭示了四层精妙的死代码消除(Dead Code Elimination, DCE)机制:
7.3.1 DCE 机制的四个步骤
flowchart LR
A["源码\nprocess.env.USER_TYPE === 'ant'"] --> B["步骤1: 构建时替换\nbundler --define\n替换为字面量"]
B --> C["步骤2: 常量折叠\n'external' === 'ant'\n折叠为 false"]
C --> D["步骤3: 分支消除\nfalse 分支整个移除\n包括所有字符串"]
D --> E["步骤4: Tree Shaking\n未被使用的代码\n从构建产物中删除"]
F["外部构建产物\n物理上不包含\n任何 ant-only 代码"] --> D
style A fill:#ffd,stroke:#333
style B fill:#fdf,stroke:#333
style C fill:#dfd,stroke:#333
style D fill:#9df,stroke:#333
style E fill:#ddf,stroke:#333
style F fill:#fdd,stroke:#333,stroke-width:2px图 7-2:死代码消除(DCE)机制的四个步骤。 从源码到构建产物,内部代码被彻底移除,物理上不存在于外部构建中。
步骤详解:
构建时替换(Build-time Substitution):打包工具(如 esbuild、webpack)的
--define选项在编译时将process.env.USER_TYPE替换为字符串字面量。对于外部构建,替换为'external';对于内部构建,替换为'ant'。常量折叠(Constant Folding):编译器分析表达式
'external' === 'ant',在编译时确定其值为false。这是编译优化技术,不需要等到运行时才求值。分支消除(Branch Elimination):由于条件编译时确定为
false,整个if分支(包括其中的所有代码、字符串、对象)被从抽象语法树(AST)中移除。Tree Shaking:由于分支中的代码从未被引用,最终的打包工具会将其从构建产物中完全删除,包括该分支内导入的模块。
关键约束:内联要求
注释中强调了一个关键约束:"It MUST be inlined at each callsite (not hoisted to a const)"。这意味着不能这样写:
// ❌ 错误:提取为变量会导致 DCE 失效
const IS_ANT = process.env.USER_TYPE === 'ant'
if (IS_ANT) {
// ant-only code
}而必须在每个调用点直接写:
// ✅ 正确:内联表达式使 DCE 有效
if (process.env.USER_TYPE === 'ant') {
// ant-only code
}这是因为如果提取为变量,打包工具的常量折叠可能无法识别变量值,导致分支无法消除。
7.3.2 编译时安全 vs 运行时检查
传统软件通常使用运行时权限检查来保护内部功能:
// 传统方式:运行时检查
function internalFeature() {
if (user.role !== 'internal') {
throw new Error('Unauthorized')
}
// internal code
}这种方式的问题是:即使外部用户无法触发功能,代码仍然存在于构建产物中。通过反编译、内存分析或调试工具,攻击者可以读取内部逻辑、字符串常量、算法实现等。
Claude Code 采用的编译时安全则完全不同:
// Claude Code 方式:编译时消除
function internalFeature() {
if (process.env.USER_TYPE === 'ant') {
// internal code - 在外部构建中物理不存在
}
}在外部构建中,整个 if 分支(包括注释、字符串、内部逻辑)都被完全移除。即使反编译外部构建,也找不到任何内部代码的痕迹——它根本就不存在。
安全性对比:
| 维度 | 运行时检查 | 编译时 DCE |
|---|---|---|
| 代码存在性 | 代码存在但不可达 | 代码物理不存在 |
| 反编译风险 | 内部逻辑可被读取 | 无代码可读 |
| 内存占用 | 代码占用内存空间 | 无内存占用 |
| 包体积 | 包含所有代码 | 仅包含需要的代码 |
| 绕过可能性 | 可能被调试工具绕过 | 无法绕过(代码不存在) |
表 7-2:运行时检查 vs 编译时 DCE 的安全性对比。 编译时安全在所有维度上都优于运行时检查。
7.3.3 ant-only 门控完整清单
下表列出了 src/constants/prompts.ts 中所有受 USER_TYPE === 'ant' 门控的内容:
| 行号范围 | 功能描述 | 门控内容 | 解除条件 |
|---|---|---|---|
| 136-139 | ant 模型覆盖段落 | getAntModelOverrideSection() |
Feature Flag 控制 |
| 205-209 | 过度注释缓解 | 三条注释哲学指令 | 新模型不再默认过度注释 |
| 210-211 | 彻底性缓解 | 验证任务完成的指令 | 经 A/B 测试验证后推广 |
| 225-228 | 主动性缓解 | 协作者而非执行者指令 | 经 A/B 测试验证后推广 |
| 238-241 | 虚假声明缓解 | 准确报告结果的指令 | 新模型 FC rate 降低 |
| 243-246 | 内部反馈渠道 | /issue 和 /share 命令推荐 |
仅限内部用户 |
| 621 | Undercover 模型描述压制 | 压制系统提示词中的模型名称 | Undercover 模式激活时 |
| 660 | Undercover 简化模型描述压制 | 同上,简化提示词版本 | Undercover 模式激活时 |
| 694-702 | Undercover 模型家族信息压制 | 压制模型列表、平台信息 | Undercover 模式激活时 |
表 7-3:prompts.ts 中的 ant-only 门控完整清单。 注意最后一行双重门控的特殊性:getAntModelOverrideSection 有 isUndercover() 的二次检查。
getAntModelOverrideSection 的双重门控:
function getAntModelOverrideSection(): string | null {
if (process.env.USER_TYPE !== 'ant') return null
if (isUndercover()) return null // 二次门控
return getAntModelOverrideConfig()?.defaultSystemPromptSuffix || null
}源码位置: src/constants/prompts.ts:136-141
这个函数有双重门控:
- 第一层:
USER_TYPE !== 'ant'- 确保只有内部用户才能获取 ant 模型配置 - 第二层:
isUndercover()- 即使是内部用户,在向公开仓库贡献代码时也不注入 ant 专属后缀
这种**防御深度(Defense in Depth)**设计确保即使是内部用户,在向公开仓库贡献代码时也不会意外泄露内部模型配置。即使第一层门控被绕过(理论上不可能),第二层门控仍然提供保护。
7.4 Undercover 模式:开源贡献的隐身术
Undercover 模式是 Claude Code 最独特、最精巧的功能之一。它解决了一个非常具体但极其重要的问题:Anthropic 内部工程师使用 Claude Code 向公开/开源仓库贡献代码时,如何确保不泄露任何内部信息?
7.4.1 激活逻辑:三层防护机制
export function isUndercover(): boolean {
if (process.env.USER_TYPE === 'ant') {
// 第一层:强制开启
if (isEnvTruthy(process.env.CLAUDE_CODE_UNDERCOVER)) return true
// 第二层:自动检测
return getRepoClassCached() !== 'internal'
}
// 第三层:外部用户永远 false
return false
}源码位置: src/utils/undercover.ts:28-37
激活规则有三个层级,形成了一个**"安全默认"(Secure by Default)**的设计:
flowchart TD
A["isUndercover() 调用"] --> B{USER_TYPE === 'ant'?}
B -->|否| C["返回 false\n外部用户永远不受限"]
B -->|是| D{CLAUDE_CODE_UNDERCOVER\n环境变量设置?}
D -->|是| E["返回 true\n强制开启模式"]
D -->|否| F{getRepoClassCached()\n仓库分类检测}
F -->|'internal'| G["返回 false\n确认是内部仓库"]
F -->|'external'/'none'/null| H["返回 true\n自动开启保护"]
style E fill:#fdd,stroke:#333,stroke-width:2px
style G fill:#dfd,stroke:#333,stroke-width:2px
style H fill:#ffd,stroke:#333,stroke-width:2px
style C fill:#ddf,stroke:#333图 7-3:Undercover 模式的三层激活逻辑。 注意"无法强制关闭"的设计——不存在任何可以让内部用户在公开仓库中强制关闭 Undercover 的机制。
三层防护详解:
强制开启(Force ON):设置环境变量
CLAUDE_CODE_UNDERCOVER=1,即使在内部仓库中也强制激活。这在测试、演示或需要额外谨慎的场景下有用。自动检测(Auto Detection):检查当前仓库的远程地址是否在内部白名单中。如果不在白名单中(
'external'、'none'或检查尚未运行返回null),自动激活。无法强制关闭(No Force OFF):这是设计的精髓——不存在任何可以让内部用户在公开仓库中强制关闭 Undercover 的机制。源码注释解释了原因:
There is NO force-OFF. This guards against model codename leaks —
if we're not confident we're in an internal repo, we stay undercover.这种**"安全默认为开启"(Security by Default, On by Default)**的设计哲学值得深思:宁可在内部仓库中多余地隐藏信息,也不冒在公开仓库中泄露的风险。即使误判导致内部仓库被视为外部,最坏的结果只是失去了一些便利性(不能使用内部模型名),而不会导致灾难性的信息泄露。
7.4.2 仓库分类:白名单机制
getRepoClassCached() 函数维护了一个内部仓库的白名单:
const INTERNAL_MODEL_REPOS = [
'github.com:anthropics/claude-cli-internal',
'github.com/anthropics/claude-cli-internal',
'github.com:anthropics/anthropic',
'github.com/anthropics/anthropic',
'github.com:anthropics/apps',
// ... 更多内部仓库
]
export function getRepoClassCached(): 'internal' | 'external' | 'none' | null {
const remoteUrl = await getRemoteUrlForDir(repoRoot)
const isInternal = INTERNAL_MODEL_REPOS.some(repo => remoteUrl.includes(repo))
return isInternal ? 'internal' : 'external'
}源码位置: src/utils/commitAttribution.ts:30-88, 98-126
关键设计决策:
仓库级别白名单,而非组织级别:注释明确说明:"intentionally a repo allowlist, not an org-wide check"。这是因为
anthropics和anthropic-experimental组织包含公开仓库(如anthropics/claude-code)。如果使用组织级别检查,会导致在这些公开仓库中错误地关闭 Undercover 模式。SSH 和 HTTPS 格式都包含:白名单同时包含
github.com:user/repo(SSH)和github.com/user/repo(HTTPS)两种格式,确保不同 Git 配置都能正确匹配。缓存优化:函数名中的
Cached表明仓库分类结果会被缓存,避免每次调用都执行 Git 命令。这在性能关键路径上很重要。返回值包含三种状态:
'internal':确认是内部仓库'external':确认是外部仓库'none':不是 Git 仓库或没有远程地址null:检查尚未运行
Undercover 模式的逻辑是:只有明确确认为 'internal' 时才关闭,其他所有情况('external'、'none'、null)都开启保护。这是典型的"默认拒绝"(Default Deny)安全策略。
7.4.3 三层压制:系统提示词、归因信息、行为指令
Undercover 模式激活时,执行三个层级的压制操作,形成一个完整的信息泄露防线:
flowchart TB
A["Undercover 模式激活"] --> B["第一层:系统提示词"]
A --> C["第二层:归因信息"]
A --> D["第三层:行为指令"]
B --> B1["computeEnvInfo (L621)\nmodelDescription = ''"]
B --> B2["computeSimpleEnvInfo (L660)\nmodelDescription = null"]
B --> B3["模型家族列表 (L694-696)\nnull - Opus/Sonnet/Haiku ID"]
B --> B4["Claude Code 平台信息 (L697-699)\nnull - CLI/桌面/Web"]
B --> B5["Fast 模式说明 (L700-702)\nnull - 使用相同模型说明"]
B --> B6["getAntModelOverrideSection (L138)\nreturn null - ant 后缀不注入"]
C --> C1["getAttributionTexts()\ncommit: ''\npr: ''"]
C --> C2["不生成 Co-Authored-By 行"]
C --> C3["PR 描述无 AI 工具归因"]
D --> D1["getUndercoverInstructions()\n注入详细反泄露指令"]
D --> D2["禁止内部代号(Capybara, Tengu)"]
D --> D3["禁止未发布版本号"]
D --> D4["禁止内部仓库/项目名"]
D --> D5["禁止内部工具/Slack 频道"]
D --> D6["禁止 'Claude Code' 字样"]
D --> D7["要求像人类一样写 commit"]
style B fill:#ffd,stroke:#333
style C fill:#fdf,stroke:#333
style D fill:#ddf,stroke:#333图 7-4:Undercover 模式的三层压制工作流程。 从系统提示词到归因信息再到行为指令,形成完整的纵深防御体系。
第一层:系统提示词压制
源码中的注释(第 612-616 行)解释了为什么压制范围如此广泛:
// Undercover: keep ALL model names/IDs out of the system prompt so
// nothing internal can leak into public commits/PRs. This includes the
// public FRONTIER_MODEL_* constants — if those ever point at an
// unannounced model, we don't want them in context. Go fully dark."Go fully dark"——即使是公开的常量(如 FRONTIER_MODEL_NAME)也被压制,因为如果这些常量指向了一个尚未公布的模型,它们本身就成了泄露源。这是一种**零信任(Zero Trust)**的安全策略:不信任任何看起来"安全"的信息,因为当前时刻的"安全"可能在下一刻变成"敏感"。
第二层:归因信息压制
export function getAttributionTexts(): { commit: string; pr: string } {
if (process.env.USER_TYPE === 'ant' && !isUndercover()) {
return {
commit: '\n\nCo-Authored-By: Claude ',
pr: '\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)',
}
}
return { commit: '', pr: '' }
} 源码位置: src/constants/system.ts:50-60
Undercover 模式下,归因信息被完全清空:
- commit 归因:不生成
Co-Authored-By行 - PR 归因:不添加"Generated with Claude Code"标记
这确保了提交历史和 PR 描述中不出现任何 AI 工具的痕迹,让贡献看起来完全是由人类开发者完成的。
第三层:行为指令注入
getUndercoverInstructions() 函数注入了一段详细的反泄露指令(src/utils/undercover.ts:39-69)。这段指令的精妙之处在于使用正反示例并列的教学方式:
好的 commit message 示例:
"Fix race condition in file watcher initialization"
"Add support for custom key bindings"
"Refactor parser for better error messages"绝不能写的 commit message 示例:
"Fix bug found while testing with Claude Capybara"
"1-shotted by claude-opus-4-6"
"Generated with Claude Code"
"Co-Authored-By: Claude Opus 4.6 " 这种教学方式比单纯的禁止清单更有效——它不仅告诉模型"不要做什么",还示范了"应该做什么"。模型可以通过对比正例和反例,学习到人类开发者撰写 commit message 的风格和规范。
指令的结构化组织:
Undercover 指令采用结构化的清单格式,将禁止项分为清晰的类别:
- 内部模型代号:animal names(Capybara, Tengu 等)
- 未发布版本号:opus-4-7, sonnet-4-8 等
- 内部仓库/项目名:claude-cli-internal, anthropics/... 等
- 内部工具/频道:go/cc 短链接、Slack 频道名等
- AI 身份暗示:"Claude Code" 字样或任何 AI 相关描述
- 归因信息:Co-Authored-By 行
这种分类使得模型更容易理解和记忆规则,而不是面对一大段无结构的文本。
7.4.4 一次性通知机制
首次自动激活 Undercover 模式时,Claude Code 会显示一个一次性解释对话框:
export function shouldShowUndercoverAutoNotice(): boolean {
if (process.env.USER_TYPE === 'ant') {
// 如果强制开启,用户已经知道,不显示通知
if (isEnvTruthy(process.env.CLAUDE_CODE_UNDERCOVER)) return false
if (!isUndercover()) return false
if (getGlobalConfig().hasSeenUndercoverAutoNotice) return false
return true
}
return false
}源码位置: src/utils/undercover.ts:80-91
显示逻辑的三个条件:
- Undercover 通过自动检测激活:如果用户通过环境变量强制开启,说明用户已经知道这个功能,不需要通知。
- 当前处于 Undercover 模式:如果不在 Undercover 模式(如内部仓库),显然不需要通知。
- 用户之前没有看过通知:通过
hasSeenUndercoverAutoNotice标志防止反复打扰用户。
这个标志存储在全局配置中,确保通知只在用户首次遇到自动激活的 Undercover 模式时显示一次。这种用户友好的设计平衡了安全性和可用性——既要确保用户知道重要的安全功能在运行,又不能频繁打断工作流。
7.5 GrowthBook 集成:Feature Flag 的远程控制平面
Claude Code 使用 GrowthBook 作为其 Feature Flag 和实验平台,形成了一个控制平面与数据平面分离的架构。所有的 Feature Flag 遵循 tengu_* 命名约定——"tengu" 是 Claude Code 的内部代号。
7.5.1 多层回退机制
GrowthBook 客户端的初始化和特性值获取遵循一个精心设计的多层回退机制:
优先级(从高到低):
1. 环境变量覆盖 (CLAUDE_INTERNAL_FC_OVERRIDES) — ant-only, 用于评估线束
2. 本地配置覆盖 (/config Gates 面板) — ant-only, 用于本地调试
3. 内存中的远程评估值 (remoteEvalFeatureValues) — 最权威,来自 GrowthBook 服务器
4. 磁盘缓存 (cachedGrowthBookFeatures) — 跨进程持久化
5. 默认值 (defaultValue 参数) — 最后的兜底核心的值读取函数是 getFeatureValue_CACHED_MAY_BE_STALE:
export function getFeatureValue_CACHED_MAY_BE_STALE(
feature: string,
defaultValue: T,
): T {
// 1. 环境变量覆盖
const overrides = getEnvOverrides()
if (overrides && feature in overrides) return overrides[feature] as T
// 2. 本地配置覆盖
const configOverrides = getConfigOverrides()
if (configOverrides && feature in configOverrides)
return configOverrides[feature] as T
// 3. 内存远程评估值
if (remoteEvalFeatureValues.has(feature))
return remoteEvalFeatureValues.get(feature) as T
// 4. 磁盘缓存
const cached = getGlobalConfig().cachedGrowthBookFeatures?.[feature]
return cached !== undefined ? (cached as T) : defaultValue
} 源码位置: src/services/analytics/growthbook.ts:734-775
如其名称所示,这个函数返回的值可能是过期的(MAY BE STALE)——它优先从内存或磁盘缓存读取,不会阻塞等待网络请求。这是一个有意的设计决策:在启动关键路径上,陈旧但可用的值好过等待网络而卡住的 UI。
设计权衡:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 阻塞等待最新值 | 总是返回最新配置 | 启动慢,网络故障时无法启动 |
| 使用缓存值 | 启动快,离线可用 | 可能返回过期配置 |
| 混合方案(实际采用) | 兼顾启动速度和准确性 | 复杂度高,需要处理过期 |
Claude Code 选择混合方案的原因:Feature Flag 的值通常不会频繁变化(实验配置相对稳定),而启动速度对用户体验至关重要。即使在极少数情况下使用了过期值,也可以通过定期刷新(20分钟/6小时)快速纠正。
7.5.2 远程评估与本地缓存同步
GrowthBook 使用 remoteEval: true 模式——特性值在服务器端预评估,客户端只需缓存结果。processRemoteEvalPayload 函数(growthbook.ts:327-394)在每次初始化和定期刷新时运行,将服务器返回的预评估值写入两个存储:
- 内存 Map(
remoteEvalFeatureValues):用于进程生命周期内的快速读取 - 磁盘缓存(
syncRemoteEvalToDisk,第 407-417 行):用于跨进程持久化
磁盘缓存的替换策略:
function syncRemoteEvalToDisk(payload: GrowthBookPayload): void {
// 整体替换,而非合并
const features = payload.features || {}
getGlobalConfig().cachedGrowthBookFeatures = features
saveGlobalConfig()
}源码位置: src/services/analytics/growthbook.ts:407-417
磁盘缓存采用整体替换而非增量合并策略——服务器端删除的特性会从磁盘中清除。这保证了磁盘缓存始终是服务器状态的完整快照,而非不断累积的历史沿积。
源码注释记录了一个曾经的故障(第 322-325 行):
// Without this running on refresh, remoteEvalFeatureValues freezes at
// its init-time snapshot and getDynamicConfig_BLOCKS_ON_INIT returns
// stale values for the entire process lifetime — which broke the
// tengu_max_version_config kill switch for long-running sessions.这个故障说明了为什么定期刷新至关重要:如果只在初始化时读取一次,长时间运行的会话(如 IDE 插件、桌面应用)将无法响应紧急的远程配置变更。kill switch 的失效可能导致有问题的功能无法及时关闭,造成用户影响。
7.5.3 实验曝光追踪:A/B 测试的基础
GrowthBook 的 A/B 测试功能依赖于实验曝光(exposure)追踪。logExposureForFeature 函数(growthbook.ts:296-314)在特性值被访问时记录曝光事件,用于后续的实验分析。
关键设计:会话级去重
const loggedExposures = new Set()
function logExposureForFeature(feature: string): void {
if (loggedExposures.has(feature)) return // 去重
loggedExposures.add(feature)
// 记录曝光事件到 GrowthBook
const experimentData = experimentDataByFeature.get(feature)
if (experimentData) {
logGrowthBookExperimentTo1P(experimentData)
}
} 源码位置: src/services/analytics/growthbook.ts:296-314
loggedExposures Set 确保每个特性每次会话最多记录一次曝光,防止在热路径(如渲染循环、频繁的配置读取)中重复调用导致的曝光事件爆炸。
延迟曝光机制:
如果特性在 GrowthBook 初始化完成前被访问,pendingExposures Set 会暂存这些访问:
// 在初始化前访问
if (!experimentDataByFeature.has(feature)) {
pendingExposures.add(feature) // 暂存
}
// 初始化完成后补录
function onGrowthBookReady() {
pendingExposures.forEach(feature => {
logExposureForFeature(feature) // 补录
})
pendingExposures.clear()
}这确保了即使在初始化竞态条件下,所有的曝光事件都能被准确记录,不会丢失实验数据。
7.5.4 已知的 tengu_* Feature Flag
从代码库中可以识别出以下 tengu_* Feature Flag 的用途:
| Flag 名称 | 用途 | 读取方式 | 备注 |
|---|---|---|---|
tengu_ant_model_override |
配置 ant-only 模型列表、默认模型、系统提示词后缀 | _CACHED_MAY_BE_STALE |
本章重点 |
tengu_1p_event_batch_config |
第一方事件批处理配置 | onGrowthBookRefresh |
性能优化 |
tengu_event_sampling_config |
事件采样配置 | _CACHED_MAY_BE_STALE |
降低日志量 |
tengu_log_datadog_events |
Datadog 事件日志门控 | _CACHED_MAY_BE_STALE |
监控开关 |
tengu_max_version_config |
最大版本 kill switch | _BLOCKS_ON_INIT |
紧急关闭 |
tengu_frond_boric |
Sink 总开关(kill switch) | _CACHED_MAY_BE_STALE |
混淆命名 |
tengu_cobalt_frost |
Nova 3 语音识别门控 | _CACHED_MAY_BE_STALE |
功能开关 |
tengu_hive_evidence |
TODO 写入证据追踪 | _CACHED_MAY_BE_STALE |
实验功能 |
表 7-4:部分已知的 tengu_ Feature Flag。* 注意某些 Flag 使用了混淆命名(如 tengu_frond_boric),这是出于安全考量——即使 Flag 名称被外部观察到,也无法推断其用途。
混淆命名的安全性:
Feature Flag 的名称可能会通过以下途径泄露:
- 网络请求(GrowthBook API 调用)
- 错误日志(包含 Flag 名称)
- 调试输出(开发模式)
如果 Flag 名称本身就包含了功能描述(如 tengu_internal_model_list),外部观察者可以推断出内部功能的存在。使用混淆命名(如 tengu_frond_boric)可以增加推断难度,即使名称泄露也难以理解其用途。
_BLOCKS_ON_INIT 后缀的含义:
大多数 Flag 使用 _CACHED_MAY_BE_STALE 读取方式,允许在初始化前返回缓存值。但某些关键 Flag(如 tengu_max_version_config)使用 _BLOCKS_ON_INIT,意味着必须等待 GrowthBook 初始化完成才能获取值。
这种区别反映了不同 Flag 的关键性:
- 非关键 Flag(如实验功能):可以暂时使用缓存值,即使过期也不会造成严重问题
- 关键 Flag(如版本限制):必须使用最新值,否则可能导致不兼容的版本继续运行
7.5.5 环境变量覆盖:评估线束的后门
CLAUDE_INTERNAL_FC_OVERRIDES 环境变量允许在不连接 GrowthBook 服务器的情况下覆盖任意 Feature Flag 值:
// Example: CLAUDE_INTERNAL_FC_OVERRIDES='{"my_feature": true}'
export function getEnvOverrides(): Record | null {
if (process.env.USER_TYPE !== 'ant') return null
const raw = process.env.CLAUDE_INTERNAL_FC_OVERRIDES
if (!raw) return null
try {
const overrides = JSON.parse(raw)
logForDebugging(
`GrowthBook: Using env var overrides for ${Object.keys(overrides).length} features`
)
return overrides
} catch (error) {
logError(new Error(`Failed to parse CLAUDE_INTERNAL_FC_OVERRIDES: ${raw}`))
return null
}
} 源码位置: src/services/analytics/growthbook.ts:161-192
这个机制的专门用途:评估线束(Eval Harness)
自动化测试需要在确定性条件下运行:
- 不能依赖远程服务的可用性(网络故障会导致测试失败)
- 不能使用随机分配的 A/B 测试分组(每次运行结果不同)
- 需要测试特定的 Flag 配置组合(边缘情况)
环境变量覆盖允许测试脚本指定 Feature Flag 的值,确保每次运行都在完全相同的配置下:
# 测试 Flag A 开启、Flag B 关闭的场景
CLAUDE_INTERNAL_FC_OVERRIDES='{"tengu_feature_a": true, "tengu_feature_b": false}' \
npm test
# 测试内部模型列表的场景
CLAUDE_INTERNAL_FC_OVERRIDES='{"tengu_ant_model_override": {"antModels": [...]}}' \
npm test覆盖优先级最高(高于磁盘缓存和远程评估值),且仅在 ant 构建中可用。这确保了:
- 评估线束的确定性:测试结果可重现
- 外部用户的影响为零:外部构建没有这个环境变量,也无需使用
- 安全性:只有内部用户才能利用这个"后门"
7.6 tengu_ant_model_override:模型热切换的实现
tengu_ant_model_override 是所有 tengu_* Flag 中最复杂的一个。它通过 GrowthBook 远程配置 ant-only 模型的完整列表,支持运行时热切换,无需发布新版本。
7.6.1 配置结构
export type AntModel = {
alias: string // 命令行别名(如 "capybara-fast")
model: string // 模型 ID(如 "claude-opus-4-6-capybara")
label: string // UI 显示标签(如 "Capybara (Fast)")
description?: string // 人类可读的描述
defaultEffortValue?: number // 默认 effort 值
defaultEffortLevel?: EffortLevel // 默认 effort 级别
contextWindow?: number // 上下文窗口大小
defaultMaxTokens?: number // 默认最大 token 数
upperMaxTokensLimit?: number // 最大 token 数上限
alwaysOnThinking?: boolean // 是否始终启用思考模式
}
export type AntModelOverrideConfig = {
defaultModel?: string // 默认模型 ID
defaultModelEffortLevel?: EffortLevel // 默认 effort 级别
defaultSystemPromptSuffix?: string // 追加到系统提示词的后缀
antModels?: AntModel[] // 可用模型列表
switchCallout?: AntModelSwitchCalloutConfig // 切换提示配置
}源码位置: src/utils/model/antModels.ts:5-30
AntModelSwitchCalloutConfig 的作用:
export type AntModelSwitchCalloutConfig = {
modelAlias?: string // 推荐切换到的模型别名
description: string // 推荐理由
version: string // 版本标识(用于去重)
}这个配置允许在 UI 中向用户展示模型切换建议。例如,当某个模型出现已知问题时,可以通过 GrowthBook 远程推送一个通知,建议用户切换到另一个模型。
示例场景:
{
"switchCallout": {
"modelAlias": "capybara-stable",
"description": "Capybara Fast 模式在处理大型文件时可能遇到超时,建议切换到 Stable 模式以获得更好的稳定性。",
"version": "2025-04-01"
}
}用户在 CLI 或 IDE 中会看到这个通知,并可以一键切换到推荐的模型。这种主动的运维干预能力,使得产品团队能够在不需要发布新版本的情况下,快速响应用户遇到的问题。
7.6.2 模型解析流程
resolveAntModel 函数将用户输入的模型名称解析为具体的 AntModel 配置:
export function resolveAntModel(
model: string | undefined,
): AntModel | undefined {
if (process.env.USER_TYPE !== 'ant') return undefined
if (model === undefined) return undefined
const lower = model.toLowerCase()
return getAntModels().find(
m => m.alias === model || lower.includes(m.model.toLowerCase()),
)
}源码位置: src/utils/model/antModels.ts:51-64
双重匹配逻辑:
精确别名匹配:
m.alias === model- 用户输入
--model capybara-fast - 直接匹配到
alias: "capybara-fast"的模型
- 用户输入
模糊模型 ID 匹配:
lower.includes(m.model.toLowerCase())- 用户输入
--model claude-opus-4-6-capybara - 模型 ID 是
claude-opus-4-6-capybara-20250401 - 通过字符串包含匹配成功
- 用户输入
这种设计提供了灵活性和鲁棒性:
- 精确匹配用于命令行快捷方式(简短的别名)
- 模糊匹配允许用户直接使用完整的模型 ID,无需记住别名
使用示例:
# 使用别名(推荐)
claude-code --model capybara-fast
# 使用完整模型 ID(也支持)
claude-code --model claude-opus-4-6-capybara-20250401
# 使用部分模型 ID(模糊匹配)
claude-code --model opus-4-6-capybara7.6.3 冷缓存启动问题与解决
src/main.tsx 中的一段注释(第 2001-2014 行)记录了一个棘手的启动顺序问题:
问题描述:
- ant 模型别名通过
tengu_ant_model_overrideFeature Flag 解析 _CACHED_MAY_BE_STALE在 GrowthBook 初始化完成前只能读取磁盘缓存- 如果磁盘缓存为空(冷缓存),
resolveAntModel会返回undefined - 导致用户指定的显式模型别名无法解析
问题的根本原因:
用户启动命令:claude-code --model capybara-fast
↓
GrowthBook 初始化(异步)
↓
resolveAntModel("capybara-fast") 调用
↓
getAntModels() 调用
↓
getFeatureValue_CACHED_MAY_BE_STALE("tengu_ant_model_override")
↓
磁盘缓存为空 → 返回 null
↓
antModels = [] → resolveAntModel 返回 undefined
↓
错误:无法解析模型别名解决方案:同步等待初始化
// 检测到 ant 用户指定了显式模型且磁盘缓存为空
if ('external' === 'ant' && explicitModel && !hasDiskCache()) {
// 同步等待 GrowthBook 初始化完成
await initializeGrowthBook()
}
// 现在可以安全地解析模型别名
const resolvedModel = resolveAntModel(explicitModel)这是整个代码库中极少数需要阻塞等待 GrowthBook 的场景之一。
大多数情况下,代码使用 _CACHED_MAY_BE_STALE 读取 Flag,允许异步加载。但在这种特定场景下,用户明确指定了模型别名,如果解析失败会导致命令无法执行,因此必须等待初始化完成。
为什么不在所有地方都阻塞等待?
因为阻塞等待会严重影响启动速度,尤其是网络较慢时。大多数 Feature Flag 的值可以暂时使用缓存,不会导致功能失效。只有在这个特定场景(用户显式指定模型别名且冷缓存)下,阻塞等待才是合理的权衡。
性能影响评估:
// 正常启动(不指定模型)
// 时间:~100ms(使用缓存,不等待网络)
// 启动时指定模型别名(热缓存)
// 时间:~100ms(使用缓存,不等待网络)
// 启动时指定模型别名(冷缓存)
// 时间:~500ms(等待网络请求)只有在冷缓存 + 显式指定模型别名的情况下才会付出 500ms 的额外代价,这是一个可以接受的权衡。
7.7 知识截止日期映射:模型的记忆边界
getKnowledgeCutoff 函数维护了一个从模型 ID 到知识截止日期的映射表:
// @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model.
function getKnowledgeCutoff(modelId: string): string | null {
const canonical = getCanonicalName(modelId)
if (canonical.includes('claude-sonnet-4-6')) {
return 'August 2025'
} else if (canonical.includes('claude-opus-4-6')) {
return 'May 2025'
} else if (canonical.includes('claude-opus-4-5')) {
return 'May 2025'
} else if (canonical.includes('claude-haiku-4')) {
return 'February 2025'
} else if (
canonical.includes('claude-opus-4') ||
canonical.includes('claude-sonnet-4')
) {
return 'January 2025'
}
return null
}源码位置: src/constants/prompts.ts:712-730
7.7.1 使用 includes 而非精确匹配的原因
这个函数使用 includes() 而非 === 进行匹配,使其对模型 ID 后缀具有鲁棒性。模型 ID 通常包含日期标签或其他后缀:
claude-opus-4-6 ← 基础 ID
claude-opus-4-6-20250401 ← 带日期标签
claude-opus-4-6-capybara ← 带代号
claude-opus-4-6-experimental ← 带实验标记如果使用精确匹配,每个变体都需要单独添加到映射表中。使用 includes() 可以让所有变体自动映射到同一个知识截止日期。
源码中的 getCanonicalName() 函数(src/utils/model/model.ts)会对模型 ID 进行标准化处理,移除一些后缀和前缀,但仍然保留足够的区分度。getKnowledgeCutoff 再次使用 includes() 提供额外的容错性。
7.7.2 知识截止日期的注入
截止日期被注入到系统提示词的环境信息段落中(src/constants/prompts.ts:635-638):
const cutoff = getKnowledgeCutoff(modelId)
const knowledgeCutoffMessage = cutoff
? `\n\nAssistant knowledge cutoff is ${cutoff}.`
: ''这会在系统提示词中生成类似这样的文本:
Assistant knowledge cutoff is August 2025.作用:
- 设定用户期望:让用户知道模型的知识边界,避免询问超出范围的问题
- 引导模型行为:模型在回答时会更谨慎地处理接近知识边界的主题
- 版本识别:用户可以通过截止日期推断自己使用的是哪个版本的模型
7.7.3 Undercover 模式下的特殊处理
当 Undercover 模式激活时,整个环境信息段落中的模型特定部分都被压制:
export async function computeEnvInfo(
modelId: string,
additionalWorkingDirectories?: string[],
): Promise {
// ... 其他环境信息收集 ...
// Undercover: 压制所有模型名称/ID
let modelDescription = ''
if (process.env.USER_TYPE === 'ant' && isUndercover()) {
// suppress - 不添加任何模型描述
} else {
const marketingName = getMarketingNameForModel(modelId)
modelDescription = marketingName
? `You are powered by the model named ${marketingName}. The exact model ID is ${modelId}.`
: `You are powered by the model ${modelId}.`
}
const cutoff = getKnowledgeCutoff(modelId)
const knowledgeCutoffMessage = cutoff
? `\n\nAssistant knowledge cutoff is ${cutoff}.`
: ''
return `...环境信息...${modelDescription}${knowledgeCutoffMessage}`
} 源码位置: src/constants/prompts.ts:600-640
注意知识截止日期本身仍然保留,即使 Undercover 激活。这是因为截止日期(如 "August 2025")本身不包含内部信息,而模型名称和 ID 可能包含未发布的代号。
Undercover 下的系统提示词示例:
# Environment
Working directory: /Users/developer/my-project
Is directory a git repo: Yes
Platform: darwin
Shell: zsh
OS Version: Darwin 25.3.0
Assistant knowledge cutoff is August 2025.注意这里没有 "You are powered by..." 的模型描述,只有知识截止日期。这平衡了安全性和功能性——既不会泄露内部模型信息,又保留了模型的自我认知(知道自己的知识边界)。
7.8 工程哲学与最佳实践
通过对 Claude Code 模型特定调优和 A/B 测试体系的深入分析,我们可以提炼出一些深刻的工程哲学和最佳实践。
7.8.1 渐进式发布的三段管道
Claude Code 的模型调优揭示了一个清晰的三段发布管道:
flowchart LR
subgraph Stage1["阶段1:内部验证"]
A1["发现行为问题\n量化指标 (FC Rate)"] --> A2["PR 引入缓解\n代码审查"]
A2 --> A3["ant-only 门控\n内部用户验证"]
end
subgraph Stage2["阶段2:外部 A/B 测试"]
B1["解除 ant-only 门控"] --> B2["GrowthBook A/B 测试\n外部用户分组"]
B2 --> B3["统计分析\n显著性检验"]
end
subgraph Stage3["阶段3:全量发布"]
C1["全量推广"] --> C2["持续监控"]
C2 --> C3["下一次模型发布\n重新评估"]
end
Stage3 --> Stage1
style A3 fill:#ffd,stroke:#333,stroke-width:2px
style B2 fill:#9df,stroke:#333,stroke-width:2px
style C1 fill:#dfd,stroke:#333,stroke-width:2px图 7-5:渐进式发布的三段管道。 内部验证 → 外部 A/B 测试 → 全量发布,形成持续改进的闭环。
每段的关键决策点:
- 阶段1(内部验证):问题是否真实存在?缓解措施是否有效?
- 阶段2(外部 A/B):在外部用户群体中效果如何?是否有意外副作用?
- 阶段3(全量发布):是否保留到下一个模型世代?是否可以移除?
回滚机制:
每个阶段都有明确的回滚标准:
- 阶段1:如果内部反馈负面,调整措辞或完全移除缓解措施
- 阶段2:如果 A/B 测试显示负面统计显著性,回滚到 ant-only
- 阶段3:如果全量后出现问题,通过 Feature Flag 紧急关闭
7.8.2 编译时安全优于运行时检查
USER_TYPE 的构建时替换 + 死代码消除(DCE)机制,确保了内部代码在外部构建中物理不存在,而非仅仅"不可访问"。这种编译时安全(Compile-time Safety) 比运行时权限检查更强:
传统运行时检查的局限:
// 传统方式
function internalFeature() {
if (user.role !== 'internal') {
throw new Error('Unauthorized')
}
// internal code - 仍然存在于构建产物中
}攻击向量:
- 反编译构建产物,读取
internal code的逻辑 - 调试器修改
user.role变量 - 内存补丁跳过检查
编译时 DCE 的优势:
// Claude Code 方式
function internalFeature() {
if (process.env.USER_TYPE === 'ant') {
// internal code - 在外部构建中被完全移除
}
}安全保障:
- 外部构建中没有这段代码,无法通过反编译获取
- 没有运行时检查,无法通过调试器绕过
- 不占用内存空间,无法通过内存补丁修改
零信任原则:
这种设计体现了零信任原则:不信任任何运行时保护机制,因为它们理论上都可能被绕过。唯一绝对安全的方式是让敏感代码根本不存在于攻击者可以访问的环境中。
7.8.3 安全默认值的哲学:宁可误报,不可漏报
Undercover 模式的"无法强制关闭"设计、DANGEROUS_ 前缀的 API 摩擦、以及"冷缓存时阻塞等待"的启动逻辑,都体现了同一种哲学:当安全和便利冲突时,选择安全(Security over Convenience)。
这不是偏执——而是在泄露内部模型信息与稍微降低便利性之间做出的理性权衡:
| 决策 | 保守方案(安全) | 激进方案(便利) | Claude Code 的选择 |
|---|---|---|---|
| Undercover 关闭 | 无法强制关闭 | 允许强制关闭 | 保守 |
| 内部代码暴露 | DCE 物理移除 | 运行时检查 | 保守 |
| 冷缓存启动 | 阻塞等待网络 | 使用缓存值 | 条件保守 |
| 字符串泄露 | excluded-strings.txt | 依赖代码审查 | 保守 |
"宁可误报,不可漏报"(Better Safe Than Sorry) 的具体体现:
- Undercover 无法强制关闭:即使误判将内部仓库视为外部,最坏结果是失去一些便利性,但不会导致信息泄露
- excluded-strings.txt 白名单:即使某个内部代号意外写入代码,构建工具会阻止整个构建,防止泄露
- 模型描述全面压制:即使公开的常量也被压制,因为它们可能在未来指向敏感模型
- ant-only 双重门控:即使第一层门控失效,第二层仍提供保护
7.8.4 Feature Flag 作为控制平面:架构的解耦
tengu_* Feature Flag 体系将 Claude Code 从一个单一的软件产品转变为一个可远程控制的平台。这体现了控制平面与数据平面分离的架构模式:
flowchart TB
subgraph ControlPlane["控制平面 (GrowthBook)"]
A1["Feature Flag 配置"] --> A2["A/B 测试分组"]
A2 --> A3["实验数据分析"]
A3 --> A1
end
subgraph DataPlane["数据平面 (Claude Code)"]
B1["读取 Flag 值"] --> B2["执行相应逻辑"]
B2 --> B3["上报实验数据"]
end
ControlPlane -->|推送配置| DataPlane
DataPlane -->|上报数据| ControlPlane
style ControlPlane fill:#ffd,stroke:#333,stroke-width:2px
style DataPlane fill:#9df,stroke:#333,stroke-width:2px图 7-6:控制平面与数据平面的分离。 GrowthBook 作为控制平面远程控制 Claude Code 的行为,而无需发布新版本。
这种架构的优势:
- 快速响应:发现问题后可以通过 Feature Flag 立即关闭,无需等待新版本发布
- 实验驱动:可以通过 A/B 测试科学地评估新功能的效果,而非依赖直觉
- 渐进式发布:先在小群体中测试,验证效果后再推广到全量
- 个性化配置:可以根据用户属性(组织、订阅类型等)提供不同的功能
可远程控制的能力:
通过 GrowthBook,工程师可以在不发布新版本的情况下:
- 切换默认模型(
tengu_ant_model_override) - 调整事件采样率(
tengu_event_sampling_config) - 启用/禁用实验功能(各种
tengu_*实验标记) - 通过 kill switch 紧急关闭有问题的功能(
tengu_max_version_config) - 调整批处理配置优化性能(
tengu_1p_event_batch_config)
这是 SaaS 产品成熟度的标志——从"每次更新需要发布新版本"进化到"可以实时调整系统行为",大大提升了产品迭代速度和运维效率。
7.8.5 分布式知识管理:代码即文档
@[MODEL LAUNCH] 注解系统体现了一种**代码即文档(Code as Documentation)**的理念。传统的知识管理方式存在以下问题:
- 文档与代码分离:README、Wiki、Confluence 等文档容易过时,因为它们不在代码审查流程中
- 知识孤岛:发布流程的知识存在于运维手册、工程师的脑海中,难以传承
- 搜索困难:即使文档存在,也很难在正确的时机找到相关信息
分布式检查清单的优势:
- 知识就在代码旁边:
@[MODEL LAUNCH]注解直接写在需要更新的代码旁边,无法被忽略 - 自验证:每次代码审查都会看到这些注解,自然地检查是否需要更新
- 全局搜索:
grep "@\[MODEL LAUNCH\]"就能找到所有需要更新的位置 - 自文档化:注解的文本本身就是操作说明,不需要额外查阅文档
示例:模型发布工作流
# 工程师 A 发现新模型准备好发布了
git grep "@\[MODEL LAUNCH\]"
# 输出:
# src/constants/prompts.ts:117:// @[MODEL LAUNCH]: Update the latest frontier model.
# src/constants/prompts.ts:120:// @[MODEL LAUNCH]: Update the model family IDs...
# src/utils/model/antModels.ts:32:// @[MODEL LAUNCH]: Update tengu_ant_model_override...
# 工程师 A 逐个检查并更新每个位置
# 每个注解都明确说明了需要做什么
# 提交 PR,代码审查者 B 自然看到所有变更
# PR 描述不需要额外说明"需要更新这些地方",因为注解已经说明了一切
# 发布完成后,注解仍然保留在代码中
# 下一次模型发布时,下一个工程师重复这个流程这种分布式知识管理方式,让发布流程的知识得以传承,不会因为人员流动而丢失。每个注解都是一个"知识胶囊",封装了操作说明、历史原因和解除条件。
7.9 读者实践指南
基于本章对模型特定调优和 A/B 测试体系的分析,以下是读者可以在自己的 AI Agent 项目中应用的具体建议。
7.9.1 建立分布式检查清单系统
建议1:在代码中嵌入发布检查清单
如果你的系统需要在模型升级时更新多个位置(模型名称、知识截止日期、行为缓解等),采用 @[MODEL LAUNCH] 式的注解标记:
// @[MODEL LAUNCH]: Update the latest frontier model name
const LATEST_MODEL = 'Claude Opus 4.7'
// @[MODEL LAUNCH]: Add knowledge cutoff date for new model
// Format: "Month Year" (e.g., "September 2025")
function getKnowledgeCutoff(modelId: string): string | null {
// ...
}
// @[MODEL LAUNCH]: Review if this mitigation is still needed
// Remove if new model no longer exhibits this behavior
if (isNewModel()) {
prompts.push(`Mitigation instruction for specific behavior`)
}关键原则:
- 在注解文本中直接写明更新动作和解除条件
- 使用统一的注解格式(如
@[MODEL_LAUNCH]),便于全局搜索 - 包含历史原因(如 "PR #12345"),方便追溯
- 将检查清单与代码共存,而非依赖外部文档
建议2:为每个缓解措施建立生命周期文档
当引入模型行为缓解时,记录以下信息:
// @[MODEL LAUNCH]: capy v8 over-commenting mitigation
// Introduced: 2025-03-15 (PR #24302)
// Metrics: Comment density reduced from 45% to 12%
// Removal condition: When new model's comment density < 15% by default
// Status: Active (ant-only)
if (process.env.INTERNAL_USER === 'true') {
prompts.push(`Default to writing no comments...`)
}这创建了一个可量化的缓解措施档案,便于未来评估是否可以移除。
7.9.2 实现编译时代码隔离
建议3:使用构建时常量替代运行时检查
如果你的产品有内部版本和外部版本的区分,不要依赖运行时 if 判断来隐藏内部功能:
// ❌ 错误:运行时检查
function internalFeature() {
if (user.role !== 'internal') {
throw new Error('Unauthorized')
}
// internal code
}
// ✅ 正确:编译时常量
function internalFeature() {
// INTERNAL_USER 是构建时的 --define 替换
if (process.env.INTERNAL_USER !== 'true') return
// internal code - 在外部构建中被完全移除
}实现步骤:
- 配置打包工具(以 esbuild 为例):
esbuild src/index.ts \
--define:process.env.INTERNAL_USER='"true"' \
--outdir=dist/internal- 验证 DCE 效果:
# 检查外部构建中是否包含内部代码
grep -r "INTERNAL_CODE" dist/external
# 应该没有输出
# 检查内部构建中是否包含内部代码
grep -r "INTERNAL_CODE" dist/internal
# 应该找到匹配- 建立字符串排除机制:
# scripts/excluded-strings.txt
INTERNAL_MODEL_CODENAME
INTERNAL_PROJECT_NAME
SECRET_API_KEY在构建脚本中检查:
#!/bin/bash
# 检查构建产物是否包含排除的字符串
for string in $(cat scripts/excluded-strings.txt); do
if grep -r "$string" dist/external; then
echo "ERROR: Found excluded string: $string"
exit 1
fi
done7.9.3 构建远程配置体系
建议4:将提示词实验性内容通过 Feature Flag 门控
将提示词中的实验性内容(新的行为指令、数值锚定等)通过 Feature Flag 门控,而非硬编码:
// ❌ 错误:硬编码
const prompts = [
`Experimental instruction for new behavior`,
`Another experimental instruction`,
]
// ✅ 正确:Feature Flag 门控
const prompts = [
...(getFeatureFlag('enable_new_behavior', false)
? [`Experimental instruction for new behavior`]
: []),
...(getFeatureFlag('enable_secondary_instruction', false)
? [`Another experimental instruction`]
: []),
]好处:
- 可以在不发布新版本的情况下调整模型行为
- 进行 A/B 测试时可以轻松控制分组
- 发现问题时可以通过 kill switch 紧急回滚
- 可以根据用户属性提供个性化的提示词
建议5:建立多层回退机制
参考 Claude Code 的多层回退优先级:
function getFeatureValue(feature: string, defaultValue: T): T {
// 优先级 1: 环境变量覆盖(用于测试)
const envOverride = process.env[`FEATURE_${feature.toUpperCase()}`]
if (envOverride !== undefined) {
return JSON.parse(envOverride) as T
}
// 优先级 2: 本地配置覆盖(用于开发)
const localOverride = getLocalConfig()[feature]
if (localOverride !== undefined) {
return localOverride as T
}
// 优先级 3: 内存缓存(最权威)
if (remoteEvalCache.has(feature)) {
return remoteEvalCache.get(feature) as T
}
// 优先级 4: 磁盘缓存(跨进程)
const diskCache = getDiskCache()[feature]
if (diskCache !== undefined) {
return diskCache as T
}
// 优先级 5: 默认值
return defaultValue
} 设计原则:
- 缓存优先:优先使用缓存,避免阻塞启动
- 环境变量最高:允许测试时覆盖,确保确定性
- 默认值兜底:即使所有缓存都失败,仍能返回合理值
7.9.4 实现安全的开源贡献模式
建议6:建立"安全默认"的贡献模式
如果你的团队使用 AI Agent 向开源仓库贡献代码,建立类似 Undercover 的安全模式:
function isUndercover(): boolean {
// 强制开启
if (process.env.FORCE_UNDERCOVER === '1') return true
// 自动检测:如果不是明确的内部仓库,开启保护
const repoType = getRepoType()
return repoType !== 'internal'
}
function getCommitInstructions(): string {
if (isUndercover()) {
return `
UNDERCOVER MODE: You are contributing to a public repository.
DO NOT include:
- Internal project names
- Internal tool names
- AI model names or versions
- Attribution to AI tools
Write commit messages as a human developer would.
GOOD: "Fix race condition in file watcher"
BAD: "Fix bug found while testing with InternalModel v1.2"
`
}
return ''
}关键设计:
- 默认开启:除非确认是内部仓库,否则开启保护
- 无法强制关闭:不提供"我知道我在做什么,关闭保护"的选项
- 多重验证:检测仓库类型、远程地址、组织等多重信号
建议7:在提示词中注入反泄露指令
类似 Claude Code 的 getUndercoverInstructions(),在系统提示词中注入详细的反泄露指令:
function getUndercoverSystemPrompt(): string {
return `
## CRITICAL: UNDERCOVER MODE
You are contributing to a PUBLIC repository. Your commit messages,
PR titles, and descriptions MUST NOT contain any internal information.
FORBIDDEN:
- Internal codenames (e.g., "ProjectX", "ModelY")
- Unreleased versions (e.g., "v2.0-beta")
- Internal tools or processes
- Any attribution to AI tools
REQUIRED:
- Write commit messages that describe ONLY what the code does
- Use standard commit message format
- No mentions of AI, models, or internal tools
Examples:
GOOD: "Fix memory leak in image processor"
BAD: "Fix memory leak found while testing ModelX v2.0"
`
}指令设计原则:
- 明确边界:清晰地列出禁止和必需的内容
- 提供示例:正反示例比抽象规则更容易理解
- 强调严重性:使用 "CRITICAL"、"FORBIDDEN" 等强调重要性
- 解释原因:简要说明为什么需要这些规则(安全、合规)
7.9.5 建立量化评估体系
建议8:为每个模型行为建立量化指标
不要依赖主观判断评估模型行为,建立可量化的指标:
// 模型行为档案
interface ModelBehaviorProfile {
modelName: string
metrics: {
falseClaimsRate: number // 虚假声明率
commentDensity: number // 注释密度(注释字符数 / 代码字符数)
assertivenessScore: number // 主动性评分(1-10)
thoroughnessRate: number // 彻底性率(验证过的任务 / 总任务)
}
mitigations: {
promptMitigations: string[] // 提示词级缓解措施
codeMitigations: string[] // 代码级缓解措施
}
effectiveness: {
beforeMitigation: number // 缓解前的指标
afterMitigation: number // 缓解后的指标
improvement: number // 改善百分比
}
}
// 示例:Capybara v8 的档案
const capybaraV8Profile: ModelBehaviorProfile = {
modelName: 'Capybara v8',
metrics: {
falseClaimsRate: 0.29, // 29%
commentDensity: 0.45, // 45%
assertivenessScore: 3, // 1-10 分
thoroughnessRate: 0.62, // 62%
},
mitigations: {
promptMitigations: [
'src/constants/prompts.ts:204-209 (over-commenting)',
'src/constants/prompts.ts:210-211 (thoroughness)',
'src/constants/prompts.ts:224-228 (assertiveness)',
'src/constants/prompts.ts:237-241 (false claims)',
],
codeMitigations: [],
},
effectiveness: {
beforeMitigation: 0.29,
afterMitigation: 0.18, // 降低到 18%
improvement: 0.38, // 改善 38%
}
}指标收集方法:
- 自动化测试:建立测试集,评估模型的虚假声明率
- 静态分析:分析生成的代码,计算注释密度
- 用户反馈:通过
/issue命令收集用户报告的问题 - A/B 测试:对比有/无缓解措施的效果
建议9:建立缓解措施的评估标准
为每个缓解措施定义明确的解除条件:
interface MitigationCriteria {
mitigationId: string
description: string
introductionDate: Date
introducedBy: string // PR # 或责任人
removalCriteria: {
metric: string // 评估指标
threshold: number // 阈值
consecutiveSamples: number // 需要连续多少次达标
minExternalUsers: number // 最少外部用户数量
}
currentStatus: 'ant-only' | 'a-b-testing' | 'fully-released' | 'removed'
lastEvaluation: Date
}
// 示例:虚假声明缓解的评估标准
const falseClaimsMitigation: MitigationCriteria = {
mitigationId: 'fc-mitigation-capybara-v8',
description: 'False claims mitigation for Capybara v8',
introductionDate: new Date('2025-03-15'),
introducedBy: 'PR #24302',
removalCriteria: {
metric: 'falseClaimsRate',
threshold: 0.20, // FC rate < 20%
consecutiveSamples: 3, // 连续 3 次评估
minExternalUsers: 1000, // 至少 1000 外部用户
},
currentStatus: 'ant-only',
lastEvaluation: new Date('2025-04-01'),
}这创建了一个自动化的评估流程:
- 每次模型发布时自动检查解除条件
- 达到阈值时提示工程师考虑解除门控
- 记录完整的评估历史,便于追溯
7.10 本章小结
本章深入剖析了 Claude Code 的模型特定调优与 A/B 测试体系,揭示了以下核心概念:
@[MODEL LAUNCH] 注解系统:
- 分布式检查清单将发布流程的知识嵌入代码本身
- 注解文本直接说明更新动作和解除条件,形成自文档化系统
- 全局搜索
@[MODEL LAUNCH]即可找到所有需要更新的位置
模型行为缓解的生命周期:
- 每个模型世代都有独特的"个性缺陷"(如 Capybara v8 的过度注释、虚假声明)
- 通过提示词级缓解措施修正行为,记录引入原因和量化指标
- 生命周期:发现问题 → PR 引入 → ant-only 验证 → A/B 测试 → 全量发布 → 重新评估
编译时安全 vs 运行时检查:
USER_TYPE的构建时替换 + 死代码消除(DCE)确保内部代码在外部构建中物理不存在- 比运行时权限检查更强:没有代码意味着没有攻击面
excluded-strings.txt提供额外的字符串泄露防护
Undercover 模式的三层防御:
- 系统提示词压制:移除所有模型名称、ID、平台信息
- 归因信息压制:不生成 Co-Authored-By 行和 PR 归因
- 行为指令注入:详细的反泄露指令和正反示例
- "安全默认为开启":无法强制关闭,宁可误报不可漏报
GrowthBook Feature Flag 体系:
- 多层回退机制:环境变量 > 本地配置 > 内存缓存 > 磁盘缓存 > 默认值
- 远程评估模式:特性值在服务器端预评估,客户端只需缓存结果
- A/B 测试基础:实验曝光追踪支持科学的分组对比
tengu_ant_model_override:支持模型热切换,无需发布新版本
工程哲学启示:
- 渐进式发布:内部验证 → 外部 A/B → 全量发布,每步都有回滚机制
- 编译时安全:DCE 比运行时检查更强,物理上不存在代码
- 安全默认值:当安全和便利冲突时,选择安全
- 控制平面分离:Feature Flag 将产品转变为可远程控制的平台
- 代码即文档:分布式检查清单让知识与代码共存
在下一章中,我们将探讨 Claude Code 的插件系统——一个基于 MCP (Model Context Protocol) 的可扩展架构,实现了工具的动态发现、权限管理和沙箱隔离。我们将看到 Claude Code 如何通过严格的权限控制、插件信任模型和工具能力发现机制,构建一个既强大又安全的插件生态系统。
章节参考:
- 原书章节:第7章:模型特定调优与 A/B 测试
- 源码位置:
src/constants/prompts.ts,src/utils/model/antModels.ts,src/utils/undercover.ts,src/services/analytics/growthbook.ts
相关章节:
- 第6章:系统提示词的组装 - 探讨系统提示词如何被组装为发送给模型的指令集
- 第8章:插件系统与工具发现 - 深入解析基于 MCP 的插件生态系统(即将发布)