从 Session Log 到 Memory Plugin:如何扩展 DeepSeek Harness

翻看 DeepSeek Harness 的源码,会发现一件有些意外的事:它没有一个叫作 memory 的核心包,也没有定义事实记忆、情景记忆、语义记忆或者程序记忆。
这并不意味着 DSH 完全没有考虑历史信息。相反,它已经实现了一套相当完整的 Session 日志、上下文压缩、历史查询和 Skill 加载机制。只是这些组件首先服务于 Agent 的运行、恢复和上下文管理,并没有被包装成一套长期记忆系统。
Session Log 记录的是 Agent 做过什么,Compaction 解决的是上下文放不下怎么办,Session Query 解决的是如何读取过去的运行记录,Skill Registry 解决的是怎样加载已有的操作说明。它们都与记忆有关,但任何一个都不等于 Agent Memory。
如果要为 DSH 增加真正的长期记忆,更合理的做法不是重新解释这些组件,也不是把 Memory 代码写进 Agent Loop,而是在现有插件体系上增加一条新的能力链。
DeepSeek Harness 的架构原则是“everything is a plugin”。模型、工具、Session 持久化、Compaction、Skill、Subagent 乃至 Agent Loop 本身,都通过 Cordis 插件树组合起来。新增能力通常不是修改一个庞大的核心,而是定义新的服务接口,再挂载相应的 Provider 和 Consumer。
这套设计很适合快速扩展,但它也要求每个概念先把边界说清楚。
“Memory”恰好是一个边界非常模糊的词。有人把完整聊天记录称为记忆,有人把用户画像称为记忆,也有人把向量数据库、上下文摘要、项目规则和 Skill 都归到 Memory 下面。如果 DSH 直接提供一个笼统的 ctx.memory,却没有先回答存什么、如何更新、按什么作用域隔离以及怎样进入模型上下文,这个接口很快就会退化成另一个通用的文本存取 API。
所以,当前 DSH 选择先解决更底层、定义更明确的问题:运行历史如何成为可靠事实,模型上下文如何从历史中生成,长任务如何在有限窗口中继续,以及过去的 Session 如何被重新读取。
这些能力还不是 Memory,却决定了未来的 Memory 能不能被正确地实现。
可持久化、可回放的运行历史
DSH 使用 append-only SessionEvent Log 记录一次 Agent 运行。
用户消息、模型回答、工具调用、工具结果、Turn 和 Step 边界都会进入这份日志。模型看到的消息历史并不是单独保存的另一份副本,而是由 Session 根据事件日志重新投影出来。Fork、Resume、Transcript、Telemetry 和持久化也都从同一条事件流派生。
这使 Session Log 成为运行层的事实来源。
假设 Agent 声称自己修改了一个文件,仅保存最终回答是不够的。Session Log 还能保留它调用了什么工具、工具返回了什么结果、调用是否完整结束,以及这个回答来自哪个模型。Agent 中途退出后,恢复逻辑也能区分一个工具调用是尚未开始,还是已经执行但结果没有成功写入。
默认的 JSONL 持久化后端会把这些事件写入 Harness Home 下的 Session 目录;另一个 SQLite 后端则把每个事件保存成独立记录。无论采用哪种介质,上层看到的仍然是同一套 Session Persistence 接口。
对于未来的 Memory Plugin,这份日志有两个价值。
第一,它提供了比模型总结更可信的原始材料。第二,任何派生记忆都可以反向引用具体的 Session 与 Event,而不必把一段缺少来源的自然语言当作事实。
但 Session Log 本身仍然不是记忆系统。它保存全部历史,却不判断其中哪些内容值得跨 Session 使用,也不处理“以前使用 SQLite、现在已经迁移到 PostgreSQL”这样的状态变化。
原始历史与模型上下文的分离
如果每次请求都把完整 Session Log 重新发送给模型,长任务很快就会超过上下文窗口。DSH 因此在原始日志之上维护了一层 Session Surface:日志负责保存完整事实,Surface 决定哪些消息进入下一次模型请求。
当上下文压力达到阈值后,Compaction 会选择较早的一段历史,调用模型生成结构化检查点,再用这个检查点替换 Surface 中的旧消息。被替换的原始事件仍然保留在日志里,只是不再出现在模型当前上下文中。
默认检查点会整理用户目标、技术概念、文件与代码、错误和修复、未完成任务、当前工作、下一步以及关键约束。它要解决的是一个很具体的问题:让另一个模型能够在有限上下文里继续当前任务。
这是一套上下文生命周期机制,而不是长期记忆提取器。
Compaction 的目标是压缩,不是建立精确的用户状态。摘要可能合并细节,也可能舍弃当时看起来不重要、后来却重新有用的信息。它还会在后续压缩中继续合并旧检查点,因此不适合作为长期记忆的唯一事实来源。
未来的 Memory Plugin 可以复用 Compaction 的接入方式,却不应该简单地把 Compaction Summary 当作记忆写入。更稳健的方式,是让 Memory 从原始 Session Event 中提取,并把摘要只视为当前任务的工作状态。
对 Session 历史的统一查询
DSH 已经提供了独立的 ctx.sessionQuery。它可以读取当前与已持久化的 Session,过滤事件,查看 Fork 和父子关系,追踪事件引用与替换,并通过 SQLite FTS5 对历史内容进行全文搜索。
相应的模型工具包括:
这些工具目前属于可选组件,官方 Host 默认不会把它们挂载给模型。基础配置中的全文搜索也默认关闭,需要部署者显式指定索引文件并启用搜索。
即使打开,它搜索的对象仍然是 Session Event。
例如,项目最初决定使用 SQLite,后来又迁移到 PostgreSQL。Session Query 可以把两次讨论都找到,却不会自动判断哪一条是当前有效状态。它回答的是“哪里出现过这些内容”,而不是“现在应该相信什么”。
这正是 Session Query 与 Memory Query 的边界:
未来两者不应该相互替代。Memory Search 找到一条项目决策后,仍然可以通过 session_id 和 event_seq 回到 Session Query,读取当时的原始证据。
可扩展的 Skill 加载机制
DSH 的 Skill Registry 可以合并多个 Provider 提供的 Skill。Skill 可以来自用户目录、项目目录、内置包,也可以来自未来的远程服务。模型首先看到名称和描述,需要时再加载完整内容,从而避免把所有 Skill 一次性塞进上下文。Skill Registry
但 Registry 只负责发现与加载,不负责学习。
一次任务执行成功后,DSH 不会自动判断其中是否存在可复用方法,也不会生成候选 Skill、运行回归测试或者替换旧版本。Agent 可以在权限允许时修改 Skill 文件,但这是普通的文件写入能力,不是一条受治理的经验学习流水线。
因此,Skill Registry 为程序性资产准备了落点,真正的“从经验到 Skill”仍需要新的插件完成。
DSH 当前已经形成了这样一条链路:
要成为长期记忆系统,中间还需要增加一层语义处理:
其中最关键的变化,是从“历史记录”走向“可演化状态”。
假设两个 Session 中分别出现:
历史系统应该保留两条记录,因为它们都真实发生过。Memory 系统则需要额外维护:
旧的 SQLite 记录不必消失,但不能继续以当前状态进入模型上下文。
这一步涉及记忆选择、结构化、冲突处理、时间关系、作用域、来源、权限和删除传播,已经超出了 Session Persistence 或全文检索的职责。它应该由一条新的 Memory Capability Seam 负责。
按照 DSH 现有的 Capability Seam 设计,一项可替换能力通常包含三个角色:
因此,可以增加一个只定义协议的 dsh-memory 包:
这个接口不应该规定底层必须使用向量数据库或知识图谱,也不应该要求所有 Provider 采用同一种 Memory Schema。它只定义 DSH 与记忆后端之间必须保持稳定的交互边界。
具体实现可以是:
本地实现可以使用 SQLite、向量索引或图数据库,外部实现则把统一请求转换为供应商 API。
更重要的是 Consumer。仅有一个 ctx.memory 服务,Agent 仍然不会主动写入和使用记忆。至少还需要写入、召回和治理三个 Consumer。
Memory Writer 可以监听 session/event。当一个 turn/end 落盘后,它读取这一轮完整的用户消息、模型回答和工具结果,判断是否存在值得跨 Session 保存的内容。
写入过程可以分成四步:
这里不应该让模型直接修改存储。模型可以生成候选操作:
Provider 或独立治理插件再验证:
来源事件是否存在;
新事实是否真的覆盖旧事实;
作用域是用户、项目还是当前 Agent;
当前调用方是否有权修改;
是否需要保留旧值的有效时间;
更新失败后能否重试或回滚。
写入可以异步执行,但异步不能变成不可观察的最终一致性。如果系统对外报告记忆写入成功,那么它必须已经可以被搜索;如果仍在处理,就应该保留明确的任务状态,而不是让下一次 Session 偶尔找不到刚刚生成的记忆。
Memory Retrieval Consumer 可以接入 agent/pre-step。
它拿到当前用户请求后,先判断应该在哪些作用域搜索:
然后调用 ctx.memory.search(),执行时间、权限、状态和相关性过滤,再把结果组织成一段带来源信息的上下文。
这一过程必须遵守 DSH 已有的一条重要原则:
只要一条 Memory 进入模型上下文,它就必须可以从当前 Session 重建。否则,同一个 Session 恢复后可能拿到不同的外部记忆,导致模型请求无法复现。
一种可行方式,是把召回结果保存成带专用 MessageSource 的 user/message:
这样,Memory Provider 后续发生更新或删除,也不会悄悄改变已经完成的历史请求。当前 Session 保存的是“当时模型实际看到了什么”,Memory Store 保存的则是“现在应该提供什么”。
新增 Memory Plugin 后,现有组件不需要被改造成另一套东西。
Session Log 继续保存原始运行事实。它是 Memory 的证据来源,也是出错后重新抽取和审计的依据。
Compaction 继续管理当前 Session 的上下文容量。它可以压缩已经注入的 Memory Context,但不负责决定长期状态。Memory Writer 应优先读取原始事件,而不是从多次压缩后的摘要中继续抽取。
Session Query 继续查询完整历史。当 Memory 返回的结论存在争议时,可以沿着来源回到具体 Session 和 Event。全文搜索也可以作为 Memory Provider 的一种召回通道,但不能替代状态更新。
Skill Registry 继续加载已经发布的 Skill。另一个 Experience-to-Skill 插件可以分析成功或失败的任务轨迹,生成 Candidate Skill,经过测试和审批后再注册到 Skill Provider:
这条链路不能在每次任务结束后直接修改正式 Skill。一次成功可能只是偶然,一次失败也可能来自网络、权限或者工具异常。程序记忆需要比普通事实记忆更严格的验证。
插件化并不意味着把所有东西都塞进一个 Memory Provider。
它不应该复制完整 Session Log。原始事件已经有自己的可靠存储,Memory 只保存派生状态和必要的来源引用。
它也不应该把 Compaction Summary 当成唯一输入。摘要的目标是缩短上下文,不是保持所有事实与限定条件。
它不应该只有一个全局向量库。至少需要区分用户、项目、Agent 和组织作用域,否则跨项目污染和权限泄漏几乎不可避免。
它更不应该让外部 Memory 的最新结果直接进入模型而不记录。那会破坏 DSH 已经建立的可回放性。
真正适合 DSH 的 Memory Plugin,应该接受一个看似矛盾的设计要求:Memory Store 可以不断演化,但每一次模型实际使用的 Memory Context 必须是不可变、可追溯的历史快照。
DeepSeek Harness 当前没有定义 Agent Memory。它实现的是一套运行基础设施:Session 保存事件,Persistence 让事件跨进程存在,Surface 决定模型看见什么,Compaction 控制上下文规模,Session Query 重新读取历史,Skill Registry 加载可复用指令。
这些组件并没有组成长期记忆闭环,但它们解决了 Memory 最容易被忽略的底层问题:原始证据在哪里,派生信息如何进入模型,历史请求怎样复现,以及新的能力应该挂载到什么位置。
因此,为 DSH 增加 Memory 最合理的路线,并不是在 Agent Loop 中加入一段固定的“总结并写向量库”逻辑,而是增加一条完整的插件能力链:
DSH 没有提前替开发者决定记忆应该长什么样。换个角度看,这可能正是它最适合实验 Agent Memory 的地方。








