Skip to content

16 上下文管理实现实战(可直接复刻)

这篇只讲“怎么做出来”,不讲空概念。

你要实现的能力

  1. 历史消息不会无限增长把模型窗口撑爆。
  2. tool_calltool_result 永远成对,避免模型 400。
  3. 发生超窗时,系统能自动压缩和恢复,不直接崩。
  4. 压缩中超时时,仍能返回可用快照。

对应源码入口(先看这几个文件)

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

实际执行链路(按真实顺序)

  1. runEmbeddedPiAgent(...) 先做窗口守卫。
    它会调用 resolveContextWindowInfo(...)evaluateContextWindowGuard(...),如果窗口低于硬下限直接阻断,不进入 attempt。

  2. runEmbeddedAttempt(...) 进入会话前做历史卫生。
    关键顺序是:

  • sanitizeSessionHistory(...)
  • validateGeminiTurns(...) / validateAnthropicTurns(...)
  • limitHistoryTurns(...)
  • sanitizeToolUseResultPairing(...)
  • activeSession.agent.replaceMessages(...)
  1. prompt 执行后等待压缩重试。
    subscribeEmbeddedPiSession(...) 内部用 compactionInFlight + pendingCompactionRetry + compactionRetryPromise 协调压缩生命周期,attempt 侧调用 waitForCompactionRetry() 等待稳定。

  2. 发生上下文溢出时自动恢复。
    run.ts 会先走 compactEmbeddedPiSessionDirect(...)(最多 3 次),还不行再走 truncateOversizedToolResultsInSession(...)

  3. 压缩期间超时时选快照。
    selectCompactionTimeoutSnapshot(...) 优先回退到压缩前快照,减少“半压缩状态”的脏输出。

关键数据结构(照着抄)

ts
// src/agents/context-window-guard.ts
type ContextWindowInfo = {
  tokens: number;
  source: "model" | "modelsConfig" | "agentContextTokens" | "default";
};
ts
// src/agents/session-transcript-repair.ts
type ToolUseRepairReport = {
  messages: AgentMessage[];
  added: ToolResultMessage[];
  droppedDuplicateCount: number;
  droppedOrphanCount: number;
  moved: boolean;
};
ts
// src/agents/pi-embedded-subscribe.ts(核心状态)
type CompactionState = {
  compactionInFlight: boolean;
  pendingCompactionRetry: number;
  compactionRetryPromise: Promise<void> | null;
};

最小复刻版本(MVP 代码骨架)

ts
async function prepareMessages(raw: AgentMessage[], sessionKey: string, cfg: Config) {
  const cleaned = sanitizeSessionHistory(raw);
  const validated = validateProviderTurns(cleaned); // 按 provider 选 gemini/anthropic 校验
  const limited = limitHistoryTurns(validated, getHistoryLimitFromSessionKey(sessionKey, cfg));
  const repaired = sanitizeToolUseResultPairing(limited);
  return repaired;
}

async function runWithContextGuard(input: {
  provider: string;
  modelId: string;
  modelCtx?: number;
  cfg: Config;
  prompt: string;
}) {
  const win = resolveContextWindowInfo({
    cfg: input.cfg,
    provider: input.provider,
    modelId: input.modelId,
    modelContextWindow: input.modelCtx,
    defaultTokens: 32000,
  });
  const guard = evaluateContextWindowGuard({ info: win, hardMinTokens: 16000 });
  if (guard.shouldBlock) {
    throw new Error(`context window too small: ${guard.tokens}`);
  }

  for (let i = 0; i < 3; i++) {
    try {
      return await runSingleAttempt(input.prompt);
    } catch (e) {
      if (!isContextOverflow(e)) throw e;
      const compacted = await compactSessionOnce();
      if (!compacted) break;
    }
  }

  const truncated = await truncateOversizedToolResults();
  if (truncated) return await runSingleAttempt(input.prompt);
  throw new Error("context overflow unresolved");
}

你必须做的 6 条验收

  1. 同一会话跑 100 轮后,messages 长度仍受控。
  2. 任意 assistant toolCall 后都能找到匹配 toolResult
  3. 手工注入一个超长 toolResult,系统会触发压缩或截断恢复。
  4. 压缩开始/结束事件可观测(日志或 event bus)。
  5. 压缩中超时仍能返回快照,不是空响应。
  6. 模型窗口过小会在 run 前被阻断,不会进入昂贵调用。

开发者最常犯错(直接避坑)

  1. 先截断再修复配对,导致孤儿 toolResult
  2. cacheRead/cacheWrite 直接累计成“当前上下文”,误判成 200k。
  3. 压缩超时后继续用当前快照,导致返回脏状态。
  4. 不做窗口硬下限,线上才发现某模型根本不可用。

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