Skip to content

26 函数级剖析:压缩与截断恢复

模块目标

拆解 context overflow 场景下的两级恢复策略:compaction(摘要压缩)+ tool result truncation(工具结果截断)。

核心文件

  • src/agents/pi-embedded-runner/compact.ts
  • src/agents/pi-embedded-runner/tool-result-truncation.ts
  • src/agents/session-tool-result-guard.ts
  • src/agents/compaction.ts

一、overflow 恢复的完整决策树

context overflow 触发


[Step 1] compactEmbeddedPiSessionDirect(...)
         最多 MAX_OVERFLOW_COMPACTION_ATTEMPTS 次

         ├─ compacted: true → continue(重试 prompt)

         └─ compacted: false(nothing to compact)


         [Step 2] sessionLikelyHasOversizedToolResults(...)
                  用 messagesSnapshot 快速检测(启发式)

                  ├─ false → 没有可截断的工具结果
                  │          → 给用户可读错误,退出

                  └─ true → [Step 3] truncateOversizedToolResultsInSession(...)
                             toolResultTruncationAttempted = true(只尝试一次)

                             ├─ truncated: true → 重试 prompt

                             └─ truncated: false → 给用户可读错误,退出

toolResultTruncationAttempted 标志保证截断只尝试一次,防无限重试。

二、truncation 核心常量(源码精确)

ts
// src/agents/pi-embedded-runner/tool-result-truncation.ts

/**
 * 单条工具结果文本块的硬性上限。
 * 即使 2M token 的最大上下文窗口,单条结果也不应超过 ~400K 字符(~100K tokens)。
 * 这是在不知道上下文窗口大小时的安全兜底。
 */
export const HARD_MAX_TOOL_RESULT_CHARS = 400_000;

/**
 * 截断时最少保留字符数。
 * 总保留第一段内容让模型理解结果的含义。
 */
const MIN_KEEP_CHARS = 2_000;

三、calculateMaxToolResultChars(动态上限计算)

ts
// 启发式:~4 字符 ≈ 1 token(保守估算)
export function calculateMaxToolResultChars(contextWindowTokens: number): number {
  const maxTokens = Math.floor(contextWindowTokens * MAX_TOOL_RESULT_CONTEXT_SHARE);
  const maxChars  = maxTokens * 4;
  return Math.min(maxChars, HARD_MAX_TOOL_RESULT_CHARS);
}

MAX_TOOL_RESULT_CONTEXT_SHARE ≈ 30%(由测试验证:128K tokens → calculateMaxToolResultChars 返回 > 100K chars)。

128K 上下文 → 约 128_000 * 0.3 * 4 = 153_600 chars(受 HARD_MAX 上限约束)。

四、截断操作的两个作用域

in-session(持久化截断)

truncateOversizedToolResultsInSession(params):

  1. 打开 SessionManager
  2. 遍历当前分支,找到所有超过 maxCharstoolResult 消息,记录索引。
  3. 无超大结果 → { truncated: false, truncatedCount: 0, reason: "no oversized tool results" }
  4. 有超大结果 → 从首个超大条目的父节点创建新分支
  5. 重新 appendMessage 每条消息,超大项替换为截断版本。
  6. 返回 { truncated: true, truncatedCount }
原分支: [msg0][msg1][msg2-oversized][msg3][msg4]
                        ↓ 从 msg1 创建新分支
新分支: [msg0][msg1][msg2-truncated][msg3][msg4]

这不是原地修改,而是"重放分支":保留历史,新分支不含超大内容。

in-memory(发送前拦截)

truncateOversizedToolResultsInMessages(messages, contextWindowTokens):

  • 不修改 session 文件,只处理内存中待发给 LLM 的消息数组。
  • 用作发送前的预防性守卫。

五、截断位置策略

ts
// 尽量在换行处切断(不要切到单词中间)
let cutPoint = blockBudget;
const lastNewline = textBlock.text.lastIndexOf("\n", blockBudget);
if (lastNewline > blockBudget * 0.8) {
  cutPoint = lastNewline;  // 最后 20% 范围内有换行,就用换行处
}

截断后追加后缀(持久化守卫 session-tool-result-guard.ts):

ts
const GUARD_TRUNCATION_SUFFIX =
  "\n\n⚠️ [Content truncated during persistence — original exceeded size limit. " +
  "Use offset/limit parameters or request specific sections for large content.]";

六、compaction 的完整运行环境

compactEmbeddedPiSessionDirect(...) 不是轻量操作,它完整构建运行环境:

  1. repairSessionFileIfNeeded — 先修复可能损坏的 session 文件
  2. loadWorkspaceSkillEntries — 技能加载
  3. resolveSkillsPromptForRun — 技能 prompt
  4. 构建 system prompt + tools(与正常 run 相同)
  5. SessionManager.open — 打开 session
  6. ensurePiCompactionReserveTokens — 确保保留 token 预算
  7. 触发 before_compaction Hook,执行摘要,触发 after_compaction Hook

之所以要完整构建,是因为压缩本身也是一次 LLM 调用,需要完整上下文。

七、sessionLikelyHasOversizedToolResults(启发式检测)

ts
// 这是启发式(heuristic),而不是精确检测
// 用于快速判断"值不值得尝试截断"
export function sessionLikelyHasOversizedToolResults(params: {
  messages: AgentMessage[];
  contextWindowTokens: number;
}): boolean {
  const maxChars = calculateMaxToolResultChars(contextWindowTokens);
  for (const msg of params.messages) {
    if (msg.role === "toolResult" && getToolResultTextLength(msg) > maxChars) {
      return true;
    }
  }
  return false;
}

传入的 messagesattempt.messagesSnapshot(快照),不需要重新读 session 文件。

八、自检清单

  1. compaction 失败后是否检查 sessionLikelyHasOversizedToolResults,而不是直接报错。
  2. toolResultTruncationAttempted 标志是否只允许截断一次。
  3. 截断是否用"重放分支"而不是原地修改(保留历史可追溯)。
  4. calculateMaxToolResultChars 结果是否受 HARD_MAX_TOOL_RESULT_CHARS = 400_000 上限约束。
  5. 截断后缀是否添加,让模型知道内容被裁剪(而不是无声截断)。
  6. MIN_KEEP_CHARS = 2_000 是否确保至少保留 2000 字符(让模型能理解工具结果)。
  7. compaction 是否构建了完整运行环境(包括 skills、tools、system prompt)。

九、开发避坑

  1. 不要把压缩失败直接当 fatal error,要先检查是否有超大工具结果。
  2. 截断函数有两个版本(in-session 和 in-memory),场景不同,不要混用。
  3. sessionLikelyHasOversizedToolResults 是启发式,不是精确判断,但足够快。
  4. compaction 是重量级操作(完整 LLM 调用),不应在普通流程里频繁触发。

用工程视角拆解 AI 智能体框架