news 2026/9/10 1:34:32

oh-my-pi 回合前缀摘要机制解析:compaction-turn-prefix 提示词的源码级解读

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
oh-my-pi 回合前缀摘要机制解析:compaction-turn-prefix 提示词的源码级解读

oh-my-pi 回合前缀摘要机制解析:compaction-turn-prefix 提示词的源码级解读

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

oh-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, ): Promise<string> { 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_PROMPT(L1442),并与其他提示词(如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 doesn't 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 = 16384MAX_SUMMARY_TOKENS = DEFAULT_RESERVE_TOKENS,可以算出:默认配置下完整历史摘要最多可占用约 13107 tokens(floor(0.8 × 16384)),而回合前缀摘要最多约 8192 tokens(floor(0.5 × 16384))。这与提示词中MUST concise的要求形成了机制层面的呼应——提示词约束模型行为,token 预算约束输出上限,双重保险确保前缀摘要不会膨胀。

4. 遥测与错误处理。调用通过instrumentedCompleteSimple执行,遥测分类为oneshotKind: "compaction_turn_prefix",这意味着前缀摘要的每次调用都会作为独立的一次性请求被记录和度量。若模型返回stopReason === "error",则抛出createSummarizationError("Turn prefix summarization failed", response),与完整历史摘要的错误处理路径保持一致。

四、触发条件:findCutPoint 与 isSplitTurn 的判定

前缀摘要的触发完全由切割点判定逻辑驱动。findCutPoint(L505-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 message's size const messageTokens = tokenizer.countMessage(entry.message); accumulatedTokens += messageTokens; // Check if we've 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才为trueprepareCompaction据此分流(L1376-L1392):

const 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(会被丢弃并纳入历史摘要),而turnStartIndexfirstKeptEntryIndex之间的是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_PROMPTserializeConversationForSummaryinstrumentedCompleteSimple等),只是以不同的模板注入不同的结构约束。

六、配置参数与调优参考

回合前缀摘要的行为受CompactionSettings中若干参数间接控制,定义于packages/agent/src/compaction/compaction.tsL173-L194:

export 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 上下文压缩体系中"回合级信息保真"这一设计诉求的浓缩体现。回顾全文,可以提炼出四个核心设计原则:

  1. 场景化提示:提示词第一行即声明"Turn prefix too large; recent-work suffix retained",让模型明确自己的角色是补全者而非复述者;
  2. 结构即协议Original Request/Early Progress/Context for Suffix三个固定小节不仅是输出格式,更是下游合并逻辑(**Turn Context (split turn):**标记)依赖的稳定契约;
  3. 预算双重约束:提示词的MUST concise约束模型行为,floor(0.5 × reserveTokens)的 token 上限约束输出规模,防止前缀摘要喧宾夺主;
  4. 数据保真优先:文件路径、函数名、错误消息、工具输出、命令结果等不可再生信息被强制保留,确保被截断回合的后缀在恢复时仍可被精确理解与继续执行。

对于希望在长期编码会话中保持上下文物有所值的开发者而言,理解这套机制有助于回答一个实际问题:当上下文窗口即将耗尽时,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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 1:32:50

两周自学的理论全废:特征存储让我重新报了AI入门课

两周自学的理论全废:特征存储让我重新报了AI入门课 我在后端写了四年 Java,今年年初决定转 AI,照着网上最火的路线图刷了两周视频和教材:吴恩达的机器学习课、西瓜书前五章、PyTorch 官方教程。那两周我每天上下班地铁都挂着耳机,笔记记了满满一个 Notion。周末拿出一个信用卡…

作者头像 李华
网站建设 2026/9/10 1:32:40

Sourcetrail:半小时摸清一个陌生代码库的依赖结构

Sourcetrail&#xff1a;半小时摸清一个陌生代码库的依赖结构 【免费下载链接】Sourcetrail Sourcetrail - free and open-source interactive source explorer 项目地址: https://gitcode.com/GitHub_Trending/so/Sourcetrail 接手一个陌生的代码库&#xff0c;最先卡住…

作者头像 李华
网站建设 2026/9/10 1:30:48

图片无损压缩实战:从4MB到400KB的免费工具与参数详解

做图这行干久了&#xff0c;你会发现一个特别魔幻的现实&#xff1a;拍出来一张5MB的照片&#xff0c;传到网页上显示出来大概也就占几百KB的屏&#xff0c;剩下的全在暗处烧你的流量和服务器带宽。尤其是做电商、做新媒体、搞个人博客的朋友&#xff0c;图片体积控制不好&…

作者头像 李华
网站建设 2026/9/10 1:29:34

微信小程序阅读网站管理系统全栈开发实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华