Appearance
上下文管理 ⭐
长任务里,对话历史与工具结果会不断累积,最终撞上模型的上下文长度上限。AgentScope 2.x 没有独立的 "Memory"模块,而是把上下文预算管理拆成三件事--压缩(把旧消息总结成结构化摘要)、截断 (把超限工具结果裁到 tool_result_limit 以内)、卸载(把被裁掉的内容持久化到 Offloader, 通常是 Workspace)。这三件事由 ContextConfig 配置、由 Agent 在 ReAct 循环中自动触发,也可以 用 await agent.compress_context() 手动触发。
版本基线:本文以 Python
agentscope 2.0.4(PyPI,2026-07-07)为准,要求 Python>=3.11。 导入路径为from agentscope.agent import Agent, ContextConfig、from agentscope.workspace import LocalWorkspace, Offloader(源:src/agentscope/agent/__init__.py、src/agentscope/workspace/__init__.py)。2.x 文档站存在版本化陷阱:非版本化的/en/URL 会 404, 本页所有官方链接均使用versions/2.0.4/en/形式。
为什么需要上下文管理
ReAct Agent 每完成一轮工具调用,都会把 ToolResultBlock 回灌进上下文。一次文件读取、一次 Web 搜索、一次代码执行,都可能把成千上万 token 塞进历史。几轮之后,上下文就会逼近模型上限,导致:
- 后续模型调用因超长而失败或被截断,丢失最新指令;
- 推理质量下降--模型注意力被稀释在大量旧工具结果里;
- 进程内存随消息累积而膨胀,极端情况下 OOM。
2.x 的解法不是"扩容上下文",而是预算管理:在 token 占用超过阈值时,把旧消息压缩成结构化摘要, 只保留最近一段消息,超限的工具结果截断或卸载到外部存储。注意 1.x 的独立 Memory 模块在 2.x 已移除,压缩与卸载逻辑直接内建在 Agent 里,由 ContextConfig 与 Offloader 协议承担。
前端类比
如果你来自前端,可以把这套机制类比为 Redux Store 的状态裁剪 + 虚拟列表分页:
ContextConfig像 Redux 里决定"保留多少最近状态、把多老的状态归档"的 reducer 策略--trigger_ratio是触发归档的水位线,reserve_ratio是归档后保留的"视窗"大小。- 工具结果截断像虚拟列表只渲染视口内的行:超限的
ToolResultBlock不会全量留在 DOM(上下文)里。 Offloader像把虚拟列表视口外的数据分页到 IndexedDB:被裁掉的内容不直接丢弃,而是落盘到一个Workspace可访问的存储,留下一个引用ref,需要时还能取回。
AgentScope 原生语义:压缩不是简单的"删除旧消息",而是用模型按 SummarySchema 生成结构化摘要 (任务概览 / 当前状态 / 重要发现 / 下一步 / 需保留上下文),再把摘要 + 最近消息组成新上下文。卸载 通过 Offloader 这一 typing.Protocol 抽象--offload_context 处理被压缩的消息,offload_tool_result 处理被截断的工具结果,二者都返回一个引用字符串替换原内容。Workspace(LocalWorkspace / DockerWorkspace / E2BWorkspace 等)实现了该协议,所以同一个 offloader= 既能用于沙箱执行也能 用于上下文卸载。这与 Redux 的"纯 reducer"不同:压缩本身是一次模型调用,有成本也有信息损耗。
ContextConfig 参数
ContextConfig 是 Pydantic model,构造时全部参数可选。下表只列需要调整的三项,其余 (compression_prompt / summary_template / summary_schema)使用默认值即可,除非你要自定义 压缩摘要的结构或提示词。
| 参数 | 作用 | 典型值 |
|---|---|---|
trigger_ratio | 当 token 占用超过 trigger_ratio × model.context_size 时自动触发压缩;取值必须 >0 且 <0.9(为压缩调用本身预留空间) | 0.8(默认) |
reserve_ratio | 压缩后保留的最近消息 token 比例,应小于 trigger_ratio | 0.1(默认) |
tool_result_limit | 单条工具结果的最大 token 数,超出则截断;超限部分可卸载到 Offloader | 50000(默认);预算紧张时可调小,如 3000 |
两点约束来自源码(src/agentscope/agent/_config.py):
trigger_ratio上限硬编码为lt=0.9。设到0.9或以上会直接校验失败,因为压缩本身要调用模型, 必须给压缩请求留出 token 余量。reserve_ratio也受lt=0.9约束,且语义上应小于trigger_ratio--它决定压缩后"视窗"里保留多 少最近消息,留得太多就失去了压缩意义。
压缩 / 截断 / 卸载流转
下面这张流程图说明一次自动压缩触发时,上下文如何被拆解与重组。图前先说明:触发条件是 estimated_tokens >= trigger_ratio × model.context_size,由 Agent 在 ReAct 循环的特定节点 检查;压缩调用走模型,按 SummarySchema 产出结构化摘要。
节点解释:
- token 检查:
Agent调用model.count_tokens(...)估算当前上下文 token 数,与trigger_ratio × model.context_size比较。低于阈值则什么都不做,直接继续循环。 - 结构化摘要:压缩不是无脑截断,而是一次模型调用,按
SummarySchema的五个字段 (task_overview/current_state/important_discoveries/next_steps/context_to_preserve) 生成续接摘要,目的是让 Agent 在"换了一个新上下文窗口"后仍能高效恢复工作。 - 保留最近消息:按
reserve_ratio保留最近一段原始消息,确保当前正在进行的工具调用与即时上下文 不丢失。 - 工具结果截断与卸载:单条工具结果 token 超过
tool_result_limit时,超出部分包装成ToolResultBlock调用offloader.offload_tool_result(...)卸载,返回的引用字符串替换原内容; 被压缩掉的旧消息则通过offloader.offload_context(...)卸载。若未设置offloader,则只截断不 卸载,超出部分直接丢弃。
构造带上下文管理的 Agent
把 context_config 与 offloader 一起传给 Agent 即可启用自动压缩与卸载。凭据通过环境变量注入, 不要写死在代码里:
python
import asyncio
import os
from agentscope.agent import Agent, ContextConfig
from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.workspace import LocalWorkspace
async def main() -> None:
# Offloader 通常即 Workspace;LocalWorkspace 把卸载内容落到本地目录
workspace = LocalWorkspace(workdir="./workspace_data")
await workspace.initialize()
agent = Agent(
name="long_task_agent",
system_prompt="你是一个能处理长任务的中文助手,善于在多轮工具调用中保持目标一致。",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen-plus",
),
context_config=ContextConfig(
trigger_ratio=0.8,
reserve_ratio=0.1,
tool_result_limit=3000,
),
offloader=workspace,
)
# 后续 reply / reply_stream 中,上下文逼近上限时会自动压缩与卸载
# ...
asyncio.run(main())几个要点:
model=接收的是ChatModelBase实例(如DashScopeChatModel(...)),不是字符串。压缩用的 摘要模型就是 Agent 自己的model,因此所选模型应具备稳定的指令遵循能力。offloader=workspace把LocalWorkspace同时作为沙箱执行环境与上下文卸载目标。若你已经在用DockerWorkspace或E2BWorkspace做沙箱,直接把同一个实例传给offloader即可,无需额外装配。tool_result_limit=3000是一个偏紧的预算,适合工具结果普遍很长(如大文件读取、Web 抓取)且希望 尽早卸载的场景;若工具结果较短,保留默认50000即可,避免频繁截断。
手动压缩
自动压缩发生在 ReAct 循环的内部节点。如果你想在任意时机主动压缩(例如在一次性 observe 大量消息 后、或回复结束后清理),可以手动调用:
python
# 手动触发压缩;若当前 token 未超过 trigger_ratio × model.context_size,则为 no-op
await agent.compress_context()compress_context 是 async 方法,签名(源:src/agentscope/agent/_agent.py):
python
async def compress_context(
self,
context_config: ContextConfig | None = None,
instructions: HintBlock | None = None,
) -> None: ...context_config不传时用 Agent 自带的context_config;传入临时配置可做一次"更激进"或"更保守" 的压缩而不影响默认配置。instructions可注入额外提示,引导摘要生成的侧重点(例如"重点保留文件路径与报错信息")。- 内部第一步就是
if estimated_tokens < threshold: return,所以低于阈值时是 no-op,可以放心在 不确定是否超限时调用,不会浪费一次模型调用。
Offloader 协议
Offloader 是 typing.Protocol(源:src/agentscope/workspace/_offload_protocol.py),定义了两个 async 方法:
| 方法 | 作用 | 返回值 |
|---|---|---|
offload_context(session_id, msgs) | 卸载被压缩掉的旧消息列表 | 引用字符串 ref |
offload_tool_result(session_id, tool_result) | 卸载被截断工具结果的超出部分 | 引用字符串 ref |
返回的 ref 会替换上下文里的原内容,Agent 后续能看到"这里有一段被卸载的内容,引用是 ref",但 不再为它占用 token。需要取回时,通过 Workspace 的文件接口按 ref 读取(session_id 由 Agent 状态管理,对应当前会话)。
LocalWorkspace / DockerWorkspace / E2BWorkspace 等都实现了 Offloader 协议,因此你不需要 自己写一个 Offloader--只要把沙箱 Workspace 实例传给 offloader= 即可。只有在自定义存储后端 (如对象存储、外部数据库)时,才需要实现这两个方法的 Protocol。Workspace 的完整能力(沙箱执行、 文件读写、隔离级别)见 Workspace 沙箱。
与中间件的关系
压缩过程可以被中间件拦截。Agent 构造时会筛选实现了 on_compress_context 钩子的中间件,组成 压缩链--调用 compress_context 时,会按洋葱模型依次穿过这些中间件,再落到内部的 _compress_context_impl 执行真正的压缩与卸载。
这意味着你可以在压缩前后插入自定义逻辑:记录压缩前后的 token 数、把摘要同步写入长期记忆、在压缩 前抢救某些关键消息等。中间件的挂载方式与完整钩子列表见 中间件。
常见错误
trigger_ratio设过高导致 OOM:把trigger_ratio调到0.9会直接校验失败(源码硬约束lt=0.9);即便接近0.9,也意味着上下文几乎撑满才压缩,进程内存峰值更高,且给压缩调用本身留 的 token 余量更少。长任务建议用默认0.8或更低。offloader未设置导致卸载失败:不传offloader时,超限工具结果只会被截断,超出部分直接 丢弃,无法事后取回。如果你的工具会返回大块内容(文件、搜索结果、日志),务必把Workspace实例传给offloader=。误以为有独立 Memory 模块:1.x 的独立
Memory/state_dict在 2.x 已移除,压缩与卸载 内建在Agent里。把 1.x 的 memory 写法搬到 2.x 会找不到对应 API。迁移参见 1.x 到 2.x 迁移。⚠️ 1.x 仅作历史对照:1.x 的
ReActAgent、独立Memory模块、state_dict/load_state_dict在 2.x 全部移除,不能作为 2.x 代码使用。期望压缩零信息损耗:压缩是预算管理,不是扩容。
SummarySchema摘要会丢失细节,被卸载的工具 结果虽可取回但不再在上下文里"主动可见"。对关键信息(如文件路径、报错堆栈)应在instructions里提示压缩模型重点保留,或用长期记忆中间件单独持久化(见 长期记忆)。忘记
await workspace.initialize():LocalWorkspace需要先initialize()再使用,否则卸载 时目录可能未就绪。DockerWorkspace/E2BWorkspace的初始化更重,必须在async上下文里完成。
先修
- 计划模式 - 理解长任务下的任务列表状态,为压缩后的续接摘要提供上下文
下一步
- 中间件 - 用
on_compress_context钩子在压缩前后插入自定义逻辑