跳到主要内容

数据截至 (上游 commit b084ab075ba2)

提示词工厂:composeSystemPrompt 怎么拼出一份设计师人格

30 秒导读: Open Design 真正的"算法"不在模型里,而在一个纯函数里——它把 skill 正文、品牌设计系统、craft 规范、个人记忆、插件阶段、界面语言、执行剖面,按一个固定顺序拼成一份系统提示词。顺序不是随便排的:抗注入段必须第一,载重契约必须最后,中间每一段都挂着一个"整场会话都不变"的开关,这样这份提示词的前缀指纹才能被缓存、在续接会话时整块跳过不发。

本章只讲"怎么拼"。skill 与设计系统资产本身的格式和加载走 第 4 章;评审 agent 用的提示词走 第 6 章;这份提示词最终怎么随 run 一起送进 CLI 子进程,见 第 1 章第 2 章


1. 这是什么(零基础也能懂)

一句话定义: composeSystemPrompt 是一个把十几种素材按固定顺序拼接成一个长字符串的纯函数,这个字符串就是"AI 设计师"的人格、工作流和硬约束。

它要解决的问题。 Open Design 对接 25 家 CLI(Claude Code、Codex、Gemini、Cursor Agent……),每家的能力都不一样:有的有文件工具,有的只有一根纯文本流;有的项目绑了品牌设计系统,有的什么都没绑;有的要做网页原型,有的要生成一段音频。如果给每种组合手写一份提示词,组合数会爆炸。

它的做法。 只写一份,但把它切成可开关的段。每段自带一个判断条件,条件不成立就整段不进最终字符串。

一句话直觉: 把它当成流水线上的分装机——传送带上有 34 个料斗,每个料斗上方有一个闸门;一份订单(一次 run)经过时,闸门按订单参数开合,落下来的就是这一份专属提示词。闸门的开合规则本身是死的,变化的只有订单。

用起来什么样。 调用方只递一个大对象,函数吐一个字符串:

// 示意,非源码
const prompt = composeSystemPrompt({
skillBody, // 激活 skill 的 SKILL.md 正文
designSystemBody, // 品牌 DESIGN.md 正文
designSystemTokensCss,// 该品牌的 tokens.css(机器可读版)
craftBody, // 通用工艺规范(字距、配色克制度、反 AI 味)
memoryBody, // 从历史对话沉淀的用户偏好
metadata, // 建项目时用户勾的结构化选项
streamFormat: 'plain',// 'plain' = 无工具的纯 API 模式
sessionMode: 'chat', // 'chat' = 聊天模式,不主动造产物
});
// prompt 就是最终 system prompt

真实签名在 apps/daemon/src/prompts/system.ts:843 composeSystemPrompt,入参类型是同文件 :402ComposeInput——共 35 个字段,全部可选


2. 顶层全景(它大概怎么转)

这条链路分三段。素材在 daemon 的路由层攒齐,纯函数负责拼,拼完的结果和用户这一回合的话再合成一条消息喂给 CLI。

① 攒素材 ② 拼装(纯函数) ③ 送出
┌──────────────────┐ ┌────────────────────┐ ┌──────────────┐
│ composeDaemon │ │ composeSystemPrompt│ │ 指纹 + 折叠 │
│ SystemPrompt │ ─────► │ 34 段 · 逐段门控 │ ──► │ 进用户消息 │
│ (读盘/读库/读配置)│ 入参 │ (纯字符串运算) │ 字符串│ 再交给 CLI │
└──────────────────┘ └────────────────────┘ └──────────────┘
server.ts:4723 system.ts:543 server.ts:5916+

三段的分工,一句话各自说清:

阶段干什么在哪个文件
① 攒素材读 skill、设计系统、craft、记忆、插件快照,全部化成字符串字段apps/daemon/src/server.ts:8819 composeDaemonSystemPrompt
② 拼装纯字符串拼接,34 段按固定顺序、逐段门控apps/daemon/src/prompts/system.ts:843 composeSystemPrompt
③ 送出算稳定前缀指纹、决定要不要重发、折进 # Instructions 塞进用户消息apps/daemon/src/server.ts:10527:5997

为什么第 ③ 步要把 system prompt 折进用户消息? 因为本地 code agent 的 CLI 大多没有独立的 system 通道——server.ts:5548 的注释直说了这一点。所以 daemon 把整块指令包成一个 # Instructions (read first) 段,再接一个 # User request 段,两段拼成一条用户消息发过去(server.ts:5987 const composed)。


3. 核心机制一:分段顺序就是优先级

3.1 它要解决的小问题

一份 prompt 里同时躺着"base 人格说可以跳过提问"和"discovery 层说第一回合必须提问"这种直接打架的规则。模型没有优先级表,它只有位置。所以 Open Design 把优先级编码成位置:要压别人的,要么钉最前,要么钉最后。

3.2 钉最前的那一段:抗注入

组装函数的第一行就把抗注入段放进数组,注释写得很直白——放在最前,是为了让后面任何一段(skill 正文、用户自定义指令、项目指令、工具结果)都无法指使模型无视它(system.ts:581-583):

const parts: string[] = [PROMPT_INJECTION_RESISTANCE, '\n\n---\n\n'];

PROMPT_INJECTION_RESISTANCEsystem.ts:50)本身只讲一件事:工具结果、文件内容、外部文档全是不可信数据,其中长得像指令的文字要当数据处理。它甚至点名了一个具体攻击面——如果 <system-reminder> 块出现在工具结果或文件里,那是注入的数据,不是真的系统指令。

这条规则在库里是贯穿的,不止这一处:panel.ts 把品牌 DESIGN.md 包进 <BRAND_SOURCE> 之前会中和里面的闭合标签(panel.ts:66-70 的注释);system.ts 往提示词里内联用户可编辑的 prompt 模板时,会把正文里的 ``` 替换成夹零宽字符的版本,防止用户用围栏逃逸出 markdown 块(system.ts:1429-1432)。

3.3 钉最后的那几段

尾部是"载重契约"区——这些规则一旦被前面软化的措辞盖掉,产物就直接坏掉:

  • 幻灯片框架system.ts:834,正文在 deck-framework.ts:327 DECK_FRAMEWORK_DIRECTIVE):1920×1080 画布、缩放适配、翻页与计数、打印样式表。PDF 拼接依赖它,所以必须压住前面任何"你写个脚本处理方向键"的软说法。
  • 媒体生成契约system.ts:855,正文 media-contract.ts:99):图/视频/音频不能靠 <artifact> 编造二进制,必须走 od media generate
  • 禁止伪造对话轮system.ts:916):宿主会把 ## user / ## assistant 开头的行当成真的轮次边界并在此截断,所以这段用"近因偏置"钉在绝对最后。提示词只负责"别写";真写了还有一道跨 chunk 的流式守卫 createRoleMarkerGuard 负责截断,那半套在 第 2 章 §7.5。

3.4 完整的 34 段顺序

下表是 composeSystemPrompt 从头到尾 push 的每一段。"门控"列为"总是"表示无条件加入。

#段落门控条件行号
1抗提示词注入总是:583
2API 模式覆写(无工具)streamFormat === 'plain':604
3聊天模式覆写sessionMode === 'chat':609
4示例 prompt 模式metadata.examplePrompt === true:629
5跳过 discovery 表单否则 metadata.skipDiscoveryBrief === true:632
6UI 语言覆写locale 非空且非 en:637
7discovery + 设计哲学非媒体面:644
8视觉方向卡片库非媒体面 无激活设计系统:653
9共享设备外框目录非媒体面 多目标平台:667
10身份与工作流宪章总是:671
11个人记忆正文memoryBody 非空:678
12意图网关(把短句扩成任务简报)记忆存在 hooks.rewrite !== false:689
13自校验记分卡记忆存在 hooks.verify !== false:695
14从纠错中提议新规则记忆存在:700
15用户级自定义指令userInstructions 非空:706
16项目级自定义指令projectInstructions 非空:712
17设计系统用法 + DESIGN.md + 导入模式designSystemBody 非空:722/:726/:732
18tokens.css 契约designSystemTokensCss 非空:749
19组件清单(否则退回组件 fixture)二选一,各自非空:755/:759
20按需拉取文件索引designSystemPullIndex 非空:765
21craft 工艺规范craftBody 非空:775
22激活 skill 正文 + pre-flightskillBody 非空:782
23激活插件块pluginBlock 非空:787
24流水线阶段块(逐条)activeStageBlocks 非空:799
25项目元数据块metadata 存在:811
26幻灯片框架(或 freeform 条件版)deck 项目 / freeform 项目,且 skill 未自带种子:834/:846
27媒体生成契约(否则媒体调度提示)媒体面 / 非媒体面:855/:860
28Codex 内置图像生成覆写见 §7:869
29Critique Theater 面板协议cfg.enabled 品牌与 skill 都解析到 非媒体面:885
30激活设计系统 = 视觉方向(收尾重申)designSystemBody 非空:889
31已认证外部 MCP 服务器清单列表非空:893
32Gemini 的 todo 工具映射agentId === 'gemini':896
33文件系统交付覆写执行剖面为 filesystem:902
34会话中途澄清问题 / 禁止伪造轮次总是:910/:916

有两处顺序值得单独品:

设计系统出现了两次。 第 17 段给出 DESIGN.md 正文(前置,让后面的 skill 有 token 可绑),第 30 段再重申一遍"它就是本项目的视觉方向,不要再问用户选主题色"(ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDEsystem.ts:371)。前者是资料,后者是禁令——禁令必须晚于 skill 正文出场,否则 skill 里"请用户挑个方向"的措辞会赢。

skill 正文在插件块之前。 阶段块的注释明确写着"上面的激活 skill 正文仍然是优先级载体"(system.ts:790-795),阶段块只补充逐阶段的 atom 指引。


4. 核心机制二:门控为什么只敢挂"整场不变"的信号

4.1 先看一个反直觉的设计

方向卡片库(directions.ts:242 renderDirectionSpecBlock)把 5 张方向卡逐张摊开,每张带 OKLch 调色板和字体栈,源码注释估作约 6.7KB。有激活设计系统时,这坨东西没用——所以跳过。

跳过的判断依据activeDesignSystemBody 这个"组装器可见的信号",而不是"这一回合用户有没有说要换方向"。源码注释把理由写死了(system.ts:648-651):

Gate it on the composer-visible active-DS signal (stable for the whole session, so the stable-prompt fingerprint stays cacheable).

多设备外框目录(第 9 段)的注释也是同一句话(system.ts:658-661):门控挂在建项目时就定下的 metadata.platform / metadata.platformTargets 上。

4.2 指纹到底是什么

指纹是"稳定指令块"的 sha256。稳定指令块 = daemon 提示词 + 运行时工具契约 + 这份 system prompt(server.ts:5916):

const stableInstructionFingerprint = [daemonSystemPrompt, runtimeToolPrompt, systemPrompt]
.map((part) => (typeof part === 'string' ? part.trim() : ''))
.join('\n\n---\n\n');
const currentStableHash = hashStableInstructions(stableInstructionFingerprint);

hashStableInstructionsapps/daemon/src/agent-session-resume.ts:307,就是一次 sha256。判定函数在同文件 :230

export function computeIncludeStable(
isResuming: boolean,
storedStableHash: string | null,
currentStableHash: string,
): boolean {
return !isResuming || storedStableHash !== currentStableHash;
}

读法:只有在"确实在续接一个已有会话"且"这一坨和上次发的一字不差"时,才跳过整块不发。 新建会话发全量;老会话没存过 hash(null)比较不相等,也发全量。跳过的动作发生在 server.ts:5948——clientInstructionParts 里那一项 systemPrompt 直接不入列。

4.3 所以为什么门控不能挂"这一回合的话"

因为一旦某段的开合取决于用户这一回合说了什么,指纹每回合都会变,computeIncludeStable 每回合都返回 true,整块指令每回合重发一遍。省下来的那几百 token 会被"每回合多发几千 token"吃掉还倒赔。

挂 session 级信号 挂 per-turn 信号
──────────────────── ────────────────────
turn1 指纹 A → 全量发 turn1 指纹 A → 全量发
turn2 指纹 A → 跳过 ✔ turn2 指纹 B → 全量发 ✘
turn3 指纹 A → 跳过 ✔ turn3 指纹 C → 全量发 ✘

命中/未命中还会被记录成 run.promptCacheserver.ts:5928,取值由 chat-prompt-inputs.ts:207 describeStablePromptCache 给出,miss 原因分 new-session / missing-stored-hash / stable-prompt-changed),所以这条设计是被观测的,不是口头约定。

4.4 四个门控信号的来源与稳定性

门控信号来源会话内稳定?
streamFormat === 'plain'适配器定义(哪家 CLI)
sessionMode === 'chat'会话创建时选的模式
metadata.kind / platform / examplePrompt建项目面板的结构化选项
designSystemBody 非空项目/插件/全局默认解析出的品牌

5. 核心机制三:几个关键闸门的实现细节

5.1 执行剖面:一根开关切两套交付语义

同一份人格宪章,在有文件工具的运行里说"把文件写到项目目录",在纯 API 运行里说"把完整 HTML 放进 <artifact> 块"。实现靠占位符替换,不靠两份文案。

official-system.ts:13-14 在宪章正文里挖了两个洞:

const EXECUTION_CONTEXT_PLACEHOLDER = '%%OPEN_DESIGN_EXECUTION_CONTEXT%%';
const WORKFLOW_HANDOFF_PLACEHOLDER = '%%OPEN_DESIGN_WORKFLOW_HANDOFF%%';

renderOfficialDesignerPromptofficial-system.ts:164)按剖面选填 FILESYSTEM_EXECUTION_CONTEXT:120)或 TEXT_ARTIFACT_EXECUTION_CONTEXT:122),以及 FILESYSTEM_WORKFLOW_HANDOFF:124)或 TEXT_ARTIFACT_WORKFLOW_HANDOFF:146)。discovery.ts:262 renderDiscoveryAndPhilosophy 用同一手法填 %%OPEN_DESIGN_HANDOFF_INVARIANT%%

剖面从哪来?packages/contracts/src/execution-profile.ts:3 executionProfileFromStreamFormat 一行定死:

return streamFormat === 'plain' ? 'text_artifact' : 'filesystem';

组装器里则是 executionProfile ?? executionProfileFromStreamFormat(streamFormat)system.ts:593)——显式传入优先,否则按流格式推。

5.2 streamFormat === 'plain':API 模式覆写为什么要钉顶

API_MODE_OVERRIDEsystem.ts:943)解决一个真实回归(issue #313):纯流适配器(如 DeepSeek)没有工具,但提示词后面几千字都在教它 TodoWrite / Read / Bash,模型于是编造伪工具标记——吐出 <todo-list>…</todo-list>[读取 template.html …] 这种假协议文本泄进聊天。

关键在于位置。源码注释直说了旧方案为什么失败(system.ts:934-941):过去这段叫 ## API mode rule,追加在末尾,结果输给了 discovery 层自带的"以下规则覆盖后文一切"的标题。现在它被钉在 PROMPT_INJECTION_RESISTANCE 之后、discovery 之前——先声明没有工具,再让模型去读那堆讲工具的规则,模型就知道那些是被覆写的。

5.3 isMediaSurfaceEarly:整段跳过一层规则

图/视频/音频项目里,discovery 层那三千 token 的问卷规则、方向选择器、HTML 产物检查表全是废话。组装器算一个早判标志(system.ts:621-627):

const isMediaSurfaceEarly =
skillMode === 'image' || skillMode === 'video' || skillMode === 'audio' ||
metadata?.kind === 'image' || metadata?.kind === 'video' || metadata?.kind === 'audio';

if (!isMediaSurfaceEarly) 一次性罩住第 7/8/9 三段(system.ts:643-669)。注释给的理由不止省 token:把这些规则塞进去,模型必须先解析再逐条推翻它们,才能开始干活,这是额外的推理时间。

5.4 resolveExclusiveSurface:晚判用的另一个函数

早判 isMediaSurfaceEarly 只是 || 串联,逻辑粗但够快。真正决定"钉哪一份尾部契约"的是导出函数 resolveExclusiveSurfacesystem.ts:228),它有明确的优先级:

metadata.kind (用户建项目时选的,最权威)
↓ 没有则
skillMode (主 skill 的模式)
↓ 没有则
skillModes 里唯一的独占模式(多个则放弃,返回 null)

最后一条是重点:composedSurfaceModes.length === 1 ? … : nullsystem.ts:252)——用 @ 提及组合了两个独占面的 skill 时,函数拒绝猜,返回 null,尾部谁也不钉。

它的两个消费点:isDeckProjectsystem.ts:829)和 isMediaSurfacesystem.ts:850)。

5.5 examplePromptskipDiscoveryBrief:两把互斥的"别问了"

这两段是 if / else ifsystem.ts:629-635),永远只进一个。

buildExamplePromptOverride (:342)SKIP_DISCOVERY_BRIEF_OVERRIDE (:257)
触发用户从画廊点了策展示例 prompt项目由 daemon API 带 skipDiscoveryBrief: true 创建
形态函数,把 examplePromptTitle + examplePromptBrief 键值对渲进"已回答的简报"常量字符串
语气"这是展示件,按你的最高水准做""把首条消息和元数据当简报,直接开工"
追问完全禁止允许一条必要的追问

buildExamplePromptOverride 把 brief 的下划线键名转成人话再列出来(system.ts:356-358key.replace(/_/g, ' ')),使模型读到的是"pre-filled creative brief",而不是一个 JSON。

5.6 skill 侧文件的 pre-flight

derivePreflightsystem.ts:1559)不解析 skill 的 frontmatter,它直接对 skill 正文做正则匹配,命中哪个就把哪个列进"动手前必读"清单:assets/template.htmlreferences/layouts.mdreferences/themes.mdreferences/components.mdreferences/checklist.mdreferences/html-in-canvas.md

为什么要这么土?注释说明了(system.ts:1548-1557):skill 正文自己的工作流已经写了这件事,但skill 在上下文压力下会被截断,模型于是跳过 Step 0。把清单提到 skill 正文之前一句话点破,是廉价的加固。

同一个 assets/template.html 正则还兼任另一个职责——hasSkillSeedsystem.ts:831-832)。skill 自带种子模板时,通用 deck 骨架就不再注入,避免双份框架打架。


6. 各段素材从哪来

6.1 提示词文件分工

文件导出的关键符号负责哪一段
prompts/official-system.tsOFFICIAL_DESIGNER_PROMPT / renderOfficialDesignerPrompt身份宪章、工作流、内容哲学、React/Babel 与 Tweaks 约定
prompts/discovery.tsDISCOVERY_AND_PHILOSOPHY / renderDiscoveryAndPhilosophy / renderSharedFramesBlock三条硬规则(turn1 出表单 → turn2 分支 → turn3 TodoWrite)、反 AI 味清单、多设备外框
prompts/directions.tsDESIGN_DIRECTIONS / renderDirectionSpecBlock / renderDirectionFormBody5 张视觉方向卡的调色板与字体栈
prompts/deck-framework.tsDECK_SKELETON_HTML / DECK_FRAMEWORK_DIRECTIVE幻灯片固定框架与"只准填 SLOT"的契约
prompts/media-contract.tsrenderMediaGenerationContract / MEDIA_GENERATION_CONTRACT媒体面必须走 od media generate,以及本次 run 的模型白名单
prompts/research-contract.tsrenderResearchCommandContract开启 Research 时的检索命令契约与结果落盘要求
prompts/panel.tsrenderPanelPromptCritique Theater 面板协议(详见第 6 章
prompts/system.tscomposeSystemPrompt 及一堆本地常量组装 + 所有覆写段

BASE_SYSTEM_PROMPTsystem.ts:255)是 renderOfficialDesignerPrompt('filesystem') 的一次性求值,作为对外导出的默认基线;组装器内部不用它,而是按本次 run 的剖面重新渲染(system.ts:673)。

6.2 方向卡片库的双重身份

directions.ts 顶部的注释点明它有两个用途(directions.ts:11-18):

  1. 渲染期 —— renderDirectionFormBody:193)把 5 张卡序列化成 direction-cards 类型的问卷题,用户点一下就锁定调色板+字体栈,不给模型即兴空间。
  2. 构建期 —— renderDirectionSpecBlock:242)把每张卡的完整规格内联进 system prompt,模型据此把种子模板的 :root 换成这套值。

第二个的输出长这样(内层就是要落到产物里的 CSS):

### Editorial — Monocle / FT magazine `(id: editorial-monocle)`

**Palette (drop into `:root`):**

```css
:root {
--bg: …;
--accent: …;
--font-display: 'Iowan Old Style', 'Charter', Georgia, serif;
}
```

5 张卡的 id 分别是 editorial-monoclemodern-minimalhuman-approachabletech-utilitybrutalist-experimentaldirections.ts:55/79/105/131/158)。

6.3 craft:跨品牌的通用工艺规范

apps/daemon/src/craft.ts:78 loadCraftSections 是全库最短的加载器之一,逻辑只有三步:

  1. 校验 slug(/^[a-z0-9][a-z0-9-]*$/craft.ts:10)并去重;
  2. <craftDir>/<slug>.md
  3. 每份加一个 ### <slug> 三级标题,用 \n\n---\n\n 串起来。

读不到就静默丢弃craft.ts:97-100)。注释给的理由很实在:skill 可以前向引用一个还没落地的 craft 章节(比如 motion),不该因此崩掉。

craft 在最终 prompt 里的位置很讲究——在设计系统之后、skill 正文之前(第 21 段)。附带的裁决话术也写清了冲突规则(system.ts:775):token 取值以品牌为准,但品牌没覆盖的部分(字距、强调色用量上限、反套路模式)仍由 craft 说了算。

6.4 记忆:正文 + 两个可关的钩子

memoryBodyapps/daemon/src/memory.ts:620 composeMemoryBody 产出,规则值得记住:

  • 主开关 cfg.enabled 关 → 直接返回空串(memory.ts:592);
  • 只有MEMORY.md 索引链接到的条目才进正文(memory.ts:596-598parseIndexLinkIds 过滤)——用户从索引里删掉一条,就等于停用它,不必删文件;
  • 章节顺序固定:### Profile 打头(可被 profileEnabled 单独关掉,memory.ts:611-613),### Verified rules 收尾。

这个顺序不是审美,是给两个钩子用的。组装器接着按 memoryHooks 逐个决定要不要加钩子段(system.ts:687:693),且缺省为开memoryHooks?.rewrite ?? true):

钩子时机产物配对的记忆章节
rewrite回合一张 <od-card type="task-brief"> 折叠卡,把短请求扩成任务简报### Profile
verify回合一张 <od-card type="verify-scorecard"> 记分卡,逐条核对### Verified rules

第三段(提议新规则,system.ts:700)不带开关,只要有记忆就加,且限制"每回合至多提一条"。

两段钩子的措辞里都反复强调它们是加法不是替换:任务简报可以顶掉 turn-1 的 discovery 表单,但顶不掉 TodoWrite 计划和反 AI 味自检;记分卡是在原有品牌自检之后追加的一张卡。这是在防"模型拿新流程当借口跳过旧流程"。

listActiveRuleEntriesmemory.ts:682)复用同一套 index 过滤,供回合后的程序化校验(memory-verify.ts)读同一份规则;memory-rules.ts:1-16 则负责把画布上的批注蒸馏成候选规则草稿——启发式一遍、LLM 一遍(suggestWithLLM 来自 memory-llm.ts:1366),合并去重后仍要走用户 Keep 确认,从不自动落库

6.5 设计系统:一份内容的四种形态

品牌资产在 prompt 里被拆成四块,各自独立门控、缺哪块跳哪块:

内容给模型的指令行号
DESIGN.md 正文散文式的视觉主张"颜色/字体/间距以此为准,不要发明 token":726
tokens.css:root 自定义属性全集"逐字粘进产物的第一个 <style>":749
组件清单从 components.html 提炼的结构摘要"按这份清单匹配选择器与类名":755
组件 fixture清单提炼失败时的 components.html 原文"照抄片段可以,保留 var(--*) 引用":759

清单与 fixture 是 else if 关系(system.ts:753-761),不会同时出现。另有第五块"按需拉取索引"(:765),只列出可读文件的路径清单,让模型自己决定要不要用 od tools design-systems read --path 去取——推送保持轻,深挖留给拉取

designSystemImportMode 再叠一层裁决口径(system.ts:387 renderDesignSystemImportModeGuidance):normalized 优先用 OD 归一化 token;hybrid 先归一化、必要时看源证据;verbatim 尽量保留源语义与源命名。

6.6 插件与阶段块

pluginBlockpackages/contracts/src/prompts/plugin-block.ts:12 renderPluginBlock 从插件快照渲染,输出三个小节:## Active plugin(插件标识与描述)、## Plugin inputs明确告诉模型这些是插件作者预填的答案,不要再问用户)、## Plugin atoms

activeStageBlockspackages/contracts/src/prompts/atom-block.ts:91 renderActiveStageBlock 逐阶段渲染,每块是 ## Active stage: <id> 加若干 ### <atomId> 正文。daemon 侧在 server.ts:5180-5204 组装:受 OD_BUNDLED_ATOM_PROMPTS 环境变量控制(!== '0' 才开),loadAtomBodies 取不到正文时块为空串、被组装器的 block.trim().length > 0 过滤掉——流水线阶段解析不出 atom 正文时,提示词零增重system.ts:796-802)。

6.7 元数据块:把用户勾的选项翻成规则

renderMetadataBlocksystem.ts:1129)不是简单的键值罗列。它的开场白定了基调(system.ts:1140):已知字段视为权威,标了 (unknown — ask) 的字段必须出现在 turn-1 的 discovery 表单里。

它随后按 kind 展开成一整套硬规则。原型/模板/其它类项目会拿到七条(system.ts:1162-1184):

  • screen-file-first —— 每个屏必须是独立 HTML 文件,别把落地页、仪表盘、设置页堆成一张长页;
  • product-realism —— 产物里不许出现"屏数统计""demo only"标签、视口选择器、设计流程卡这类元信息;导航必须是真实产品导航,不是切换 mockup 的开关;
  • visual-system —— 用户没指定配色时也要有意图,至少给中性面、主操作色、次级强调、状态色,且避开泛滥的米色/桃色 AI 味;
  • app-specific modules / CJX-ready UX / interaction-fidelity —— 要有领域模块、要有真 JS 行为、有输入动作的屏必须做成能点的控件而不是静态截图;
  • artifact-output —— 产物是唯一事实来源,聊天里只写简短的产品向总结,不许把整份 HTML 源码倒回聊天

platformTargets 多于一个时还会追加"跨平台交付规则"(system.ts:1157-1161):每个目标必须是自己的文件(mobile-ios.htmltablet.html……),不许折叠成一个带 tab 的对比页

音频面还有一个特别的动作:当 kind=audio + audioKind=speech + audioModel=elevenlabs-v3 + 未选音色 + 拿到了音色列表(五个条件全中,system.ts:1489 shouldRenderElevenLabsVoiceOptions),组装器直接把一份现成的 <question-form> 塞进系统提示词system.ts:1312-1314),选项标签是音色描述、value 是精确的 voice_id。取列表失败时则走 formatElevenLabsVoiceOptionsErrorForPromptsystem.ts:124),把原始错误规整成"缺 key / HTTP 状态码 / 兜底"三种安全话术——不把原始 error 文本直接拼进 prompt


7. 特例覆写:Codex 的内置图像生成

这是全库最"脏"但也最诚实的一段——它公开承认自己是媒体契约的例外。

触发条件收得极紧,三个函数层层收窄:

resolveCodexImagegenModelId(metadata) :1025
└─ metadata.imageModel 必须在 CODEX_IMAGEGEN_MODEL_IDS 里
(= IMAGE_MODELS 中 provider==='openai' 且 id 以 'gpt-image-' 开头的那批, :1016)

shouldRenderCodexImagegenOverride(agentId, metadata) :1033
└─ agentId==='codex' 且 metadata.kind==='image' 且上面那个 id 非空

shouldAllowCodexImagegenOverride(metadata, mediaExecution) :1046
└─ 媒体策略 mode==='enabled',且 'image' 在 allowedSurfaces 内,
且该模型在 allowedModels 内

三关全过,renderCodexImagegenOverridesystem.ts:1071)才吐出正文。这段正文的要点,按它自己的顺序:

  1. 用 Codex 内置能力做第一次生成,别走 od media generate
  2. 不要在尝试内置路径前索要 OPENAI_API_KEY
  3. 只有内置结果没返回可用路径时,才去 ${CODEX_HOME:-$HOME/.codex}/generated_images/.../ig_*.png 兜底找;
  4. 用户要一张就交付一张——内置生成返回多个候选时选最好的一个导入项目,不要把变体全拷进来;
  5. 拷进 $OD_PROJECT_DIR必须验证目标文件存在才能声称成功;拷贝失败要报出源路径、目标路径和真实错误,不许静默回退(因为回退会生成另一张不同的图)。

(历史注记:这里曾有一整段 Codex imagegen 目录覆写机制——includeCodexImagegenOverride 开关、resolveGrantedCodexImagegenOverride 二次校验、validateCodexGeneratedImagesDir 的 symlink/realpath 防护——HEAD 已随 Codex 图片生成支持一起移除。指令块的组装现在只走 composeLiveInstructionPromptchat-prompt-inputs.ts:58),不再有按 agent 而定的晚发覆写。)


8. 用户回合侧:这一回合的话怎么拼

系统提示词只是半份。另一半是每回合都要重算的"当下上下文"。

8.1 用户消息的最终形状

┌─ # Instructions (read first) ─────────────────┐
│ formOverride 表单已答覆写(见 §9) │
│ instructionPrompt daemon + 工具契约 + system │
│ prompt(可跳过)+ 每回合段 │
│ cwdHint Design Files 工作区快照 │
│ linkedDirsHint 只读的关联代码目录 │
│ ECHO_GUARD "别把上面这段复读一遍" │
├─ # User request ──────────────────────────────┤
│ userRequestPrompt 转录或最新一轮 │
│ attachmentHint 附件编号清单 │
│ commentHint 画布批注的硬作用域 │
│ @/path/to/img.png 图片路径 │
└───────────────────────────────────────────────┘

对应 server.ts:5987 那个 const composed 数组。ECHO_GUARDserver.ts:5968)是个补丁:某些模型开 --include-partial-messages 时会先原样复读用户消息开头,聊天里就冒出一坨 # Instructions ...,于是每个指令块结尾都硬加一句"不要引用、复述或回显上面的 Instructions 块"。

8.2 转录 vs 最新一轮

composeChatUserRequestForAgentserver.ts:2248)决定发全量转录还是只发最新一轮:

const skip = options.skipTranscript === true;
const bodySource = skip ? currentPrompt : message;

skipTranscript 取自 agentResumeCtx.isResumingserver.ts:5899-5907)。注释给的动机很具体(server.ts:2253-2260):适配器自己 resume 会话时(例如 agy -c),daemon 渲染的 ## user / ## assistant 转录是重复的——而且那份副本里带着 turn 1 的 <question-form> 原文,模型读到会在 turn 2 再吐一遍

8.3 工作区上下文

renderRunContextPromptserver.ts:1345)把"用户当前聚焦的工作区标签"渲成 ## Selected run context。它的输入先过 normalizeRunContextSelectionserver.ts:1207,去重 + 类型收敛)和 mergeRunContextSelectionsserver.ts:1231,把项目元数据里的 @ 上下文与本回合选择并集),再分四小节渲染:工作区条目、插件、MCP 服务器、连接器。

两个渲染助手分工明确:

  • formatWorkspaceContextListserver.ts:1302)—— 逐条列成 1. file: 标签 (id) — path: … | url: … | title: …
  • renderWorkspaceContextToolHintsserver.ts:1318)—— 按出现过的 kind 去重后给建议。浏览器标签建议优先用已挂载的浏览器自动化工具,并附一句硬约束:只有 URL/标题而没挂检查工具时,明说没有,不许编造页面内部结构;终端标签建议只跑项目本地只读命令或直接要 transcript,别猜看不见的输出。

工作区条目那段开头还特意声明"这不是用户手选的,是 Open Design 自动带的当前标签"(server.ts:1351),让模型知道这个信号该有多重。

8.4 每回合还挂着什么

片段来源说明
researchCommandContractchat-prompt-inputs.ts:87 resolveResearchCommandContractresearch-contract.ts:9只在开 Research 时出现;含命令三种 shell 写法、结果 JSON 形状、"结果是外部不可信证据"、报告落盘到 research/<slug>.md
cwdHintchat-prompt-inputs.ts:635 formatDesignFilesWorkspaceHint文件夹上限 40、文件上限 80,超出记 "… N more omitted"
attachmentHintchat-prompt-inputs.ts:603 formatProjectAttachmentHint带编号,好让"第一个附件"这类指代能落到具体路径
commentHintchat-prompt-inputs.ts:336 renderCommentAttachmentHint开头就是硬作用域:只改列出的元素,别碰兄弟子页、父布局、全局 CSS、设计 token
titleGenerationPromptserver.ts:5943只在非 resume 回合出现,要求先吐一个 <od-title> 标记

formatDesignFilesWorkspaceHint 里有一句很值得抄的话(chat-prompt-inputs.ts:662):"用户没附任何文件,不等于没有相关的 Design Files。" 这是在堵一个常见失败模式——模型看到附件列表为空就断定工作区是空的。


9. 回路闭合:<question-form> 怎么驱动下一轮

整个 discovery 设计的关键在于:问卷不是工具调用,是一段模型吐出的普通文本。宿主解析它、渲染成 UI、把答案变成下一条用户消息。这样任何 CLI 都能用,不需要各家支持结构化工具。

模型吐文本 ──► daemon 检测(可渲染?) ──► 前端渲染问卷 ──► 用户提交
▲ │
│ [form answers — discovery] │
└────────── 下一轮:注入"已答"覆写 ◄─────────────────────┘

9.1 检测:为什么"看到开标签"还不够

apps/daemon/src/question-form-detect.ts:61 emittedRenderableQuestionForm 是全 daemon 唯一的判定入口。它做三件事,缺一不可:

  1. 匹配开标签(:14 QUESTION_FORM_OPEN_RE<question-form> 或模型常漂移到的别名 <ask-question>);
  2. 找到对应闭合标签;
  3. 把中间的 body 拿去验:20 questionFormBodyIsRenderable):剥掉可能的 ```json 围栏、JSON.parse、必须是对象、必须有非空 questions 数组。

第 3 步是重点。注释说明了理由(:56-60):只匹配开标签的话,一个畸形的、根本渲染不出来的 body,或者产物里当代码示例展示的 <question-form> 字面量,都会被误判成"这一轮在向用户提问"。验不过就继续往后扫(:73 cursor = closeIdx + closeTag.length),不是直接返回 false。

还有一个易忽略的细节值得单独学:findQuestionFormCloseTag:45)是逐字符切片再小写比较的,而不是先把整串 toLowerCase() 再找。注释给的原因(:41-44)——有些码点小写后会变长("İ" -> "i̇"),整串小写会让它之后的所有偏移量错位,body 切片就被切坏,一个合法的表单反而被判失败。

这一个函数同时供三个消费方使用:server.ts:1854 assistantMessageEmittedQuestionForm(判"这轮是在等用户输入",server.ts:1876 用它来避免在等待期弹插件候选卡)、server.ts:8341(插件创作 run 的产物缺失守卫)、runtimes/run-artifacts.ts:251 runAskedUserQuestionrun_finished.asked_user_question 分析信号)。

9.2 答案回流的形状

前端 apps/web/src/artifacts/question-form.ts:786 formatFormAnswers 把提交结果渲成纯文本,首行是个可解析的头:

[form answers — discovery]
- What are we making? : Slide deck / pitch
- Target platform: Fixed canvas (1920×1080)
- Who is this for?: (skipped)

未答的题渲成 (skipped)——空白也是信息,模型据此知道"这条用户主动跳过了,用默认值"。

9.3 下一轮的覆写

daemon 用 FORM_ANSWERS_HEADER_REserver.ts:2167)从 currentPrompt 里抠出表单 id,据此二选一(server.ts:5976-5981):

表单 id注入的覆写内容
discovery / task-typeFORM_ANSWERED_SYSTEM_OVERRIDE (server.ts:2182)逐条枚举禁止输出,并明确指向 RULE 2 / RULE 3
其它FORM_ANSWERED_GENERIC_OVERRIDE (server.ts:2207)只压制重复提问,不导流

强版本为什么要逐条枚举禁止的反模式?注释写得很清楚(server.ts:2169-2181):强模型看短提示就懂;但 GPT-OSS-120B Medium、Gemini 3.5 Flash 这类中等强度模型即使理解了"表单已答",仍会把 RULE 1 里那段带围栏的表单示例复读给用户。所以强版本点名四类禁止输出——任何 id 的 <question-form> 标签、复读 schema 的 ```json 围栏块、"请告诉我以下信息"式的提问话术、以及"子 agent 已停止""服务器重启"这类编造的系统事件叙述

同一份"已答"信号还会在用户消息侧再说一遍:formAnswerTransitionForCurrentPromptserver.ts:2215)在 # User request 顶部加 ## Latest user turn - form answers submitted。注释特意交代这两处措辞要与主干保持一致,不许各自漂移(server.ts:2226-2233)。


10. 巧妙之处(可以直接借鉴的)

1. 用位置编码优先级,并把"为什么在这个位置"写进注释。 system.ts:934-941 记下了 API 模式覆写从"追加末尾"改成"钉在顶部"的完整原因(输给了 discovery 层自带的覆盖声明)。这类知识如果只活在人脑里,下一个人重构时一定会把它挪回去。

2. 门控信号的稳定性 = 缓存能力。 system.ts:648-651 明确要求"只用整场会话不变的信号做门控,好让稳定前缀指纹保持可缓存"。这条约束把一个纯粹的省 token 优化,升级成了对组装器接口设计的硬要求。

3. 缺省为开的钩子开关。 memoryHooks?.rewrite ?? truesystem.ts:687)。没配记忆的调用方(以及 contracts/BYOK 兜底路径)自动拿到完整行为,不需要显式打开。

4. 用户可编辑内容进 prompt 前必须做围栏逃逸。 system.ts:1432 把用户改过的 prompt 模板里的 ``` 换成夹零宽字符的版本,注释直说是为了防止用户用围栏逃逸出 markdown 块、往系统提示词里注入自由指令。

5. 把外部错误规整成安全话术再进 prompt。 formatElevenLabsVoiceOptionsErrorForPromptsystem.ts:124)只认三种情况(缺 key / 已知 HTTP 状态码 / 兜底),状态码文案取自白名单常量 PROMPT_SAFE_HTTP_STATUS_LABELSsystem.ts:72),原始 error 文本不会原样落进提示词。

6. 加固不等于替换。 记忆的两个钩子段落里反复写"这是 ADDITIVE 的,不替换 TodoWrite 计划,不替换反 AI 味自检"(system.ts:689/:695)。给模型加新流程时,必须显式声明它和旧流程的关系,否则模型会用新的顶掉旧的。

7. 拒绝猜。 resolveExclusiveSurface 在 skillModes 里有多个独占面时返回 null(system.ts:252);derivePreflight 无侧文件时返回空串而不是一句废话(system.ts:1573);loadCraftSections 读不到文件就静默丢(craft.ts:97)。三处都是同一个态度:信息不足时产出零字节,而不是产出一句含糊的话


11. 边界与局限

  • 两份组装器要人肉保持同步。 daemon 侧 apps/daemon/src/prompts/system.ts:843 和 contracts 侧 packages/contracts/src/prompts/system.ts:256 是两个独立实现。system.ts:596-603 的注释明说 API 模式覆写"要和 contracts 那份逐字节一致,两条路径才产生同样的可观测行为"——靠注释和测试约束,不是靠共享代码。
  • question-form 解析器也是双份。 question-form-detect.ts:4-8 说明 daemon 不许 import apps/web/src,所以这份实现是"有意的镜像",漂移了就要把共享解析器提升进 packages/contracts
  • derivePreflight 是正则匹配,不是结构化声明。 skill 里改个路径写法(比如写成 references/layouts.markdown)就静默失效,没有任何告警(system.ts:1559-1575)。
  • hasSkillSeed 复用同一个正则。 只要 skill 正文里任何地方出现 assets/template.html 字样,通用 deck 骨架就会被跳过(system.ts:831-832)——哪怕那句话只是在举反例。
  • 门控条件散在函数体内。 34 个 if 混在一个 390 行的函数里(system.ts:543-932),没有声明式的段落表。要回答"什么条件下会出现 X 段"必须读代码,代码本身也没有自检不变量。
  • 元数据块的规则文本极长。 单是原型类项目就会展开七条硬规则加跨平台/响应式契约(system.ts:1152-1184),全部无条件写死在 renderMetadataBlock 里,无法按项目裁剪。

12. 代码地图(导航索引)

主题文件路径符号名
主组装函数apps/daemon/src/prompts/system.tscomposeSystemPrompt
入参契约(35 个字段,全可选)apps/daemon/src/prompts/system.tsComposeInput
抗注入段(钉顶)apps/daemon/src/prompts/system.tsPROMPT_INJECTION_RESISTANCE
API 模式覆写 / 聊天模式覆写apps/daemon/src/prompts/system.tsAPI_MODE_OVERRIDE / CHAT_MODE_OVERRIDE
示例 prompt / 跳过 discoveryapps/daemon/src/prompts/system.tsbuildExamplePromptOverride / SKIP_DISCOVERY_BRIEF_OVERRIDE
独占面解析(deck/image/video/audio)apps/daemon/src/prompts/system.tsresolveExclusiveSurface
skill 侧文件 pre-flightapps/daemon/src/prompts/system.tsderivePreflight
元数据 → 硬规则apps/daemon/src/prompts/system.tsrenderMetadataBlock
Codex imagegen 三层门apps/daemon/src/prompts/system.tsresolveCodexImagegenModelId / shouldRenderCodexImagegenOverride / renderCodexImagegenOverride
身份宪章 + 剖面占位符apps/daemon/src/prompts/official-system.tsOFFICIAL_DESIGNER_PROMPT / renderOfficialDesignerPrompt
discovery 三条硬规则apps/daemon/src/prompts/discovery.tsDISCOVERY_AND_PHILOSOPHY / renderDiscoveryAndPhilosophy
多设备外框目录apps/daemon/src/prompts/discovery.tsrenderSharedFramesBlock
方向卡片库(问卷 + 规格)apps/daemon/src/prompts/directions.tsDESIGN_DIRECTIONS / renderDirectionFormBody / renderDirectionSpecBlock
幻灯片固定框架apps/daemon/src/prompts/deck-framework.tsDECK_FRAMEWORK_DIRECTIVE / DECK_SKELETON_HTML
媒体生成契约 + 策略裁剪apps/daemon/src/prompts/media-contract.tsrenderMediaGenerationContract
检索命令契约apps/daemon/src/prompts/research-contract.tsrenderResearchCommandContract
评审面板协议apps/daemon/src/prompts/panel.tsrenderPanelPrompt
执行剖面推导packages/contracts/src/execution-profile.tsexecutionProfileFromStreamFormat
插件块 / 阶段块渲染packages/contracts/src/prompts/plugin-block.ts · atom-block.tsrenderPluginBlock / renderActiveStageBlock
素材汇总入口apps/daemon/src/server.tscomposeDaemonSystemPrompt
craft 章节加载apps/daemon/src/craft.tsloadCraftSections
记忆正文 / 活跃规则apps/daemon/src/memory.tscomposeMemoryBody / listActiveRuleEntries
批注 → 规则草稿apps/daemon/src/memory-rules.ts(模块级,配合 memory-llm.tssuggestWithLLM
稳定前缀指纹apps/daemon/src/agent-session-resume.tshashStableInstructions / computeIncludeStable
缓存命中描述apps/daemon/src/runtimes/chat-prompt-inputs.tsdescribeStablePromptCache
指令块最终拼装apps/daemon/src/runtimes/chat-prompt-inputs.tscomposeLiveInstructionPrompt
工作区 / 附件 / 批注提示apps/daemon/src/runtimes/chat-prompt-inputs.tsformatDesignFilesWorkspaceHint / formatProjectAttachmentHint / renderCommentAttachmentHint
运行上下文渲染apps/daemon/src/server.tsrenderRunContextPrompt / formatWorkspaceContextList / renderWorkspaceContextToolHints
用户回合拼装apps/daemon/src/server.tscomposeChatUserRequestForAgent / formAnswerTransitionForCurrentPrompt
表单已答覆写apps/daemon/src/server.tsFORM_ANSWERED_SYSTEM_OVERRIDE / FORM_ANSWERED_GENERIC_OVERRIDE
问卷检测(唯一入口)apps/daemon/src/question-form-detect.tsemittedRenderableQuestionForm / questionFormBodyIsRenderable / findQuestionFormCloseTag
答案回流格式apps/web/src/artifacts/question-form.tsformatFormAnswers

继续读: 这份提示词随一次 run 的完整生命周期见 01-run-lifecycle.md;它怎么被 25 家 CLI 各自接收见 02-runtime-adapters.md;skill / 设计系统 / 插件三类资产在磁盘上的格式见 04-assets-filesystem.md;产物怎么落盘与预览见 05-artifacts-and-preview.md;Critique Theater 面板协议的完整展开见 06-quality-loop.md