Skip to content

08 上下文工程原理(为什么系统不会越聊越炸)

这篇只讲“上下文怎么做才稳定”。

核心源码入口

  • src/agents/context-window-guard.ts
  • src/agents/pi-embedded-runner/history.ts
  • src/agents/pi-embedded-runner/google.ts
  • src/agents/session-transcript-repair.ts
  • src/agents/pi-embedded-subscribe.ts
  • src/agents/pi-embedded-runner/run/compaction-timeout.ts
  • src/agents/pi-embedded-runner/run.ts

先记一个总公式

上下文稳定 = 窗口预检 + 历史卫生 + 配对修复 + 压缩重试 + 超时快照兜底

少任何一个环节,长会话就会出问题。

模块一 窗口预检(运行前拦截)

context-window-guard.ts 定义了两条红线:

  1. CONTEXT_WINDOW_HARD_MIN_TOKENS = 16000
  2. CONTEXT_WINDOW_WARN_BELOW_TOKENS = 32000

实际流程:

  1. resolveContextWindowInfo(...) 决定窗口来源:
  • modelsConfig
  • model
  • agentContextTokens(配置硬上限)
  • default
  1. evaluateContextWindowGuard(...) 给出 shouldWarn/shouldBlock
  2. run.ts 中,如果 shouldBlock=true,直接抛错,不进入昂贵调用。

这一步是“防止明知会炸还去调用模型”。

模块二 历史截断(按会话类型动态限额)

history.ts 不是固定截断,而是“按会话类型解析”:

  1. getHistoryLimitFromSessionKey(...) 从 sessionKey 识别:
  • dm/direct:走 dmHistoryLimitdms.<user>.historyLimit
  • channel/group:走 historyLimit
  1. limitHistoryTurns(...) 只保留最近 N 个 user turn 及其关联上下文。

这比“按消息条数”更靠谱,因为 turn 更接近真实对话语义。

模块三 转录卫生(防 provider 400)

run/attempt.ts 的顺序非常关键:

  1. sanitizeSessionHistory(...)
  2. validateGeminiTurns(...) / validateAnthropicTurns(...)
  3. limitHistoryTurns(...)
  4. sanitizeToolUseResultPairing(...)

为什么第 4 步要在截断后再跑一次:

limitHistoryTurns(...) 可能把某个 tool_use 删掉,却留下 tool_result,变成孤儿消息,Anthropic 类接口会直接 400。

session-transcript-repair.ts 会做三件事:

  1. 移动错位 toolResult 到对应 assistant toolCall 后面
  2. 删除重复 toolResult
  3. 对缺失的结果插入 synthetic error result(防断链)

模块四 压缩重试状态机

subscribeEmbeddedPiSession(...) 内部维护:

  1. compactionInFlight
  2. pendingCompactionRetry
  3. compactionRetryPromise

waitForCompactionRetry() 的语义是:

  1. 如果还在压缩或待重试,等待 promise
  2. 如果已 unsubscribe,抛 AbortError
  3. 否则立即返回

这保证 run 不会在压缩还没结束时“误判完成”。

模块五 压缩超时快照选择

run/compaction-timeout.ts

  1. shouldFlagCompactionTimeout(...):只有“超时 + 压缩相关状态”为真才算 compaction timeout
  2. selectCompactionTimeoutSnapshot(...):优先 pre-compaction 快照;没有就回当前快照

这能避免拿到“半压缩”状态的脏消息。

模块六 上下文溢出的恢复路径

run.ts 在检测到 overflow 时按顺序恢复:

  1. 尝试 compactEmbeddedPiSessionDirect(...)
  2. 若仍失败,尝试 truncateOversizedToolResultsInSession(...)
  3. 超过上限后返回可读错误,提示用户 reset 或换大窗口模型

这是实战里非常关键的“降损链路”。

最小复刻骨架

ts
async function prepare(messages: AgentMessage[], sessionKey: string, cfg: Config) {
  const cleaned = await sanitizeSessionHistory({ messages, policy: resolvePolicy() });
  const validated = validateProviderTurns(cleaned);
  const limited = limitHistoryTurns(validated, getHistoryLimitFromSessionKey(sessionKey, cfg));
  return sanitizeToolUseResultPairing(limited);
}

async function runTurnWithCompactionWait(session: Session, prompt: string) {
  const sub = subscribe(session);
  let snapshotBefore = session.messages.slice();
  try {
    await session.prompt(prompt);
    await sub.waitForCompactionRetry();
    return session.messages.slice();
  } catch (err) {
    return selectCompactionTimeoutSnapshot({
      timedOutDuringCompaction: isCompactionTimeout(err),
      preCompactionSnapshot: snapshotBefore,
      preCompactionSessionId: session.sessionId,
      currentSnapshot: session.messages.slice(),
      currentSessionId: session.sessionId,
    }).messagesSnapshot;
  } finally {
    sub.unsubscribe();
  }
}

自检清单

  1. 长会话 100 轮后是否仍能稳定回复。
  2. 是否不存在孤儿 toolResult
  3. 压缩期间超时是否仍能返回一致快照。
  4. 窗口过小模型是否在 run 前就被拦截。
  5. overflow 后是否按“压缩 -> 截断 -> 报错”顺序执行。

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