压缩前持久化关键事实插件。
在 Compactor 触发前将即将被压缩丢掉的消息里的关键事实提取并追加到
working_dir/MEMORY.md,下一次会话由 MemoryLoader 自动加载回 system prompt,
实现"长会话不失忆"。
工作流程
- 订阅
:before_compact事件(Agent 在 compact 触发前 emit) - 调用
extract_fn.(messages, opts)(2 元)或extract_fn.(messages, ctx, opts)(3 元)从待压缩消息里提取 N 条关键事实。 3 元签名让提取器拿到ctx.user_data等业务标识,便于按租户路由不同 LLM。- 默认内置启发式
default_extract/2(无 LLM 依赖,完全离线可测) 可覆盖为真实 LLM 提取函数(签名:
([Message.t()], keyword()) -> {:ok, [String.t()]} | {:error, any})
- 默认内置启发式
- 用
:crypto.hash(:sha256, fact)去重(与已写 facts 比对) - 追加到
working_dir/MEMORY.md末尾,格式为- fact_text - emit 内部事件
{:memory_flushed, %{facts, count, session_id}} - emit
{:plugin_event, :memory_flush, payload}便于集成方订阅并持久化到数据库 / 长期记忆系统
配置
{CMDC.Plugin.Builtin.MemoryFlush,
file: "MEMORY.md", # 持久化目标文件(相对 working_dir)
max_facts_per_flush: 10, # 单次 flush 最多提取多少条
extract_fn: &MyApp.extract/2, # 自定义提取器(返回 {:ok, [fact]} | {:error, _})
dedupe: true # 是否 sha256 去重(默认 true)
}失败降级
extract_fn 抛异常 / 返回 {:error, _} 时:
- Plugin 记录
Logger.warning - 不阻塞 compact(返回
:continue) - 不写文件
保证"MemoryFlush 出问题也不影响主 Agent Loop"。
与 MemoryLoader 闭环
# 第一次会话
opts = [
plugins: [
CMDC.Plugin.Builtin.MemoryLoader,
CMDC.Plugin.Builtin.MemoryFlush
],
working_dir: "/project/path"
]
# 长对话 → 触发 compact → MemoryFlush 追加 facts 到 MEMORY.md
# 第二次会话(新进程)
# MemoryLoader 自动读取 MEMORY.md 并注入 <agent_memory> 到 system prompt
# → Agent 仍然记得第一次的关键事实emit 事件协议
{:memory_flushed, %{facts: [String.t()], count: pos_integer, session_id: String.t()}}{:plugin_event, %{kind: :memory_flush, facts, count, session_id, occurred_at, file, v: 1}}
Summary
Functions
默认事实提取器:从消息列表里抽启发式关键句子。
Functions
@spec default_extract( [CMDC.Message.t()], keyword() ) :: {:ok, [String.t()]}
默认事实提取器:从消息列表里抽启发式关键句子。
不依赖 LLM,只做简单规则匹配,方便单元测试。生产建议覆盖:
extract_fn: fn messages, opts ->
ReqLLM.chat(
model: "anthropic:claude-haiku-4",
messages: messages,
system: opts[:extract_prompt]
)
end启发式规则:
- 只看 role 为
:user的消息(用户显式声明的事实最重要) - 按换行切分,保留非空行
- 匹配关键字模式:
以"记住 | remember | 注意 | 约定 | 以后"开头
含"必须 | 一定 | always | never | must"
- 长度 20-200 字符(过短无信息量,过长是完整指令不是 fact)