跳转至

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。