Context Manager 新范式:Agent 的注意力操作系统

Context Manager 新范式:Agent 的注意力操作系统

模型这一轮应该看见什么?

那么这一章回答的是更底层的问题:

整个 Agent 怎样保存、投影、压缩、分支和恢复它的工作现场?

核心观点先放前面:

Agent Context Manager 要管理的远不止聊天历史。它更像一层注意力治理系统:把事件事实、当前状态、长期记忆、外部证据、工具环境和系统策略,编译成下一次行动所需的最小充分上下文。

换句话说,Context Manager 的工作不能停留在管理 messages[],也不能等到上下文满了再补一段"总结"。它更像 Agent 的 Attention Operating System:负责决定 Agent 此刻应该知道什么、为什么知道、来源是什么、是否可信、是否过期、是否应该暴露给模型、是否应该写入长期记忆,以及如何在有限预算下保持推理连续性。

一、从第一性原理定义 Context

很多 Agent demo 会把上下文理解成一个不断追加的 messages[]

text 复制代码
user message
-> assistant message
-> tool result
-> assistant message
-> tool result
-> ...

这个模型能跑最小 demo,但它撑不起一个成熟 Agent。

因为成熟 Agent 的上下文不只是聊天记录。它还包含用户目标、当前任务状态、历史决策、工具调用证据、文件和环境状态、长期记忆、压缩摘要、分支会话、预算控制、权限边界和验证结果。

messages[] 只是其中一层,可靠性也常常排不到最前面。

更稳定的定义应该是:

text 复制代码
Context_t = f(
  Intent,
  State,
  Evidence,
  Memory,
  Policy,
  Tools,
  Environment,
  Recent Interaction,
  Budget
)

也就是:

某一时刻的上下文,是 Agent 为了完成下一步判断、行动和表达所需的信息环境。

这带来一个重要转变:

text 复制代码
先别急着问:历史消息怎么塞进模型?
更关键的问题是:下一步行动需要哪些信息?

Context Manager 要持续回答这些问题:

text 复制代码
Agent 当前目标是什么?
当前任务完成到哪一步?
哪些事实已经被验证?
哪些只是猜测?
哪些工具结果是关键证据?
哪些用户约束不可丢?
哪些旧信息已经不相关?
哪些内容虽然旧,但未来仍必须保留?
哪些信息可以压缩?
哪些信息必须原样保留?
哪些信息可以进入模型?
哪些信息只能留在系统内部?

这个抽象比较耐久。即使未来模型上下文窗口越来越大,Agent 仍然需要处理注意力噪音、证据溯源、权限隔离、记忆污染、状态恢复、分支探索、成本预算和可解释性问题。

这一章的新口径可以先压成一句话:

Context Manager 如果只做 messages[] 拼接,很快会被长任务拖垮。更稳的形态,是一套"事件溯源 + 状态投影 + 上下文编译 + 可审计压缩"的运行时系统。

换句话说:

text 复制代码
Context Manager 的核心职责,是在任何时刻重建 Agent 做出下一步行动所需的最小充分上下文。保存对话只是其中一项输入来源。

我们仍然沿用前文的例子:一个 CLI Agent 正在修复测试失败。它读文件、跑测试、改代码、再验证。任务一长,context 管理就从"拼 prompt"变成了"管理运行时现场"。

二、先看旧模型为什么不够

最朴素的实现通常长这样:

ts 复制代码
const messages = [
  { role: "system", content: systemPrompt },
  { role: "user", content: userGoal },
];

while (true) {
  const response = await model.chat({ messages, tools });
  messages.push(response.message);

  if (response.toolCall) {
    const result = await runTool(response.toolCall);
    messages.push({
      role: "tool",
      content: result.text,
    });
    continue;
  }

  break;
}

这段代码最大的优点是容易理解。

它最大的缺点也是太容易理解了:它把所有东西都当成同一种消息。

在修测试的任务里,messages[] 很快会混进这些内容:

text 复制代码
用户目标
系统规则
项目规则
模型猜测
工具调用参数
测试日志
grep 输出
文件片段
旧文件内容
新文件 diff
权限拒绝
压缩摘要
长期记忆
验证结果

这些内容的性质完全不同。

有些是事实,有些是猜测。

有些是高优先级指令,有些是不可信工具输出。

有些已经过期,有些必须永远保留。

有些应该进入模型输入,有些只应该留给审计和回放。

如果系统只有一个 messages[],它就很难回答这些问题:

text 复制代码
哪条信息是事实源?
当前 state 从哪里来?
压缩摘要总结了哪些事件?
工具输出有没有被截断?
模型这一轮到底看见了什么?
这个分支和主线是什么关系?
恢复时应该重放副作用,还是只重建状态?

所以 messages[] 当然可以保留。

问题在于,它不能承担整个 context 系统的事实源。

三、新范式:Context 更像编译产物

错误范式是:

text 复制代码
messages 就是上下文。
summary 可以替换历史。
模型看到什么,系统就只保存什么。

这种设计早期快,但后期会遇到一堆问题:

text 复制代码
无法恢复。
无法审计。
无法回放。
无法分支。
无法解释。
无法知道 summary 丢了什么。
无法区分事实、推测和用户偏好。
无法做权限与隐私治理。

更稳的分层是这样:

text 复制代码
Raw Event Log
  append-only,尽量不可变,记录真实发生过什么
        ↓
State Projection
  从事件流投影出当前目标、计划、事实、决策、文件和工具状态
        ↓
Context Builder
  按 token budget、优先级、相关性、信任边界编译模型输入
        ↓
Model Request
  发给模型的最终上下文快照

这四层里,只有最后一层会真正发送给模型。

前面三层都属于 Harness 的运行时控制面。

可以用一张表固定边界:

回答的问题 是否事实源 是否可以压缩
Raw Event Log 实际发生过什么? 不应该被摘要覆盖
State Projection 当前现场是什么? 否,是投影 可以重建
Context Builder 这一轮模型该看什么? 否,是编译器 每轮重算
Model Request 模型实际收到什么? 否,是快照 可以保存引用

这就是新范式里最重要的一步:

LLM context 是运行时编译产物,系统事实源应该放在更底层。

更完整地说:

text 复制代码
Raw Events / Raw Messages / Tool Results / Artifacts 是事实源。
State / Summary / Memory / Trace 是语义投影。
ContextBundle / Prompt 是一次性编译产物。

可以画成:

所以,发给模型的上下文只是一次临时视图。它可以被压缩、裁剪、重排、脱敏、适配不同模型协议,但不能成为系统唯一事实来源。

一句话:

Context 更接近编译产物,别把它当数据库。

只要接受这件事,很多设计会自然清楚。

原始历史尽量不丢。

模型上下文可以丢、可以压缩、可以重建。

summary 是派生物,不能当源数据用。

推理链路保存可审计证据,不依赖隐藏 chain-of-thought。

会话是树,不只是线性数组。

Context budget 处理的是资源分配,简单截断解决不了这个问题。

四、Context Manager 的模块和平面

成熟一点的 Context Manager 不应该只有一个 buildMessages()

它至少会长成下面这种模块图:

也可以从五个平面看:

text 复制代码
Policy Plane
  permission / privacy / safety / budget

Semantic Plane
  state / memory / summary / trace

Evidence Plane
  events / messages / artifacts / tools

Compilation Plane
  retrieve / rank / compress / redact / fit

Execution Plane
  model calls / tool calls / validators

这些模块不用一开始都实现。

但它们是很好的边界清单。因为每一个模块都在回答一个不同问题。

EventStore 问:事实发生过什么?

StateProjector 问:现在我们处在什么任务现场?

ContextBuilder 问:这轮模型该看到哪些材料?

Compressor 问:哪些旧信息可以被摘要替代?摘要是否保留来源?

TraceManager 问:这次失败到底断在哪个因果节点?

如果这些问题都塞进一个 messages 数组里,后面就会很难改。

这部分可以直接抽成十二条长期稳定的设计原则:

最后一条尤其重要。不要把核心数据结构绑定到某个模型厂商的 message 格式、tool calling 格式、tokenizer 或 prompt 模板。更稳的是:

text 复制代码
Canonical Internal Model
  ↓ adapter
Provider-specific Payload

五、稳定的 Context Ontology

一个成熟 Agent 的上下文系统,至少应该区分这些对象:

text 复制代码
Session        工作容器
Event          发生过的事实
Message        用户、模型、工具之间的通信记录
State          当前任务状态投影
Stats          token、成本、延迟、压缩率等指标
Memory         可跨时间复用的知识
Artifact       大对象、文件、diff、日志、截图、工具输出
Trace          可解释的决策与行动链路
Policy         权限、安全、隐私、预算、工具边界
ContextBundle  一次模型调用的上下文编译结果

它们的职责不同,不能混在一个 messages[] 里。

Session:工作身份和生命周期

session 管的是这次 Agent 工作的身份、边界和分支关系。

最小字段可以先这样设计:

ts 复制代码
type AgentSession = {
  sessionId: string;
  rootSessionId?: string;
  parentSessionId?: string;
  branchId?: string;
  leafEventId?: string;

  status: "active" | "paused" | "completed" | "failed" | "archived";
  cwd?: string;
  repoRoot?: string;

  modelConfig: {
    provider: string;
    model: string;
    maxOutputTokens?: number;
  };

  contextConfig: {
    contextWindowTokens: number;
    reservedOutputTokens: number;
    maxRecentTokens: number;
    compressionPolicyId: string;
  };

  permissionConfig: {
    sandboxMode?: "read_only" | "workspace_write" | "danger_full_access";
    approvalPolicy?: "never" | "on_request" | "on_failure" | "always";
  };

  createdAt: string;
  updatedAt: string;
};

这里的重点不在字段数量。

重点是 session_id 不只代表"一段聊天"。它还挂住工作目录、模型配置、权限配置、context budget、branch 关系和恢复位置。

如果没有这些信息,所谓 resume 就只能恢复一段聊天,而不能恢复一个工作现场。

Event:事实源

event 是最容易被低估的对象。

成熟 Agent 不应该只保存 message log,而应该保存 append-only event log。

ts 复制代码
type AgentEvent = {
  eventId: string;
  sessionId: string;
  runId?: string;
  parentEventId?: string;
  seq: number;
  timestamp: string;

  type:
    | "SessionStarted"
    | "UserPromptSubmitted"
    | "ContextBuilt"
    | "ModelRequestStarted"
    | "ModelResponseReceived"
    | "ToolCallRequested"
    | "ToolCallStarted"
    | "ToolCallFinished"
    | "ToolCallFailed"
    | "ApprovalRequested"
    | "ApprovalGranted"
    | "ApprovalDenied"
    | "FileRead"
    | "FileWritten"
    | "DiffApplied"
    | "StateUpdated"
    | "CompactionStarted"
    | "CompactionCompleted"
    | "VerifierFinished";

  actor: "user" | "agent" | "model" | "tool" | "system" | "subagent";
  payload: Record<string, unknown>;

  causality: {
    messageId?: string;
    toolCallId?: string;
    artifactId?: string;
    compactionId?: string;
  };
};

为什么 event 比 message 更底层?

因为很多关键事实根本不会出现在 message 里。

上下文构建时选了哪些片段,也不会出现在 message 里。

哪次工具调用被权限拒绝,不一定是 message。

某个文件被修改、某个 diff 被应用、某次验证失败、某次压缩触发,这些都应该是事件。

如果它们不进 event log,后面做 replay、trace、eval、audit 都会失去事实基础。

Message:模型交互记录

message 仍然重要。

但它应该采用 typed message blocks,避免把 provider 原始格式当作内部数据结构一路透传。

ts 复制代码
type Message = {
  messageId: string;
  sessionId: string;
  runId?: string;
  role: "system" | "developer" | "user" | "assistant" | "tool" | "summary" | "custom";
  content: MessageBlock[];

  toolCall?: {
    toolCallId: string;
    toolName: string;
    args: unknown;
    status: "pending" | "running" | "success" | "error";
  };

  toolResult?: {
    toolCallId: string;
    outputRef?: string;
    outputPreview?: string;
    truncated: boolean;
  };

  provenance: {
    source: "user" | "model" | "tool" | "memory" | "summary" | "system";
    sourceRefs?: string[];
  };

  contextPolicy: {
    includeInContext: boolean;
    priority: number;
    maxTokens?: number;
  };
};

type MessageBlock =
  | { type: "text"; text: string }
  | { type: "file_ref"; uri: string; summary?: string }
  | { type: "tool_call"; toolCallId: string; name: string; args: unknown }
  | { type: "tool_result"; toolCallId: string; outputRef?: string; preview?: string }
  | { type: "summary"; summaryId: string; text: string };

这里有几个硬不变量:

text 复制代码
tool call 和 tool result 必须成对保留。
不能从中间截断一个未完成 tool call。
大型 tool output 应该进 ArtifactStore,message 只放 preview + ref。
summary message 必须知道自己总结了哪些 message/event。
custom/internal message 要区分是否进入 LLM context。

State:当前现场

state 是从 event log 投影出来的当前工作态。

它不等于历史消息。

ts 复制代码
type AgentState = {
  sessionId: string;
  sourceEventId: string;
  version: number;

  goal: {
    userGoal: string;
    currentTask?: string;
    acceptanceCriteria?: string[];
  };

  plan: {
    steps: PlanStep[];
    currentStepId?: string;
    status: "planning" | "executing" | "blocked" | "verifying" | "done";
  };

  constraints: {
    hard: string[];
    soft: string[];
    userPreferences: string[];
  };

  facts: Array<{
    factId: string;
    content: string;
    confidence: number;
    sourceRefs: string[];
  }>;

  decisions: Array<{
    decisionId: string;
    decision: string;
    rationaleSummary: string;
    evidenceRefs: string[];
  }>;

  artifacts: Array<{
    artifactId: string;
    kind: "file" | "diff" | "command_output" | "url" | "image";
    pathOrUri: string;
    summary?: string;
  }>;

  openQuestions: string[];
  lastCompactionId?: string;
};

state 是压缩后仍能继续工作的核心。

即使早期自然语言对话被压成 summary,下面这些东西也不能糊:

text 复制代码
用户最终目标
当前执行到哪一步
已做决定
已验证事实
已读和已改文件
未完成事项
工具失败和重试策略
用户偏好和硬约束

如果这些只藏在旧 messages 里,compaction 之后 Agent 就会失忆。

如果它们进入 state projection,context 可以随时重建。

Stats:观测指标,不要和 State 混在一起

Stats 管的是运行指标,任务语义应该留给 State

ts 复制代码
type AgentStats = {
  sessionId: string;
  runId?: string;

  tokenUsage: {
    inputTokens: number;
    outputTokens: number;
    cachedInputTokens?: number;
    reasoningTokens?: number;
    contextWindowTokens: number;
    contextUsedTokens: number;
    reservedOutputTokens: number;
  };

  latencyMs: {
    contextBuild?: number;
    retrieval?: number;
    modelCall?: number;
    toolExecution?: number;
    endToEnd?: number;
  };

  compression: {
    compactionCount: number;
    tokensBeforeLastCompaction?: number;
    tokensAfterLastCompaction?: number;
    compressionRatio?: number;
  };

  toolMetrics: {
    toolCallCount: number;
    failedToolCallCount: number;
    approvalRequestedCount: number;
    approvalDeniedCount: number;
  };

  qualityMetrics?: {
    verifierPassed?: boolean;
    contextMissingRisk?: "low" | "medium" | "high";
    hallucinationRisk?: "low" | "medium" | "high";
  };
};

没有 stats,你很难回答:

text 复制代码
为什么某类任务总是在 compaction 后失败?
哪个 tool output 最占 token?
哪些 memory 经常被召回但没用?
context builder 的不同策略哪个更省钱?

Memory:治理后的知识,别当缓存池

Memory 回答的是哪些知识值得未来复用。"把所有 summary 丢进向量库"只是在堆材料,离真正的记忆治理还很远。

ts 复制代码
type Memory = {
  memoryId: string;
  scope: "user" | "project" | "workspace" | "session" | "global";
  kind: "preference" | "fact" | "instruction" | "skill" | "artifact_summary" | "decision";

  content: string;
  sourceRefs: string[];
  confidence: number;

  createdAt: string;
  updatedAt: string;
  expiresAt?: string;

  accessPolicy: {
    sensitive: boolean;
    readableByAgents: string[];
    redactionPolicy?: string;
  };

  retrieval: {
    embeddingId?: string;
    keywords?: string[];
    priority: number;
  };
};

Memory 设计最怕污染。更稳的原则是:

text 复制代码
长期记忆必须有 scope、source、confidence、ttl、sensitivity、priority 和 usage 记录。

当前任务里的猜测不应该污染长期用户记忆。某个项目的测试命令也不应该污染另一个项目。

Artifact:大对象和证据实体

Artifact 用来保存完整证据,prompt 里只放真正需要被模型看到的摘要、引用和片段。

ts 复制代码
type Artifact = {
  artifactId: string;
  sessionId: string;

  kind:
    | "file"
    | "diff"
    | "tool_output"
    | "command_log"
    | "screenshot"
    | "dataset"
    | "url_snapshot";

  uri: string;
  summary?: string;
  contentHash?: string;

  sourceEventId: string;
  createdAt: string;

  accessPolicy?: {
    sensitive: boolean;
    allowedScopes: string[];
  };
};

正确模式是:

text 复制代码
context 中放摘要、引用、关键片段。
artifact store 中放完整证据。

这对 coding agent 尤其重要。文件全文、测试日志、命令输出、截图、diff 都可能很大,不应该直接进入 message history。

Trace:可解释链路,避开隐藏思维链

Trace 回答的是 Agent 为什么这么做、依据是什么、验证过什么、下一步是什么。

不要把"保持推理链路"理解为保存模型隐藏 chain-of-thought。工程上更稳的做法是保存一条可公开、可审计、可回放的 Accountable Reasoning Trace

text 复制代码
Goal
Assumptions
Evidence
Decisions
Actions
Observations
Validation
Next Steps

这比保存自然语言 CoT 更稳、更安全,也更适合审计和恢复。

六、Context Builder:真正的模型输入编译器

有了 session、event、message、state,才轮到 Context Builder。

Context Builder 的输入不该是一整个巨大的 messages[]

它的输入是一组有来源、有优先级、有信任等级的材料:

ts 复制代码
type ContextBundle = {
  system: PromptSegment[];
  developerInstructions: PromptSegment[];
  projectInstructions: PromptSegment[];

  currentRequest: {
    userMessageId: string;
    text: string;
    acceptanceCriteria?: string[];
  };

  sessionState: AgentState;
  summaries: SummaryBlock[];
  recentMessages: Message[];
  retrievedMemories: RetrievedMemory[];
  retrievedArtifacts: RetrievedArtifact[];

  toolContext: {
    availableTools: ToolSpec[];
    pendingToolPairs: Message[];
    recentToolResults: Message[];
  };

  traceContext: {
    runId: string;
    recentDecisions: string[];
    evidenceRefs: string[];
  };

  budget: {
    maxContextTokens: number;
    reservedOutputTokens: number;
    usedTokens: number;
  };
};

这条编译链路可以画成:

然后它按优先级裁剪:

优先级 内容 是否可丢
P0 system / developer / safety / 必需 tool schema 不可丢
P0 当前用户请求 不可丢
P0 未完成 tool call / tool result pair 不可丢
P1 当前 goal、acceptance criteria、plan、open questions 基本不可丢
P1 当前工作文件、diff、测试结果、错误信息 可摘要
P1 最近 N 轮完整对话 可裁剪但不能破坏 turn
P2 历史决策、关键事实、用户偏好 可摘要
P2 检索出的 memory / artifact 可裁剪
P3 旧闲聊、重复解释、冗长工具输出 优先丢弃或摘要

伪代码大概是这样:

ts 复制代码
async function buildContext(
  sessionId: string,
  userMessageId: string
): Promise<ContextBundle> {
  const session = await sessionStore.get(sessionId);
  const state = await stateProjector.project(sessionId);
  const latestSummary = await compressor.getLatestSummary(sessionId);

  const recentMessages = await messageStore.selectRecentCoherentTurns({
    sessionId,
    maxTokens: session.contextConfig.maxRecentTokens,
    preserveToolPairs: true,
  });

  const memories = await memoryStore.retrieve({
    query: state.goal.currentTask ?? state.goal.userGoal,
    session,
    state,
  });

  const artifacts = await artifactStore.retrieveRelevant({
    sessionId,
    state,
  });

  const bundle = assembleByPriority({
    session,
    state,
    latestSummary,
    recentMessages,
    memories,
    artifacts,
    currentUserMessage: await messageStore.get(userMessageId),
  });

  return fitToBudget(bundle, {
    preserve: ["system", "current_request", "pending_tool_pairs", "state.goal"],
    evictionOrder: [
      "verbose_tool_outputs",
      "low_relevance_artifacts",
      "old_assistant_chatter",
      "old_user_messages",
      "retrieved_memories",
    ],
  });
}

这就是 00-15 那篇文章的位置。

00-15 讲的是 Context Builder 内部的选择、排序、压缩、隔离、预算和决策记录。

本章讲的是 Context Builder 上游还有事实源、状态投影、压缩账本、会话树和恢复机制。

七、Compaction:做语义蒸馏,别只缩短文本

长任务一定会遇到上下文窗口。

压缩阶段不能只做:

text 复制代码
请总结上面对话。

压缩应该是:

text 复制代码
从旧上下文中提取未来行动仍然需要的信息,并保留其来源关系。

一次好的 compaction 至少应该产出三类东西:

text 复制代码
1. Summary
   给模型看的压缩上下文

2. State Patch
   更新 Agent 当前状态

3. Memory Candidate
   候选长期记忆,经过审核后才写入 memory

不要把这三者混成一坨自然语言 summary。

最危险的压缩设计是:

text 复制代码
把旧 messages 总结一下,然后删掉旧历史。

这会把系统事实源从可检查事件变成一段 lossy 自然语言。

更稳的设计要分两层:

text 复制代码
Lossless layer:
  raw messages / raw events / raw tool outputs / artifacts

Lossy layer:
  summaries / state snapshots / branch summaries / memory notes

也可以把它理解成一条有审计边界的语义蒸馏链:

原则很简单:

text 复制代码
原始事件尽量保留。
发给模型的上下文可以压缩。
summary 必须有 provenance。
summary 不能覆盖 raw transcript。
summary 不能拆断 tool pair。
summary 之后必须能继续执行任务。

一个合格 compaction record 至少应该有这些字段:

yaml 复制代码
compaction_id: cmp_123
session_id: sess_456
summarized_range:
  from_message_id: msg_001
  to_message_id: msg_120
first_kept_message_id: msg_121
tokens_before: 82000
tokens_after: 18000

goal:
  user_goal: "修复测试失败,并验证。"
  current_task: "定位 serializer.test.ts 的失败断言。"

constraints:
  hard:
    - "不要修改 public API。"
    - "修改后必须运行相关测试。"

progress:
  done:
    - "parser 测试已经通过。"
  in_progress:
    - "serializer 测试仍然失败。"

files:
  read:
    - path: "src/parser.ts"
  modified:
    - path: "src/parser.ts"
      change_summary: "修复空格 token 处理。"

tools:
  important_results:
    - tool_call_id: "tool_123"
      summary: "pnpm test --filter parser 通过。"
      artifact_ref: "artifact_789"

open_questions:
  - "serializer 是否需要保留加号两侧空格?"

next_steps:
  - "读取 src/serializer.ts 和失败测试。"

provenance:
  source_event_ids: ["event_1", "event_2"]
  source_message_ids: ["msg_001", "msg_120"]
  summary_prompt_version: "v3"

压缩之后还应该跑轻量检查:

ts 复制代码
type CompressionCheck = {
  preservesGoal: boolean;
  preservesConstraints: boolean;
  preservesCurrentPlan: boolean;
  preservesOpenToolPairs: boolean;
  preservesFileState: boolean;
  hasSourceRefs: boolean;
  estimatedInformationLoss: "low" | "medium" | "high";
};

几个硬规则可以直接写成测试:

text 复制代码
summary 没有 current_task,压缩失败。
summary 没有 next_steps,压缩失败。
压缩范围里有 file write,但 summary 没有 modified files,压缩失败。
压缩范围里有 tool error,但 summary 没有 errors/retries,压缩失败。
tool call/result pair 被拆,压缩失败。

Compaction 只追求"让上下文变短"是不够的。

它的目标是让上下文变短之后,Agent 仍然知道自己是谁、在做什么、做过什么、下一步该往哪走。

八、推理链路:保存证据,不保存隐藏 CoT

说"保持推理链路"时,要避免一个误区:

text 复制代码
不要试图保存模型隐藏 chain-of-thought。

工程上真正需要保存的是可审计 reasoning trace:

ts 复制代码
type ReasoningTrace = {
  traceId: string;
  sessionId: string;
  runId: string;

  userGoal: string;

  assumptions: Array<{
    assumption: string;
    sourceRefs?: string[];
    confidence: number;
  }>;

  decisionLog: Array<{
    decisionId: string;
    decision: string;
    rationaleSummary: string;
    alternatives?: string[];
    evidenceRefs: string[];
  }>;

  evidenceLog: Array<{
    evidenceId: string;
    kind: "tool_result" | "file" | "user_message" | "memory" | "test" | "observation";
    ref: string;
    summary: string;
  }>;

  actionLog: Array<{
    actionId: string;
    actionType: "message" | "tool_call" | "file_edit" | "memory_write" | "branch" | "compact";
    eventId: string;
    resultEventId?: string;
  }>;

  validationLog: Array<{
    check: string;
    result: "pass" | "fail" | "skipped";
    evidenceRefs?: string[];
  }>;
};

这样你能回答的是:

text 复制代码
Agent 为什么调用这个工具?
这个结论来自哪个文件?
哪次压缩后丢了什么?
哪个分支引入了错误?
哪个用户约束被违反了?
最终回答有没有验证证据?

这些问题不需要 hidden CoT。

它们需要的是事件、证据、决策摘要、操作记录和验证结果。

九、Branch、Memory、Retrieval 与 Subagent 都不能绕过 Context Manager

很多上下文系统后期失控,是因为外部能力绕开了 Context Manager。

长期记忆一旦可以直接进入 prompt,就会把未验证经验变成当前事实。

检索结果一旦可以直接进入 prompt,就会把相似文本变成证据。

子 Agent 一旦可以直接把长报告塞回主上下文,就会把隔离上下文重新污染回来。

所以这三类能力都应该按同一条路进入模型输入:

text 复制代码
Memory / Retrieval / Subagent output
-> source refs
-> scope / trust / freshness check
-> artifact or state update
-> Context Builder
-> Model Input

Memory 要有 scope、confidence、TTL、source refs。

Retrieval 要有 query plan、citation、permission boundary、audit snapshot。

Subagent 要返回 summary、evidence refs、artifacts、risks 和 next steps。只返回一段"我完成了",主线 Agent 很难判断这个结果能不能继续使用。

所以前面的 Memory Governance、Scoped Retrieval、Delegation Runtime 都不只是额外专题。它们像 Context Manager 的外围血管,决定外部材料怎样进入主线工作现场。

它们最终都要回到同一个问题:

text 复制代码
这些材料能不能进入本轮模型上下文?
如果能,以什么优先级、什么信任等级、什么预算进入?
如果不能,是否保存为 artifact 或 audit event?

复杂任务也天然很少是线性的。Agent 经常会:

text 复制代码
尝试方案 A
失败
回退
尝试方案 B
开 subagent 做研究
从旧状态 fork
比较两个结果

所以 session 应该天然支持树结构:

推荐抽象:

ts 复制代码
type SessionNode = {
  nodeId: string;
  sessionId: string;
  parentNodeId?: string;

  eventId: string;
  messageId?: string;

  branchType: "main" | "fork" | "subagent" | "what_if";
  branchSummary?: string;

  createdAt: string;
};

一句话:

分支是探索隔离,subagent 是上下文隔离。

十、完整生命周期:一次请求怎样流过 Context Manager

把这些层合起来,一次请求的生命周期大概是:

这个流程里,模型只是其中一个节点。

模型只负责其中一部分状态判断。

决策记录也要落回 Harness 的运行时系统。

Harness 负责把模型的判断放回一个可恢复、可审计、可验证的工程系统里。

十一、MVP 应该先做什么

不要一开始就做完所有模块。

本地 CLI Agent 的 MVP 可以先做这些:

text 复制代码
sessions
messages
events
state_snapshots
artifacts
compactions

暂时可以不做:

text 复制代码
复杂向量 memory
多 agent 协作
自动 branch pruning
高级 eval
跨项目长期记忆

但 MVP 里最好一开始就保留几个能力:

text 复制代码
每次用户输入写入 message + event。
每次工具调用写入 event。
大型工具输出写 artifact,message 只放摘要和引用。
每次模型调用前构建 ContextBundle。
超过 token 阈值时生成 structured summary。
保证 tool call/result pair 不被截断。
支持 resume。
支持导出 JSONL。
支持 /context 查看当前上下文构成。
支持 /compact 手动压缩。

这样即使系统还很小,边界也不会错。

第一版 ContextManager 可以只有这个接口:

ts 复制代码
interface ContextManager {
  appendEvent(event: AgentEvent): Promise<void>;
  projectState(sessionId: string): Promise<AgentState>;
  buildContext(sessionId: string, input: BuildContextInput): Promise<ContextBundle>;
  compact(sessionId: string, policy?: CompressionPolicy): Promise<CompactionResult>;
  resume(sessionId: string, branchId?: string): Promise<AgentSession>;
}

复杂性藏进三个 pipeline:

text 复制代码
Event Pipeline:
  input / tool / model / system events -> append-only log

Projection Pipeline:
  events / messages / artifacts -> state snapshot

Context Pipeline:
  instructions + state + summary + recent messages + retrieval -> model context

这已经足够支撑一个可演进的 Harness。

十二、关键工程不变量

这套范式最后要落到测试。

下面这些不变量应该直接写进单元测试或 replay verifier:

text 复制代码
Invariant 1:
  Every tool_result must have a preceding tool_call.

Invariant 2:
  No pending tool_call can be removed by compaction.

Invariant 3:
  Every compaction summary must include source message range.

Invariant 4:
  Every file write event must be represented in state.artifacts or summary.files.modified.

Invariant 5:
  Current user request, active goal, and open questions must always be included in context.

Invariant 6:
  ContextBuilder output must be deterministic given same session state and retrieval result.

Invariant 7:
  Raw event log is append-only.

Invariant 8:
  Summary cannot overwrite raw transcript.

Invariant 9:
  Branch must not mutate ancestor branch.

Invariant 10:
  Memory writes require source refs.

Invariant 11:
  Long-term memory must have scope and expiry/update policy.

Invariant 12:
  ContextBundle must be explainable: every included segment should have reason and source.

这些规则比"prompt 写好一点"更重要。

因为它们把 Agent 的可靠性从模型感觉,拉回了工程约束。

十三、常见反模式

反模式一:messages[] 即世界

text 复制代码
所有历史都存在 messages 里。

问题是难恢复、难压缩、难审计、难分支、难解释,也很难做权限控制。

反模式二:summary 覆盖历史

text 复制代码
压缩后只保留 summary,不保留原始记录。

问题是无法回放、无法纠错、无法验证,也不知道 summary 丢了什么。

反模式三:memory 无作用域

text 复制代码
所有记忆都进一个向量库。

问题是项目污染、用户污染、临时事实长期化、旧知识误用和敏感信息扩散。

反模式四:工具结果直接塞上下文

text 复制代码
命令输出、文件全文、日志全部塞进 prompt。

问题是 token 爆炸、噪声过多、重点丢失,安全风险也会上升。

反模式五:压缩只做自然语言总结

text 复制代码
"请总结上面对话。"

问题是目标、约束、文件状态、错误、决策和下一步都可能被压没。

反模式六:把 prompt 当 policy

text 复制代码
只在 system prompt 里说不要做危险事。

问题是不可验证、不可强制、不可审计、不可测试。确定性规则应该进入 policy、permission、hook、validator,而不只是 prompt。


GitHub 地址: 01-context-manager-attention-os.md

博客地址: blog

相关推荐
岁月宁静2 小时前
Hermes Agent:让你的AI智能体越用越聪明
python·agent
AImatters2 小时前
左手Anthropic,右手OpenAI:亚马逊云科技不押模型,押入口
openai·agent·亚马逊云科技·anthropic·amazon quick
宋哥转AI2 小时前
Spring AI Graph:从0到Supervisor(二)并行执行+HITL实战
java·agent
IvanCodes3 小时前
让技能自己成长——JiuwenSwarm Swarm Skills 团队知识管理深度评测
agent·openjiuwen
摸鱼同学3 小时前
从0到1打造AI学习体系,从LLM到Multi-Agent
ai·agent·vibe coding·claudecode·skills
新酱爱学习3 小时前
手搓 10 个 Skill 踩出来的坑,我做成了一套工程化工具链
前端·人工智能·agent
Artech4 小时前
[MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣
ai·agent·react·maf·ichatclient
莫逸风4 小时前
【AgentScope】3. 工作空间(Workspace)详解
java·ai·agent·springai·agentscope