记忆(Memory)¶
作用¶
让 agent 能“记住跨会话的事实”,同时避免对话上下文无限增长。harness 把记忆拆成两层:高频低质量的“流水账” + 低频高质量的“策划后长期记忆”,并补上一套 FTS5 检索 + 后台维护。
触发¶
时机 |
动作 |
|---|---|
推理前( |
|
|
|
上下文溢出( |
|
工具返回超大( |
|
后台调度 |
|
关键逻辑¶
双层记忆模型¶
graph LR
Conv[对话 messages] -->|超阈值| Compactor[ConversationCompactor]
Compactor -->|offload| Sess[sessions/<id>.log.jsonl]
Compactor -->|flushMemories| Flush[MemoryFlushManager]
Flush -->|append + index| Daily[memory/YYYY-MM-DD.md]
Daily -. 后台二次加工 .-> Cons[MemoryConsolidator]
MEM[MEMORY.md作为上下文] -->|读取去重| Cons
Cons -->|重写| MEM
MEM -->|每次推理注入| Hook[WorkspaceContextHook]
Daily -.不直接注入.- Hook
Daily --> Idx[(MemoryIndex<br/>SQLite FTS5)]
MEM --> Idx
第一层·流水账
memory/YYYY-MM-DD.md:MemoryFlushManager专属,只追加,不去重;是“刚刚在说什么”的原始记录。第二层·策划后长期记忆
MEMORY.md:MemoryConsolidator专属,整体重写;MemoryFlushManager 不会去动它。每次推理都走WorkspaceContextHook注入到 system prompt。索引
MemoryIndex:启动时indexAllFromWorkspace一次;每次 flush 写今日流水账后增量重建该文件索引;SQLite 文件位于<workspace_parent>/memory_index.db。
对话压缩(ConversationCompactor)¶
检查阈值 → 找 cutoff(不切开 ASSISTANT/TOOL 对)
→ (可选) flushMemories(prefix)
→ (可选) offloadMessages(messages → sessions/.../<id>.jsonl)
→ LLM 提炼 summary
→ [summaryUserMsg] + tail 返回给 hook重装 memory
默认值(全部可调):
参数 |
默认 |
说明 |
|---|---|---|
|
|
按条数触发( |
|
|
按 token 估算触发( |
|
|
保留尾部条数 |
|
|
非 0 时按 token 预算从后往前扫描,覆盖 |
|
|
压缩前提取记忆到今日流水账 |
|
|
压缩前将原始消息追写到会话 |
|
内置模板 |
包含 |
CompactionConfig.builder()
.triggerMessages(30)
.keepMessages(10)
.build(); // flush/offload 默认都是 true
TruncateArgsConfig—轻量预处理(可选)¶
在 LLM 摘要之前,可以先走一个不走 LLM 的干预:对老消息里不那么重要的 ToolUseBlock 参数做字符串截断(默认阈值 25 条 / 40k tokens,参数超 2000 字符被裁揉)。适合 write_file 这种入参体量大、后期不需要原貌的场景。
CompactionConfig.builder()
.triggerMessages(80)
.truncateArgs(TruncateArgsConfig.builder().build())
.build();
上下文溢出自动恢复¶
当模型返回 context_length_exceeded / maximum context 之类错误,HarnessAgent.recoverFromOverflow → forceCompactAndRetry 会拼一个临时 triggerMessages=1 的 CompactionConfig 走一轮压缩,清空 Memory 后重试;前提是配置了 compaction(...),否则直接抛错。
记忆提取(MemoryFlushManager)¶
flushMemories(messages):拿当前MEMORY.md和今日流水账作为“去重参考”丢给 LLM,要求输出 仅新增的 bullet;“NO_REPLY” 表示什么都不写。写入位置固定是
memory/YYYY-MM-DD.md,不会动MEMORY.md(以防二层被一层覆写)。写完后立刻
indexFromString重建该文件索引,并调MemoryMaintenanceScheduler.requestConsolidation()提示“能合并了合并下”。
二次合并(MemoryConsolidator)¶
读 mtime 超过 watermark 的日流水账 + 当前
MEMORY.md,调 LLM 合并、去重、裁剪。输出限制:默认
maxMemoryTokens=4000(约 16k 字符),prompt 会以字符预算的形式告诉 LLM。写后推进 watermark,存于
memory/.consolidation_state;下次只看 mtime 超 watermark 的日文件。合并仅在后台 executor 跳:定期 tick 或
requestConsolidation()发起,永不阻塞推理循环。
后台维护(MemoryMaintenanceScheduler)¶
HarnessAgent.build() 中自动创建并 start(),每个 tick 顺序跑:
expireDailyFiles— 超过dailyFileRetentionDays的日文件归档到memory/archive/(默认 90 天)consolidateMemory— 调MemoryConsolidator.consolidate()pruneOldSessions— 删除 mtime 超sessionRetentionDays的会话文件(默认 180 天)reindex—MemoryIndex.indexAllFromWorkspace
默认间隔 Duration.ofHours(6);opportunistic 调用实际节流间隔 30 分钟,避免频繁 flush 打爆 LLM。
工具结果卸载(ToolResultEvictionConfig)¶
与压缩独立。某次 tool_call 返回的文本超过阈值时,全文写到 evictionPath 下的文件,原位置只留一个“首尾预览 + 路径”的占位符,agent 需要完整内容时走 read_file。
参数 |
默认 |
说明 |
|---|---|---|
|
|
超过则卸载 |
|
|
首尾预览字符数 |
|
|
卸载文件根路径 |
|
内置集(含 |
不参与卸载的工具 |
HarnessAgent.builder()
...
.toolResultEviction(ToolResultEvictionConfig.defaults())
.build();
配置与代码示例¶
HarnessAgent agent = HarnessAgent.builder()
.name("MyAgent")
.model(model)
.workspace(workspace)
.compaction(CompactionConfig.builder()
.triggerMessages(30)
.keepMessages(10)
.build())
.toolResultEviction(ToolResultEvictionConfig.defaults())
.build();
// agent 可随时调用 memory_search
MemoryIndex index = new MemoryIndex(workspaceAgentScopeDir);
index.open();
List<MemoryIndex.SearchHit> hits = index.search("数据库迁移", 10);
// hit: { path, lineNumber, content, rank }