4.3. memory_tool 的 add、replace、remove 操作实现了精细的冲突处理
memory_tool 的 add、replace、remove 操作实现了精细的冲突处理
翻看 Agent 的对话记录,你会看到这样一个让人抓狂的瞬间:用户纠正了一个事实,Agent 乖乖认错,但十分钟后,它又把老版本搬了出来。
问题不在态度,而在记忆。
大部分 Agent 的记忆像一块任人涂写的白板——新的盖上去,旧的沉在下面,谁先浮上来全靠概率。Hermes 的设计者显然受够了这种混乱。截至当前调研资料(2026 年 6 月),memory_tool 只暴露了三个操作:add、replace、remove。没有 read,没有 list,没有花哨的查询语法。这三个原子操作被塞进一个 3,600 字符的记忆槽里,每一次调用都是一次生死裁决——要么精确落笔,要么被系统拒绝。
本章我们就来解剖这套裁决机制。
你需要什么
- 一个运行中的 Hermes Agent 实例(本地 Docker 或远程均可)
- 能向 Agent 发送指令的终端或 Webhook 接口
- 预计阅读 + 实操时间:20 分钟
最终成果
你将掌握三个记忆操作的精确定义与冲突处理规则,能在自己的 Agent 中安全地更新事实记忆,避免“新旧版本反复弹跳”的经典故障。
添加操作与精确重复跳过
add 是最简单的入口,但它的底层逻辑藏着第一个安全阀。
假如 Agent 要记住"用户的名字叫小明",它调用:
# 伪代码示例:Agent 内部调用 memory_tool
memory_tool(
action="add",
content="用户的名字叫小明。"
)
系统不会立刻写入。它会先扫描当前 MEMORY.md 的完整内容,做一次精确字符串匹配。如果这段文本已经一字不差地存在,操作直接返回成功,但底层什么也不做——不会产生重复条目,不会浪费珍贵的字符配额。
踩坑经验:如果你用
add写入"用户叫小明"(缺少"的名字"),而记忆里已有"用户的名字叫小明",系统会认为这是两条不同的记忆。精确匹配不是语义匹配,多个拼写变体会像野草一样挤满 3,600 字符的空间。
这一设计的意图很明确:add 的调用者(即 Agent 内部的决策模型)不需要先检查记忆内容,只管"有就加"。去重的责任完全在工具层。这降低了模型端的状态管理负担,但也意味着模型不能把 add 当作"无脑日志"来用——每一条新增记忆都应该经过提炼,否则变种表述会迅速耗尽容量。
接下来看一个更复杂的场景。假设记忆里已有:
用户的名字叫小明。
小明喜欢喝咖啡。
现在模型想把"咖啡"更新为"拿铁"。它有两个选择:
- 方案 A:
add("小明喜欢喝拿铁。") - 方案 B:
replace(old_text="小明喜欢喝咖啡。", new_text="小明喜欢喝拿铁。")
方案 A 会让记忆变成三条,其中两条矛盾——Agent 下次读取时,不知道"咖啡"和"拿铁"哪个是真的。这引出第二个操作,也是最需要仔细理解的一个。
替换操作的 old_text 匹配规则
replace 签名的核心是两个参数:
| 参数 | 作用 | 陷阱 |
|---|---|---|
old_text |
用于在记忆中定位要替换的片段 | 不是 ID,不是行号,是子串 |
new_text |
替换后的完整文本 | 会完全覆盖匹配到的内容 |
调用形式如下:
memory_tool(
action="replace",
old_text="小明喜欢喝咖啡。",
new_text="小明喜欢喝拿铁。"
)
完全匹配:一步成功
当 old_text 与记忆中的某一段完全一致(字符对字符),替换直接发生。原句消失,新句上位,其他记忆不受影响。
操作前:
用户的名字叫小明。
小明喜欢喝咖啡。
本周末计划去爬山。
操作后:
用户的名字叫小明。
小明喜欢喝拿铁。
本周末计划去爬山。
部分匹配:替换的最小单元
危险出现在 old_text 只匹配目标的一部分时。如果模型传了:
old_text="喜欢喝咖啡",
new_text="喜欢喝拿铁"
系统仍然会匹配——它用的是子串匹配,不是整行匹配。但这里有一个关键行为:替换的范围是 old_text 在记忆中的出现区间,只替换那一段子串,不会自动扩展为整行。
这意味着什么?如果记忆内容是"小明喜欢喝咖啡,尤其是在早上。",而 old_text 是"喜欢喝咖啡",替换后的结果是"小明喜欢喝拿铁,尤其是在早上。"——标点和后续文字保留。
踩坑经验:子串匹配可能命中多行。假设记忆里有两条"咖啡"相关的内容,而模型
old_text只写了"咖啡"两个字,系统可能会匹配到第一个出现的"咖啡"并替换整条记忆。让old_text尽量长且独特,是在 3,600 字符内安全操作的前提。
匹配失败时:系统不静默
如果 old_text 在记忆中完全找不到,replace 不会悄悄失败。当前调研资料显示,系统会将当前记忆的完整内容夹在错误响应中返回给模型,模型必须重新决策。
这个设计堪称精妙:它把冲突升级成了"显式纠错循环"。模型不会在不知情的情况下反复尝试同一个错误的 old_text——它看到了真实的记忆状态,必须据此修正自己的匹配策略。
删除与批量清理
remove 的语义最直白,但误伤风险也最高。
memory_tool(
action="remove",
old_text="本周末计划去爬山。"
)
和 replace 一样,remove 使用 old_text 做子串匹配。匹配到的内容被整段删除。操作前:
用户的名字叫小明。
小明喜欢喝拿铁。
本周末计划去爬山。
爬山要带水壶。
执行后:
用户的名字叫小明。
小明喜欢喝拿铁。
爬山要带水壶。
但这里有一个微妙之处:old_text="爬山" 会命中两条记忆。系统会删除第一个匹配项("本周末计划去爬山。"),然后停止。它不是全局替换,而是首次匹配即止。
如果你需要清理与某个主题相关的多条记忆,模型需要反复调用 remove 或使用更长的 old_text 来精准命中。一个实践中的脚本思路:
# 伪代码:模型端的批量清理逻辑
# 场景:用户换了名字,需要清除所有旧名字相关记忆
# 步骤 1:构造足够长的 old_text 以精准命中
old_entries = [
"用户的名字叫小明。",
"小明喜欢喝拿铁。",
"小明的工作是工程师。",
]
# 步骤 2:逐个删除,每次检查错误响应
for entry in old_entries:
result = memory_tool(action="remove", old_text=entry)
if result.success:
continue
else:
# 错误响应中包含当前记忆全文
# 模型根据实际内容调整 old_text
retry_with_corrected_old_text(result.current_memory)
踩坑经验:不要试图用一次
remove清空整段记忆。没有"删除全部"的快捷方式,这是刻意的安全阀——防止模型在一次幻觉中清空用户的所有历史。
回顾
这一章我们走了多远?
add:精确字符串去重,不做语义合并;多写变体 = 浪费配额。replace:子串匹配,替换最小命中单元;匹配失败时返回当前记忆全文,迫使模型重新决策。remove:首次匹配即止,没有全局删除;批量清理需要逐条处理。
三个操作,一套铁律。没有 read 是因为记忆始终注入在 prompt 里,模型不需要主动读取。没有 update_by_id 是因为行号和 ID 对于大模型来说是不可靠的锚点——把定位责任转嫁给子串匹配,是务实的选择。
耗时大约 20 分钟。
行动清单
- 在你的 Agent 中测试
add重复内容,确认系统是否跳过写入。 - 用
replace尝试只覆盖一个长句中的部分子串,观察边界的处理结果。 - 构造一个
old_text匹配失败的场景,看错误响应中是否包含完整记忆。 - 用
remove删除一条记忆,再用更短的old_text尝试,观察首次匹配即止的行为。
上一章我们理解了 3,600 字符的容量上限是一种蒸馏策略。本章我们看到,在这个狭小的空间里,每一次写入都必须通过精确匹配这道安检门。但问题还没完:当记忆内容被完美写入后,Agent 在每次对话中如何决定哪些记忆应该被检索?如果所有记忆始终参与推理,3,600 字符的萃取可能毫无意义。下一章,《记忆检索让历史信息从默认参与者变为条件参与者》,我们将看到 session_search 和按需检索如何把记忆从一个"全员出席"的冗长会议,变成一场"只请关键人物"的高效决策。
Hermes Agent 系统设计与工程落地
关于 LearnKu