Skip to content
Chenghao Liao
Go back

Agent 长任务里的上下文管理:不要把所有历史都塞进 Context

最近看到一个关于长任务 Agent 上下文管理的讨论,其中一个思路让我很有共鸣:

当上下文快满时,与其不断压缩旧上下文,不如结束当前窗口,保留必要状态和历史引用,再用一个新的上下文继续执行。

仔细想下来,这和我目前在 Agent 系统里采用的策略其实很接近。

我的核心思路一直不是“怎么把越来越多的信息塞进上下文”,而是:

完整信息应该持久化保存,但只有当前真正需要的信息才进入模型上下文。

随着 Agent 执行时间越来越长,我越来越觉得,Context Window 不应该承担 Agent 的全部记忆。它更像工作内存,而不是数据库。

这篇文章主要整理一下这个问题,以及我目前采用的一套方案。

核心观点


一、问题:Agent 的 Context 为什么迟早会出问题

一个最简单的 Agent 可能长这样:

用户请求
  ↓
LLM
  ↓
调用工具
  ↓
Tool Result
  ↓
继续推理
  ↓
继续调用工具

如果每一步的信息都继续追加到 messages[]:

System Prompt
User Message
Assistant Message
Tool Call
Tool Result
Assistant Message
Tool Call
Tool Result
...

短任务没有什么问题。

但一旦 Agent 开始执行几十分钟甚至几个小时,这种设计很快会遇到麻烦。

1. Tool Result 往往非常大

例如一个 Code Review Agent 可能读取:

完整 Git Diff
多个源代码文件
静态扫描结果
编译日志
测试日志
Git History
多个子 Agent 的输出

其中一个工具结果就可能有几千甚至几万 token。

真正影响当前决策的内容可能只有几十行。

如果把所有原始结果永久留在 Context 里,本质上是在用最昂贵的存储空间保存大量低频数据。


2. 历史信息不一定还有价值

Agent 在调查问题过程中可能经历:

怀疑 A
→ 调查
→ 排除 A

怀疑 B
→ 调查
→ 排除 B

最终确认 C

如果完整历史一直留着,那么模型每一次后续推理都要继续看到:

关于 A 的大量讨论
关于 B 的大量讨论
各种失败尝试
已经废弃的假设
已经过期的代码

Token 够用并不意味着这些内容有帮助。

很多时候恰恰相反:

旧信息太多本身就是噪声。


3. 一直做 Summary 也不是完美方案

一个常见办法是:

Context 快满
→ Summary
→ 继续

又快满
→ Summary of Summary
→ 继续

问题在于压缩本身就是有损的。

比如原始约束是:

不要修改 auth.go,
这里只允许调查问题。

第一次压缩可能变成:

需要调查 auth 模块。

再经过几轮:

当前任务与 auth 有关。

最重要的“不要修改”可能已经消失了。

所以我不太喜欢:

Summary1
→ Summary2
→ Summary3
→ Summary4

这种无限递归压缩。

它很像重复压缩一张 JPEG。

每次损失一点,长期运行以后误差可能会不断累积。


二、核心思路:Context 只是工作内存

我目前更倾向于把 Agent 的信息分成三个层级。

Persistent History
        ↓
Structured Working State
        ↓
Current Context

分别承担不同职责。

第一层:Persistent History

这一层保存完整信息:

历史消息
Tool Call
完整 Tool Result
日志
Diff
文件内容
测试结果
Agent 输出

原则是:

尽量不丢。

但是“不丢”不代表“全部放进 Context”。

它们更像数据库里的原始记录。

例如:

artifact://test-run-184
artifact://security-scan-91
artifact://git-diff-abc123

需要的时候再读。


第二层:Structured Working State

这是 Agent 当前真正需要长期记住的东西。

例如:

goal
current_step
confirmed_facts
constraints
unresolved_items
next_actions
evidence_refs
version

举个 Code Review 的例子:

Goal:
Review PR #123

Current PR Head:
abc123

Confirmed Findings:
- F17: Potential SQL Injection

Pending:
- Verify whether input is sanitized upstream

Constraint:
- Do not modify public API

Evidence:
- security-scan-91#42
- diff-abc123#dao.go:80-96

这一层应该很小。

因为它不是保存“发生过什么”,而是在回答:

我们现在知道什么?正在做什么?接下来要做什么?


第三层:Current Context

真正调用 LLM 时,再根据当前任务组装 Context:

System Prompt
+
Current Task
+
Working State
+
Relevant Evidence
+
Recent Important Events

而不是:

整个 Agent 历史

例如 Security Agent 当前正在验证 SQL Injection,那么它可能只需要:

当前 finding
相关代码
相关 diff
数据流证据
必要约束

之前扫描过另外 30 个文件的完整结果没有必要一起带进来。

于是整个过程变成:

Persistent Memory
       ↓
Context Builder
       ↓
只挑当前需要的信息
       ↓
LLM

我认为这里最重要的不是“压缩”,而是:

Context Projection:当前这次推理到底需要看到什么?


三、完整结果不进入 Context,但必须随时可以回来

我目前非常认可一个原则:

完整结果不默认进入 Context。

注意,是“不默认”,而不是“永远不允许”。

比如静态扫描工具返回了 500 行结果。

第一次可以只告诉 Agent:

Scanner found 3 suspicious locations.

F17:
dao.go:83
Possible SQL Injection

Full result:
security-scan-91

模型不需要立刻看到完整 500 行。

如果之后 Critic 想确认:

这个 finding 的原始证据到底是什么?

再执行:

read(security-scan-91#F17)

把需要的部分加载进来。

所以更合理的模式是:

默认:
Summary + Reference

必要时:
Fetch Full Evidence

而不是两个极端:

所有东西一直塞进 Context

或者:

压缩之后原始结果直接消失

四、Summary 最好带 Evidence Reference

这里还有一个我认为很重要的设计。

不要让 Working State 只有结论:

CI failed because of dependency conflict.

最好是:

Conclusion:
Dependency conflict caused the CI failure.

Evidence:
build-log-92#L420
dependency-tree-17

这样即使后面发现 Summary 有问题,Agent 仍然可以返回原始证据重新判断。

这其实是在把:

State

和:

Evidence

分开。

当前 Agent 只需要背着结论走。

证明这个结论的几十 KB 日志可以留在后面。

可以概括成一句:

状态向前走,证据留在身后。

需要的时候再回来查。


五、不要做 Summary of Summary,而是定期 Rebuild State

这是我觉得长期运行 Agent 很容易踩的一个坑。

不太理想的是:

Summary V1
+ 新内容
→ Summary V2

Summary V2
+ 新内容
→ Summary V3

因为一旦 V1 丢掉一个细节,后面几乎永远恢复不了了。

我更倾向于:

Persistent History
+
Current Structured State
+
Recent Important Events
        ↓
重新构建
        ↓
State V3

也就是:

Rebuild,而不是 Recursive Summarization。

两者看起来都叫“压缩”,但逻辑不一样。

Recursive Summary 是:

压缩上一次的压缩结果

Rebuild 是:

重新计算“当前世界状态”

后者更像数据库里的 Materialized View。

原始数据仍然存在,只是定期重新生成一份适合当前 Agent 使用的视图。


六、仅靠 Agent 主动 Retrieval 也不够

到这里,这套方案还有一个明显的问题。

假设完整历史都保存着,并且 Agent 可以随时搜索:

search_history()
read_artifact()

是不是就解决了?

还没有。

因为这里有两种情况。

第一种:

Agent 知道自己缺信息

例如:

我需要刚才那份测试日志。

很好,它会主动查。

但更危险的是第二种:

Agent 不知道自己缺信息

比如 Working State 里写:

数据库问题已经排除。

但实际上历史里只检查了 Primary DB,没有检查 Replica。

如果 Agent 相信当前摘要,它根本不会产生:

search history about replica

这个动作。

这就是经典的:

unknown unknowns。

所以 Retrieval 不能全部依赖 LLM 自己决定。


七、Context Builder 应该承担一部分主动补充职责

因此我觉得更成熟的方案应该是:

LLM-driven Retrieval
+
Policy-driven Retrieval

比如 Code Review 场景,可以制定确定性的 Context Policy:

验证 Finding
→ 自动加入相关代码 + Diff

重新验证旧 Finding
→ 自动加入旧 Evidence

执行 AutoFix
→ 自动加入相关测试结果

PR Head 改变
→ 自动标记旧 Diff / Finding 可能 stale

也就是说:

Agent 可以主动找信息,但系统也应该根据任务类型自动给它补必要信息。

于是 Context Builder 不再只是:

token 截断工具

而是一个真正的 Runtime 组件。


八、信息还应该有 Version 和 Freshness

这一点在真实 Agent 系统里很重要。

假设 Security Agent 在:

PR head = abc123

时生成:

Finding F17

后来开发者又 push:

PR head = def456

那么之前保存的:

Diff
代码片段
Finding
Summary

并不是全部仍然有效。

所以 Memory 不能只保存:

这是什么信息?

还应该保存:

它基于哪个世界状态产生?

例如:

Finding {
    id: F17
    source_sha: abc123
    evidence_ref: ...
    created_at: ...
    status: confirmed
}

之后 Context Builder 发现:

current_head != source_sha

就可以决定:

重新验证
标记 stale
重新加载最新代码

这相当于把我们熟悉的乐观锁 / Freshness Check 也带进了 Context Management。


九、Context 本身也应该有 Budget

最后还有一个比较工程化的问题:

即使已经做了按需加载,应该优先放什么?

我比较喜欢把信息简单分层:

Tier 0
当前任务 + 强约束
永远保留

Tier 1
Working State
默认保留

Tier 2
当前步骤相关 Evidence
按需加载

Tier 3
完整 History / Raw Tool Result
默认不加载

如果 Context Budget 开始紧张:

先 Drop Tier 3
再减少 Tier 2

而不是碰:

任务目标
用户约束
当前状态

也就是说,Context Window 应该有自己的资源管理策略。


十、新窗口到底有没有必要?

对于 ChatGPT、Codex 这种长会话 Agent,新窗口是比较自然的解决方式。

因为整个系统天然是:

Conversation
→ Message
→ Message
→ Tool Result
→ Message
→ ...

Conversation 本身就是 Working Memory。

跑久以后,显式:

保存 Notes
保存 History
结束旧 Context
创建新 Context
恢复 State

是合理的。

但对于自己开发的 Agent Runtime,我认为不一定需要把“开新窗口”做成一个非常明显的动作。

因为我们本来就控制每次模型请求。

完全可以做到:

Persistent State
      ↓
Context Builder
      ↓
一次全新的 LLM Call

从 Runtime 的逻辑建模上看,每一次 LLM 调用都可以基于当前状态重新组装输入,等价于获得一个新的轻量 Context:

这里的“新 Context”指重新构建模型输入,不代表必须创建新的会话实体。

Agent 的连续性不再来自:

messages[] 永远不断

而来自:

Persistent State + History + Retrieval

我觉得这是一个更干净的模型。


十一、这套方案的优点

最大的优点当然是 Context 更干净。

模型主要看到当前真正相关的信息,而不是大量历史噪声。

其次是 Token 成本更可控。

一个 100KB 的 Tool Result 不会在之后每一次 LLM 调用里反复付费。

第三是 长任务更稳定。

Agent 的连续性由外部状态保证,而不是依赖某一个永远不能结束的 Conversation。

第四是 可以追溯。

Summary 错了,还能通过 evidence_ref 回到原始结果。

最后是 系统职责更清楚:

History
负责保存发生过什么

Working State
负责保存现在是什么状态

Context Builder
负责决定这次模型应该看到什么

LLM
负责推理

这比让 messages[] 同时承担所有职责清晰很多。


十二、它的缺点也很明显

首先,系统复杂度会提高。

以前:

messages.append(...)

就结束了。

现在需要:

Persistent Store
Artifact Store
Working State
Context Builder
Retrieval
Version Management

工程量明显增加。

其次,Context Selection 本身可能选错。

如果系统漏掉了某条重要信息:

信息明明存在
但没有进入 Context

对于模型来说和“不存在”没有区别。

第三,Working State 本身仍然可能产生错误。

即使有 Evidence Reference:

错误 Summary

仍然可能误导后续 Agent。

所以必须允许重新验证。

最后,Retrieval Policy 很难做到完美。

完全交给 LLM 不可靠;

全部写死规则又缺乏灵活性。

最终大概率还是需要:

Deterministic Policy
+
LLM Retrieval

两者结合。


十三、我目前更倾向的 Agent Memory 架构

如果把前面的思路组合起来,我目前比较认可的是:

                Persistent Source of Truth
              /            |             \
         Messages       Tool Results     Artifacts
              \            |             /
                       History
                          │
                          ▼
                 Structured State
                          │
                  evidence references
                          │
                          ▼
                  Context Builder
                  /              \
          Policy Retrieval    Agent Retrieval
                  \              /
                          ▼
                    Context Window
                          │
                          ▼
                         LLM

并且所有信息最好再带:

version
timestamp
source
scope
evidence_ref

这样 Agent 不仅知道:

我知道什么。

还知道:

我为什么知道,以及这个信息现在是否仍然有效。


结语

我现在越来越觉得,长任务 Agent 的核心问题并不是:

怎么造一个更大的 Context Window?

而是:

怎么让 Agent 不需要一直依赖一个巨大的 Context Window?

完整历史应该持久化。

原始 Tool Result 应该可追溯。

当前状态应该显式维护。

真正进入 Context 的内容应该根据当前任务动态构建。

必要时重新读取证据,而不是把所有过去一直背在身上。

最终:

Agent Memory
≠ Conversation History

更合理的模型可能是:

Agent Memory
=
Persistent History
+ Structured State
+ Evidence
+ Retrieval

而:

Context Window

只是这套 Memory System 在某一个时间点投影出来的一小部分。

Context 是工作台,不是仓库。


Share this post:

Previous Post
ChatGPT Plus 国内支付记录:Google Play + 银联卡
Next Post
当 Agent 的 Tool 太多时,是否应该加一层“工具路由”?