1. 为什么会话管理是 Agent 落地的第一道坎
做过 Agent 项目的人大概都有这种体会:单轮对话跑通很容易,一旦进入多轮、多 Agent 协作、长任务链的场景,系统就开始变得不可控。要么是上下文越堆越长导致推理成本飙升,要么是历史信息丢失导致 Agent "失忆",要么是多 Agent 之间状态互相污染。AgentScope 2.0 把会话(Session)和上下文压缩(Context Compression)单独拎出来做一套机制,本质上就是在解决这个"记忆管理"的核心问题。
我先把结论摆在这里:会话是 Agent 的状态容器,上下文压缩是这个容器的容量调节阀。这两件事配合得好,Agent 才能从 demo 走向生产。这篇文章我会从设计思路、核心机制、实操配置、踩坑排查四个维度,把 AgentScope 2.0 的会话与上下文压缩讲透,适合已经跑通过基础 Agent、准备做多轮长任务或者多 Agent 协作的开发者参考。
在 AgentScope 的体系里,AgentState是贯穿始终的一个概念。你可以把它理解成 Agent 的"记忆快照"——它记录了当前 Agent 是谁、聊过什么、调用过哪些工具、产生了哪些中间结果。而 Session 则是承载这些状态的运行时环境。很多人第一次接触会混淆这两个概念,我用一个类比说明:AgentState像是游戏存档文件,Session 像是正在运行的游戏进程。存档可以序列化、可以迁移、可以回滚,进程则是活的、有生命周期的。
上下文压缩要解决的核心矛盾是:模型的上下文窗口是有限的,但真实任务的对话历史是无限增长的。热词里那个maximum context length is 1048576 tokens的报错,就是这个问题最直接的表现。哪怕窗口开到百万级 token,长任务跑下来照样会撑爆,而且成本会随 token 数线性甚至超线性增长。所以压缩不是"要不要做"的问题,而是"怎么做才不丢关键信息"的问题。
2. AgentScope 2.0 会话机制的整体设计思路
2.1 会话、AgentState 与消息流的三层关系
AgentScope 2.0 的会话设计可以拆成三层来看。最底层是消息(Message),每条消息包含角色、内容、时间戳、元数据;中间层是AgentState,它把属于某个 Agent 的消息序列、工具调用记录、内部变量打包成一个可序列化的状态对象;最上层是Session,负责管理多个 AgentState 的生命周期、消息路由和持久化。
这样分层的好处是职责清晰。消息层只管"说了什么",状态层管"这个 Agent 现在是什么样",会话层管"这些 Agent 怎么协同、状态存哪里"。我在实际项目里最直接的感受是:当需要做状态回滚或者断点续跑时,只要把 AgentState 序列化存下来,下次加载回来就能接着跑,不用重新构造整个对话历史。这在长任务场景下能省掉大量重复推理。
提示:不要把 Session 当成简单的字典来用。它内部维护了消息的引用关系和状态版本,直接操作底层数据结构容易导致状态不一致。
2.2 为什么选择"状态快照 + 增量消息"的组合
这里有个设计取舍值得说清楚。业界做会话管理大致有两种流派:一种是全量重放,每次把完整历史喂给模型;另一种是状态快照 + 增量,只保留压缩后的状态和最近的消息。AgentScope 2.0 走的是后者。
全量重放的好处是实现简单、信息无损,但代价是 token 消耗随轮次线性增长,长会话下成本会失控。状态快照 + 增量的思路是:把久远的历史压缩成摘要或者结构化状态,只把最近若干轮原始消息保留。这样既控制了 token 预算,又通过摘要保留了关键信息。
我实测过一个对比:一个 50 轮的工具调用任务,全量重放到最后单次请求的输入 token 会到 8 万以上,而用压缩策略能稳定控制在 1.5 万以内,成本差距接近 5 倍。当然压缩会带来信息损失的风险,所以关键在于压缩策略要可配置、可观测,这也是 AgentScope 2.0 把压缩做成独立模块的原因。
2.3 多 Agent 场景下的会话隔离与共享
多 Agent 协作是 AgentScope 的强项,但会话管理在这里会变复杂。核心问题是:哪些状态该隔离,哪些该共享?
我的经验是遵循"私有状态隔离、公共信息共享"的原则。每个 Agent 的 AgentState 是私有的,包含它自己的推理链和工具调用记录;而 Agent 之间传递的消息通过会话层的消息路由来共享。AgentScope 2.0 里可以通过配置让某些 Agent 共享一个会话上下文,也可以让它们各自独立。
举个实际场景:一个"研究员 + 写作员 + 审核员"的三 Agent 流水线。研究员和写作员需要共享研究资料,但各自的推理过程不需要互相可见;审核员只需要看到最终稿和审核标准。如果全部共享,上下文会爆炸且互相干扰;如果全部隔离,信息又传不过去。合理的做法是让共享部分走会话消息,私有部分留在各自 AgentState 里。
3. 上下文压缩的核心机制与参数详解
3.1 压缩触发的三种时机
上下文压缩不是随时都在做的,AgentScope 2.0 里主要有三种触发时机,理解它们对调优很关键。
第一种是阈值触发:当累计 token 数超过设定阈值(比如窗口的 70%)时自动触发压缩。这是最常用的方式,好处是可控,坏处是如果单条消息特别长,可能在触发前就已经超限了。
第二种是轮次触发:每 N 轮对话后压缩一次。适合对话节奏稳定的场景,但对突发长消息不敏感。
第三种是手动触发:在关键节点(比如一个子任务完成)主动调用压缩。这种方式最灵活,我在做长任务链时经常用,因为子任务边界是天然的压缩点,此时压缩语义最清晰。
实际项目里我一般组合使用:阈值触发兜底,手动触发优化。纯靠阈值容易在关键时刻压缩导致信息丢失,纯手动又容易忘记。
3.2 压缩策略的选择:摘要、截断还是结构化提取
AgentScope 2.0 支持多种压缩策略,选哪种取决于你的任务类型。
| 压缩策略 | 适用场景 | 信息保留度 | 额外成本 |
|---|---|---|---|
| 滑动窗口截断 | 短对话、对历史依赖低 | 低 | 无 |
| LLM 摘要 | 长对话、需要语义连贯 | 中高 | 有推理成本 |
| 结构化提取 | 工具调用密集、状态明确 | 高 | 中等 |
| 混合策略 | 复杂长任务 | 高 | 中等偏高 |
滑动窗口截断最简单,就是只保留最近 K 轮,前面的直接丢。适合那种"历史不重要、只看最近上下文"的场景,比如客服问答。但用在长任务上会丢关键信息。
LLM 摘要就是用模型把历史对话浓缩成一段摘要。好处是语义连贯,坏处是摘要本身要花 token,而且摘要质量依赖模型能力。我踩过的坑是:摘要如果太激进,会把工具调用的具体参数丢掉,导致后续 Agent 无法复现之前的操作。
结构化提取是我最推荐的策略,尤其适合工具调用密集的场景。它不把历史压成自然语言,而是提取成结构化状态,比如"已查询的订单号列表""已确认的用户偏好"。这样信息无损且紧凑。AgentScope 2.0 里可以通过自定义压缩函数来实现。
3.3 关键参数配置与计算过程
配置压缩时几个参数必须搞清楚,我结合实际计算说明。
max_context_tokens:模型窗口上限。假设你用的是 128K 窗口的模型,这个值设 128000。但注意,这是输入 + 输出的总和,所以要给输出留空间。
compression_threshold:触发压缩的比例。我一般设 0.7,也就是累计到 89600 token 时触发。为什么不是 0.9?因为压缩本身需要一次推理调用,这次调用的输入就是当前上下文,如果等到 0.9 才压缩,压缩调用自己就可能超限。
reserve_tokens:压缩后保留的 token 预算。设太小会丢信息,设太大压缩没意义。我的经验值是窗口的 20%~30%,128K 窗口下大概 25000~38000。
keep_recent_rounds:保留最近几轮原始消息不压缩。这个很重要,因为最近的消息往往和当前任务最相关。一般设 3~5 轮。
计算一下:假设窗口 128K,threshold 0.7,reserve 25K,keep_recent 4 轮。当累计到 89600 token 时触发压缩,把 4 轮之前的历史压缩到 25K 以内,加上最近 4 轮的原始消息(假设 8K),压缩后总上下文约 33K,留出充足空间继续跑。这个配置我在多个项目里验证过,比较稳。
注意:不同模型的 tokenizer 不一样,同样的文本 token 数可能差 20% 以上。配置时最好用目标模型自己的 tokenizer 来估算,别用通用估算。
4. 实操:从零配置一套带压缩的会话系统
4.1 环境准备与基础会话搭建
先把基础环境跑起来。AgentScope 2.0 的安装和基础 Agent 创建这里不展开,重点讲会话和压缩的配置。
from agentscope.agent import ReActAgent from agentscope.session import Session from agentscope.state import AgentState from agentscope.model import OpenAIChatModel # 初始化模型 model = OpenAIChatModel( model_name="your-model", api_key="your-key", ) # 创建 Agent agent = ReActAgent( name="assistant", model=model, sys_prompt="你是一个严谨的助手。", ) # 创建会话,绑定 Agent session = Session( agents=[agent], max_context_tokens=128000, compression_threshold=0.7, reserve_tokens=25000, keep_recent_rounds=4, )这段代码的关键在于 Session 的初始化参数。max_context_tokens要和实际模型窗口对齐,compression_threshold和reserve_tokens按前面算的比例来。keep_recent_rounds我建议从 4 开始调,任务越复杂可以适当加大。
4.2 自定义压缩函数的实现
内置策略不够用时,AgentScope 2.0 允许你传入自定义压缩函数。这是它比较灵活的地方。下面是一个结构化提取的示例思路。
def structured_compress(messages, reserve_tokens): # 分离工具调用记录和普通对话 tool_calls = [m for m in messages if m.role == "tool"] dialogues = [m for m in messages if m.role != "tool"] # 工具调用提取成结构化摘要 tool_summary = extract_tool_state(tool_calls) # 普通对话用 LLM 摘要 dialogue_summary = llm_summarize(dialogues, max_tokens=reserve_tokens // 2) return build_compressed_context(tool_summary, dialogue_summary)这个函数的核心逻辑是分类处理:工具调用记录提取成结构化状态(比如"已调用 search 工具 3 次,查询关键词为 X、Y、Z"),普通对话用 LLM 摘要。这样既保住了工具调用的可复现性,又压缩了对话。
extract_tool_state需要你自己实现,思路是遍历工具调用消息,把工具名、参数、结果的关键字段抽出来。llm_summarize就是调一次模型做摘要,注意控制输出长度。
提示:自定义压缩函数里不要做太重的推理,否则压缩本身的开销会抵消掉压缩带来的收益。我一般把压缩调用的输出限制在 reserve_tokens 的一半以内。
4.3 多 Agent 会话的配置方式
多 Agent 场景下,会话配置要区分共享和隔离。下面是一个三 Agent 流水线的配置示例。
researcher = ReActAgent(name="researcher", model=model, sys_prompt="...") writer = ReActAgent(name="writer", model=model, sys_prompt="...") reviewer = ReActAgent(name="reviewer", model=model, sys_prompt="...") session = Session( agents=[researcher, writer, reviewer], shared_context=True, # 共享会话消息 isolate_agent_state=True, # 隔离各自状态 max_context_tokens=128000, compression_threshold=0.7, )shared_context=True让三个 Agent 看到同一份会话消息,isolate_agent_state=True让各自的 AgentState 独立。这样研究员的研究资料能通过共享消息传给写作员,但各自的推理链互不干扰。
如果某个 Agent 需要完全独立(比如它要处理敏感信息),可以把它单独放到另一个 Session 里,通过显式的消息传递来通信。这种"会话间通信"的模式在多团队协作场景下很常见。
4.4 状态持久化与断点续跑
长任务最怕中途挂掉重跑。AgentState 的序列化能力就是为这个准备的。
# 保存状态 state_dict = session.dump_state() with open("session_state.json", "w") as f: json.dump(state_dict, f) # 恢复状态 with open("session_state.json") as f: state_dict = json.load(f) session.load_state(state_dict)dump_state会把所有 Agent 的 AgentState 和会话消息序列化。恢复时load_state直接还原。我实测下来,一个跑了 30 轮的任务,状态文件大概几百 KB,加载几乎瞬间完成。
这里有个坑:序列化时要注意消息里的非 JSON 类型。比如工具返回的 datetime 对象、自定义类实例,直接 dump 会报错。我的做法是在消息进入会话前就统一转成可序列化格式,或者自定义序列化器处理这些类型。
5. 常见问题与排查技巧实录
5.1 上下文超限报错的排查路径
maximum context length报错是最常见的。排查顺序我总结成一张表。
| 排查项 | 检查方法 | 常见原因 |
|---|---|---|
| 单条消息过长 | 打印每条消息 token 数 | 工具返回了超大结果 |
| 压缩未触发 | 检查 threshold 配置 | 阈值设太高或未启用 |
| 压缩后仍超限 | 检查 reserve_tokens | 保留预算设太大 |
| tokenizer 不匹配 | 用目标模型 tokenizer 重算 | 估算偏差 |
| 输出预留不足 | 检查 max_tokens 设置 | 输出空间被挤占 |
我遇到最多的是工具返回超大结果。比如一个搜索工具返回了 5 万字的网页内容,单条消息就把上下文撑爆了。解决办法是在工具层做截断,或者对工具结果单独做压缩。AgentScope 2.0 里可以给工具配置结果长度上限。
另一个高频问题是压缩后仍超限。这通常是 reserve_tokens 设太大,压缩后保留的内容加上最近几轮还是超了。这时候要么调小 reserve,要么调小 keep_recent_rounds。
5.2 压缩导致信息丢失的应对
压缩最怕丢关键信息。我踩过的典型坑是:Agent 在早期确认了用户的一个约束条件(比如"预算不超过 5000"),压缩时被摘要掉了,后续 Agent 推荐了超预算方案。
应对办法有三个。第一,关键信息结构化保留,不要依赖 LLM 摘要。像用户约束、任务目标这类信息,在压缩时单独提取成结构化字段强制保留。第二,压缩点选在子任务边界,此时语义完整,压缩损失最小。第三,保留可追溯性,压缩后的摘要里标注"原始对话有 N 轮,如需详情可回溯",配合状态持久化,必要时能查回原文。
注意:不要指望压缩做到无损。压缩的本质是取舍,关键是取舍的规则要符合你的业务优先级。金融、医疗这类场景,宁可多花 token 也别丢关键约束。
5.3 多 Agent 状态污染的排查
多 Agent 场景下,状态污染表现为:A Agent 的推理结果莫名其妙出现在 B Agent 的上下文里,或者 B Agent 基于 A 的私有状态做了错误决策。
排查思路是先确认隔离配置。检查isolate_agent_state是否为 True,检查是否有 Agent 被错误地放进了共享会话。然后检查消息路由,看是不是有消息被广播到了不该到的 Agent。
我遇到过一次诡异的状态污染:两个 Agent 共享了同一个 model 实例,而 model 实例内部缓存了上一次的请求上下文。这种情况要确保每个 Agent 用独立的 model 实例,或者确认 model 实现是无状态的。
5.4 性能与成本的平衡技巧
压缩本身有成本,用不好会得不偿失。几个实测有效的技巧。
压缩频率别太高。每次压缩都是一次推理调用,如果每轮都压缩,成本反而上升。我一般让压缩间隔至少 5 轮以上。
摘要用便宜模型。压缩摘要不需要最强模型,用一个小模型做摘要,成本能降一个数量级,质量损失可接受。
缓存压缩结果。如果一段历史被压缩过,后续不要再重复压缩。AgentScope 2.0 里可以通过状态版本号来判断,只压缩新增部分。
监控 token 曲线。我会在会话里记录每次请求的 token 数,画成曲线。正常应该是锯齿状(增长到阈值后压缩下降),如果看到单调上升,说明压缩没生效。
6. 我在实际项目中的几点体会
最后分享几个文档里不会写、但实际很关键的经验。
第一,会话设计要前置。很多人是先写 Agent 逻辑,最后才想会话怎么管,结果发现状态到处乱飞。我的做法是项目一开始就定好:哪些状态属于 Agent 私有,哪些属于会话共享,压缩策略是什么。这个设计定下来,后面写代码会顺很多。
第二,压缩策略要可观测。压缩是"黑盒"操作,出了问题很难查。我会在压缩函数里打日志,记录压缩前后的 token 数、保留了哪些关键信息、丢弃了什么。这些日志在排查"Agent 为什么失忆"时是救命稻草。
第三,别过度依赖自动压缩。自动压缩是兜底,不是万能。关键节点的手动压缩往往效果更好,因为你知道此刻什么信息重要。我习惯在子任务完成、用户确认关键信息后主动触发一次压缩,把这段历史固化下来。
第四,AgentState 的版本管理值得投入。长任务里状态会不断演化,如果能有版本号,就能实现"回到某个检查点重跑"。这在调试和容错上价值很大。AgentScope 2.0 的状态对象支持版本标记,建议用起来。
这套会话与压缩机制,本质上是在"记忆"和"成本"之间找平衡点。没有一劳永逸的配置,只有贴合业务的调优。我上面给的参数是起点,具体数值还得根据你的任务长度、工具调用密度、模型窗口来调。多跑几个长任务,把 token 曲线和压缩日志盯紧,慢慢就能找到适合自己场景的那组参数。