AgentScope Java 从零(03):Agent 的记性默认全开,我在第 50 轮翻了车

图:三层抽屉柜、混乱的工单号、E03 小旗------Agent 的记性默认全开,但它把"第一个"记错了

50 轮对话,上下文压成 8 条消息,压缩摘要里工单 1024 的每个字段都在------然后它把「最开始问的那个工单」答成了 1025。

这是 E03 验收第 50 轮的翻车现场。更有意思的是翻车之前那个发现:Agent 的记性根本不是这一集「接进去」的,它从 E01 第一天就默认全开------状态落盘、长期记忆、上下文压缩,一样没配置过,一样都没闲着。

本文基于 AgentScope Java 2.0.1 (截至 2026-09-07)编写,代码来自实战仓库 ginkgo-agentdev-E03 分支(commit ee031de),该领域迭代极快,请以官方文档为准。

新读者一句话定位:这是「手搓企业智能体」系列第 3 集------用 AgentScope Java 从零养一个 IT 服务台智能体,路线是「能对话(E01)→ 能查数(E02)→ 有记性(本集) → 有知识 → 能干活 → ...... → MVP 上线」。每集代码先跑通再写稿,PRD 与仓库全公开。

它把「第一个」答成了「最常问的」

E03 的验收标准是 PRD 里写死的:构造 50 轮超长对话,第 1 轮问工单 1024 的状态,中间 48 轮灌别的话题(1025、Outlook 排查、邮箱申请......),第 50 轮问「回到我最开始问的那个工单,它现在什么状态?」------答案应该指向 1024。

前 49 轮一切正常。第 50 轮,判定行打出一个刺眼的 ❌:

text 复制代码
[第 1 轮] 问:我的工单 1024 现在什么状态?
[第 1 轮] 答:工单 1024(VPN 连不上)当前状态是处理中,由张工跟进......

(中间 48 轮:工单 1025 状态 × N 轮、Outlook 排查 × N 轮、工单 1026 × N 轮、邮箱申请 × N 轮)

[第 50 轮] 问:回到我最开始问的那个工单,它现在什么状态?
[第 50 轮] 答:结合上下文,你最开始询问的工单是 1025(Outlook 收不到邮件)。......

>> 判定(回答应指向工单 1024 / VPN):❌ 首轮信息丢失
>> 压缩观察:状态文件 context 消息条数 = 8(未压缩应为 100 条以上)

注意它的原话:「结合上下文,你最开始询问的工单是 1025」------它不是瞎猜的,它理直气壮地推理过。

排查从落盘的状态文件开始。~/.agentscope/state/ginkgo-service-desk/<userId>/<sessionId>/agent_state.jsoncontext 数组只剩 8 条------压缩确实发生了(50 轮怎么也该攒下 100 多条消息)。第一条消息就是压缩摘要,5 千字符。翻到摘要正文,反直觉的事来了:工单 1024 明明白白在里面,状态、处理人、创建时间一个不缺。

信息没丢。丢的是别的东西。

记性不是接进来的,是默认全开的

按 E03 开工前的规划,我以为这一集要写「接入会话记忆」------写存储、写加载、写上下文拼接。动手前照例先翻框架源码(E02 的教训:文档不写的,源码里都有),HarnessAgent.Builder 的字段默认值给了我第一个反转:

java 复制代码
// agentscope-harness 2.0.1,HarnessAgent.Builder 摘录
CompactionConfig compactionConfig = CompactionConfig.builder().build();  // 压缩:默认启用
MemoryConfig memoryConfig = MemoryConfig.defaults();                     // 长期记忆:默认启用
boolean disableCompaction = false;
boolean disableSessionPersistence = false;                               // 会话落盘:默认启用

翻译一下:E01、E02 两集我一行记忆代码都没写过,但这套东西从第一天就在跑。证据不用去别处找------

状态落盘~/.agentscope/state/ 下面躺着 4 个历史会话的 agent_state.json,全是前两集跑出来的。每一轮 call() 结束,AgentScope 把整份 AgentState 按 (userId, sessionId) 寻址整体写盘,进程退出不丢。

长期记忆.agentscope/workspace/cli-user/MEMORY.md,E02 运行时自动生成的。打开一看,我的 Agent 已经背着我知道了很多事------名下三个工单的完整状态、我每次查询的时间、「用户多次查询 1024,对该工单进展较关注」。最直接的证据是 E03 冒烟时新会话的第一句回答:「工单 1024 目前仍是处理中,状态较上次查询没有变化」------这是个全新 sessionId 的会话,它凭什么知道「上次查询」?MEMORY.md 在每轮推理时被注入 system prompt,这就是跨会话记忆在工作。

上下文压缩:默认动态模式------token 逼近「模型窗口 − 20K」就触发(模型不报窗口时回退 16 万 token 或 50 条消息,源码为准;官方文档页写的 8 万 token 静态默认值与 2.0.1 源码不一致,反编译才看得准)。压缩后保留尾部若干条,前缀蒸馏成一条摘要。

所以 E03 的功课不是「接入记忆」,是「看懂并管住这套已经在跑的记忆」。

三层记忆,各管一段

把源码和落盘产物对齐之后,AgentScope Java 的记忆体系在我脑子里拼成了三层(外加一份档案):

图:左走廊那条竖排虚线就是 flush(异步记忆写入),绿实线是反向注入 system prompt------两条走廊刻意不走层内,跟主数据流分开

  1. 会话上下文 (AgentState):模型当轮可见的消息历史,按 (userId, sessionId) 分桶隔离,call 结束落盘。多轮指代消解(「那它啥时候能好」指向上一轮的 1024)靠的就是这层------上一集其实已经在用了。

  2. 压缩摘要 (Compaction):上下文超限时,把前缀蒸馏成摘要消息替换原历史,尾部保留最近几条。压缩前还会先把原文全文落一份「永不压缩」的日志(sessions/*.jsonl),事后审计随时可回查------我那个 50 轮会话的原始历史,122KB,完整躺在里面。

  3. 长期记忆MEMORY.md):每次 call() 结束后异步跑一次「flush」,从对话里抽取值得长期记住的事实,追加进当天的日流水账,再周期性合并成 MEMORY.md 注入后续所有会话。

  4. 会话上下文 (AgentState):模型当轮可见的消息历史,按 (userId, sessionId) 分桶隔离,call 结束落盘。多轮指代消解(「那它啥时候能好」指向上一轮的 1024)靠的就是这层------上一集其实已经在用了。

  5. 压缩摘要 (Compaction):上下文超限时,把前缀蒸馏成摘要消息替换原历史,尾部保留最近几条。压缩前还会先把原文全文落一份「永不压缩」的日志(sessions/*.jsonl),事后审计随时可回查------我那个 50 轮会话的原始历史,122KB,完整躺在里面。

  6. 长期记忆MEMORY.md):每次 call() 结束后异步跑一次「flush」,从对话里抽取值得长期记住的事实,追加进当天的日流水账,再周期性合并成 MEMORY.md 注入后续所有会话。

我的 50 轮翻车就发生在第 2 层。

教摘要记时序

回头细读那份摘要,真凶现形了。工单 1024 在摘要里确实有,但只有一行;而 1025------被中间 48 轮反复问到的那个------占了摘要的最大篇幅,被标成「Primary open, unresolved issue」,还排在 SESSION INTENT 的第一条:

text 复制代码
## SESSION INTENT
The user's current concerns are: (1) repeatedly checking the status of
Ticket 1025 (Outlook not receiving email); (2) ...

## SUMMARY
- Ticket 1025 (Outlook收不到邮件): ... Primary open, unresolved issue. ...
- Ticket 1024 (VPN连不上): Status 处理中. Assigned to 张工. ...

摘要里没有任何时序信息。而「最开始问的」恰恰是个纯时序指代。模型拿不到顺序,只能拿主题权重当顺序------1025 篇幅最大、位置最靠前、被标记为「主要未决事项」,于是它成了「第一个」。

我给 E03 配置的摘要保留清单本来已经有实体规则(工单号必须原样保留),翻车之后补上了时序规则(代码节选自 AgentFactory.java,追加在框架默认摘要 prompt 之后):

java 复制代码
private static final String ENTITY_RETENTION_RULES = """

        Additional entity retention rules (mandatory):
        - Ticket IDs (e.g. 1024) mentioned anywhere in the conversation MUST be preserved
          verbatim in the SUMMARY section, together with their topic and current status.
        - Preserve the chronological order of the topics the user raised. In particular,
          explicitly mark the FIRST topic/ticket the user asked about in this conversation
          (e.g. "First topic raised: Ticket 1024 (VPN issue)"), and keep that marker across
          re-compactions.
        - Do NOT infer "first" or "earliest" from topic frequency or recency; only the
          recorded chronological order and the first-topic marker are authoritative.
        - Unresolved user questions / pending follow-ups MUST be preserved in NEXT STEPS.
        """;

一句话结论:压缩摘要的 prompt 是记忆质量的杠杆------同样的框架、同样的模型,加五条规则,命运就变了。

复跑 50 轮。这次摘要的 SUMMARY 段开头赫然是:

text 复制代码
**First topic raised: Ticket 1024 (VPN 连不上)** --- mentioned at the very start of
the overall conversation; status unchanged since then: 处理中, assigned to 张工, ...

第 50 轮:

text 复制代码
[第 50 轮] 问:回到我最开始问的那个工单,它现在什么状态?
[第 50 轮] 答:工单 1024(VPN 连不上):
        - 状态:处理中    - 处理人:张工    - 创建时间:2026-09-01 09:20
        这张单子目前是处理中,张工还在跟进,尚未完成。

>> 判定(回答应指向工单 1024 / VPN):✅ 首轮记忆存活

上下文还是 8 条------压缩照常发生,只是这次它记对了顺序。

顺带一提翻车期间的另一个发现:摘要消息里其实明示了「完整对话历史存在 sessions/*.jsonl,需要可回查」,Agent 手里也有 session_search 工具。但它宁可拿摘要猜,也没去翻档案------模型的默认行为是信摘要不查原文,指望它主动回查不如把摘要写对。

/user、/sessions、/resume:把落了灰的状态变成资产

前两集落盘的 4 份 agent_state.json,从来没被恢复过------E01 的 Main 每次启动生成新 UUID,存了等于没存。E03 补上这块,三个命令:

  • /user zhangsan 切换对话用户:各用户独立会话状态与记忆命名空间(会话隔离的验收:A 聊完切到 B 问「我刚才问了什么」,B 的回答是「这是本次对话的第一条消息」------顺带,CLI 期工单数据仍以 mock 默认用户查询,数据级权限 E07 才接)
  • /sessions 列出当前用户的历史会话(扫描状态目录)
  • /resume <序号或会话ID> 挂载历史会话,接着聊

/resume 的恢复效果值得单独贴一段。重启进程、恢复上一场会话、问「刚才我们聊到哪了」:

text 复制代码
你(cli-user) > [已挂载历史会话 cli-cb3ced4f-492f-4671-b8df-ad9a9925bea8]

你(cli-user) > 刚才我们聊到哪了?接着说
agent > 我们还是围绕工单 1024「VPN 连不上」在聊,目前结论没变:处理中,
        张工负责,无预计解决时间。
        我之前一直提供帮你催办张工这个选项,但你还没确认。你想让我催一下吗?

它不只记得聊过什么,还记得对话停在哪一步 ------上一场结束在「要不要催办」的悬而未决上,这场无缝接上。顺带:最初 /resume 只认序号,我验证时习惯性粘贴了 /sessions 列出的完整会话 ID,被用法提示怼了回来------已改成序号、完整 ID 都认。这个交互坑和修复,读者跟练时大概率也会遇到。

本集判断

记忆的默认值是「全开」,企业场景的功课是「管住」。 AgentScope 的选择对原型期很友好------不配置也有记性;但默认全开意味着 MEMORY.md 里我看到了日期幻觉(引用了一个不存在的 2025-06-11.md)和大量重复记录------flush 是每轮 call 后异步跑的,成本和噪音都在暗处长。上线前这些旋钮(flush 频率、保留天数、摘要规则)得一个一个拧到自己的场景上,假装它不存在,它就按自己的性子记。

压缩摘要是一次「转述」,转述必有取舍。 我的实体保留清单保住了工单号的每个字段,却没保住「谁先被提到」------因为写清单的人(我)下意识觉得顺序不重要,直到第 50 轮打脸。摘要 prompt 是这套记忆体系里最值得花心思的一行配置:它决定 50 轮之后 Agent 还记得什么、忘掉什么、记错什么。

状态存了没人用,等于没存。 前两集的会话状态安静落了两集的盘。/resume 十几行代码,把存量状态变成可续接的资产------重启接续、故障恢复、跨端接续(文档里 Redis 状态存储的跨节点会话漂移,走的就是同一套寻址机制)。这大概是本集性价比最高的一处改动。

【写完这集落下个后遗症:跟它聊天时会想,这句会被写进 MEMORY.md 吗?那个文件里躺着一行「用户多次查询 1024,对该工单进展较关注」------像无意翻到同事的笔记本,里面全是关于你的观察记录。】

下集预告:给它一个知识库

50 轮验收里有个反复出现的回答:「我检索了工作区知识库,目前没有收录相关的流程文档」------新员工邮箱申请这个问题,它被问了九遍,诚实了九遍。E04 就去填这个空:文档切分、向量化、检索问答,让「VPN 连不上怎么办」能答得有出处。RAG 的坑,比记忆只多不少。

相关推荐
积硅步致千里1 小时前
Word 里的 .wmf 其实是 EMF:一次 metafile 转 PNG 排障
前端·后端
爱读源码的大都督1 小时前
DeepSeek面试官问:生产RAG系统回答不准确,该如何定位和优化?这样回答,能让面试官当场给你Offer!
java·后端·python
“AI国潮设计-小江”2 小时前
【Python实战】SDXL精准控制“普宁英歌舞×星空蛋糕”IP落地,附核心Prompt与商用授权思路
开发语言·人工智能·python·prompt·aigc
杨运交2 小时前
[069][公共模块]Spring Boot 全局异常处理与参数校验实战(下):校验异常精细化处理与 WebFlux 适配
java·spring boot·后端
airank2 小时前
品牌AI可见性优化实战:六大GEO平台能力对比与选型指南
aigc·geo·生成式引擎优化·品牌ai可见性·ai品牌提及率
Andya_net2 小时前
Spring Boot | 条件注解完全指南:从 @Conditional 到 @ConditionalOnExpression 的原理、实践与避坑
spring boot·后端·python
ly76893 小时前
Spring 中的 @Configuration 与 @Component 差异:为何代理时机决定 Bean 生命周期行为
java·后端·spring·注解·代理·bean生命周期
明月_清风3 小时前
字符串匹配四大经典算法:BF、RK、BM、KMP 到底有什么区别?
后端·算法
卷无止境3 小时前
测试全绿,功能能跑,代码却烂到没法上线:AI编程助手留下的十个坑
后端·python