oh-my-pi 回合前缀摘要机制解析compaction-turn-prefix 提示词的源码级解读【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pioh-my-pi 的上下文压缩context compaction机制在长期会话管理中扮演核心角色。当压缩切割点恰好落在某一轮会话turn的中间时会触发一种特殊的摘要流程——回合前缀摘要turn prefix summarization。本文以packages/agent/src/compaction/prompts/compaction-turn-prefix.md这份提示词文档为主线结合packages/agent/src/compaction/compaction.ts的源码实现完整还原该机制的设计意图、触发条件、提示词结构与输出约束帮助读者理解 oh-my-pi 如何在信息密度与上下文保留之间取得平衡。一、为什么需要回合前缀摘要在解释这份提示词之前必须先理解它存在的场景。oh-my-pi 的压缩器在上下文即将耗尽时触发压缩它会从会话记录中寻找一个切割点cut point将较旧的记录摘要为一份结构化总结同时保留最近的原始消息。问题在于切割点未必恰好落在回合边界上。一个回合turn通常以一条用户消息为起点后续跟随若干条助手消息与工具调用结果。如果上下文窗口的剩余空间只够保留一个回合的后半段那么压缩器就必须拦腰截断这个回合——前半段被丢弃或摘要后半段被完整保留。此时就出现了信息断层的风险被保留的后半段suffix可能包含工具调用结果、代码修改、命令输出但读者新的 LLM 上下文却看不到这些操作是为什么发生的。回合前缀摘要正是为弥合这个断层而设计的它对被截断的回合前半段prefix生成一份结构化摘要附着在保留的后半段之前确保后续模型能理解保留内容的来龙去脉。在源码中这一场景被显式建模为CutPointResult接口// packages/agent/src/compaction/compaction.ts export interface CutPointResult { /** Index of first entry to keep */ firstKeptEntryIndex: number; /** Index of user message that starts the turn being split, or -1 if not splitting */ turnStartIndex: number; /** Whether this cut splits a turn (cut point is not a user message) */ isSplitTurn: boolean; }当isSplitTurn true时prepareCompaction会额外收集turnPrefixMessages——即从turnStartIndex该回合起始的用户消息到firstKeptEntryIndex第一个被保留的条目之间的所有消息这些消息正是需要被前缀摘要处理的对象。二、compaction-turn-prefix 提示词全文解读关联文档packages/agent/src/compaction/prompts/compaction-turn-prefix.md是回合前缀摘要的提示词模板全文如下Turn prefix too large; recent-work suffix retained. MUST summarize prefix for retained suffix: ## Original Request [What did the user ask for in this turn?] ## Early Progress - [Key decisions and work done in the prefix] ## Context for Suffix - [Information needed to understand the retained recent work] MUST output only the structured summary; NEVER extra text. MUST concise. MUST preserve exact file paths, function names, error messages, relevant tool outputs, and command results if present. MUST focus on information needed to understand the retained suffix.这份提示词结构清晰可以拆解为四个层次1. 场景声明第一行Turn prefix too large; recent-work suffix retained.这一行直接向模型宣告当前的处理背景被截断的回合前缀过大无法直接保留而近期的工作内容suffix仍然保留在上下文中。这告诉模型——你的任务是补充信息而不是重复已经存在的内容。2. 结构约束三个固定小节提示词强制输出三个小节## Original Request用户在本回合中最初的请求是什么。这是后缀理解的锚点——所有后续的工具调用和修改都服务于这个原始诉求。## Early Progress前缀阶段完成的关键决策与工作。使用无序号列表-组织聚焦决策和工作成果而非流水账。## Context for Suffix理解被保留的近期工作所需的信息。这是与前缀摘要见下文对比最大的不同点——它的输出目标不是一份自包含的交接文档而是服务于后缀的上下文补充。3. 输出纪律MUST 指令提示词连续使用强制语气MUST output only the structured summary; NEVER extra text.—— 只输出结构化摘要绝不附带任何额外文本。这保证了模型输出可以被直接拼接到压缩结果中无需后处理清洗。MUST concise.—— 必须简洁。注意这是与完整历史摘要compaction-summary.md的显著差异前缀摘要是配角不应喧宾夺主。MUST preserve exact file paths, function names, error messages, relevant tool outputs, and command results if present.—— 必须保留精确的文件路径、函数名、错误消息、相关工具输出和命令结果。这是可恢复性的关键后续模型如果需要在保留的后缀中继续工作必须能精确定位此前操作涉及的实体。MUST focus on information needed to understand the retained suffix.—— 必须聚焦于理解被保留后缀所需的信息。这是整份提示词的核心判断准则不是所有信息都值得保留只有后缀理解所必需的信息才值得写入。4. 数据保真原则最后两行共同构成一条完整的数据保真原则简洁concise与精确exact看似矛盾实际上指向同一个目标——用最少的 token 传递最不可丢失的信息。文件路径、函数名、错误消息属于不可再生信息一旦丢失无法从上下文中重新推导而修饰性描述则属于可压缩信息应尽量省略。三、源码中的调用链路generateTurnPrefixSummary提示词在源码中的消费点是generateTurnPrefixSummary函数packages/agent/src/compaction/compaction.tsL1879-L1932。该函数是回合前缀摘要的唯一入口其实现细节完整呈现了这份提示词的设计意图。// packages/agent/src/compaction/compaction.ts export async function generateTurnPrefixSummary( messages: AgentMessage[], model: Model, reserveTokens: number, apiKey: ApiKey, signal?: AbortSignal, options?: SummaryOptions, ): Promisestring { const maxTokens Math.min(Math.floor(0.5 * reserveTokens), MAX_SUMMARY_TOKENS); // Smaller budget for turn prefix const llmMessages (options?.convertToLlm ?? defaultConvertToLlm)(messages); const conversationText serializeConversationForSummary(llmMessages, preferredDialect(model.id)); const promptText conversation\n${conversationText}\n/conversation\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}; // ... const response await instrumentedCompleteSimple( model, { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages }, { maxTokens, signal, apiKey, reasoning: resolveCompactionEffort(model, options?.thinkingLevel), // ... }, { telemetry: options?.telemetry, oneshotKind: compaction_turn_prefix, completeImpl: options?.completeImpl, retry: summaryOneshotRetry(options), }, ); if (response.stopReason error) { throw createSummarizationError(Turn prefix summarization failed, response); } return response.content .filter((c): c is { type: text; text: string } c.type text) .map(c c.text) .join(\n); }从实现中可以看到几个值得注意的设计决策1. 提示词模板的渲染时机。模块加载时即通过prompt.render(compactionTurnPrefixPrompt)渲染为常量TURN_PREFIX_SUMMARIZATION_PROMPTL1442并与其他提示词如compactionSummaryPrompt并列导入。注意导入方式使用了 Vite 风格的文本导入import compactionTurnPrefixPrompt from ./prompts/compaction-turn-prefix.md with { type: text };2. 输入组装方式。待摘要消息首先通过convertToLlm默认defaultConvertToLlm转换为 LLM 消息再经serializeConversationForSummary序列化为纯文本最后用 XML 风格标签包裹后与提示词拼接conversation {conversationText} /conversation {TURN_PREFIX_SUMMARIZATION_PROMPT}这种先序列化再拼接的方式与generateSummary完全一致——其注释说明了原因Serialize conversation to text so model doesnt try to continue it序列化为文本避免模型试图续写对话。3. 更小的 token 预算。这是前缀摘要与完整历史摘要最关键的差异。generateSummary的预算为Math.min(Math.floor(0.8 * reserveTokens), MAX_SUMMARY_TOKENS)L864而generateTurnPrefixSummary只有一半const maxTokens Math.min(Math.floor(0.5 * reserveTokens), MAX_SUMMARY_TOKENS); // Smaller budget for turn prefix源码注释明确标注了Smaller budget for turn prefix。结合DEFAULT_RESERVE_TOKENS 16384与MAX_SUMMARY_TOKENS DEFAULT_RESERVE_TOKENS可以算出默认配置下完整历史摘要最多可占用约 13107 tokensfloor(0.8 × 16384)而回合前缀摘要最多约 8192 tokensfloor(0.5 × 16384)。这与提示词中MUST concise的要求形成了机制层面的呼应——提示词约束模型行为token 预算约束输出上限双重保险确保前缀摘要不会膨胀。4. 遥测与错误处理。调用通过instrumentedCompleteSimple执行遥测分类为oneshotKind: compaction_turn_prefix这意味着前缀摘要的每次调用都会作为独立的一次性请求被记录和度量。若模型返回stopReason error则抛出createSummarizationError(Turn prefix summarization failed, response)与完整历史摘要的错误处理路径保持一致。四、触发条件findCutPoint 与 isSplitTurn 的判定前缀摘要的触发完全由切割点判定逻辑驱动。findCutPointL505-L570 附近从最新的会话条目开始向后累加消息的估算 token 数直到达到keepRecentTokens预算// Walk backwards from newest, accumulating estimated message sizes let accumulatedTokens 0; let cutIndex cutPoints[0]; // Default: keep from first message (not header) for (let i endIndex - 1; i startIndex; i--) { const entry entries[i]; if (entry.type ! message) continue; // Estimate this messages size const messageTokens tokenizer.countMessage(entry.message); accumulatedTokens messageTokens; // Check if weve exceeded the budget if (accumulatedTokens keepRecentTokens) { // Find the closest valid cut point at or after this entry for (let c 0; c cutPoints.length; c) { if (cutPoints[c] i) { cutIndex cutPoints[c]; break; } } break; } }随后通过findTurnStartIndex判断切割点是否处于回合中间const turnStartIndex isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex); return { firstKeptEntryIndex: cutIndex, turnStartIndex, isSplitTurn: !isUserMessage turnStartIndex ! -1, };只有当切割点不是用户消息且能找到该回合的起始用户消息时isSplitTurn才为true。prepareCompaction据此分流L1376-L1392const historyEnd cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex; // Messages to summarize (will be discarded after summary) const messagesToSummarize: AgentMessage[] []; for (let i boundaryStart; i historyEnd; i) { /* ... */ } // Messages for turn prefix summary (if splitting a turn) const turnPrefixMessages: AgentMessage[] []; if (cutPoint.isSplitTurn) { for (let i cutPoint.turnStartIndex; i cutPoint.firstKeptEntryIndex; i) { const msg getMessageFromEntry(pathEntries[i]); if (msg) turnPrefixMessages.push(msg); } }这里historyEnd被设为turnStartIndex意味着被截断回合的前半段全部进入messagesToSummarize会被丢弃并纳入历史摘要而turnStartIndex到firstKeptEntryIndex之间的是turnPrefixMessages单独交给前缀摘要。一个被截断的回合因此被拆成两条摘要路径回合起始之前的完整历史 → 完整历史摘要generateSummary被截断回合的前缀 → 回合前缀摘要generateTurnPrefixSummary。两条路径在compact函数中并行执行最后合并L1803-L1821} else if (isSplitTurn turnPrefixMessages.length 0) { // Generate both summaries in parallel const [historyResult, turnPrefixResult] await Promise.all([ messagesToSummarize.length 0 || previousSummaryForCompaction ? generateSummary(/* ... */) : Promise.resolve(No prior history.), generateTurnPrefixSummary(turnPrefixMessages, model, reserveTokens, apiKey, signal, summaryOptions), ]); // Merge into single summary summary ${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}; }合并时使用**Turn Context (split turn):**作为分隔标记将前缀摘要与历史摘要拼接到同一个summary字段中。Promise.all表明两条摘要路径互不依赖可以并发执行以减少压缩延迟。五、与完整历史摘要提示词的对比将compaction-turn-prefix.md与同目录下的compaction-summary.md对比可以更清晰地理解各自的定位。完整历史摘要的提示词packages/agent/src/compaction/prompts/compaction-summary.md要求输出## Goal ## Constraints Preferences ## Progress (### Done / ### In Progress / ### Blocked) ## Key Decisions ## Next Steps ## Critical Context ## Additional Notes两者存在显著差异维度compaction-summary.md完整历史摘要compaction-turn-prefix.md回合前缀摘要输出目标另一 LLM 接手整个任务的交接文档补充被保留后缀缺失的前置信息结构7 个小节面向完整任务生命周期3 个小节面向回合-后缀衔接预算floor(0.8 × reserveTokens)floor(0.5 × reserveTokens)关键指令若会话以未回答问题结束必须原样保留该问题聚焦理解后缀所需信息数据保真保留文件路径、函数名、错误消息、仓库状态保留文件路径、函数名、错误消息、工具输出、命令结果两者共同遵循的原则是结构强制 数据保真 禁止额外文本。compaction-summary.md同样要求 You MUST output only the structured summary; you NEVER include extra text. 并同样强制 preserve exact file paths, function names, error messages, and relevant tool outputs or command results。可以推断这份提示词家族共享同一套摘要生成基础设施SUMMARIZATION_SYSTEM_PROMPT、serializeConversationForSummary、instrumentedCompleteSimple等只是以不同的模板注入不同的结构约束。六、配置参数与调优参考回合前缀摘要的行为受CompactionSettings中若干参数间接控制定义于packages/agent/src/compaction/compaction.tsL173-L194export interface CompactionSettings { enabled: boolean; strategy?: context-full | handoff | shake | snapcompact | off; thresholdPercent?: number; thresholdTokens?: number; midTurnEnabled?: boolean; reserveTokens?: number; keepRecentTokens: number; autoContinue?: boolean; remoteEnabled?: boolean; remoteEndpoint?: string; remoteStreamingV2Enabled?: boolean; v2RetainedMessageBudget?: number; }与本文主题直接相关的参数及默认值如下keepRecentTokens默认 20000决定压缩后保留的最近消息 token 预算。findCutPoint以此为基准向后累计切割点越靠后被截断回合出现的概率越低该值越小压缩越激进回合前缀摘要被触发的概率越高。reserveTokens默认DEFAULT_RESERVE_TOKENS 16384为压缩后的下一次提示与响应预留的 token 数。它同时决定完整历史摘要0.8 倍与回合前缀摘要0.5 倍的输出上限。midTurnEnabled默认 true控制是否允许在回合中间进行切割。从命名推断若设为false压缩器应避免产生isSplitTurn切割点从而根本不会触发回合前缀摘要路径。thresholdPercent/thresholdTokens默认 -1触发压缩的上下文占用阈值间接影响切割点的出现时机。需要说明的是当reserveTokens未显式设置时resolveBudgetReserveTokens会按上下文窗口的 15% 比例计算保留值Math.max(Math.floor(contextWindow * 0.15), settings.reserveTokens ?? DEFAULT_RESERVE_TOKENS)L314因此 100 万 token 的大窗口模型会获得更大的摘要预算但输出仍受MAX_SUMMARY_TOKENS 16384的绝对上限约束——正如源码注释所言这是为了避免窗口越大、模型越倾向于复制而非压缩的退化。七、总结一份提示词背后的压缩设计哲学compaction-turn-prefix.md虽然只有 17 行却是 oh-my-pi 上下文压缩体系中回合级信息保真这一设计诉求的浓缩体现。回顾全文可以提炼出四个核心设计原则场景化提示提示词第一行即声明Turn prefix too large; recent-work suffix retained让模型明确自己的角色是补全者而非复述者结构即协议Original Request/Early Progress/Context for Suffix三个固定小节不仅是输出格式更是下游合并逻辑**Turn Context (split turn):**标记依赖的稳定契约预算双重约束提示词的MUST concise约束模型行为floor(0.5 × reserveTokens)的 token 上限约束输出规模防止前缀摘要喧宾夺主数据保真优先文件路径、函数名、错误消息、工具输出、命令结果等不可再生信息被强制保留确保被截断回合的后缀在恢复时仍可被精确理解与继续执行。对于希望在长期编码会话中保持上下文物有所值的开发者而言理解这套机制有助于回答一个实际问题当上下文窗口即将耗尽时oh-my-pi 并不会简单粗暴地丢弃旧消息而是通过历史摘要 回合前缀摘要的双轨压缩尽量让每一份被丢弃的上下文都转化为后续模型可用的结构化信息。相关的提示词模板全部集中在 packages/agent/src/compaction/prompts 目录下核心实现位于 packages/agent/src/compaction/compaction.ts感兴趣的读者可以直接深入源码继续探索。【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考