2026-05-12 学习日志
今日主题
- OpenAI Model Spec 指令层级
- Anthropic vs OpenAI 指令层级哲学差异
- cc 的 CLAUDE.md 缓存与热更新机制
- cc 与 Codex 的上下文注入方式对比
- 微前端+code split 模块级单例陷阱
- dev 正常生产异常的排查思路
新增认知
OpenAI Model Spec 指令层级
-
Developer Message 解决的是指令冲突优先级:OpenAI 引入 developer 角色并非技术改进,而是语义建模。
2024 版中 system 同时承载 OpenAI 平台指令和开发者应用指令,冲突时无法判断优先级。
拆分为 system(OpenAI 注入)和 developer(开发者注入)后,冲突按 Platform > Developer > User 逐级裁决。
实际推理行为上两者无差异,改的是架构语义。 -
Root 与 System 的拆分体现"宪法 vs 法律"的设计:2025 版将 2024 版的 Platform 拆为 Root 和 System。
Root 是不可覆盖的禁止性规则(如禁止协助暴力犯罪),仅来自 Model Spec 本身,冲突时默认不行动。
System 是 OpenAI 按产品表面(ChatGPT vs API)和用户特征(如年龄)动态注入的指令,可被新的 system 消息替换。
这解决了"有些规则是普适的,有些规则需要按场景调整"的建模需求。 -
Guideline 的"隐式覆盖"机制是最轻量的约束:五级中最底层是 Guideline,它的关键特征是可被隐式覆盖——
不需要用户显式说"覆盖 guideline X",上下文暗示就足够。例如 guideline 说"避免脏话",但用户说"扮演粗鲁的海盗",
模型就会隐式覆盖脏话限制。这体现了 OpenAI"尽可能避免家长式管理"的设计哲学。
Anthropic vs OpenAI 指令层级哲学差异
-
Anthropic 是"不是严格层级"的主体模型:
Claude Constitution 定义了三主体 Anthropic > Operators > Users,但明确声明"不是严格层级"。
用户有些权利 operator 不能剥夺,Claude 甚至可以拒绝 Anthropic 的指令(做"良心拒服兵役者")。
冲突裁决用整体判断(holistic judgment),教 Claude 为什么这样做而非只告诉它做什么。
对比 OpenAI 五级严格指令链 Root > System > Developer > User > Guideline,逐级不可越权,是规则驱动。
本质差异是一个像企业文化手册,一个像操作系统权限模型。 -
Claude API 只有 user/assistant 两个 role 是物理隔离而非弱点:
system prompt 不在 messages 数组里,是独立的顶层参数。这从传输层就杜绝了用户输入被误认为系统指令的可能。
OpenAI 需要 developer role 和五级层级在 messages 数组内部裁决冲突,Claude 结构上就不可能混淆。
指令层级是训练进模型权重的(Wallace et al. 2024 就是 Anthropic 研究员写的),不需要靠 role 标签来提示工程。 -
Anthropic 的 prompt injection 防御实际更强:Claude Opus 4.5 单次攻击成功率仅 4.7%,
外部红队评测 23 模型中最低,MCP 工具场景防御率 94%,Bash 场景 99.4%。
Constitutional AI + RLAIF 让模型在训练阶段就内化了安全判断,不是运行时拼 prompt 实现的。
简单 API 结构 + 训练内化的安全约束 = 更强的抗注入能力。
cc 的 CLAUDE.md 缓存与热更新机制
-
CLAUDE.md 只在首轮读盘:getUserContext() 外层用 lodash memoize 包裹,
getMemoryFiles() 内层也有独立 memoize。首轮执行后永远命中缓存,后续轮次不产生磁盘 I/O。
只有 compaction 或 /clear 时两层缓存一起清除,下一轮才重新读盘。 -
getChangedFiles 轮询感知文件变更:cc 没有用 chokidar 监听 CLAUDE.md,
而是每轮在 getChangedFiles() 中对比 readFileState 里所有文件的 mtime。
发现变更后产出 edited_text_file attachment 注入上下文。但模型看到的是文件被编辑了的通知,
不是新内容替换 system prompt——新指令要等 compaction 刷新后才生效。 -
两层缓存必须一起清:
postCompactCleanup 同时调用 getUserContext.cache.clear() 和 resetGetMemoryFilesCache()。
如果只清内层、不清外层,下次调用时外层缓存命中就直接返回旧值,永远不会走到内层的 getMemoryFiles(),
InstructionsLoaded hook 也不会触发。源码注释明确写了 this 原因。
cc 与 Codex 的上下文注入方式对比
-
cc 用 prepend 而非 append:
cc 的 prependUserContext() 把 CLAUDE.md 内容作为合成 user message 放在 messages 数组头部。
设计逻辑不是保持 messages 前缀稳定让 cache 命中,而是 memoize 冻结内容本身——内容不变,不管放哪都不会引起 cache miss。
这和之前误读的 Codex(diff 追加到尾部)是两条不同的实现路径。 -
prompt cache key 就是 thread_id:cc 和 Codex 一样,cache key 不做内容哈希,不绑定指令版本。
配合内容冻结策略(sticky-on latch、工具 schema 会话缓存、yield 前 clone),保证同一会话内请求序列化字节稳定,
前缀 cache 持续命中。
微前端+code split 模块级单例陷阱
-
lazy() 破坏模块单例:React.lazy() 让 Vite 把页面打成独立动态 chunk,chunk 加载时模块顶层代码重新执行,
createStore() 产生新实例,与主 bundle 的实例完全独立,状态永远不同步。本地 vite dev 不做 code split,
问题完全无法复现,只在生产部署后出现。 -
定位线索:部分 hook 能感知状态、部分不能:
排查时发现 AppLayout(主 bundle)里的 envs 接口感知到 dtLoading=false,
但 lazy 页面里的 summary/trend 看到的还是 true。「同一状态、不同 hook 读到不同值」直接指向模块边界问题,
即跨越了 lazy chunk 边界。 -
根治方案是去掉 lazy,而非打补丁:页面代码本身体积小(几 KB),首屏瓶颈在 vendor(antd 800KB+),
去掉 lazy 对性能无实质影响。window 注册表是应急补丁,正确姿势是静态 import,彻底消除 code split。
dev 正常生产异常的排查思路
-
dev vs build 的本质差异是 code split:遇到「本地正常、生产异常」,首先排查构建差异而非网络/认证。
vite dev 所有模块单上下文执行一次;vite build 有 code split,lazy 路由打成独立 chunk,模块顶层重跑。
这个维度是最容易被忽略的。 -
「请求根本没发出去」= 代码层 ready=false 阻断:Network 面板里请求完全不存在(不是 4xx/5xx),
说明是 JS 层的 ready/enabled 守卫拦截,而非网络问题。此时应找所有守卫条件,分别打 log 输出每个条件的值,不能只看最终 ready。