OpenHuman 工具级记忆(Tool-Scoped Memory):把“绝不要给 Sarah 发邮件”变成 Agent 必须遵守的硬约束
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
本文基于 OpenHuman 官方文档 Tool-Scoped Memory 展开,完整讲解 OpenHuman 中“工具作用域记忆层”的设计与实现:它如何为每个工具建立独立的规则命名空间、如何从用户指令和重复失败中自动捕获规则、如何通过三级优先级让 Critical 规则免疫会话内 Token 压缩,以及六个memory.tool_rule_*RPC 方法的参数细节。读完本文,你将能够理解并操作这一持久化、工具级安全规则系统,并能在源码层面验证它的捕获、存储与注入全链路。
什么是工具级记忆:定位与命名空间设计
工具级记忆(tool-scoped memory)层 捕获的是Agent 应当如何使用某个具体工具的“可执行指导”。它有两个明确的分界:
- 它区别于 Memory Tools 提供的通用
recall/store/forget检索能力——后者是面向 Memory Tree 知识库的通用读写; - 它也区别于纯统计性质的
tool_effectiveness统计命名空间——后者只记录“发生了什么”(调用次数、错误模式),而工具级记忆记录“该怎么做”。
一句话概括它的价值:把用户在对话中随口说出的 “never email Sarah”(绝不要给 Sarah 发邮件),转写成 Agent 在后续每一轮都必须遵守的硬约束。这正是 OpenHuman issue #1400 要求的一等公民级“持久化学习 + 高优先级规则”存储与检索系统。
命名空间隔离
从源码 capture.rs 的模块注释可以看到其隔离原则:每个工具拥有自己的独立命名空间tool-{tool_name},与global、skill-{id}以及仅供统计的tool_effectiveness命名空间完全分开。存储层实现 中的命名空间识别也印证了这一点——常量TOOL_NAMESPACE_PREFIX = "tool-"专门用于识别工具级规则命名空间,而所有写入一律经由tool_memory_namespace()统一构造,保证“trim + 小写”的规范化规则只存在于一处:
// src/openhuman/memory/tool_memory/store.rs /// Namespace prefix every tool-scoped rule namespace carries. const TOOL_NAMESPACE_PREFIX: &str = "tool-";统计信息(tool_effectiveness/tool/{name})与规则(tool-{name}/rule/{id})有意分属不同命名空间——一个跟踪“发生了什么”,另一个跟踪“对此该怎么办”。这种分离让安全规则永远不会被统计数据的读写路径污染。
数据模型:ToolMemoryRule 字段详解
命名空间内的每条条目都是一个ToolMemoryRule结构。官方文档定义的字段语义如下表(完整继承自原文档):
| 字段 | 用途 |
|---|---|
id | 每条规则的稳定 UUID。Upsert 时会复用同一个 id。 |
tool_name | 规则适用的工具(如send_email、shell)。 |
rule | Agent 必须遵守的自然语言指导。 |
priority | critical、high或normal,同时驱动检索与压缩策略。 |
source | user_explicit、post_turn或programmatic——记录规则来源(provenance)。 |
tags | 自由标签(safety、permission等)。 |
created_at/updated_at | RFC3339 时间戳。 |
结合 store.rs 的put_rule实现,还可以补充几个文档未展开、但实操中重要的行为细节:
- Upsert 语义:
tool_name和rule均为空时直接报错;id为空时自动通过ToolMemoryRule::generate_id()铸造 UUID。tool_name会先做 trim + 小写化,与命名空间构造使用同一套规范化,因此调用方用原始名称读回时命中的是同一个命名空间。 created_at保留:对同一(tool_name, id)的重复 upsert 会保留最初的created_at,只刷新updated_at。实现上用一把进程级互斥锁(rule_mutation_lock)串行化“先 get 再 store”的读改写序列,防止并发 upsert 复活旧的created_at。- 分类落库:每条规则写入时带上
MemoryCategory::Custom("tool_memory")分类,便于在统一记忆后端中与通用记忆区分。 - 读取容错:
list_rules中对无法反序列化的损坏行是跳过而非报错——一条坏数据不能把该工具的其他规则从提示词中全部隐藏;同理fetch_rule将坏行视为“不存在”,使其不阻塞后续 upsert 覆盖。
另外有一个特殊哨兵值值得注意:当用户指令中没有任何工具调用可以挂载时,规则会落到__unscoped__工具名下(capture.rs),以便在下一轮由 Agent 重新归位;这类规则不会进入提示词预取,避免把与任何真实工具无关的指导强行钉进任意会话的提示词。
优先级与压缩免疫:Critical / High / Normal 三级
官方文档给出的优先级矩阵如下(完整继承):
| 优先级 | 存储位置 | 是否抗压缩(compression-resistant) |
|---|---|---|
critical | 通过ToolMemoryRulesSection钉入系统提示词 | 是——系统提示词按会话冻结,绝不会被会话内压缩器改写。 |
high | 同一个系统提示词块,排在 critical 之后。 | 是——同一机制。 |
normal | 存于命名空间内,按需经memory_recall检索。 | 否——像其他命名空间记忆一样可被压缩。 |
这种“抗压缩”特性是结构性的:critical 与 high 规则寄宿在系统提示词中,而推理后端的 prefix cache 会把系统提示词在整个会话期间冻结。没有任何 Token 压缩路径能悄悄丢掉一条critical规则。
源码层面的两个关键证据:
- 预取过滤:store.rs 的
rules_for_prompt用priority.is_eager()过滤出 Critical + High 规则,先按 critical 优先、同级内按updated_at新者优先排序,然后截断至TOOL_MEMORY_PROMPT_CAP:
// src/openhuman/memory/tool_memory/store.rs /// A cap on the **High**-priority remainder only... /// dropping one to fit a budget is the failure this surface exists to prevent. pub const TOOL_MEMORY_PROMPT_CAP: usize = 30;注意这是一个文档化的权衡:上限主要作用于 High 优先级的剩余部分,一旦 Critical+High 规则的总数超过 30 条,排在队尾的规则(理论上包括 Critical)会被截断——源码注释明确保留了这一与引擎一致的语义,而不是“悄悄改进”成无脑保留全部 Critical。
- 提示词区块构造即快照:prompt.rs 中
ToolMemoryRulesSection在构造时就把渲染结果冻结为快照字符串,PromptSection::build()直接原样返回、不依赖任何运行时上下文——注释里写得很直白:这是为了保持推理 prefix cache 始终温热。
捕获管线:两条自动路径
捕获钩子 的实现在 ToolMemoryCaptureHook。它实现 harness 的PostTurnHook接口(钩子名为tool_memory_capture),在每一轮对话结束后自动触发两条捕获路径:
路径一:用户指令(User Edicts)→ Critical 规则
用户消息中出现never <动词> <名词>、don't <动词> ...、do not <动词> ...或stop <动词>ing ...这类祈使句式时,会被提升为匹配工具上的Critical规则。extract_user_edicts 的具体匹配逻辑:
- 按
.、换行、;切分用户消息,逐句检查是否以或包含上述祈使前缀(小写化后匹配); stop一词只在句边界出现时才算祈使句(消息开头、. stop或\nstop),避免 “I want to stop working” 这类普通表达误触发;- 命中的句子会被截断到
MAX_RULE_LEN = 240字符,防止异常输入撑爆命名空间; - 工具归位:先尝试
pick_tool_for_edict——检查本轮实际调用过的工具名是否作为词出现在指令文本中,再看一组刻意保持精简的常用名词别名表(tool_aliases):
| 工具名包含 | 用户口语别名 |
|---|---|
*mail* | email、mail |
*shell*/*bash*/*exec* | shell、terminal |
*browser*/*web*/*http* | browser、web |
*slack* | slack、dm |
也就是说email会被映射到名为send_email的工具,shell映射到bash/exec。注释特别说明:这张表故意很小,更复杂的语义抽取“属于 LLM 抽取器的事”。
- 别名也未命中时:规则落到本轮第一个执行的工具下,让它紧邻相关调用点;若本轮完全没有工具调用,则落到
__unscoped__。
捕获成功后以ToolMemoryPriority::Critical+ToolMemorySource::UserExplicit+ 标签["user-edict"]落库。
路径二:重复工具失败 → Normal 观察记录
extract_repeated_failures 统计本轮中每个工具的失败次数:同一工具在一个轮次内失败 ≥ 2 次时,才生成一条Normal优先级观察记录;单次瞬时失败被忽略,防止命名空间被噪声填满。记录正文内联总结失败概要,例如:
Tool failed 2 times in one turn (<首次失败的 output_summary>). Consider an alternative approach before retrying.来源标记为ToolMemorySource::PostTurn、标签["repeated-failure"],这样 Agent 下次考虑使用该工具时就有上下文。
两条路径都刻意保守——只在信号明确时触发,且捕获的规则正文始终回指用户自己的原话,让审阅者能看出到底什么触发了这条规则。
开关:默认开启,可单独关闭
钩子在学习子系统(learning subsystem)开启时默认启用,可用环境变量单独关闭(env 覆盖层 确认了取值解析):
OPENHUMAN_LEARNING_TOOL_MEMORY_CAPTURE_ENABLED=0从解析代码看,0/false/no/off均被识别为关闭,写入learning.tool_memory_capture_enabled配置项。
工具选择时刻的检索:预取 + 系统提示词钉装
检索侧的设计(完整继承原文档并补充源码印证):
- 会话开始时,harness 通过
ToolMemoryStore::rules_for_prompt预取所有 Critical 和 High 规则; - session builder 将其渲染为
## Tool-scoped rules区块(常量TOOL_MEMORY_HEADING),经 ToolMemoryRulesSection 钉入系统提示词; - 因为提示词在会话生命周期内冻结,这些规则在每一轮的工具选择时刻、任何实际工具执行之前都可见。
渲染格式由 render_tool_memory_rules 决定:规则先按tool_name→ 优先级 → 规则文本 → id 排序,然后按工具分组输出。实际进入系统提示词的内容长这样:
## Tool-scoped rules These rules are pinned by the user or by the safety pipeline. Treat every entry as a hard constraint when considering the matching tool — do not override them silently. Lower-priority guidance lives in the `tool-{name}` memory namespace and can be queried via `memory_recall` if needed. ### `send_email` - **[critical]** Never email Sarah at sarah@example.com.低优先级的指导则被排除在提示词预算之外,Agent 通过调用memory_recall查询tool-{name}命名空间来按需获取。
RPC 接口:memory 命名空间下的六个方法
schemas/tool_memory.rs 注册的FUNCTIONS与原文档表格一一对应,六个方法均暴露于memory命名空间下:
| 方法 | 用途 | 参数(snake_case JSON) |
|---|---|---|
memory.tool_rule_put | Upsert 一条规则。安全关键条目使用priority='critical'。 | tool_name(必填)、rule(必填)、priority(可选,默认normal)、source(可选,默认programmatic)、tags(可选数组)、id(可选,提供则原位 upsert) |
memory.tool_rule_get | 按(tool_name, id)取单条规则,不存在时返回 null 而非报错。 | tool_name、id |
memory.tool_rule_list | 列出某工具的全部规则,按优先级(critical → high → normal)再按updated_at降序排序。 | tool_name |
memory.tool_rule_delete | 删除一条规则,返回布尔值(规则存在过则为true)。 | tool_name、id |
memory.tool_rules_for_prompt | 返回渲染好的 Markdown 块 + 结构化规则快照——即 session builder 钉装的内容。 | tools(可选数组;空或省略时扫描所有已知工具命名空间) |
memory.tool_rules_json | 返回某工具规则的原始 JSON 列表(供 envelope 消费方使用)。 | tool_name |
参数默认值可在 ops/tool_memory.rs 的ToolRulePutParams中确认:priority与source均带#[serde(default)],省略时分别为normal与programmatic;tool_rules_for_prompt的返回结构为{ rendered: string, rules: ToolMemoryRule[] }。
所有方法都走与其他 memory RPC 相同的active_memory_client/MemoryGuard管道(ops 层 的tool_memory_guard()统一获取 guard,再经as_tool_memory()路由到工具级记忆家族);JSON 载荷统一 snake_case(priority: "critical"、source: "user_explicit")。若当前 memory driver 未宣告ToolMemory能力,handler 会返回memory driver does not support the tool_memory family错误——内嵌驱动总是宣告该能力,因此这条路径仅在 null/兜底绑定时可达。
端到端安全用例:“Never email Sarah”
原文档将该用例作为回归测试覆盖,全链路如下(结合源码路径逐条印证):
- 用户说:在某轮调用过
send_email的对话中说“Never email Sarah at sarah@example.com.” - 捕获:ToolMemoryCaptureHook 提取指令,将
email别名映射到send_email工具,在tool-send_email/rule/{uuid}下写入一条 Critical 规则(来源user_explicit,标签user-edict)。 - 下一会话:
prefetch_tool_memory_rules_blocking拉取全部 Critical 与 High 规则,session builder 把ToolMemoryRulesSection追加进系统提示词。 - 生效:Agent 在选择任何工具之前就看到
### \send_email`分组下的-[critical]Never email Sarah at sarah@example.com.`,且该规则在任何会话内 Token 压缩中都会存活。
相关覆盖与集成测试位于 src/openhuman/memory/tool_memory/(store_tests.rs、capture_tests.rs、prompt_tests.rs),RPC 与 schema 层另有 ops 层测试 与 schema 层测试。
小结与延伸阅读
工具级记忆层的三个设计支点值得记住:
- 命名空间即边界:
tool-{name}与统计、全局、技能命名空间物理隔离,规则与观测互不污染; - 优先级即生命周期:critical/high 借道冻结的系统提示词获得结构性抗压缩能力,normal 留在命名空间内按需
memory_recall; - 捕获即取证:两条自动捕获路径都保守触发、保留用户原话,并可用
OPENHUMAN_LEARNING_TOOL_MEMORY_CAPTURE_ENABLED=0单独关闭。
相关文档:
- Memory Tools —— 通用
recall、store、forget记忆工具。 - Smart Token Compression —— 系统提示词所要防御的压缩机制。
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考