news 2026/9/24 9:36:01

Ekko Agent 单一记忆系统深度解析:memory_nodes 前台链路、受控写入与数据库迁移机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ekko Agent 单一记忆系统深度解析:memory_nodes 前台链路、受控写入与数据库迁移机制
  • AI 应用
  • 人工智能
  • AI Agent
  • 本地部署
  • 前端
  • 后端
  • 工作流自动化

【免费下载链接】ekko-studio

Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.

项目地址:https://gitcode.com/gh_mirrors/he/ekko-studio
点击查看免费下载

本篇技术指南以 Ekko Agent 记忆系统规划文档 为核心骨架,结合 ekko-agent 包中src/memory/src/database.ts的完整源码实现,系统讲解 Ekko Agent 如何用唯一一张记忆卡片集合memory_nodes承载全部长期记忆:从前台运行链路、四个记忆工具的参数契约,到受控 kind 与 canonical key 的生成规则、并发安全的 revision 机制,再到ekko.db的表结构与崩溃安全迁移策略。读完本文,你将掌握 Ekko Agent 记忆系统的完整数据流,能准确理解memory_search/memory_get/memory_write/memory_forget的正确调用姿势,以及数据库在旧进程仍写入时为何宁可阻止启动也不重建。

设计哲学:只有一种可操作的长期记忆

Ekko Agent 的记忆模型遵循一条极简且严格的原则:整个 Agent 只维护一种可操作的长期记忆——memory_nodes中的记忆卡片。系统内不存在独立的审批队列、后台记忆评审任务,也没有隐藏的 Session 摘要作为第二套记忆。

这一点可以从源码中得到直接印证。在 types.ts 中,MemoryNode定义了记忆卡片的完整结构:idkey(服务端生成的 canonical key)、revision(乐观锁版本)、statusactive/superseded/expired/deleted)、domaincategoryPathtypevalueJsonconfidenceimportancesourceMessageIds(证据来源)等字段,它就是记忆的唯一事实载体。

与之相对,memory_messages表只保存受信任的对话证据MemoryMessage,见 types.ts),用途是让记忆卡片能够追溯到具体的用户/助手消息;它不参与形成另一套记忆。也就是说:

  • 证据(对话消息)→ 存在memory_messages
  • 记忆(可检索、可注入、可增删的持久知识)→ 只存在memory_nodes
  • 所有增删改行为 → 直接落盘到memory_audit_events审计表,不存在待审批状态

前台运行链路:记忆的增删改只有一条路

原文档给出了记忆系统的核心运行链路,这里完整保留并结合源码展开:

Ekko Agent turn -> 保存当前用户消息证据 -> 按宿主授权的 scope 检索 active memory_nodes -> 注入相关记忆卡片 -> 主模型调用 memory_search / memory_get / memory_write / memory_forget -> memory_write / memory_forget 在当前 run 中直接生效 -> run 完成后仅补录助手消息证据

这条链路对应的实现事实:

  1. 保存用户消息证据MemoryService.captureMessages()(service.ts)在 turn 开始阶段把当前用户消息按确定性 ID 写入memory_messages,作为后续sourceMessageIds的唯一合法来源。
  2. 按 scope 检索 active 记忆:自动召回受宿主授权 scopetoken budget双重约束,默认自动召回 token 预算为DEFAULT_AUTOMATIC_MEMORY_TOKEN_BUDGET = 4000,最近消息证据上限为DEFAULT_MEMORY_RECENT_MESSAGE_LIMIT = 20,工具检索结果上限为DEFAULT_MEMORY_SEARCH_RESULT_LIMIT = 50(见 config.ts)。
  3. 注入记忆卡片buildMemoryContextPrompt()selectMemoryNodesByTokenBudget()(见 service.ts)把候选记忆组织成上下文注入主模型。
  4. 模型调用四个记忆工具memory_search/memory_get/memory_write/memory_forgetcreateMemoryTools()注册(tools.ts),其中memory_writememory_forgetwritable: false时不会暴露,从而实现只读模式。
  5. 当前 run 直接生效:写操作不做后台异步、不进入审批队列;run 完成后仅补录助手消息证据。

一个关键保证是:工具失败时 Runtime 立即返回真实错误。例如memory_forget在缺少reason、缺少显式遗忘意图时会直接返回失败(tools.ts),模型不允许继续声称操作成功。

四个记忆工具:参数契约与正确用法

memory_search:结构化检索与全量枚举

memory_search的核心语义是:已知类别优先用结构化kinds,开放问题才用queryText(tools.ts)。其参数包括:

参数类型说明
allboolean枚举当前宿主授权 scope 下所有 active 记忆,不做相关性过滤
queryTextstring开放问题的自然语言查询
kindsarray精确匹配一个或多个受控 kind,如profile_namehome_location
domain/categoryPathPrefix/types/keystring/array结构化过滤条件
tags/entitiesarray标签与实体过滤
valueJsonany按结构化值过滤
limitnumber1~50

值得注意的实现细节:中文"所有/全部记忆"与英文 "all/every memories" 等旧式 list-all 表达会被自动转换为全量枚举isListAllMemoryQuery()用正则匹配了中英文两类表达(tools.ts),命中后queryText被置空、listAll置真,从而执行不带相关性过滤的全量枚举——这正是文档所述"不得把 list-all 意图写成 queryText"的底层原因。

memory_get:按 id 取整张卡片

memory_getid返回完整记忆卡片(含 canonical key 与当前 revision),未提供id时退化为一次 limit=2 的精确检索(tools.ts)。它的核心用途是为后续写操作获取targetIdexpectedRevision

memory_write:原子批量写入

memory_write支持create/update/supersede/expire四种操作,并优先推荐operations数组的批量形态:整个数组先整体校验、再在一个事务中原子提交,任一操作非法则整批回滚(tools.ts)。成功返回done: true,并明确提示"此更新已完成,不要重复执行"。

写操作的关键约束(均有源码强制校验):

  • create必须提交受控kind;对 itemized 类 kind 还必须提供itemKey(短而稳定的概念/实体标识,如kind=project_context, itemKey=hermes_studio,严禁使用句子、时间戳或随机值)。
  • update/supersede/expire必须携带来自memory_search/memory_gettargetIdexpectedRevision(tools.ts)。
  • nodetitlecontent必填,结构化值必须用字段名valueJson(兼容旧字段名value,见normalizeToolMemoryNode)。
  • sourceMessageIds只能从宿主提供的受信任用户证据中选取,模型不能伪造来源(tools.ts)。
  • explicitUserIntent仅在用户明确要求记住/修改/纠正/删除时才置 true;若宿主设置了memoryWritePolicy === 'explicit-only',未显式意图的写入会被直接拒绝(tools.ts)。

memory_forget:精确删除与全量清除

memory_forget只服务于用户的显式遗忘请求context.memoryForgetIntent !== true时直接拒绝),支持四种形态:按all: true全量删除、按targets数组批量精确删除、按id + expectedRevision删除、按domain/key等宽泛选择器删除(tools.ts)。mode可选soft(保留审计状态)或hard(同时清理节点、FTS 与 embedding)。

受控 kind 与 canonical key:服务端生成的槽位机制

文档强调"创建时模型提交受控kind和可选itemKey,服务端生成 canonical key"。这背后的实现是MEMORY_SLOTS槽位表(schema.ts),每个受控 kind 都被映射到固定的槽位定义(key、domain、categoryPath、type、是否 itemized):

kindcanonical key(非 itemized)domain / categoryPath类型
interaction_contractinteraction.relationshipinteraction / relationshippreference
profile_nameprofile.identity.nameprofile / identityfact
home_locationprofile.location.homeprofile / locationfact
occupationprofile.occupationprofile / occupationfact
timezone_preferencepreference.timezonepreference / timezonepreference
language_preferencepreference.languagepreference / languagepreference
hard_constraintconstraint.hardconstraintconstraint
long_term_goalgoal.long_termgoal / long_termtask
durable_decisiondecision.durabledecisiondecision
custom_factcustom.factcustomfact

对于 itemized kind(如accessibility_needcommunication_preferenceproject_contextfood_avoidancepersonal_relationship等),canonical key 由槽位key + ":" + 规范化后的 itemKey组成(schema.ts)。itemKey会经过 NFKC 归一化、小写化、空白/分隔符替换为下划线、剔除非法字符等处理(normalizeCanonicalItem,schema.ts),保证同一概念得到稳定 key。

canonicalizeMemoryDraft()还会对部分 kind 做受控值校验:例如interaction_contract必须包含userRole/assistantRole/addressUserAs至少一个字段,home_location必须含citylocation(schema.ts)。normalizeMemoryNode()则负责最终节点归一化,并依据explicitUserIntent调整默认置信度(显式意图 0.98,默认 0.7)与重要性(0.9 / 0.6)(schema.ts)。

增删改规则:noop、supersede 与并发防护

文档的增删改规则全部能在源码中找到对应实现:

  • 同槽位同内容返回 noop:写入时若目标槽位的 active 卡片内容与提交内容一致,服务端判定无变更,返回action: 'noop',不产生新 revision。
  • 同槽位新内容建立下一 revision 并 supersede 旧版MemoryStoreMutationsupersede形态(types.ts)会把旧节点标记superseded、新节点记录supersedesId并递增 revision。
  • 更新、过期和按 id 删除必须携带expectedRevisionupdateNodeStatus/deleteNode在 revision 不匹配时返回失败,防止并发覆盖(types.ts)。
  • sourceMessageIds只能来自宿主提供的当前用户证据:前文已述,parseMemoryMutation会逐一校验 ID 是否在context.sourceMessageIds白名单内。
  • soft delete 保留审计状态;hard delete 清理节点、FTS 与 embedding:从 store.ts 的DELETE FROM memory_nodes_fts WHERE node_id = ?可见,hard delete 会同步清理 FTS5 索引(store.ts)。
  • 增删改直接写入memory_audit_eventsMemoryAuditEvent.eventType枚举为create | update | supersede | expire | delete(types.ts),每笔变更即时落审计,无待审批状态。

检索规则:token budget、scope 与"不能据此宣称记忆库为空"

检索的四个要点在resolveMemoryQuery()(retrieval.ts)中均有落地:

  1. 自动召回不等于完整记忆库:自动召回受 token budget(默认 4000)与宿主授权 scope 限制,MemoryContextDiagnostics会记录retrievedNodeCount/omittedNodeCount/tokenBudget/usedTokens(types.ts)。
  2. 已知类别用kinds,开放问题用queryTextmemory_search的工具描述明确要求优先kindsALWAYS_RECALLED_MEMORY_KINDS常量还会让interaction_contractlanguage_preferenceaccessibility_needcommunication_preferencehard_constraint等始终参与自动召回(service.ts)。
  3. 全量枚举用all: true:旧式 list-all 查询会被自动归一化(见前文isListAllMemoryQuery)。
  4. 无匹配不得宣称记忆库为空:只有执行过全量枚举才能下"库为空"的结论。

检索结果的冲突处理同样严谨:resolveConflicts()会过滤superseded/deleted/expired/confidence < 0.35的节点,并在同一 conflictKey(scope + domain + key)下只保留胜出者,其余记为conflict_lost(retrieval.ts);超限结果记为over_limit。相关性打分relevanceScore()对 title(+5)、entities/key(+4)、tags/value(+3)、category/content(+2)加权,并叠加importance * 2 + confidence(retrieval.ts)。

删除规则:一句话即算明确意图,不再有确认弹窗

文档列出的删除规则非常"激进"但也非常清晰:

  • 用户明确要求忘记某条内容 → 调用一次memory_forget精确删除(带id+expectedRevision)。
  • 用户明确要求清除全部记忆 → 调用一次memory_forget({ all: true })(且要求context.memoryForgetAllIntent === true)。
  • "清掉、清除、清空、删除、忘掉"等表达都属于明确删除意图,直接进入删除链路,不再经过确认弹窗或审批任务
  • 权限边界由"当前用户意图 + 宿主授权 scope"双重保证:memoryScopeAllowed()校验写入 scope 是否在writeScopes白名单内(scope.ts)。

scope 共有三种类型(types.ts):

scope含义可见性
{ type: 'profile' }当前 profile 跨会话共享默认 scope
{ type: 'session', id }仅当前会话可见会话级
{ type: 'context', namespace, id }宿主定义的上下文范围可见上下文级

数据库:ekko.db、四张核心表与 schema version 8 迁移

数据库位于<baseDirectory>/.ekko/ekko.db,记忆相关表共四张(文档原述,源码见 store.ts 的建表逻辑):

职责
memory_messages受信任的用户/助手对话证据(来源追溯)
memory_nodes唯一的长期记忆卡片集合
memory_audit_events创建、覆盖、过期和删除审计
memory_embeddings语义检索数据

memory schema version 8会删除旧的memory_review_jobsmemory_summariesmemory_session_state表——这从侧面印证了文档的核心主张:早期设计中存在过的"后台记忆评审"与"Session 摘要"体系已被彻底废弃,统一收敛到单一memory_nodes模型。迁移不会删除memory_nodes中现有的记忆卡片,即升级是保留数据的平滑迁移。

崩溃安全的迁移策略

迁移基础设施在 database.ts 中实现,其设计层层递进:

  1. 逐版本、逐组件迁移:每个组件(如 memory)按版本号排序依次执行,记录在schema_migrations表中,已应用版本跳过(database.ts)。
  2. BEGIN IMMEDIATE事务 + 重试:每个迁移运行在BEGIN IMMEDIATE事务中(database.ts),立即获取写锁。锁冲突最多尝试三次,每次受PRAGMA busy_timeoutmigrationBusyTimeoutMs)约束(database.ts)。
  3. 仍被占用时阻止启动:三次重试仍失败则直接抛出EkkoDatabaseMigrationError阻止启动(database.ts),绝不会在旧进程仍写入时重建数据库
  4. 其他迁移错误 → 备份 + 恢复:先把原数据库及 WAL/SHM 移动到带时间戳的备份路径(${databasePath}.migration-failed-${timestamp}-${randomUUID()}.bak,database.ts),再创建新库,并按兼容列恢复记忆、证据、审计和 Ekko 会话,随后重建 FTS 索引;若新库本身也无法建立,则恢复原数据库并终止启动
  5. 绝不降级:整个迁移过程不会以"禁用 Memory"或"切换到临时空库"作为降级方案——要么成功迁移并保留全部数据,要么回滚并终止。

这一策略的工程含义很明确:记忆数据被视为不可丢失的资产,宁可拒绝启动、等待锁释放,也不允许在数据竞争条件下产生半迁移的脏状态。

小结:一套模型、一条链路、一份保证

Ekko Agent 的记忆系统可以浓缩为三句话:

  • 一套模型:长期记忆只有memory_nodes一种事实载体,memory_messages只是证据,审计表只做记录,不存在第二套记忆或审批队列。
  • 一条链路:所有记忆的增删改都发生在 Agent turn 的前台链路中,由memory_search/memory_get/memory_write/memory_forget四个工具直接驱动,失败即报错、成功即生效。
  • 一份保证:canonical key 由服务端依据受控 kind 生成,expectedRevision并发防护杜绝覆盖,sourceMessageIds白名单防止伪造来源,而数据库迁移在BEGIN IMMEDIATE+ 重试 + 备份恢复的组合下做到数据零丢失。

对希望深入理解的读者,建议按以下路径继续阅读源码:先读 schema.ts 理解槽位与规范化,再读 tools.ts 掌握工具参数契约,随后用 service.ts 串联运行链路,最后通过 database.ts 与 store.ts 验证迁移与存储的工程细节。

  • AI 应用
  • 人工智能
  • AI Agent
  • 本地部署
  • 前端
  • 后端
  • 工作流自动化

【免费下载链接】ekko-studio

Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.

项目地址:https://gitcode.com/gh_mirrors/he/ekko-studio
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 9:35:02

MTF/SFR详解:摄像头清晰度量化测试原理与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:33:10

js定时器与异步基础讲解

JS 默认是单线程&#xff1a;代码一行一行排队执行&#xff0c;同一时间只能干一件事。 如果一段代码执行很久&#xff0c;后面代码就会卡住&#xff08;阻塞&#xff09;。异步 定时器解决的问题&#xff1a; 不让长时间 / 等待类的任务卡住后面代码&#xff0c;先把等待任务…

作者头像 李华
网站建设 2026/9/24 9:30:23

校园云机房改进方案:IPv6双栈桌面池与网络虚拟化落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:30:16

EMC四大测试本质:CE/RE/CS/RS物理耦合路径解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:18:06

六相PMSM与双三相PMSM怎么选?从绕组拓扑到工程容错一次讲透

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:15:51

HeliosEquipment透传解耦:机台与业务解耦的轻量级通道设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华