Claude Code CLI 2.1.235 Prompt Assembly:一次请求究竟怎样被编译出来¶
版本:
2.1.235| 证据:Static+Public| 视图:reverse/javascript/cli.readable.js
读者问题: 用户明明只输入了一句话,为什么模型还知道工作目录、CLAUDE.md、可用 Skills、IDE 选区、后台任务和文件刚被外部修改?这些内容谁的优先级更高,哪些只是建议,哪些能真正阻止工具执行?
一句话模型: Claude Code 不会把 transcript 原样发给模型;启动阶段先形成model/provider/tool候选与permission context,每次API attempt再编译稳定system前缀、最终tools/betas/body和有效消息历史;用户提交及工具批次后产生的动态attachment则按模型能力编码成中途system消息或带<system-reminder>的user/meta消息。
这篇文章补的是此前文档最关键的一处断层:已有文章分别讲了 system prompt、CLAUDE.md、Prompt Cache、Hooks、MCP 和 compact,却没有回答它们在同一次真实请求里怎样排队、怎样获得权威、怎样失效,以及为什么有时出现在 user/tool result 里却不是用户说的话。
先看一张图:模型看到的不是磁盘 transcript¶
flowchart TD
A[CLI / SDK 输入] --> B[解析启动参数与有效配置]
B --> C[形成 model / provider / tool 候选与 permission context]
B --> D[构造 top-level system blocks]
E[JSONL message graph] --> F[选择 leaf / compact boundary / repair tool pairs]
G[磁盘与运行状态] --> H[提交时 / 工具批次后的 attachment generators]
H --> I[typed AttachmentMessage]
I --> J{模型支持 mid-conversation system?}
J -->|是| K[api_system role=system]
J -->|否或 400 降级| L[isMeta user + system-reminder]
F --> M[规范化有效 messages]
K --> M
L --> M
D --> N[request-time tools / betas / body + cache breakpoints]
C --> N
M --> N
N --> O[最终 Messages request]
O --> R[模型流式响应]
R --> S[完整 client tool_use block]
S --> P[registry / permission / policy / 前置 Hook / sandbox]
P --> Q[tool execution]
Q --> H
图里最重要的不是箭头数量,而是两条完全不同的控制线:
- 模型上下文线决定 Claude 知道什么、倾向怎样行动;system、user context、reminder 和 tool result 都在这条线上。
- 本地执行线决定动作能不能真正发生;tool registry、deny/ask/allow、managed policy、前置Hook、适用sandbox和OS权限不依赖模型是否听话。PostToolUse/PostToolBatch等后置Hook只能改变反馈或后续控制流,不能撤销已完成副作用。
所以“项目 CLAUDE.md 要求不要运行某命令”和“managed permission deny 拒绝该命令”不是同一种优先级。前者是模型应遵循的上下文,后者是客户端在 tool.call 前执行的硬门。
四种看起来都像“指令”的对象¶
| 对象 | Wire / 内部形态 | 谁生成 | 能否单独强制阻止副作用 | 主要失效方式 |
|---|---|---|---|---|
| Top-level system prompt | API 顶层 system[] blocks |
Claude Code preset、--system-prompt、append、output style |
否,仍需本地控制管线 | model/provider/mode/tool-set/section 变化,或整个 session 重建 |
| Mid-conversation system | messages[] 中 role:"system" 的 api_system |
支持该能力时由 harness 提升动态状态 | 否;权威高于普通 user 文本,但执行仍受本地门控 | 模型/代理不支持时 400,随后对本会话 sticky 降级 |
| Meta reminder / attachment | 内部attachment,发送前常变成isMeta user message与<system-reminder> |
文件监控、Memory、Skill、MCP、IDE、Hook、task、mode等 | 否;它告诉模型当前事实或约束 | 去重、提交/工具批次重算、compact重建、超时/生成失败、resume过滤 |
| 本地 enforcement | registry、permission、policy、前置Hook、sandbox、凭据和OS | 客户端与管理员配置 | 前置层可以;后置Hook不能撤销已完成副作用 | 配置来源变化、session重建、平台能力或外部policy变化 |
不能把这张表简化成一条“system > user > tool”的固定排序。2.1.235 同时使用模型 authority 和客户端 enforcement:即使 Plan Mode reminder 在消息里告诉模型只读,客户端仍用 permission mode 和工具专属规则阻止不允许的写;即使某个外部内容伪造 <system-reminder>,来源标记和本地权限也不能因此被提升。
一次请求的完整编译顺序¶
1. 先确定运行合同,而不是先拼字符串¶
启动层先解析:
- 默认或自定义 system prompt;
--append-system-prompt及文件版本;--exclude-dynamic-system-prompt-sections;- model、provider、effort、fast mode、permission mode;
- settings、managed policy、workspace trust;
- 内置、条件、Plugin、MCP、Agent 和动态注册工具。
--system-prompt 会替换默认 preset;append 只追加。动态 section 排除只对默认 preset 生效,显式自定义 system prompt 时忽略。对应 CLI 合同位于 readable L603768。
这一步已经决定了后面哪些 section、工具和 beta 有资格出现。一个功能字符串存在于 bundle,不表示它会进入当前 request;host、account、feature、provider、session、policy 与 Tool Search 仍会继续缩小能力集合。
外层一次用户提交会先解析 runtime model,再构造 systemPrompt/userContext/systemContext 快照;同一个 Agent Loop 内的后续模型迭代复用这份基础快照,而工具批次后的动态 attachment 会重新计算。换句话说,“system/context 编译一次”与“每轮状态不再更新”不是一回事。入口见 readable L597468-L597668。
2. Top-level system prompt 由 section 编译,不是一整段常量¶
默认 builder 按 section key 生成 communication、task execution、action caution、tool use、memory、环境、context management、Agent、权限与 Hook 等块。不同模型、provider、模式和工具集合会改变 section 集合。
为了缓存,system prompt 还会划分稳定与动态区域:稳定规则尽量进入可共享前缀;cwd、Git、Memory path 等机器/项目状态留在较晚区域。启用 --exclude-dynamic-system-prompt-sections 时,这批动态内容被移动到第一条 user message,而不是被删除。
这说明“system prompt 长度”不是唯一问题。顺序和稳定性决定后续整个消息历史能否复用缓存。 官方 2026-04-30 的设计文章把顺序概括为 static system/tools、CLAUDE.md、session context、conversation messages;它用于解释设计动机,本版具体 block、scope、TTL 和 gate 仍以本地实现为准。见 Prompt Cache 专题。
3. User context 与 system context 独立构造¶
k7(session) 与 O2(session) 分别生成两类上下文:
k7():CLAUDE.md、rules、Memory等user context;O2():Git status、Perforce mode等system context。
cwd、日期、平台与shell原本来自默认system builder的environment sections,不是k7()/O2()普通输出。只有dynamic-section exclusion开启时,这些容易变化的system sections才被重定向到user context。
这些上下文能影响模型判断,却不等同于 settings merge 或 permission engine。官方当前文档也明确区分:CLAUDE.md/rules 是模型读取的 guidance,settings/permissions/hooks 是客户端执行的 configuration。当前文档只用于提出这个区分;2.1.235 的实际载体与 consumer 由本 bundle 证明。
主循环真正发请求时,systemContext 被追加到 top-level system blocks,userContext 则作为首部 reminder 放进消息视图;启用 dynamic-section exclusion 时,原本容易变化的 system 内容也并入 user context,并清空对应 system context。见 readable L367319-L367323、L271653-L271655 与 L408415-L408429。
4. Transcript 先变成“当前有效消息图”¶
JSONL 物理顺序不是 API 的最终 messages[]。发送前还要:
- 选择当前 session leaf 与 parent chain;
- 应用最后一个有效 compact boundary;
- 按 preserved UUID/segment 修复 compact 后的逻辑链;
- 保证
tool_use与tool_result合法配对; - 丢弃 progress、被 tombstone/supersede 的临时分支和不应送模的 UI 事件;
- 对 provider/model 不接受的 block 做定向 strip 或 fallback。
这一步解释了两个常见误解:resume 不是读取 JSONL 最后 N 行;compact 也不是把历史文件覆盖成一段 summary。物理 transcript、逻辑消息图和本次 wire view 是三个对象。
5. 在用户提交和工具批次后并行计算动态 attachment¶
lFf() 是多数动态上下文的汇合点。attachment会在初始用户提交,以及工具批次完成、准备下一次模型决策时重算;同一模型轮次内的网络retry、400兼容性strip或stream fallback通常复用当前消息快照,不会把全部generator重跑一遍。汇合顺序:readable L322002-L322021
下表描述最终会进入attachment/message正规化的主要动态对象;其中多数由lFf()汇合,read truncation与同步UserPromptSubmit Hook等对象由工具或Hook管线稍后追加。
| 类别 | 典型 attachment | 解决的问题 |
|---|---|---|
| 输入展开 | file、directory、MCP resource、Agent/peer mention |
用户输入中的 @... 实际指向什么 |
| 文件新鲜度 | edited_text_file、edited_image_file;工具管线另产read truncation |
模型先前看到的文件是否已被外部改变 |
| 指令与记忆 | nested memory、relevant memories、memory update | 哪些持久文本刚进入或已经过期 |
| 扩展能力 | skill listing、dynamic skill、deferred tools、MCP instructions、dropped tools、Agent listing | 当前会话的能力目录发生了什么变化 |
| 运行模式 | Plan、Auto、output style、critical reminder、token/budget | 本轮该怎样行动及何时收尾 |
| 编辑器与诊断 | IDE selection/open file、IDE/LSP diagnostics | 用户正在看的内容与新诊断是什么 |
| 异步工作 | queued command、task status、Agent pending message、team context | 正在运行或刚完成的后台工作怎样回到主循环 |
| 可编程扩展 | sync/async Hook result、additional context、system message | 外部 Hook 怎样阻断或增加上下文 |
generator共用一个约1秒后触发的abort signal;每个vw(label, generator)单独计时并捕获异常。某个attachment计算失败时,它记录diagnostic并返回空集合,不会因为“IDE状态没取到”或“某个附件构造失败”就终止整个Agent Loop。这个timer只会触发signal,不保证忽略AbortSignal的外部代码立刻停止。异常隔离:readable L322023-L322037
这是一种明确取舍:动态辅助信息可以缺席,主请求仍要继续;权限、tool pair 和 schema 合法性则不能按同样方式忽略。
当前 user message 与基础 attachment 构造完成后,客户端才运行 UserPromptSubmit Hook;Hook additional context/system message继续追加到尾部,blocking Hook则让本次提交根本不进入模型。工具批次结束后又会运行一次attachment extractor,把文件变化、MCP generation、队列消息和task状态带入下一次model iteration。见 readable L462452-L462588、L272372-L272424。
6. Typed attachment 被渲染成 API 消息¶
内部先保留{type:"attachment", attachment:{type:...}},而不是立即拼XML。readable L322843的dc()只增加UUID和timestamp。发送前WHi()先把attachment映射成零个或多个内部消息:多数动态状态成为meta user reminder,human-origin queued command可保持非meta,支持时部分纯文本reminder还会提升为api_system。wy()会把适用text block包进:
对应实现见 readable L398727-L398799 与 attachment renderer L398948-L400100。
因此:
<system-reminder>是发送编码,不是所有动态状态的内部存储格式;- attachment 才保留类型、字段、UUID、时间和 resume/compact 语义;
WHi()可以保留某些 attachment 为内部 SDK/transcript frame 而不生成模型文本,不能用“记录存在”推断模型一定看见;- 并非用户输入中出现同样标签就自动获得 harness 身份;
- meta message 虽使用 user-role 兼容载体,但
isMeta、origin 和类型判断让客户端不会把它当作普通 human prompt。
qtt()/yyt()会排除tool result、isMeta、compact summary、非human origin和多种系统envelope,避免它们进入“真实用户prompt数”等逻辑。human/meta:readable L397283-L397321;origin predicates:L72174-L72206
7. 支持时提升为 mid-conversation system,失败时降级¶
2.1.235 已包含 api_system:内部消息携带 role:"system",wire serializer 会发成 mid-conversation system message。readable L111799、L410214
这条路径受 model/provider capability 和 session latch 约束。支持时,客户端可以把聚合后的 reminder 提升为 api_system;若 API 或代理拒绝 mid-conversation system,客户端 sticky关闭该能力,重新构造兼容的user-role reminder,避免每轮重复撞同一个400。若拒绝的只是api_system尾部cache marker,则只把breakpoint降到trailing message后重试,不需要改写top-level system。readable L409347-L409351、L409575-L409582
所以 2.1.235 不是“所有 reminder 永远都是普通 user 消息”,也不是“所有动态状态都能获得 system authority”。实际结果由模型支持、beta/capability 与失败恢复共同决定。
8. Reminder 会被放进合法的消息位置¶
模型 API 对 role 顺序和 tool pair 有严格要求。客户端不能随便在 assistant.tool_use 与对应 user.tool_result 中间插一条独立消息。
jaT()将内部 attachment 移到合适的 user/assistant 边界;Jwm()可以把独立的 reminder text 合并进最后一个符合条件的 tool result;api_system必须出现在允许的 role 位置;- 正规化器还会过滤已不可用的 tool reference、非法 media 和 orphan block。
这就是为什么抓包时 <system-reminder> 可能出现在 user message,也可能附着在 tool result 内。它是为了保持 API 消息合法性和 cache 前缀,而不是在声称“这段内容由用户输入”。相关装配见 readable L397730-L398210。
9. 最后才放 cache breakpoint 并发请求¶
系统块、tools 和规范化 messages 都确定后,客户端才决定 cache marker:
- system 稳定/动态 boundary;
- 最后一条合法消息;
skipCacheWrite时向前移动;- fork point pin;
- thinking、不可缓存 block 与
api_system尾部的特殊处理。
在既有前缀字节、model、betas和active tool schemas稳定时,新消息通常只追加到旧前缀之后;动态context、工具generation或外层提交重建仍可能改写前缀。客户端把effort等request属性记为相邻调用的cache-break候选,但服务端真实cache key仍是Boundary。详细归因见 上下文治理与多层缓存。
文件为什么“刚改完不用重读”,外部修改又会被发现¶
readFileState 是 Prompt Assembly 与文件工具之间的一份共享事实表。2.1.235 使用路径归一化的 LRU:最多 5,000 条、总预算 25 MiB;普通内容超过 4,096 bytes 时默认只保留 hash/length,必要对象可用 keepContent 保留正文。readable L195260-L195335
不同写入来源故意留下不同状态:
| 来源 | 保存的状态 | 后续含义 |
|---|---|---|
普通 Read |
content/hash、mtime、offset/limit;默认 offset 为 1 |
相同范围且 mtime 未变时可 dedup;部分视图不能证明整文件新鲜 |
Edit / Write |
新 content/hash、写后 mtime、offset/limit=undefined |
harness 知道写入成功;后续外部变化可进入 changed-file attachment |
| CLAUDE.md / nested memory seed | 原内容、mtime、seededFromContext、keepContent |
后续 Read 可返回“启动时内容仍有效”,变更时重新装入 |
| relevant memory | 内容、mtime 与可选 limit | 防止同一 memory 在本轮重复 surface |
贯穿例子:模型 Read config.ts,Edit 把端口从 3000 改到 4000。Edit 成功后状态表已经记录写后内容,所以模型无需为了“确认写成功”再 Read。此时 formatter 又把引号改了:
hFf()只检查offset/limit均未设置的完整 owner;- 比较当前 mtime 与记录时间;
- 重新通过 Read validator/consumer 读取,而不是直接信任磁盘路径;
- 文本未变化则不提醒;变化则产生 diff snippet;
- 同一轮所有 changed text snippet 总预算
16,384字符,超过后仍保留“发生变化”事实,但省略后续 diff; - 这次重读会更新规范化 key,使 reminder 成为一次性状态迁移,而不是每轮重复注入。
实现见 changed-file detector L322450-L322490;Edit/Write 的 stale-read 强制检查见 L196150-L196430。
Compact 后哪些动态信息回来¶
Summary生成成功后,客户端才快照并清空readFileState,随后构造compact boundary,并由wmi()从旧状态与当前owner按预算重建必要附件;总结失败不会清空这份状态。reactive path:readable L232845-L232868、full path:L263015-L263036、restore:L263175-L263230
重建不是“把所有提醒复制一遍”:
- 最近最多
5个合格文件; - 单文件恢复读取最多约
5,000token; - 文件附件合计预算
50,000; - invoked Skill 单项截到
5,000,合计25,000; - 当前 plan、task status、MCP/deferred-tool/Agent delta;
- 重新运行的 SessionStart Hook 消息。
PostCompact在恢复attachment之后执行;其当前consumer只汇总用户显示文本,不作为这组model-facing restored attachment注入模型。
文件正文太大时只留 compact_file_reference,要求需要时重新 Read。Skill attachment 会明确标注“此前已调用”,防止 summary 后把一次性 setup 再执行。compact_boundary 还保存 discovered tools 与 preserved UUID,使 Tool Search 和合法消息链能够恢复。
所以 compact 后的连续性来自四部分:semantic summary、条件性 preserved messages、按预算重建的 attachments/hooks、压缩后的新用户输入。只说“summary + 最后一轮对话”会漏掉能力和文件状态的恢复层。
Resume 不是全局去重器¶
Resume只对invoked_skills、hook_success、skill_listing、hook_additional_context等少数高风险attachment执行专门结构校验;其他类型默认通过,随后另有obsolete-type删除和旧记录displayPath补齐。它还能从invoked_skills与skill_listing恢复已调用/已公布Skill状态。这不是覆盖整个attachment union的完整schema validation。guards:readable L323147-L323178、restore/drop:L323307-L323365
但去重按类型完成,而不是把所有 reminder 文本放进一个全局 Set:
- SessionStart Hook message 用内容 identity 去重;
- Skill listing 恢复 sent-name state;
- MCP/Agent delta 根据当前集合与历史 attachment 计算;
- plan/todo/task reminder按各自 turn counter;
- changed file 依赖重建后的 read state 和当前 mtime。
因此“resume 后看见一条相似 reminder”不自动等于重复 bug。必须先判断它属于一次性事件、当前状态重申、delta 重建还是确实失效的去重 owner。
信任与提示注入:标签不创造权威¶
2.1.235 的 system prompt明确告诉模型:standard 模式下,tool result 和 user message 可能带 <system-reminder>,标签由系统提供的信息组成,与它所在的具体 user/tool result 没有直接关系;另一分支更直白地说标签由 harness 注入、不是用户。readable L407999
但标签本身仍是文本。真正的信任来自客户端掌握的结构和 origin:
- genuine human input、channel、peer、task notification、observer 和 unclassified input 使用不同 origin;
- 多数非用户来源会按origin获得专用envelope或meta标记;具体文案和是否显式声明untrusted取决于source renderer,
observer-activity等存在例外; - MCP instruction、resource、Hook stdout、IDE text 和文件内容仍可能携带恶意指令;
- permission/sandbox 不会因为文本自称 system 就自动放行;
- mid-conversation
role:"system"由客户端构造,外部文件不能直接创建该内部对象。
这也是全局风控必须同时看 Prompt Assembly 和执行管线的原因:prompt injection protection 只能降低模型被误导的概率;permission、sandbox、credential boundary 和网络 policy 才限制误导成功后的真实影响。
失败与恢复矩阵¶
| Failure | Detection | Client action | Preserved state | User effect |
|---|---|---|---|---|
| 某 attachment generator 超时/抛错 | 1s abort 或 vw() catch |
记录 diagnostic,本项返回空 | 主 messages/system/tools | 请求继续,但缺一项辅助上下文 |
| 文件 mtime 变但内容相同 | hash/content compare | 更新/忽略,不生成 diff | read state | 无无效提醒 |
| changed-file diff 总量超预算 | 累计超过 16,384 chars | 后续保留变化事实、隐藏 snippet | 文件路径与状态 | 模型需按需 Read |
| mid-conversation system 被拒 | 400/capability error | sticky 降级为兼容 reminder 后重试 | top-level system 与历史 | 少一次反复失败;authority lane 降级 |
api_system 尾 cache marker 被代理拒 |
定向 400 | marker 降到 trailing message | 消息内容不变 | 可能多一次请求,不必丢动态状态 |
| attachment 破坏 tool pair 位置 | role/pair normalizer | 移动或合并进合法 tool result | tool ID 因果关系 | 模型仍能把结果配回调用 |
| compact 后旧文件太大 | restore budget / Read cap | 只写 file reference | path、旧读取事实 | 真正需要时重新读取 |
| resume遇到受专门guard保护的malformed attachment | limited schema guard | 丢弃该attachment并记录错误 | 其余消息图 | 局部上下文缺失;其他attachment并非全union完整校验 |
Token、延迟、隐私和正确性影响¶
| 维度 | Prompt Assembly 带来的收益 | 代价 / 风险 |
|---|---|---|
| Token | 动态更新尽量追加到尾部,稳定system/tools/history可命中cache;Skill/MCP/文件按需恢复 | 提交/工具批次后的reminder、listing、diagnostic仍占上下文;过多扩展会持续抬高基线 |
| 延迟 | attachment 并行计算,单项失败不阻断主请求 | 文件 stat/diff、IDE、MCP、Hook 和 memory prefetch会增加请求前等待 |
| 正确性 | file state、tool pair、mode、task 和 diagnostics让模型拿到最新事实 | 附件缺席、过预算裁剪、旧 summary 或外部恶意内容仍可能误导 |
| 隐私 | 类型与 gate让不同来源可审计,Read deny可阻止 IDE/文件内容进入 context | CLAUDE.md、Memory、选区、Hook、MCP resource 和文件 diff一旦进入 wire,就会发送给当前 model provider |
| 安全 | origin wrapper、system/user区分和本地执行门形成纵深 | <system-reminder> 不是密码学签名;模型提示不能替代 permission/sandbox |
| 恢复 | compact/resume 可按 owner重建文件、Skill、MCP、task和 Hook 上下文 | 预算重建不保证逐字保留所有旧内容,外部副作用也不随消息重建回滚 |
本版可确认与不能确认¶
Static 可确认: section/request builder、typed attachment 集合、并行计算、meta/system-reminder renderer、api_system、role normalizer、file-state owner、compact attachment重建和 resume 类型恢复。
Public 只用于设计解释: Anthropic 当前文章确认 Claude Code 围绕稳定前缀、消息式动态更新、固定工具集合、deferred schema 和 cache-safe compact设计。它不替代本版具体阈值、功能开关和 wire Probe。
Boundary: 模型内部如何权衡冲突指令、服务端对不同 authority lane 的全部训练细节、未抓包路径的最终 cache hit、外部 Hook/MCP 内容是否可信,以及 provider保存/处理 prompt 的内部实现。
可复核源码索引¶
| 主题 | reverse/javascript/cli.readable.js |
|---|---|
readFileState LRU、path normalization、hash/content budget |
195260-195335 |
| Edit/Write stale-read 与写后 state | 196150-196430 |
| summary成功后清state、构造boundary与重建附件 | 232845-232868、263015-263036、263175-263230 |
| attachment汇合与单项失败隔离 | 322002-322021、322023-322037 |
| changed-file 检测与 16KiB snippet budget | 322450-322490 |
| attachment internal record | 322843 |
| resume有限attachment guard、drop与Skill state恢复 | 323147-323178、323307-323365 |
| human prompt / meta / origin分类 | 397283-397321、72174-72206 |
| role、tool-result 与 attachment 正规化 | 397730-398210 |
<system-reminder> wrapper 与 attachment renderer |
398727-400100 |
| system prompt 对 reminder authority 的说明 | 407999 |
cache marker 与 api_system request serialization |
408719-410219 |
| custom/append system 与 dynamic-section CLI contract | 603768 |
相关专题:上下文治理与多层缓存、Session、Checkpoint 与 Memory、工具、权限与 Hooks、MCP、Agents 与后台协作、全局数据流与隐私。