Agent Scope Java 2.x 系列【24】Harness:记忆配置(MemoryConfig)
文章目录
1. 概述
MemoryConfig 是 AgentScope 框架 Agent 运行层的长期记忆流水线统一配置类,统一管理三大 LLM 驱动记忆流程的全部参数:
Flush(会话持久落盘记忆):从对话窗口提取长期记忆写入每日账本memory/YYYY-MM-DD.mdConsolidation(记忆合并归档):定时合并多日账本,生成精简总记忆文件MEMORY.mdCompaction(上下文压缩摘要):对话前缀压缩,该逻辑配置独立放在CompactionConfig,本类仅关联不实现
2. 常量定义
常量定义解读:
// 合并操作最大Token配额默认值
public static final int DEFAULT_CONSOLIDATION_MAX_TOKENS = 4_000;
// 两次记忆合并最小间隔:30分钟
public static final Duration DEFAULT_CONSOLIDATION_MIN_GAP = Duration.ofMinutes(30);
// 每日账本保留90天后归档
public static final int DEFAULT_DAILY_FILE_RETENTION_DAYS = 90;
// 会话日志JSONL保留180天后清理
public static final int DEFAULT_SESSION_RETENTION_DAYS = 180;
3. 核心字段
核心字段说明表:
| 字段 | 类型 | 作用 | null 逻辑 / 约束 |
|---|---|---|---|
| model | Model | 记忆流水线专用大模型 | null = 复用 Agent 主推理模型 |
| flushPrompt | String | 记忆落盘阶段自定义系统提示词 | null = 使用 MemoryFlushManager 默认提示词,无占位符要求 |
| consolidationPrompt | String | 记忆合并阶段自定义提示词 | null = 使用 MemoryConsolidator 默认提示词;自定义文本必须包含恰好 2 个 %d 占位符 |
| consolidationMaxTokens | int | 记忆合并时传给 LLM 的 Token 上限 | 默认 4000,数值必须大于 0 |
| consolidationMinGap | Duration | 两次记忆合并维护操作的最小间隔 | 默认 30 分钟,不允许 null、不允许负时长 |
| dailyFileRetentionDays | int | 每日记忆账本文件留存天数 | 默认 90,数值必须大于 0,到期移入归档目录 |
| sessionRetentionDays | int | 会话交互 JSONL 日志留存天数 | 默认 180,数值必须大于 0,到期自动清理 |
| flushTrigger | FlushTrigger | 每次 Agent 调用后的记忆落盘触发策略 | 默认 FlushTrigger.always(),构造时禁止传入 null |
4. Flush 模式
控制每次 Agent 调用时的第一层记忆落盘策略:
| 枚举值 | 含义 |
|---|---|
| ALWAYS | 每次 Agent 调用后强制落盘(Builder 默认值) |
| NEVER | 关闭单次调用触发落盘,仅后台离线落盘生效 |
| THROTTLED | 节流模式,最小间隔内只执行一次落盘 |
4.1 节流模式
默认情况下,每次 Agent 调用结束都做一次强制落盘,对长会话来说成本不低。
Flush 存在 3 条触发路径:
- 路径
1:per-call flush(单次 Agent 调用后置落盘) - 路径
2:压缩前置flush - 路径
3:溢出兜底flush
注意事项:
THROTTLED只影响路径1(per-call flush)。压缩前置的flush(路径 2)和兜底flush(路径3)按各自的触发条件照常跑 —— 压缩很少发生,那两条本来就不频繁。- 用户每一轮完整对话消息(
sessions/<会话ID>.log.jsonl)不受影响,session JSONL仍然每次写完整。session_search和会话恢复正常工作。
示例,节流到「最多每 10 分钟一次」:
HarnessAgent agent = HarnessAgent.builder()
.name("harness-demo")
.description("HarnessAgent Demo")
.model(model)
.memory(MemoryConfig.builder()
.flushTrigger(MemoryConfig.FlushTrigger.throttled(Duration.ofMinutes(10)))
.build())
.workspace(workspacePath)
.maxIters(5)
.build();
4.2 关闭模式
可完全关闭每次 call() 结束时的 Flush ,只有压缩发生时才会 Flush。
示例 :
.memory(MemoryConfig.builder()
.flushTrigger(MemoryConfig.FlushTrigger.never())
.build())
5. 关闭记忆
flushTrigger(FlushTrigger.never()) 仅关闭 Agent 每轮 call() 结束后的记忆 Flush ,使用 .disableMemoryHooks() 可以完全关闭整套记忆管线。
全部禁用:
- 所有三条
Flush触发路径(per-call/ 压缩前置 / 溢出兜底) - 后台
Consolidation定时合并维护任务 - 不再生成
memory/YYYY-MM-DD.md、MEMORY.md两层长期记忆文件
仅保留:Offload 原始会话 jsonl 落盘(原始对话日志持久化不受此开关影响)
适用场景:完全不需要 Agent 跨会话记忆、不需要长期记忆汇总,只留存原始会话日志用于检索审计
关键差异对比表:
| 配置方式 | per-call flush(路径1) | 压缩前置flush(路径2) | 溢出兜底flush(路径3) | 后台Consolidation合并 | 长期记忆文件(memory/*) | session jsonl原始日志offload |
|---|---|---|---|---|---|---|
| flushTrigger(NEVER) | ❌ 关闭 | ✅ 正常执行 | ✅ 正常执行 | ✅ 定时运行 | ✅ 正常生成 | ✅ 每次完整写入 |
| .disableMemoryHooks() | ❌ 关闭 | ❌ 关闭 | ❌ 关闭 | ❌ 停止运行 | ❌ 不再生成 | ✅ 每次完整写入 |
示例:
HarnessAgent.builder()
...
.disableMemoryHooks() // 关掉 flush + 后台维护
.disableMemoryTools() // 不注册 memory_search / memory_get / session_search
.build();
6. Flush 提示词
6.1 默认提示词
框架内置默认的记忆抽取提示词(翻译后):
public static final String DEFAULT_FLUSH_PROMPT =
"""
你是一名记忆提取助手。请分析下方对话内容,提取出需要留存、供后续会话参考的关键事实、决策、用户偏好与上下文信息。
仅以 Markdown 无序列表格式输出提取到的记忆内容。每条记录需简洁完整、独立表意;如有日期、人名、具体细节务必一并保留。
若无任何值得留存的信息,严格只输出:NO_REPLY
提取规范:
- 提取用户偏好、个人信息、项目相关决议
- 记录重要技术方案及背后考量依据
- 记下各类承诺、截止时间、待执行事项
- 留存人员协作关系(人员分工、团队组织架构)
- 忽略常规寒暄、工具调用过程、临时瞬时状态信息
重要写入与追加规则:
- 你本次写入的是**今日每日记忆流水账**(memory/YYYY-MM-DD.md),而非全局汇总文件 MEMORY.md。每日流水账仅支持追加写入,你的输出会追加到文件已有内容末尾。
- MEMORY.md 是整理后的长期记忆文件,仅作为只读参考上下文。不要重复记录已在 MEMORY.md 或今日早前条目里存在的内容;系统会通过独立的合并任务,定期将每日新增记录汇总至 MEMORY.md。
- 每条列表条目需独立完整,便于单独检索查询。
""";
这是
Flush记忆抽取流程专用系统提示词,供给LLM使用,对应三条Flush触发路径(每次调用后置、压缩前置、溢出兜底),统一指导大模型从当前对话里筛选、提炼长期记忆并按规范输出。
6.2 追加规则
复用框架内置默认的记忆抽取提示词,在末尾拼接项目专属约束规则,无需完整重写整套 flush 提示词,减少重复代码。
示例:
.memory(MemoryConfig.builder()
// 在框架默认的记忆抽取提示词尾部,追加项目专属约束规则
.flushPrompt(MemoryFlushManager.DEFAULT_FLUSH_PROMPT + """
额外项目规范要求:
- 禁止记录客户隐私敏感信息(姓名、邮箱、手机号)。
- 项目内部专有术语统一使用中文描述。
""")
.build())
##3 完全自定义
可以完全自定义记忆抽取提示词,注意事项:
- 必须包含恰好两个
%d占位符,顺序依次为最大Token上限、最大字符上限。如果不满足该要求,Builder在构建配置时会直接抛出异常拒绝创建对象。 - 该设计目的是提前拦截配置错误,避免程序运行阶段才抛出
MissingFormatArgumentException格式化缺失异常。
示例:
.memory(MemoryConfig.builder()
// 完全自定义记忆合并提示词
.consolidationPrompt("""
你需要将多份每日记忆流水账合并汇总写入 MEMORY.md 全局长期记忆文件。
输出内容总长度不能超过 %d Token(约 %d 字符)。以完整 Markdown 格式输出整个文件内容。
……此处填写你自定义的合并规则……
""")
.build())
7. 后台维护
配置项说明:
| 配置项 | 本次设置值 | 默认值 | 作用说明 |
|---|---|---|---|
| consolidationMinGap | 2小时 | 30分钟 | 控制后台合并每日账本生成MEMORY.md的最小间隔,拉长间隔减少LLM调用频次,节约token |
| dailyFileRetentionDays | 30天 | 90天 | 每日记忆流水账YYYY-MM-DD.md留存天数,到期移入archive归档目录,释放磁盘 |
| sessionRetentionDays | 60天 | 180天 | 会话原始日志sessions/*.jsonl保存时长,到期自动删除原始对话日志 |
| consolidationMaxTokens | 8000 tokens | 4000 tokens | 合并生成全局长期记忆时LLM输出token上限,放宽后可容纳更多历史记忆,推理时消耗更多输入token |
示例:
.memory(MemoryConfig.builder()
// 两次后台记忆合并任务的最小间隔:至少间隔2小时才会执行一次合并
.consolidationMinGap(Duration.ofHours(2))
// 每日记忆账本文件(YYYY-MM-DD.md)留存30天,到期移入归档目录
.dailyFileRetentionDays(30)
// 会话原始日志session/*.jsonl保留60天,到期自动清理删除
.sessionRetentionDays(60)
// 合并生成全局长期记忆MEMORY.md时,允许LLM最大输出Token提升至8000
.consolidationMaxTokens(8_000)
.build())
8. 配置小模型
记忆抽取(Flush)、记忆合并(Consolidation)、上下文摘要压缩(Compaction)都属于辅助文本加工任务,不需要强大复杂推理能力,不需要主推理级别的高精度大模型;
因此框架支持为这三类记忆流水线单独配置轻量化低成本小模型,大幅降低整体 Token 调用成本。
示例:
HarnessAgent.builder()
.model("openai:o3") // 主推理模型
.memory(MemoryConfig.builder()
.model("openai:gpt-4.1-mini") // 记忆操作用小模型
.build())
.compaction(CompactionConfig.builder()
.model("openai:gpt-4.1-mini") // 压缩摘要也用小模型
.build())
.build();
9. 记忆工具
只要开启记忆管线(不配置 .disableMemoryHooks()),框架会自动给 Agent 注册两个内置工具,Agent 可以主动调用工具查询历史记忆,弥补 MEMORY.md 容量有限、旧记忆被截断的问题。
记忆工具:
memory_search:全文关键词检索所有记忆文件memory_get:按文件路径 + 行号分片读取详情
9.1 记忆检索工具
核心注解定义对外工具信息:
@Tool(
name = "memory_search",
readOnly = true, // 只读,不会修改文件
description = "检索长期记忆文件,查询过往工作、决策、日期、人员、偏好、待办前使用"
)
public String memorySearch(
RuntimeContext runtimeContext,
@ToolParam(name = "query", description = "Keywords to search for in memory files")
String query) {
入参 query:检索关键词,模糊匹配入口
核心检索逻辑:
- 获取全部记忆文件列表:通过
workspaceManager.listMemoryFilePaths(rc)加载MEMORY.md、memory/*.md - 构造忽略大小写匹配规则:
Pattern.quote(query)转义特殊字符,仅纯文本匹配,不支持复杂正则 - 逐文件逐行遍历检索,命中记录格式
Source:文件路径#行号: 匹配内容 - 组装返回结果:无匹配则返回空结果提示;有匹配则输出命中总数+全部条目
9.2 记忆分片读取工具
核心注解定义对外工具信息:
@Tool(
name = "memory_get",
readOnly = true,
description = "读取记忆文件指定行,一般在 memory_search 命中后调取完整上下文"
)
public String memoryGet(
RuntimeContext runtimeContext,
@ToolParam(
name = "path",
description =
"Relative path to the memory file (e.g., MEMORY.md or"
+ " memory/2026-04-01.md)")
String path,
@ToolParam(name = "startLine", description = "Start line number (1-based, inclusive)")
int startLine,
@ToolParam(name = "endLine", description = "End line number (1-based, inclusive)")
int endLine)
入参三要素:
path:记忆文件相对工作区路径(MEMORY.md/memory/2026-06-18.md)startLine:起始行(从1开始,包含)endLine:结束行(从1开始,包含)
核心处理逻辑:
- 参数校验:校验文件路径非空,无参数直接返回错误
- 安全防护:路径归一化校验,禁止
../跨工作区目录穿越 - 读取文件:通过工作区管理器读取目标记忆文件,文件不存在返回报错
- 行号转换:对外行号从1开始,内部转为数组0下标,边界截断避免越界
- 边界判断:起始行超过文件总行数直接返回错误提示
- 分段输出:循环读取指定区间行,按
行号|单行内容格式拼接返回
9.3 协作流程

协作流程:
Agent遇到需要查询历史跨会话信息,发现MEMORY.md存在Token截断,信息不全Agent主动调用memory_search,传入检索关键词memory_search遍历所有记忆文件,返回一批命中条目(文件路径+行号+片段)Agent根据关键命中线索,调用memory_get,传入文件路径、起止行号memory_get做路径安全校验,读取对应区间完整原文上下文- 完整历史内容注入当前对话上下文,辅助模型完成回答
更多推荐
所有评论(0)