AgentScope第19式·化繁压境

AgentScope第19式·化繁压境

上下文一满,数字人先忘目标再胡说。

[读者] 正在用 AgentScope Java 构建长会话数字人的 Java 工程师
[痛点] 几十轮交流后上下文溢出,截断就丢“当前目标”
[现在读] v2.0.3 Harness 已给出 Compaction 与 Tool Result Eviction 路径
[读完] 能给数字人加上阈值触发、结构化摘要和工具结果驱逐

[旧方案] 全量 messages + 超大工具结果直塞
|
v
[新需求] 长会话仍保留当前目标、已完成与下一步
|
v
[冲突] 历史越长信息越全,但 token 预算固定
|
v
[后果] 截断丢状态,溢出报错,工具结果永久占位

我是老李,这轮还是围绕那个已经冻结的最小交互数字人 Demo。它不换场景,仍然是 Web 管理端、Spring Boot、MySQL、LiveKit 和 AgentScope Java 推理这一条线。前十八式已经让它可以配项目、跑实时会话、接 Tool 和 Memory,但没人想在一个几百轮的演示里被上下文长度拦住。

第18式之后,数字人已经能把跨轮状态存进 Memory,也能在 /run/{projectId} 里连续回答。问题不在“能不能记住”,而在“记多久”。当一次知识检索返回几万字,再叠上几十轮闲聊,messages 会迅速把模型窗口吃满。

压缩到底在什么阈值触发,触发后又该保留哪些东西?

结构化摘要要留下哪些字段,才能让数字人回答“当前目标/已完成/下一步”?

一次超大知识检索结果,是该硬塞、截断,还是能驱逐后按需召回?

如果只截断不压缩,任务连续性会在第几轮断掉?

01、故事

演示前二十分钟,产品经理连着问了数字人几十轮:先问“本月主推产品是什么”,再问“帮我对比竞品”,又插入一句“刚才我们到底在聊哪件事”。前面的回答还正常,到第三十轮左右,AgentScope Java 侧开始返回上下文超限;临时把老消息删掉后,数字人倒是能说话了,但张口就忘掉“当前目标是做竞品对比”,把之前的寒暄当成了主线。

任务是:别让上下文预算决定数字人的任务连续性。我们要在 chapter/19-compaction 分支上,把长对话和超大工具结果管起来。这一章不加新业务页面,也不做多 Agent 编排,只处理一个工程冲突:信息越来越多,窗口不能变大。

后来我们复盘时发现,最早的做法太“老实”:每轮都把完整 messages 交给模型,工具结果也原样塞进去。数字人不是不聪明,是被塞到缺氧。

长会话里藏长愁
一回检索压满楼
目标一丢人先乱
先冻现场再动手

02、问题

旧方案失效得很典型:在 Memory 之外,又把会话历史全量拼进推理请求;RAG 工具返回的文档整段进入消息列表,且没有过期或驱逐机制。业务影响是数字人会在长会话里丢失“当前目标”,表现为答非所问、重复提问、把早先话题当主线。技术表现是模型侧报上下文超限,或者手工截断后状态字段被切掉。

为什么不能一直靠截断?因为截断是按位置删,不是按语义删。它可能留下十句寒暄,却恰好删掉“已完成:收集了三家竞品”。数字人的任务连续性不是靠消息条数维持的,而是靠几个稳定状态字段维持的。

本篇的完成标准有三条。第一,达到阈值后能自动触发压缩,不靠人工删消息。第二,压缩后模型仍能回答“当前目标/已完成/下一步”。第三,超大工具结果不会永久占满上下文,需要时还能按引用找回。

截断看似最省事
状态一丢难再续
溢出报错非小病
先定验收再下笔

03、原理

Compaction 不是“删历史”,而是把历史重新表达成低 token、高信息密度的状态。AgentScope Java v2.0.3 的 Harness 文档在 docs/harness/compaction.md 中描述了压缩的定位与触发思路,源码侧对应 ConversationCompactor 和 CompactionMiddleware。前者的职责是把对话历史压成结构化 Summary,后者的职责是在中间件链路里判断阈值、调用压缩并回到正常推理。

Tool Result Eviction 是另一条线:工具结果不应无限期占用上下文。对于一次超大知识检索,更合理的做法是先把它压成“引用 + 摘要 + 可召回 ID”,而不是把所有原文都留在 messages 里。这里有个反直觉判断:压缩的收益首先来自“驱逐工具结果”,其次才是压缩对话。因为工具结果的 token 密度往往远高于闲聊。

第二个反直觉判断是:结构化 Summary 不是越短越好。如果摘要只写“用户聊过产品”,模型仍然回答不了“当前目标/已完成/下一步”。Compaction 的目标不是让上下文变短,而是让关键状态在预算内保持可读。

压缩并非做减法
结构化后才是家
工具结果先驱逐
再让摘要说人话

04、架构

[输入] 用户消息 / LiveKit 转写
    |
    v
[模块] CompactionMiddleware
    |
    v
[数据/状态] Conversation 消息列表 + token 预算
    |
    v
[处理] ConversationCompactor 生成结构化 Summary + 驱逐工具结果
    |
    v
[输出] 压缩后的 messages 交给 LLM

边界:Compaction 只负责上下文窗口内的压缩与驱逐,不负责长期记忆存储,也不改变 LiveKit 的实时传输职责。收益:长会话不会因为 token 超限而崩溃,任务状态可以跨轮保留。代价:摘要本身有信息损失,压缩动作也会引入额外推理调用或摘要模型调用。适用条件:会话轮次长、工具结果大、状态字段可结构化。不适用条件:短会话、强审计场景要求保留全量原文、或者任务状态无法被摘要表达。

AgentScope Java 侧的关键位置在 agentscope-harness 模块的 CompactionMiddleware 与 ConversationCompactor。按 v2.0.3 的源码路径,ConversationCompactor.java 位于 agentscope-harness/src/main/java/io/agentscope/harness/agent/memory/compaction/,CompactionMiddleware.java 位于 agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/。具体 API 签名需以固定 tag v2.0.3 源码为准。

中间件在回路中
压缩器管量体裁
驱逐策略留引用
输出仍见目标在

05、实战一次

环境仍是 AgentScope Java v2.0.3。分支从第18式继续创建,不新建 Demo 场景。示例命令如下,仅作分支与提交建议,未在当前环境执行:

git checkout chapter/18-*
git checkout -b chapter/19-compaction
# 修改完成后建议提交
git add .
git commit -m "feat(ch19): compact long conversations and tool results"

依赖侧,本章用到的是 agentscope-harness 中的 Compaction 能力,保持与 v2.0.3 一致。核心配置思路是注册 CompactionMiddleware,给它配置 ConversationCompactor、触发阈值、保留轮次和工具结果驱逐策略。下面 Java 代码是按官方文档整理的示意,实际 builder 名称与参数需按 v2.0.3 源码核验;未在当前环境实测,以下为预期结果。

CompactionMiddleware compaction = CompactionMiddleware.builder()
    .compactor(ConversationCompactor.builder()
        .maxTokens(12000)
        .keepRecentTurns(8)
        .summaryFields(List.of("currentGoal", "completed", "nextStep", "entities"))
        .build())
    .triggerThreshold(0.75)
    .toolResultEviction(ToolResultEviction.builder()
        .maxToolResultTokens(2000)
        .keepReferenceId(true)
        .build())
    .build();

Agent agent = Agent.builder()
    .name("demo-digital-human")
    .model(model)
    .middleware(compaction)
    .build();

启动方式沿用前章 Spring Boot 服务。请求侧仍是 Web 运行页 /run/{projectId},由 LiveKit 把用户语音送到 Realtime Bridge,再交给 AgentScope Java。验证时构造三段输入:先聊十轮业务目标,再触发一次返回超大文本的知识检索,最后问“当前目标是什么、已完成什么、下一步做什么”。示例输出应当包含压缩前、压缩后和模型回答三部分,这里不伪造实测结果。

分支从十八章来
配置阈值先打开
摘要字段要固定
请求一轮看状态

06、排查

第一条证据链:压缩后数字人仍忘目标。现象是模型回答“我们刚才在聊天气”,但实际当前目标是竞品对比。怀疑是 Summary 只保留了最近对话文本,没有保留任务字段。检查时应打开 ConversationCompactor 的输出,确认压缩后 messages 里是否存在 currentGoal、completed、nextStep。证据是官方文档强调结构化摘要,而不是自由文本摘要。根因是摘要字段缺失。修复方式是把 Summary schema 固定为结构化字段,并要求摘要器必须输出这四个键。

第二条证据链:超大工具结果被驱逐后,数字人又问同一个查询。现象是工具结果不再占上下文,但模型反复调用同一个知识检索。怀疑是驱逐太激进,连引用 ID 和摘要都没留。检查工具结果驱逐策略,确认是否保留了 referenceId 和一段短摘要。证据是 Tool Result Eviction 的设计目标不是“删除”,而是“把原文替换成可召回引用”。根因是驱逐策略只删不缩。修复方式是把工具结果改为“摘要 + 引用 ID + 原始结果存储位置”。

错误尝试: 一开始我们直接把 keepRecentTurns 调到 3,以为少留几轮就能省 token。这个做法错在把“保留轮次”当成了压缩策略。它确实降低了 token,但也把当前目标和已完成状态一起删掉,任务连续性反而更差。

上游贡献判断:本章未发现明确上游问题。若在最小 Demo 中复现出“文档未给出 Summary schema 推荐字段”的缺口,可作为 Issue/PR 候选,进一步补充官方 compaction 文档;但不把尚未提交的候选写成已提交。

现象先看忘目标
怀疑摘要缺关键
证据要对源码看
根因常在字段少

07、优化

基于第06章证据形成 V2:把“截断式保存最近 N 轮”改成“结构化 Summary + 工具结果驱逐”。修改点有三处。第一,Summary schema 固定为 currentGoal、completed、nextStep、entities,其中 entities 可放产品名、项目名、用户约束。第二,工具结果超过阈值时,不再原样进入 messages,而是生成短摘要和 referenceId,原始结果放到外部存储。第三,触发阈值按模型窗口百分比设置,而不是写死消息条数。

原因很直接:数字人的任务连续性依赖状态字段,不依赖聊天记录长度。新行为是达到阈值后自动压缩,模型仍能回答三个问题;超大工具结果也不会永久占满上下文。验证方法是在 chapter/19-compaction 分支上构造长会话与一次超大检索,分别检查压缩前后的 messages 和模型回答。验证结果应在本地环境实际运行后记录,本文不预设通过。

字段补齐再压缩
保留最近几轮话
驱逐改为带引用
验证三问能回答

08、演进

[同一输入]
    |
    +--[V1] 全量 messages,溢出/截断丢状态
    |
    +--[V2] 阈值触发压缩 + 工具结果驱逐,保留结构化状态
    |
[Trade-off]
得到:长会话可继续、任务状态可读、工具结果不再永久占位
失去:摘要必然有信息损失,压缩动作引入额外成本
适用边界:长会话、大工具结果、可结构化状态

正确性上,V2 用结构化字段维持任务连续性,比按位置截断更稳。稳定性上,V2 多了一次压缩调用,链路更长,但避免了上下文溢出导致的硬失败。复杂度上,V2 引入了 Summary schema 与驱逐策略,需要维护字段契约。成本上,压缩可能增加一次模型调用或摘要模型调用,但节省了长上下文 token。适用范围是长会话数字人、知识检索型 Agent。遗留问题是摘要模型本身可能遗漏关键实体,需要后续评测覆盖。

同输入走两条路
全量塞满必溢出
压缩后留结构化
得失权衡要写清

09、洞见

9.1 压缩的本质是状态重述

反直觉判断:压缩不是删除记忆,而是把记忆换成更稳定的表达。数字人回答“当前目标/已完成/下一步”靠的是字段,不是原始消息顺序。

9.2 工具结果驱逐应优先于对话压缩

反直觉判断:节省 token 的第一刀往往不是砍闲聊,而是砍工具结果。一次大检索可能比五十轮寒暄更占预算。

9.3 什么时候不应该用本章能力

短会话、强审计场景、必须保留原文证据的合规会话,不应依赖压缩摘要替代原文。此时更合适的做法是外部存档加引用,而不是把摘要当作唯一事实源。

9.4 阈值不是越早越好

触发阈值设得太低会导致压缩过频,增加调用成本并放大摘要误差;设得太高又容易在工具结果突增时直接溢出。工程上应结合模型窗口和业务轮次分布验证。

短会话别忙压缩
审计场景慎驱逐
反直觉在状态上
不是删掉是重述

10、系统落地

原来有什么:chapter/18 之后,数字人已经能跑连续会话和 Memory,但上下文随轮次线性增长,大工具结果没有驱逐策略。本篇新增什么:在 chapter/19-compaction 分支上引入 Compaction 中间件、结构化 Summary 和 Tool Result Eviction。现在能做什么:达到阈值后自动压缩,模型仍能回答当前目标、已完成和下一步,超大工具结果不再永久占上下文。还缺什么:压缩质量的评测集、摘要字段的长期一致性、以及生产环境的压缩成本观测。下一步演进:可以在后续章节里把压缩结果接入可观测指标,并用评测集验证任务连续性。

开源参与点:本章未发现明确上游问题。若在最小 Demo 中复现出文档示例不足或边界行为不清晰,可作为 Issue/PR 候选,需先搜索官方 Issues 与 Discussions,再决定是否提交。

原样全量已成障
新增压缩与驱逐
现在能答三问句
还缺评测与回放

11、小结

Q1 → 压缩在上下文达到预设阈值后触发,触发后保留最近若干轮对话,并把更早历史压成结构化摘要。
Q2 → 结构化摘要应保留 currentGoal、completed、nextStep、entities 等字段,才能让数字人回答“当前目标/已完成/下一步”。
Q3 → 超大工具结果超过阈值后应被驱逐为摘要加引用 ID,原文外置存储,需要时按引用召回。
状态 → chapter/19-compaction 可继续承载压缩与驱逐能力,后续章节可在此分支上增加评测与观测。

[长会话] -> 阈值触发 -> ConversationCompactor -> 结构化 Summary
[大工具结果] -> Tool Result Eviction -> 摘要 + referenceId
[模型] -> 当前目标/已完成/下一步

Q 一压缩何时来
Q 二摘要保什么
Q 三驱逐怎么做
状态落成分支再

12、作业

12.1 理解题:Compaction 和截断的本质区别是什么?

参考答案:截断按消息位置删除,容易切掉关键状态;Compaction 按结构化语义重述历史,把当前目标、已完成和下一步保留在摘要字段中,目标是维持任务连续性,而不只是降低 token。

12.2 实战题:请基于 chapter/19-compaction 分支,构造一次长会话加超大工具结果,观察压缩前后 messages 的变化。

参考答案:先连续提交三十轮对话,再触发一次返回超长文本的工具调用;开启 CompactionMiddleware 后,检查压缩后的 messages 是否包含结构化 Summary 和工具结果引用。实际结果需在本地环境运行后记录,不应凭想象写通过。

12.3 排障题:压缩后数字人仍忘记当前目标,第一步应检查什么?

参考答案:先检查 ConversationCompactor 的输出中是否包含 currentGoal、completed、nextStep 字段,而不是先调大 keepRecentTurns 或换模型。若字段缺失,应修复 Summary schema。

12.4 架构判断题:如果业务要求保留全部原始对话用于审计,是否还应把 Compaction 作为唯一上下文来源?

参考答案:不应。Compaction 可作为在线上下文策略,但审计原文应外置存储或走独立留痕链路。此时需打开官方源码或文档,核验 compaction 的摘要边界与工具结果驱逐策略,再决定组合方式。

理解问的是触发
实战要对官方源
排障先看摘要字段
架构判断边界先

13、思考

上下文是稀缺资源,压缩不是删除记忆,而是把记忆换成模型更易消费的结构。第19式把“化繁压境”落到两个动作上:对话历史按阈值压缩成结构化 Summary,工具结果按引用驱逐。只要当前目标、已完成和下一步还在,数字人的任务连续性就不会因为窗口满了而断掉。会用框架的人会调阈值,能参与框架演进的人会盯住源码、测试和文档里的边界。

上下文是稀缺地
压缩不是删记忆
结构化后才连续
边界之内见真章

posted @ 2026-10-11 11:08  李福春  阅读(10)  评论(0)    收藏  举报