oh-my-pi Snapcompact 上下文占位符注入:将 AGENTS.md 指令压缩为 PNG 帧的提示词工程机制
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
oh-my-pi 的 Snapcompact inline imaging 机制会在请求发出前,把系统提示中的 context 文件指令(如 AGENTS.md、<repo-rules>、## Context段落)以及冗长的历史工具结果,渲染为高密度 PNG 帧以压缩在途 token。snapcompact-context-stub.md正是这一机制的关键一环:当原文本被替换成图片后,它作为"占位符提示"留在原位,指导模型把图片当作被移走的原文来读取。读完本文,你将理解该占位符的注入时机、与其余四个 prompt 文件的配合关系、背后SnapcompactInlineTransformer的完整调用链,以及如何通过snapcompact.*配置项启用和调优这套机制。
占位符文件本身:一行指令承载的职责
snapcompact-context-stub.md 全文只有一行:
Loaded context-file instructions: PNG image(s) attached below at the first user message start. At this marker, read every frame in order; apply as if original context-file text remained here.
它被编译进 snapcompact-inline.ts,与contextFramesNote、systemFramesNote、systemStub、toolResultNote一起通过with { type: "text" }导入。它回答模型三个问题:
- 发生了什么——"你本来加载的 context-file 指令被移走了";
- 去哪找——图片附加在第一条用户消息的开头,按顺序读取;
- 怎么用——把每一帧当作被移走的原文等价物("apply as if original context-file text remained here"),而不是普通配图。
这正是一份"指令占位符(instruction stub)":图片只承载文本密度,语义权威性仍来自这行文字对模型阅读行为的约束。
占位符的注入点:replaceContextSections的文本替换逻辑
占位符并非随便贴在系统提示里,而是精确替换掉被图片化的原文段落。核心逻辑在 selectSystemPromptImageTarget 和 replaceContextSections:
const CONTEXT_SECTION_PATTERNS = [ /<repo-rules>\n[\s\S]*?\n<\/repo-rules>/g, /## Context\n<instructions>\n[\s\S]*?\n<\/instructions>/g, ] as const; function replaceContextSections(block: string, extracted: string[]): string { let replaced = block; for (const pattern of CONTEXT_SECTION_PATTERNS) { replaced = replaced.replace(pattern, match => { extracted.push(match.trim()); return contextStub.trim(); }); } return replaced; }也就是说,当snapcompact.systemPrompt设为agents-md时,系统提示中两类结构——<repo-rules>…</repo-rules>标签块,以及## Context标题下<instructions>…</instructions>包裹的段落——会整体被提取出来(存入extracted),原位替换为contextStub.trim()。这正是 AGENTS.md 等 context 文件指令被加载进系统提示后的典型形态,因此该模式又被称为"仅搬走 context 文件指令"。
被提取的原文随后拼接为一段文本,经snapcompact.frames(text, { shape })估算帧数,最终在第一条用户消息前插入渲染出的 PNG 帧,同时把contextFramesNote作为该帧前的文字说明。
三种模式与兄弟占位符:一套完整的"图片化替换"家族
SnapcompactSystemPromptMode定义了系统提示图片化的三种模式(snapcompact-inline.ts):
| 模式 | 行为 |
|---|---|
none | 系统提示保持纯文本,不做任何替换 |
agents-md | 仅替换<repo-rules>与## Context <instructions>段落,占位符为 context-stub |
all | 整个系统提示被替换为systemStub,说明文本改用systemFramesNote |
五种 prompt 文件按场景各司其职,全部位于 packages/coding-agent/src/prompts/system/:
- snapcompact-context-stub.md——context 文件指令段的原位占位符(本文主角);
- snapcompact-context-frames-note.md——附加在第一条用户消息前的说明:"读下面的图片,把它们当作被替换的 context 文件";
- snapcompact-system-stub.md——
all模式下整份系统提示的原位占位符:"图片是权威的操作指令,优先读取并按权威执行"; - snapcompact-system-frames-note.md——
all模式下用户消息前的说明文字; - snapcompact-toolresult-note.md——历史工具结果被图片化后,随帧附带的说明,其中特别注明"这是刻意的上下文节省行为,不是故障,不要重跑工具或上报问题",并解释源图片位置标记的对应关系。
注意到 snapcompact-inline.ts 中,系统提示被替换后,用户消息的content被重组为[{ type: "text", text: userNote }, ...frames, ...originalContent]——即说明文字 + 帧 + 原用户消息内容依次排列,与 stub 中"image(s) attached below at the first user message start"的描述完全对应。
触发链路:transformProviderContext中的执行顺序
占位符替换不是独立的文本处理,而是 agent 循环里每次请求前的一段转换管线。在 sdk.ts 中,SnapcompactInlineTransformer在transformProviderContext钩子里按固定顺序执行:
- obfuscateProviderContext——可选的混淆器先行;
- snapcompactInline.transform——本机制核心:计划交换(
planInlineSwaps)、渲染帧、把 stub 写回系统提示、把帧挂到第一条用户消息; - clampProviderContextImages——按提供商的图片预算裁剪超出的图片块;
- normalizeProviderContextImagesForModel——按模型归一化图片格式;
- dropUnreadableContextImages——剔除无法解码的图片块,降级为文字占位;
- blobBroker.decorateContext——将内联字节换成可服务的远程 URL(若配置了 blob broker);
- dateCwdReminder.transform——把日期/工作目录提醒注入首条用户消息。
关键约束在文件头注释中写得很明确:transform 只构造新的 message 对象,绝不修改输入的content数组引用,因为输入共享了持久化SessionMessageEntry的引用,直接改动会把渲染出的图片泄漏进session.jsonl。因此帧是"每次请求瞬态生成"的,历史文件里永远只有文本。
交换策略与门控:什么时候才值得图片化
planInlineSwaps(snapcompact-inline.ts)是唯一决策入口,实时请求与/context节省估算共用同一份规则,保证二者永不打架:
- 工具结果门控:
MIN_TOOL_RESULT_TOKENS = 3000——低于 3000 token 的工具结果绝不栅格化,文本足够便宜;isError的工具结果必须保持纯文本(提供商 API 校验需要); - 节省裕度:
SAVINGS_MARGIN = 0.9——只有帧数 × frameTokenEstimate <= textTokens × 0.9才渲染,保证图片 token 至少比原文本便宜 10%; - 系统提示帧上限:
MAX_SYSTEM_PROMPT_FRAMES = 6; - 预算约束:先扣掉上下文中已存在的图片数,得到剩余预算;系统提示交换放在工具结果之后,只用"剩下的"预算;
- 跳过而非停止:单个候选超出剩余预算时跳过它继续看后面的更小候选;且最新一条工具结果永远保持文本,保证最新输出清晰可读、利于缓存稳定性。
预算来自 snapcompact.ts 的PROVIDER_IMAGE_BUDGETS与providerImageBudget:各提供商有独立预算表,未知名提供商回退到DEFAULT_PROVIDER_IMAGE_BUDGET = 5。estimateInlineSavings(snapcompact-inline.ts)据此输出visionCapable、systemPrompt、toolResults三组估算,/context面板可预览"下一请求将省多少 token"。
缓存与懒渲染:避免重复栅格化
SnapcompactInlineTransformer内部维护两个渲染缓存(snapcompact-inline.ts):
#toolCache:以toolCallId为键、Bun.hash(text)为内容指纹;每次 transform 后用当前上下文中存活的toolCallId清掉失效条目,缓存大小受实时历史约束;#systemCache:以整个待图片化系统提示文本的 hash 为指纹,命中即复用。
#framesFor优先调用构造时注入的frameSink(blob broker 的懒渲染服务):帧在缓存里只是"小占位符"({ data: "", url }),真正的 PNG 只在提供商实际抓取 URL 时才栅格化,内存里从不囤积像素。这也是 provider-image-budget.ts 注释里提到的形态:transform 先于图片裁剪执行,因此它见到的占位符已经是"引用形状",不会误判为待解码的内联字节。
配置项与实操参数
在 settings-schema.ts 中,本机制暴露为三个实验性配置(UI 位于 context 标签页的 Experimental 分组):
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
snapcompact.systemPrompt | enum:none/agents-md/all | none | 是否将系统提示(或其 context 文件段)图片化;仅视觉模型生效;节省 token 但会失去被图片化文本的提示缓存 |
snapcompact.toolResults | boolean | false | 是否将大体积历史工具结果渲染为 PNG,省下累积的 read/search 输出 |
snapcompact.shape | enum:auto+ 各形状变体 | auto | 帧的排版形状;auto按当前模型自动挑选,回退到其提供商家族 |
兼容性细节:settings.ts 会把旧版布尔值snapcompact.systemPrompt自动迁移为枚举——true→all,false→none。
snapcompact.shape的主要变体(详见 schema 描述):
| 变体 | 特征 |
|---|---|
8x8r-bw/8x8r-sent | unscii 方块字格,每行重复两遍,后者按句子换色 |
6x6u-bw/6x6u-sent | 6x6 最密可读格,帧数最少 |
5x8-bw/5x8-sent | 最初的 snapcompact 形状(X.org 5x8 字形) |
8on22-bw | 8x13 字形配 8x22 行距,OpenAI/Google 默认 |
11on16-bw | 8x13 字形配 11x16 字距,Anthropic 默认 |
silver16-bw | 内嵌 Silver TrueType 字体 16px 网格,面向 CJK 等非拉丁文本 |
doc-8on16-bw/doc-8on16-sent/doc-8on16-sent-dim | 双栏报纸式排版,可选句子换色与虚词压暗 |
建议启用顺序:先snapcompact.toolResults = true观察长会话中累积工具输出的节省,再试snapcompact.systemPrompt = agents-md(仅图片化 AGENTS.md 等 context 指令,风险面最小),最后才考虑all。由于图片化会破坏对应文本的提示缓存(schema 描述已明确提示 "loses prompt caching for imaged text"),对依赖前缀缓存的开源权重模型,需权衡每次请求的 in-flight 节省与缓存命中的长期收益。
局限性与兜底设计
- 仅视觉模型生效:
model.input不含image时 transform 直接原样返回上下文(snapcompact-inline.ts),因为纯文本提供商会静默丢弃图片,栅格化等于丢内容; - 预算耗尽回退:一旦预算被已附带的归档图片或系统提示帧花光,工具结果原样以文本发送;
- 损坏图片兜底:若帧或历史图片无法解码,provider-image-budget.ts 的
dropUnreadableContextImages会把不可读块降级为[image omitted: undecodable …]文字,而不是让整个请求被提供商拒绝; - 不出现在历史:帧只存在于出站
Context的瞬时副本,session.jsonl中永远只有被 stub 替换前的文本形态,onToolResultSavings追加式账本(sdk.ts)是帧 token 节省的唯一痕迹。
从源码结构看,这套"文本 → 密集 PNG 帧 + 原位占位符指令"的模式是 oh-my-pi 应对长上下文预算的核心实验路径:占位符文件虽短,却负责在语义上把"图片即原文"的阅读契约固定下来,缺了它,模型将无法区分"附带截图"与"被替换的指令",整个压缩机制就会失去保真度。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考