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 轮。计算一下假设窗口 128Kthreshold 0.7reserve 25Kkeep_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_nameyour-model, api_keyyour-key, ) # 创建 Agent agent ReActAgent( nameassistant, modelmodel, sys_prompt你是一个严谨的助手。, ) # 创建会话绑定 Agent session Session( agents[agent], max_context_tokens128000, compression_threshold0.7, reserve_tokens25000, keep_recent_rounds4, )这段代码的关键在于 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_tokensreserve_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(nameresearcher, modelmodel, sys_prompt...) writer ReActAgent(namewriter, modelmodel, sys_prompt...) reviewer ReActAgent(namereviewer, modelmodel, sys_prompt...) session Session( agents[researcher, writer, reviewer], shared_contextTrue, # 共享会话消息 isolate_agent_stateTrue, # 隔离各自状态 max_context_tokens128000, compression_threshold0.7, )shared_contextTrue让三个 Agent 看到同一份会话消息isolate_agent_stateTrue让各自的 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 曲线和压缩日志盯紧慢慢就能找到适合自己场景的那组参数。
