跳转到内容

10. Prompt、上下文与压缩

Agent 产品最容易出现“功能越加越多,system prompt 越来越长”。Craft 的主要应对不是单纯缩短文案,而是把上下文按稳定性、时效性和一次性消费语义拆开,再针对 Claude/Pi 的缓存与 resume 行为放到不同位置。

一次 turn 可能包含:

基础 Craft system prompt
项目/仓库 context 文件清单
workspace capabilities
working directory
project config / asset manifest / MEMORY.md
用户 preferences
session_state(permission、plans/data path、mode transition)
source state / enabled source 指引
skill content 或读取前置要求
recovery / branch seed / transfer summary(一次性)
用户文本与 attachments

不是所有内容都应放 system,也不是每轮都应重算。

PromptBuilder 是 BaseAgent 共享模块。核心拆成:

buildVolatileContextParts

  1. 当前日期/时间;
  2. session_state:permission、paths、一次性 mode transition signal;
  3. source state。

特点:每轮可能变化、必须恰好构建一次。尤其 session state 会“消费”一次性 transition;若同一 turn 为日志和 prompt 调两次 builder,第二次内容不同。

buildStableContextParts

  • workspace capabilities;
  • working directory/长期能力描述等相对稳定内容。

特点:幂等、适合缓存前缀。

buildRecoveryContext:当 provider resume 失败时,把最近持久 user/assistant 对压成隐藏块,帮助新 SDK session 接续。

姓名、时区、城市、偏好 notes 等可固定在会话首个 system prompt 组件中,避免 compaction 后同一会话的人设/关键约束漂移。

10.3 为什么 volatile 必须只构建一次

Section titled “10.3 为什么 volatile 必须只构建一次”

一个隐蔽 bug:

debug(buildVolatileContextParts())
send(buildVolatileContextParts())

若第一次消费 modeTransition,模型实际收到的第二份就缺信息。代码注释把“exactly once”设为不变量。正确做法:

const volatile = buildVolatileContextParts(...)
debug(volatile)
send(volatile)

这类 one-shot context 应尽量显式返回“内容 + commit token”,比隐藏消费更易推理;当前实现需要调用者自律。

Claude 将 volatile + stable context 组合到 user turn tail。SDK 自己管理 system prompt/resume/compaction,统一尾注可减少恢复路径差异。

pi-agent.ts

  • stable parts 进入 system prompt 的缓存友好前缀;
  • volatile parts 进入 user message tail;
  • 每轮时间/session state 的变化不会让整个 system prefix cache 失效。
flowchart TD
ST["Stable context"] --> PIS["Pi system prefix / cache"]
VO["Volatile context"] --> TAIL["Current user tail"]
USER["User message"] --> TAIL
PIS --> REQ["Model request"]
TAIL --> REQ

这是 context engineering 而非 provider-neutral 纯抽象:同一语义根据 backend cache contract选择位置。

入口:getSystemPrompt。它组合:

  • default/mini/custom preset;
  • session tools 与工作方式;
  • permission mode/plan/data folder 规则;
  • source/skill 使用规则;
  • project context;
  • working directory 中的 context file 清单;
  • platform/headless 特征。

Mini agent 使用更短 preset 和最小工具集(Read/Edit/Write/Glob/Grep/Bash),用于摘要/小任务,不继承所有交互 UI 能力。

findAllProjectContextFiles 递归寻找大小写不敏感的 AGENTS.md/CLAUDE.md

  • 排除 node_modules/build/cache 等目录;
  • 单文件最多约 10KB;
  • 数量上限约 30;
  • 结果缓存约 5 分钟;
  • system prompt 主要列出发现位置,指示 Agent 按作用域读取,而不是盲目内联整个 monorepo。

这种做法兼顾:

  • monorepo 每个 package 有局部规则;
  • system prompt 不被几十个文件撑爆;
  • Agent 在操作目标前主动读取最近作用域规则。

Project memory、资产说明和用户文件属于不可信内容。formatProjectContextForPrompt() 把它们放进明显边界,避免其中的“忽略之前指令”被误当系统规则。

防御原则:

  1. 标明内容来源和权威级别;
  2. 用结构标签包裹;
  3. 限长;
  4. 不让用户内容闭合标签/伪造高优先级区块;
  5. 明确它是数据而非指令。

源码:formatProjectContextForPrompt

Memory 是项目层长期知识,默认上限 5k tokens,保留顶部。它与 conversation history 分工:

  • history:发生过什么,provider resume/compaction 管;
  • memory:跨 session 仍有用的结论,由 Agent/用户维护;
  • preferences:跨 workspace 的个人偏好;
  • session state:当前 turn 临时事实。

将四者混成一个“memory”会导致作用域泄漏和无穷增长。

Skill mention 或 Task spec skills 不直接把全部 SKILL.md 内联。PrerequisiteManager 记录 required reads:

prompt 出现 [skill:x]
→ 模型看到 skill 路径/要求
→ 在执行其他工具前必须 Read SKILL.md
→ pre-tool-use 检查读取是否完成
→ 完成后开放后续工具

若 skill 声明 required source,SessionManager 在 chat 前先 enable/activate source。这样“说明”和“能力”同时就绪。

Compaction 后模型可能忘记已经读过 guide/skill,代码会 reset prerequisite state,要求重新读取。这是非常关键的恢复语义。

API source 的 endpoint 文档不再全部塞 tool description。Tool description 保持短,Agent 先读 source guide.md;prerequisite manager 可强制这个顺序。

收益:

  • tool schema/token 更小;
  • 文档可由 workspace 自定义;
  • source 热更新不要求重写每个 tool description;
  • 只有真正使用 source 的 session 支付上下文成本。

附件处理按能力分层:

  • 图片/PDF 可转 provider content block;
  • Office/邮件/日历等通过内置 CLI 工具转文本或让 Agent按需读取;
  • 过大图片在 PreToolUse Read 前 resize;
  • provider/model 不支持 image 时过滤或提供文本提示;
  • 路径使用 session/workspace 语义,不能把远程服务端路径误当客户端路径。

附件不应全部 base64 持久化到 transcript;StoredAttachment 与 session data 文件保持可恢复引用。

正常优先使用 provider transcript,因为它保留精确 tool/message graph。只有 resume 失败才用 Craft messages 构造 recovery:

provider resume
├─ success → 不重复注入历史
└─ failure
→ 清无效 sdk id
→ recent persisted messages → recovery block
→ fresh provider session
→ one retry

如果 resume 成功又注入同一历史,会重复上下文、增 token,甚至让模型以为用户重复发言。

两种一次性上下文:

provider-native fork 不可用但产品允许降级时,将截止点前消息摘要/种子注入 fresh session。注入后 markBranchSeedApplied() 持久化,避免重启再注入。

跨 server bundle 未携带 provider transcript 时,源 server 生成 conversation summary;目标 session 首 turn 注入,随后 transferredSessionSummaryApplied=true

它们都不是永久 system prompt,而是“恢复一次性桥”。

对模型上下文做摘要并保留近端 entries。Claude 通过 SDK /compact;Pi 通过 session.compact() 或 SDK auto-compaction。

完整/展示消息仍保存在 Craft JSONL。Compaction 不是删除 UI 历史;它改变 provider 下一轮看到的上下文与锚点可用性。

这一区分解释了:用户仍能滚回旧消息,但旧 branch anchor 可能已被 provider compact 掉。

ClaudeAgent 识别 SDK slash command,强制走 per-turn query,而不是 persistent input shortcut,因为 compact 会修改 provider session 状态。SDK 返回 compaction status/info:

  • SessionManager 持久化 compaction complete message;
  • reset prerequisite state;
  • UI 展示 compacting/complete;
  • branch 若引用被压掉的 message UUID,走明确 fallback。

Pi 的 auto-compaction 由 SDK在 overflow 时执行并 continue。Wrapper 保持 EventQueue 在 recovery 期间打开,使恢复后的 turn 能继续到 UI。

手动 compact 必须等待正在进行的 auto-compaction;并行两个 compaction 会争用 agent abort/controller 与 entry state。请求 timeout 比普通 RPC 长,因为大对话摘要可能需要 60–120 秒。

Plan 审批有一个跨 reload 的工作流:

stateDiagram-v2
[*] --> PlanSubmitted
PlanSubmitted --> AwaitingCompaction: user Accept & Compact
AwaitingCompaction --> ReadyToExecute: compaction_complete
ReadyToExecute --> Dispatched: UI/server dispatch plan prompt
Dispatched --> [*]: clear pending state

Session 持久化:

pendingPlanExecution = {
planPath,
draftInputSnapshot?,
awaitingCompaction,
executionDispatched?
}

为什么这么细?若用户在压缩完成前 Cmd+R:

  • 没持久化 → plan 丢;
  • 只存 planPath 不存 awaiting state → 可能提前执行;
  • 不存 executionDispatched → reload 后重复执行。

SessionManager 在 compaction complete event 中更新持久状态,并可立即触发后续,不依赖 UI 恰好在线。

源码:pendingPlanExecutionSessionManager

可总结为一个优先级:

  1. 规则/安全不变量:system,短且稳定;
  2. 能力目录:system 稳定前缀或短清单;
  3. 动态 session/source:当前 user tail;
  4. 大文档/skill/source guide:按需 Read;
  5. 老对话:provider transcript + compaction;
  6. 跨 session 长期知识:Project MEMORY.md,限额;
  7. 大 tool result:落文件 + 摘要;
  8. 恢复/迁移:一次性隐藏 summary。

volatile context exactly-once 依赖调用纪律。可以重构成显式 prepareTurnContext() 返回冻结对象。

主 prompt 文件本身很长;随着功能增加,规则冲突概率上升。应持续把“如何使用特定 source/skill”移到按需文档。

SDK summary 可能遗漏权限/项目约束。固定 system components、reset prerequisites 和 session_state 重注入是防线,但需要跨 provider 测试。

Provider compaction/TTL 会让锚点失效。产品 UI最好显示分支可用性或在创建时明确降级,而不是到首消息才惊讶。

Craft 的 context engineering 核心是按“稳定/易变/按需/一次性/可压缩”分类,再结合 provider 缓存与 resume contract 放置。Compaction 只压模型上下文,不删除产品 transcript;恢复与迁移通过一次性 summary 补桥。

下一章进入工具安全:模型有了上下文后,真正执行 Bash、写文件、调用 API 之前,中央 PreToolUse 管道怎样做决定。