16 上下文管理实现实战(可直接复刻)
这篇只讲“怎么做出来”,不讲空概念。
你要实现的能力
- 历史消息不会无限增长把模型窗口撑爆。
tool_call和tool_result永远成对,避免模型 400。- 发生超窗时,系统能自动压缩和恢复,不直接崩。
- 压缩中超时时,仍能返回可用快照。
对应源码入口(先看这几个文件)
src/agents/pi-embedded-runner/run.tssrc/agents/pi-embedded-runner/run/attempt.tssrc/agents/pi-embedded-runner/history.tssrc/agents/context-window-guard.tssrc/agents/session-transcript-repair.tssrc/agents/pi-embedded-runner/run/compaction-timeout.tssrc/agents/pi-embedded-subscribe.ts
实际执行链路(按真实顺序)
runEmbeddedPiAgent(...)先做窗口守卫。
它会调用resolveContextWindowInfo(...)和evaluateContextWindowGuard(...),如果窗口低于硬下限直接阻断,不进入 attempt。runEmbeddedAttempt(...)进入会话前做历史卫生。
关键顺序是:
sanitizeSessionHistory(...)validateGeminiTurns(...) / validateAnthropicTurns(...)limitHistoryTurns(...)sanitizeToolUseResultPairing(...)activeSession.agent.replaceMessages(...)
prompt 执行后等待压缩重试。
subscribeEmbeddedPiSession(...)内部用compactionInFlight + pendingCompactionRetry + compactionRetryPromise协调压缩生命周期,attempt 侧调用waitForCompactionRetry()等待稳定。发生上下文溢出时自动恢复。
run.ts会先走compactEmbeddedPiSessionDirect(...)(最多 3 次),还不行再走truncateOversizedToolResultsInSession(...)。压缩期间超时时选快照。
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 条验收
- 同一会话跑 100 轮后,
messages长度仍受控。 - 任意 assistant
toolCall后都能找到匹配toolResult。 - 手工注入一个超长
toolResult,系统会触发压缩或截断恢复。 - 压缩开始/结束事件可观测(日志或 event bus)。
- 压缩中超时仍能返回快照,不是空响应。
- 模型窗口过小会在 run 前被阻断,不会进入昂贵调用。
开发者最常犯错(直接避坑)
- 先截断再修复配对,导致孤儿
toolResult。 - 把
cacheRead/cacheWrite直接累计成“当前上下文”,误判成 200k。 - 压缩超时后继续用当前快照,导致返回脏状态。
- 不做窗口硬下限,线上才发现某模型根本不可用。