本页目录(10 节)

长期记忆

Spark Work 三层长期记忆系统:User / Project / Agent 作用域隔离不串味,后台独立 LLM 抽取(与 OpenAI Memory、Mem0 同款架构)不干扰主对话,FTS5+sqlite-vec 混合检索,自动整合进化,search_memory / recall_memory 工具按需深挖。

阅读约 6 分钟最后核对 2026-07-04

默认情况下每次新会话都从零开始:你上次说过的偏好、项目里的技术选型、长期约定, 下个会话就没了。Spark Work 内置三层长期记忆:记忆以 markdown 文件为事实来源、 以本地 SQLite 为索引,由每轮对话结束后触发的后台抽取写入, 在每轮对话开始时按相关度检索并注入 system prompt。

1. 三层作用域:键名、scope_ref 与落盘位置

scope 只有三个取值:user、project、agent。 真正决定「这条记忆属于谁」的是 scope_ref:

scopescope_refmarkdown 落盘位置可见范围
userNULL~/.spark-agent/memory/user/<id>.md所有项目、所有 Agent 共享(身份、跨项目偏好)
projectworkspaceId<workspace>/.spark-agent/memory/<id>.md仅该工作区;跟随代码目录走,可随仓库迁移
agentagentId~/.spark-agent/memory/agent/<agentId>/<id>.md仅该 Agent 角色(含团队成员各自的 agent scope)

每个 scope 目录下还会维护一份 MEMORY.md 索引(每行- [名称](文件.md) — 描述),方便你直接翻目录理解 Agent 记住了什么。 记忆文件用「先写 .tmp 再 rename」的原子写入,避免半截文件。

两个真实约束:project 记忆必须有工作区——会话没绑定工作区时, 候选里 scope=project 的条目会被丢弃(写入端也会报 VALIDATION_FAILED);scope_ref 的空串会被归一成 NULL,避免出现既不是 NULL 也不是合法 ID 的 「孤儿 project 记忆」。

另一条容易混淆的维度是 type,它和 scope 正交,取值四个:user(关于用户的事实)、feedback(行为守则,如「先给结论」「别自动提交」)、project(项目决策)、reference(外部资料线索)。 scope 决定「谁能看到」,type 决定「怎么处理」——例如 feedback 类型永远全量注入,不靠检索。

2. 写入:每轮结束的后台抽取与五道闸门

触发时机:宿主会话的每一轮 turn 结束、且助手产生了非空文本时, 运行时异步调用记忆写入流程(fire-and-forget:任何异常只写日志,绝不影响主对话)。 团队模式下,每个成员的回复也会按该成员的 agentId 单独触发一次抽取, 所以成员发现的技术决策同样会沉淀。

用哪个模型:读取设置项 memory.extractionProviderId 与 memory.extractionModel,通过模型服务发起一次真实 LLM 调用。 支持 Anthropic 原生(/v1/messages)与 OpenAI 兼容(/chat/completions) 两种协议,不支持 responses API。两个设置项都没配时,自动回退到当前会话正在用的对话模型 (@mention 到成员时用该成员的模型),因此零配置也能用;模型不可用时这一轮就不产出记忆, 日志里会写 memory extraction LLM unavailable。

抽取结果要连续过五道闸门,任何一道没过就丢弃:

  1. 瞬时数据:命中「临时/当前这次/今天」这类模式的名字或描述被拒,避免把一次性状态当长期事实。
  2. 置信度:模型给的 confidence 必须 ≥ 0.6。
  3. 去重与合并:与该 scope 内已有记忆比对;V2 会用 FTS 召回相似条目,让模型判定 ADD / UPDATE / DELETE / NOOP,而不是无脑新增。
  4. 配额:默认上限 user 100 / project 200 / agent 50(可用设置项 memory.quota 覆盖单层数值)。
  5. 敏感内容:命中密钥、令牌、私钥等模式(以及隐私类模式)的记忆被拒,不落盘、不建索引。

通过后按顺序落三处:markdown 文件(事实来源)→ SQLite 行(memory_entry, 同事务维护全文索引)→ 刷新 MEMORY.md。落库时会把抽取出的实体(人名、库名、 模块名、系统名)写进实体关联图,供后续检索做一跳扩展。

3. 检索:FTS5 + 向量两路并行,RRF 融合后重排

  • FTS5 全文:memory_fts 是与 memory_entry rowid 对齐的 contentless FTS5 表,索引 name / description / 正文;中文在写入与查询两侧都做逐字预分词 (两侧不一致会直接查不到),排序用 bm25()。
  • 向量检索:memory_vec 是 sqlite-vec 的 vec0 虚拟表, 维度取决于你配的 embedding 模型,运行时惰性建表并把维度记在设置项 memory.vecDimension; 换模型导致维度变化时会重建表,旧向量丢弃后由后台懒回填补齐。查询走 KNN(embedding MATCH ? AND k = ?),先多取候选再回表按 scope / type / 归档 / 失效过滤。
  • 融合与重排:两路各取 top20,用 RRF 融合(score = Σ 1/(60 + rank), 同一条双路命中则相加),再乘时间衰减与置信度:finalScore = rrf × exp(-λ × 距今天数) × confidence,λ 默认 0.01(设置项 memory.timeDecayLambda,数值越大旧记忆沉降越快)。
  • 过滤:只召回 archived = 0 且 invalid_at IS NULL(仍有效)的条目。
  • 降级链:向量不可用(未配 embedding 或调用失败)→ FTS-only; FTS 本身抛错且没有向量 → 告诉调用方回退到「全量 + 类型优先级」的注入方式。 每一级降级只写日志,界面上不会报错。

4. 会话注入:每轮开始注入,不是开场注入一次

每次开始新的一轮对话时(不是每个会话只做一次),运行时都会重新组装 memory block:

  • feedback 全量:所有 feedback 类型的记忆都注入(按更新时间倒序), 不依赖检索命中——行为守则不能靠运气。这个排序刻意不依赖 hit_count, 以保证同一会话里重复渲染的字节稳定,不破坏提示词前缀缓存。
  • 其它类型按相关度取子集:以「Agent 名称 + Agent 描述 + 工作区目录名」作为种子查询, 走上面的混合检索取相关条目;检索不可用或无结果时回退到全量 + 类型优先级排序。
  • token 预算:默认上限 4000 token(设置项 memory.maxInjectTokens), 超出时按 feedback > user > project > reference 的优先级裁剪。
  • 结构:注入成 <user-memory>、<project-memory workspace="...">、<agent-memory> 三段, 末尾告诉模型「需要更多用 search_memory,需要正文用 recall_memory」。 记忆文本里的尖括号会被替换,防止内容破坏这些结构标签。
  • 来源提示:同时注入一段 [Memory Provenance] 约束:记忆摘要可能是模型生成的, 不得当成用户原话或最终决定;与用户当前明确说法冲突时以当前为准。

5. Agent 可用的记忆工具(只读)

工具参数返回
mcp__spark_memory__search_memoryquery(必填,1–500 字符)、type(可选:user / feedback / project / reference)、limit(默认 8,最大 20)命中的记忆 id + 摘要列表;检索不可用时返回明确的「已降级」提示
mcp__spark_memory__recall_memoryid(必填,取 search_memory 返回或注入摘要里方括号内的 id)该条记忆的完整 markdown 正文(含 Why / How to apply);已失效的条目仍返回正文,但会前置「已于某时失效、已被某条取代」的警示

调用 recall_memory 会把该条记忆的 hit_count +1, 但不会改 updated_at——同样是出于保护提示词缓存的考虑。

这三条运行时路径都会挂上同一组工具,语义与检索后端完全一致:

  • Claude SDK 路径:进程内 MCP server(直接访问本地数据库,没有子进程)。
  • Codex CLI / Claude CLI 路径:spark_memory stdio 子进程,通过本地 Platform Bridge 的 HTTP RPC 回到主进程。
  • Spark 引擎路径:由引擎运行时按内置 server 名单注入同名工具。

重要边界:Agent 侧没有写入或删除记忆的工具。 记忆写入只来自后台抽取,增删改只在「设置 → Agent → 记忆」面板里由人完成。 这样设计是为了防止 Agent 在对话中把临时信息固化成长期事实。

6. 整合 job:合并重复与升华规律

后台还会周期性跑一次「整合」:在记忆启用时,于每轮对话开始阶段异步触发, 对 user / project / agent 三层分别判断(fire-and-forget,失败只写日志)。

  • 触发条件:该层记忆条数达到 memory.consolidationThreshold(默认 30), 且距上次整合已超过 memory.consolidationIntervalDays(默认 7 天);memory.consolidationEnabled 默认开启。上次整合时间按lastConsolidationAt:<scope>:<scopeRef> 记录,因此三层各自计时。
  • MERGE:把多条语义重复的记忆合并成一条(保留其中一条,其余归并),减少冗余。
  • ELEVATE:把多条零散反馈升华成一条更通用的新记忆,正文末尾会附「升华来源」列表, 指向原始条目,便于追溯。

除了整合,抽取阶段本身还有演化判定(ADD / UPDATE / DELETE / NOOP),两者共同避免记忆无限膨胀。 真机调试时可以把阈值调小(例如阈值 2、间隔 0.01 天)来快速观察整合效果。

7. 失效与实体图:旧记忆不删除,但会标失效

  • bi-temporal 字段:memory_entry 有 valid_from /invalid_at / superseded_by。事实失效时不删文件, 而是写入 invalid_at 与被谁取代的 id。
  • 失效后仍可读:检索默认过滤掉失效条目(invalid_at IS NULL), 但你用 recall_memory 显式读它时仍能拿到正文,并带失效警示, 便于理解「当初为什么这么定」。
  • 实体关联图:memory_entity(规范化去重的实体)+memory_entity_link(记忆 ↔ 实体多对多)。实体名做小写与别名归一 (如 Arco Design 与 @arco-design/web-react 都归到 arco), 同一 scope 内按归一化名去重;检索命中记忆后可顺着共享实体做一跳扩展, 把「讲同一套技术栈」的记忆一起带出来。

8. 配置:设置 → Agent → 记忆

入口只有一处:桌面端「设置」页左侧 Agent 分组下的「记忆」(面板标题为「长期记忆」)。 当前没有 Web 管理端或独立记忆页——应用内虽有 memory 视图路由,但它同样渲染设置页的 记忆分区,不是一个单独的界面。面板顶部有「新增」「配置」两个按钮,页面主体是记忆列表与筛选, 所有开关都在「配置」抽屉里:

设置项(category = memory)含义默认 / 取值
enabled总开关:关闭后注入 / 写入 / 整合全停默认开
extractionProviderId / extractionModel抽取用的 Provider 与模型;可清除,清除后回退到对话模型空(回退对话模型)
embeddingProviderId / embeddingModel向量检索用的 Provider 与模型,仅支持 OpenAI 兼容(Anthropic 不提供 embedding)空(FTS-only)
consolidationEnabled是否启用整合 job开
consolidationThreshold / consolidationIntervalDays整合触发阈值(条)与间隔(天)30 / 7
maxInjectTokens每轮注入的记忆 token 上限4000
timeDecayLambda时间衰减系数,越大旧记忆沉降越快0.01
quota按 scope 的条数配额覆盖(对象:如 {"user": 100, "project": 200, "agent": 50})100 / 200 / 50
vecDimension / ftsBackfillDone内部状态:向量表维度、FTS 存量回填是否完成—(不要手改)
  • 「测试抽取配置」:按下会真的发起一次 LLM 调用,返回模型名与一段样例输出; 失败时给出原因(例如 settings 未配且当前没有可回退的对话模型)。配错 key 时用它主动验证, 不用等「记忆静默不生成」。
  • 「重建向量索引」:换了 embedding 模型或怀疑向量表异常时用, 重建后会提示「后台正按新模型回填全部记忆,条目多时可能持续几分钟,期间向量检索会逐步恢复」。
  • 改完何时生效:抽屉底部的提示写明「配置改完下一个新会话生效」—— 注入是在新一轮开始时重算的。

面板本身可以做的事:

  • 筛选:按 scope(User / Project / Agent)、按 type(全部 / User / Feedback / Project / Reference)、按项目、按 Agent、仅有效 或 含失效、按 name / description 搜索。
  • 批量操作:勾选后批量归档、批量删除。
  • 单条详情:编辑描述与正文(markdown)、保存、归档、删除;失效条目会显示失效时间与被谁取代。
  • 手动新增:指定 scope(Project / Agent 需要选具体 workspaceId / agentId)、type、name、描述、正文与实体(逗号分隔)。

9. 存储与隐私

  • 双层存储:markdown 文件是事实来源(可读、可直接编辑、项目级跟随代码目录), SQLite 存索引与元数据。相关表:memory_entry(条目)、memory_fts(全文)、memory_vec(向量)、memory_entity / memory_entity_link(实体图)。
  • 本地优先:没有云端同步,记忆不会离开本机。
  • 两道自动闸门保护隐私:敏感内容(密钥 / 令牌 / 私钥 / 隐私信息)在写入前被拒; 瞬时状态(「今天」「这次」之类)不落库。
  • 不进 prompt 的东西:记忆正文不会整篇塞进 system prompt,只注入摘要行; 模型需要细节时得显式 recall_memory,这一步在日志里可见(便于审计 Agent 到底读了什么)。
  • 删干净:面板删除会同时删数据库行与 markdown 文件;归档则保留文件、 让条目退出检索与注入。

10. 常见坑与排查

现象排查方向
聊了半天一条记忆都没有先确认总开关;再看日志里是否出现 memory extraction triggered 与随后的 memory extraction LLM unavailable(抽取模型不可用)。也可以点「测试抽取配置」直接验证连通性
对话很短/只有工具调用,没有记忆抽取需要助手产生了非空文本才触发;纯执行型回合可能不产出候选
候选没通过依次检查:是否被判定为瞬时数据、confidence 是否低于 0.6、是否命中敏感词、该层配额是否已满
项目记忆写不进去该会话没有绑定工作区。project scope 必须带 workspaceId,未绑定时候选会被丢弃
语义检索不生效,只有关键词能中没配 embedding(正常降级为 FTS-only);或 sqlite-vec 加载失败(日志 sqlite-vec load failed, vector search disabled)。换过 embedding 模型后要点「重建向量索引」
Agent 说「我不记得」本轮注入为空(三层都没条目,或 token 预算裁掉了);可以让它调 search_memory 主动检索确认,而不是直接下结论
记了但下次换个说法就找不到FTS 是关键词匹配;要让「组件写法」召回「偏好函数式组件」,必须配 embedding 才有语义通道
能不能让 Agent 自己「记住这个」Agent 没有写记忆的工具。它能做的是把信息留在对话里,由后台抽取判断是否值得长期保存;要立刻固定,请到记忆面板手动「新增」

速查表

作用域键名
user / project / agent(scope_ref 分别为 NULL / workspaceId / agentId)
记忆类型
user / feedback / project / reference(与 scope 正交)
落盘位置
~/.spark-agent/memory/user|agent/<agentId>/ 与 <workspace>/.spark-agent/memory/
抽取触发
每轮 turn 结束后异步触发(宿主 + 团队成员各自触发)
抽取模型
memory.extractionProviderId + extractionModel;空则回退对话模型
置信度阈值
confidence ≥ 0.6
配额默认
user 100 / project 200 / agent 50
检索
FTS5 BM25 + sqlite-vec KNN → RRF(k=60) → exp 时间衰减(λ=0.01) × confidence
注入预算
默认 4000 token,优先级 feedback > user > project > reference
整合默认
开启;阈值 30 条、间隔 7 天(三层分别计时)
Agent 工具
mcp__spark_memory__search_memory / recall_memory(只读)
配置入口
设置 → Agent → 记忆(长期记忆面板 → 配置)

常见问题

展开 6 条常见问题
  • 记忆会不会被别的项目看到?

    不会。project 记忆用 scope_ref = workspaceId 隔离,只在同一工作区可见,文件也存在该工作区的 .spark-agent/memory/ 下;user 记忆跨项目共享;agent 记忆按 agentId 隔离。切换工作区时项目记忆自动不可见。

  • 是谁决定「该不该记」?Agent 会看到记忆正文吗?

    后台抽取决定,Agent 不参与:每轮结束后台异步用抽取模型判断候选,过五道闸门后落库。Agent 侧只有 search_memory(拿 id + 摘要)和 recall_memory(拿正文)两个只读工具,没有写入权限。system prompt 里默认只有摘要行。

  • 配错抽取模型会怎样?

    这一轮不产出记忆,主对话完全不受影响(fire-and-forget,异常只写日志)。在「设置 → Agent → 记忆 → 配置」里点「测试抽取配置」会真实调用一次模型并返回结果或失败原因,不用等记忆静默不生成才发现。

  • 不配 embedding 模型能用吗?

    能,会降级成 FTS-only:只有关键词命中,没有语义召回。配了 OpenAI 兼容的 embedding 后会自动建向量表并后台回填;换模型后需要点「重建向量索引」重新生成向量。

  • 记忆存在哪里?我能直接改吗?

    正文是 markdown:user 在 ~/.spark-agent/memory/user/,project 在工作区 .spark-agent/memory/,agent 在 ~/.spark-agent/memory/agent/<agentId>/;索引与元数据在本地 SQLite(memory_entry / memory_fts / memory_vec / memory_entity)。可以手工编辑文件,但更推荐用面板编辑,避免文件与索引不一致。

  • 怎么让一条记忆失效?

    演化机制会在新事实与旧记忆冲突时把旧条目标记失效(写 invalid_at 与 superseded_by),文件保留但退出检索与注入;不需要历史时可在面板里归档(退出检索、保留文件)或删除(文件和记录一起删)。