oh-my-pi 分支摘要提示词解析:branch-summary.md 如何把被放弃的会话分支压缩为结构化上下文
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
本文围绕 oh-my-pi 中 branch-summary.md 这一提示词模板展开:它是会话树(session tree)导航场景下"分支摘要"(branch summary)功能的输出格式契约,规定了模型在用户离开当前对话分支时必须产出的固定结构(Goal / Constraints / Progress / Key Decisions / Next Steps)。读完本文,你能理解该提示词每个字段的用途与约束,掌握它在 branch-summarization.ts 中的完整调用链、token 预算与配置项branchSummary.*的行为,以及摘要生成后如何回注上下文供后续轮次继续对话。
一、问题背景:会话树导航中"离开分支"会丢失什么
oh-my-pi 的会话不是一条线性消息流,而是一棵树:每条消息是一个节点,用户在 agent-session.ts 的navigateTree()中可以在树内跳转到任意位置(例如回到某个早期消息重新提问,形成一条新的兄弟分支)。文件头注释(branch-summarization.ts)直接说明了动机:
"When navigating to a different point in the session tree, this generates a summary of the branch being left so context isn't lost."
如果不做处理,从分支 A 跳回公共祖先改走分支 B 时,A 上积累的目标、进度、决策就会从模型上下文中彻底消失。分支摘要机制的做法是:在跳转发生前,对被放弃的那段分支调用一次 LLM,把整段对话压缩成一份结构化摘要,并以branch_summary类型的条目持久化到新分支的目标位置,之后随上下文一起被重新注入。
而 branch-summary.md 就是这次 LLM 调用的"输出格式规范"——它不描述做什么(做什么由系统提示词和<conversation>块负责),而是强制规定"输出必须长什么样"。
二、提示词原文与逐节解析
branch-summary.md 全文如下(这是它对模型施加的完整契约):
You MUST create a structured summary of the conversation branch for context when returning. You MUST use EXACT format: ## Goal [What is the user trying to accomplish in this branch?] ## Constraints & Preferences - [Constraints, preferences, requirements mentioned] - [(none) if none mentioned] ## Progress ### Done - [x] [Completed tasks/changes] ### In Progress - [ ] [Work started but not finished] ### Blocked - [Issues preventing progress] ## Key Decisions - **[Decision]**: [Brief rationale] ## Next Steps 1. [What should happen next to continue] Sections MUST be kept concise. You MUST preserve exact file paths, function names, error messages.整份提示词由三部分组成:开头的一句任务声明("必须为该分支创建结构化摘要,以便返回时恢复上下文")、中间的 EXACT format 模板、结尾的两条硬约束(各节必须保持简洁;必须精确保留文件路径、函数名、错误信息)。逐节看其设计意图:
## Goal:这一分支要达成什么
一句话回答"用户在这条分支上试图完成什么"。它的作用是在分支被放弃后,把分支级的意图锚定下来——当用户回到这个位置继续对话时,模型不需要重读原始对话就能知道这条探索线当初在干什么。
## Constraints & Preferences:约束与偏好
以项目符号列出用户在分支中提到的约束、偏好和硬性要求;特别地,模板要求没有时必须显式写(none)。这个"占位符纪律"很典型:它让下游消费者(以及后续把摘要再压缩进 compaction 摘要的流程)能通过固定格式判断"这一节是空的",而不是把整节误读为缺失或遗漏。
## Progress:三态进度清单
这是模板中信息密度最高的一节,按状态拆成三个子节:
### Done:用- [x]勾选框列出已完成的任务/变更;### In Progress:用- [ ]未勾选框列出已启动但未完成的工作;### Blocked:列出阻碍推进的问题。
三态划分对应的是"恢复执行"所需的完整状态:哪些不需要再做、哪些要接着做、哪些被卡住了。勾选框([x]/[ ])语法让这节天然兼容 todo 类工具的呈现习惯,也让后续 LLM 在解析摘要时能机械地区分状态。
## Key Decisions:决策与理由
格式固定为- **[Decision]**: [Brief rationale]——决策加粗、冒号后跟简短理由。分支探索中模型往往会做架构选择、取舍权衡("选了方案 X 因为 Y"),这些决策是分支中最不可再生成的信息:原始对话可以压缩,但"为什么这么定"一旦丢失,恢复后的对话就可能重复讨论已经否决过的方向。
## Next Steps:有序续作清单
编号列表,回答"接下来应该做什么才能继续"。它与Progress/In Progress的区别在于:In Progress 是事实描述(进行到哪一步),Next Steps 是可执行计划(下一步动作)。
结尾两条硬约束的作用
- "Sections MUST be kept concise":摘要最终要放进上下文窗口与主对话竞争 token,冗长摘要会挤占真实对话的预算;
- "You MUST preserve exact file paths, function names, error messages":这是对"摘要失真"的防御。分支摘要最常见的失败模式是模型把
src/foo/bar.ts概括成"某个源文件"、把错误消息意译成近似描述,导致恢复后无法定位。模板通过把这三类标识符单列为"必须精确保留",压低这种失真概率。
三、提示词在代码中的位置:默认指令与可覆盖性
branch-summary.md 作为文本资源被直接导入并渲染为常量:
// packages/agent/src/compaction/branch-summarization.ts import branchSummaryPrompt from "./prompts/branch-summary.md" with { type: "text" }; import branchSummaryPreamble from "./prompts/branch-summary-preamble.md" with { type: "text" }; const BRANCH_SUMMARY_PREAMBLE = prompt.render(branchSummaryPreamble); const BRANCH_SUMMARY_PROMPT = prompt.render(branchSummaryPrompt);(branch-summarization.ts 与 branch-summarization.ts)
它作为默认指令参与提示词组装,并且可以被customInstructions覆盖:
// generateBranchSummary() 内,L332-L333 const instructions = customInstructions || BRANCH_SUMMARY_PROMPT; const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${instructions}`;也就是说,发给模型的单条 user 消息结构是:<conversation>标签包裹的序列化对话文本 + 换行 + 指令(默认即 branch-summary.md 的内容)。GenerateBranchSummaryOptions接口还暴露了其余可调项(branch-summarization.ts):
model/apiKey/signal:用哪个模型、如何取消;customInstructions?: string:整体替换默认提示词(宿主应用可以换一套格式契约);reserveTokens?: number:默认 16384,为提示词 + 响应预留的 token 空间;metadata?: Record<string, unknown>:转发给底层 API 请求的元数据;convertToLlm?: ConvertToLlm:宿主侧消息转换器;telemetry?: AgentTelemetry:提供时该次 LLM 调用会包在 OTEL chat span 中并打上pi.gen_ai.oneshot.kind = "branch_summary"标签;completeImpl?:宿主可替换底层完成传输,把请求路由到自己的 provider 并发限流器。
四、完整生成链路:从节点收集到摘要落盘
4.1 收集"被放弃分支"的条目
collectEntriesForBranchSummary(session, oldLeafId, targetId)(branch-summarization.ts)的逻辑是:
- 取旧叶子
oldLeafId到根的路径集合oldPath,以及目标targetId到根的路径targetPath; - 从
targetPath的末端向前扫,找到同时出现在oldPath中的最深节点,即公共祖先commonAncestorId; - 从旧叶子沿
parentId回溯到公共祖先,收集沿途所有SessionEntry,再反转为时间顺序。
注意注释明确说明:回溯不在 compaction 边界处停下——边界上的既有压缩摘要也会被纳入,成为分支摘要的输入上下文(getMessageFromEntry会把compaction条目转换为摘要消息,见 branch-summarization.ts)。
4.2 token 预算内的两遍遍历
prepareBranchEntries(entries, tokenizer, tokenBudget)(branch-summarization.ts)做两件事:
第一遍(不计预算):累计文件操作追踪。遍历所有条目,只从"pi 自己生成的"branch_summary条目(fromExtension !== true)的details中取readFiles/modifiedFiles并入fileOps。这样嵌套场景(一次分支摘要覆盖了此前另一段已被摘要的分支)能保持累计的文件追踪不丢失。
第二遍(受预算约束):从最新到最旧填充消息。逐条把SessionEntry转成AgentMessage,先提取 assistant 消息里工具调用涉及的文件操作,再估算 token(工具结果会先按truncateToolResultForSummary截断再计数,见estimateBranchSummaryTokens,branch-summarization.ts),超预算即停。两个细节值得注意:
- 被标记
useless === true且非错误的toolResult直接丢弃(branch-summarization.ts),避免无信息量的大 payload 吃掉分支摘要的 token 预算、挤掉更有价值的旧条目——branch-summarization.test.ts 中有专门用例验证"保留有信息量的工具结果、丢弃 useless 工具结果"; - 预算将满时,
compaction/branch_summary类型的摘要条目会被优先塞入(totalTokens < tokenBudget * 0.9时强行纳入,branch-summarization.ts),因为既有摘要本身就是高密度上下文,比普通消息更值得保留。
4.3 LLM 调用与后处理
generateBranchSummary的组装流程(branch-summarization.ts):
// L316-L318:token 预算 = 上下文窗口 − 预留 const contextWindow = model.contextWindow || 128000; const tokenBudget = contextWindow - reserveTokens; // reserveTokens 默认 16384 // L328-L329:转换并序列化对话(防止模型把它当成"要继续的对话") const llmMessages = (options.convertToLlm ?? defaultConvertToLlm)(messages); const conversationText = serializeConversationForSummary(llmMessages, preferredDialect(model.id));LLM 调用参数:系统提示词为SUMMARIZATION_SYSTEM_PROMPT,maxTokens: 2048(摘要上限),并带oneshotKind: "branch_summary"遥测标签(branch-summarization.ts)。拿到响应后还有三步后处理:
- 前置前言:拼接
BRANCH_SUMMARY_PREAMBLE(来自 branch-summary-preamble.md,内容为 "User explored another conversation branch, then returned here." + "Exploration summary:"),让摘要自带身份标识——读摘要的人/模型知道这是"探索过另一条分支后带回"的产物; - 追加文件清单:
computeFileLists(fileOps)算出 read/modified 文件列表,upsertFileOperations把它们以<files>标签写进摘要尾部(路径带(Read)/(Write)/(RW)标注,旧版本的<read-files>/<modified-files>标签会被剥除自愈,见 packages/agent/CHANGELOG.md); - 返回结构化结果:
{ summary, readFiles, modifiedFiles }加上aborted/error状态位,其中readFiles/modifiedFiles会随摘要条目一起持久化为details。
五、触发点:navigateTree 中的会话树跳转
真正的业务触发在 agent-session.ts 的navigateTree()(约 L9640 起):
- 前置校验:
options.summarize为真时必须已有可用模型("No model available for summarization"); - 计算摘要锚点:一般情况下就是
targetId;在ask工具"重新作答"的特殊协议下,锚点改为targetEntry.parentId(新兄弟节点会挂在那里),否则会漏掉旧答案条目(L9700-L9712,对应 issue #5895 的修复注释); - 调用
collectEntriesForBranchSummary(this.sessionManager, oldLeafId, summaryAnchorId)得到待摘要条目与公共祖先; - 扩展钩子优先:若注册了
session_before_tree钩子,扩展可以取消导航,或自己提供一个摘要(result.summary),此时跳过内置 LLM 摘要并置fromExtension = true(L9729-L9744); - 否则读配置组并生成摘要:
// L9755-L9771 const branchSummarySettings = this.settings.getGroup("branchSummary"); const result = await generateBranchSummary(entriesToSummarize, { model, apiKey: this.#modelRegistry.resolver(model, this.sessionId), signal: this.#branchSummaryAbortController.signal, customInstructions: this.#obfuscateTextForProvider(options.customInstructions), reserveTokens: branchSummarySettings.reserveTokens, metadata: this.agent.metadataForProvider(model.provider), convertToLlm: messages => this.#convertToLlmForSideRequest(messages), telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId), completeImpl: async (requestModel, requestContext, requestOptions) => { const stream = await this.#sideStreamFn(requestModel, requestContext, requestOptions); return stream.result(); }, });- 摘要落盘位置:注释写得很明确——"Summary is attached at the navigation target position (newLeafId), not the old branch"(L9844-L9857)。有摘要时调
sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension)在目标位置创建branch_summary条目;summaryDetails即{ readFiles, modifiedFiles }(L9780-L9783)。随后重建会话上下文、重置 advisors、从分支同步 todo,并视情况发出session_tree事件。
HTML 导出同样消费这个条目:template.js 会渲染带 "Branch Summary" 标题的独立区块,树形视图里则显示[branch summary]:前缀(L555)。
六、相关配置项
分支摘要由 settings-schema.ts 中两个设置项控制:
| 设置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
branchSummary.enabled | boolean | false | 离开分支时是否提示/执行摘要(UI 位于 context 标签的 General 组,描述为 "Prompt to summarize when leaving a branch") |
branchSummary.reserveTokens | number | 16384 | 传入generateBranchSummary的reserveTokens,即tokenBudget = contextWindow - reserveTokens中的预留量 |
适用前提:navigateTree的summarize选项开启(对应 UI 的 enabled 语义)且当前会话已选择模型;若走session_before_tree扩展钩子提供了摘要,则内置 LLM 路径不执行。
七、摘要如何回到上下文
落盘的branch_summary条目不是终点,而是新的上下文来源:
- messages.ts 的
createBranchSummaryMessage()把branch_summary条目转回消息,渲染模板是 branch-summary-context.md:
Branch-return summary: <summary> {{summary}} </summary>- 主 compaction 流程同样处理这类条目(compaction.ts 把
branch_summary条目转成BranchSummaryMessage),所以分支摘要在后续整段压缩时会作为高价值上下文参与; - 下一次再发生分支跳转时,这些摘要条目本身又会进入
collectEntriesForBranchSummary的收集范围,其details中的文件清单被累计(prepareBranchEntries第一遍遍历),实现跨多次跳转的文件追踪传递。
八、测试验证
- branch-summarization.test.ts:用 mock 模型走通
generateBranchSummary全链路,验证 useless 工具结果被丢弃、有信息量的工具结果保留,并用prepareBranchEntries在不同 token 预算(100 / 700)下验证"新→旧"填充与截断行为; - compaction-telemetry.test.ts:验证
generateBranchSummary的 OTEL span 确实打上pi.gen_ai.oneshot.kind = "branch_summary"标签。
九、实践要点小结
- 格式契约是硬性的:
## Goal/## Constraints & Preferences/## Progress(Done / In Progress / Blocked)/## Key Decisions/## Next Steps五个区块缺一不可;空约束要写(none),进度用[x]/[ ]勾选框,决策用"加粗决策: 理由"格式。 - 保真优先:文件路径、函数名、错误消息必须逐字保留——这是摘要能支撑"恢复后继续工作"的前提。
- 预算意识:摘要输入预算是
contextWindow - reserveTokens(默认预留 16384),输出上限maxTokens: 2048;"各节保持简洁"的提示词要求与这套预算机制是配套的。 - 可替换性:
customInstructions可以整体替换默认提示词,扩展可以通过session_before_tree钩子完全接管摘要生成;宿主也可以通过completeImpl把这次请求纳入自己的并发限流。 - 落点在新分支:摘要条目挂在导航目标位置而非被放弃的旧分支上,确保回到目标位置后的上下文立刻携带"另一条分支探索过什么"的信息。
对希望接入或定制该机制的开发者,建议从 branch-summarization.ts 的GenerateBranchSummaryOptions接口读起,再到 agent-session.ts 的navigateTree触发路径,最后对照 branch-summarization.test.ts 验证边界行为。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考