首页
/ DeepSeek Harness 接入 Mem0 长期记忆:deepseek-plugin 双工具实现全解析

DeepSeek Harness 接入 Mem0 长期记忆:deepseek-plugin 双工具实现全解析

2026-09-05 09:35:24作者:伍霜盼Ellen

DeepSeek Harness(Cordis)智能体在会话之间不保留任何状态,而 @mem0/deepseek-plugin 通过注册 search_memoryadd_memory 两个原生工具,让 Harness Agent 获得由 Mem0 托管后端支撑的跨会话长期记忆。本文基于仓库中 integrations/deepseek-plugin/README.md 的完整文档脉络,结合 src/index.ts 等源码实现,拆解该插件的工具注册机制、记忆作用域设计、输出截断保护与遥测方案,并给出可复制的本地运行与配置方式。

读完本文,你将能够:独立构建并加载该插件到 DeepSeek Harness、按 userId / agentId / runId 三级作用域管理记忆、理解 search 与 add 两条调用链在参数键名上的刻意差异,以及正确解读异步写入的 PENDING 响应。

插件定位:两个工具,一个托管记忆后端

插件对外只暴露两个 Agent 可调用的工具:

工具 职责
search_memory 按查询从 Mem0 召回相关事实
add_memory 将一条事实写入 Mem0,供后续会话使用

与生态中常见的本地文件型记忆插件不同,Mem0 是托管后端:抽取、语义去重与冲突消解都在服务端完成,同一份记忆库可以被 Harness、Claude Code、Codex 等其他智能体复用。这一差异直接体现在两个工具的行为设计上——add_memory 的写入是异步服务端抽取,插件描述中明确要求模型"不要立刻 search 来确认写入成功",因为事实需要片刻时间才可检索到(见 src/index.tsadd_memory 的 description 字段)。

架构图

[ mem0ai SDK ]  <-- 托管记忆,由 Mem0 拥有
      |
[ deepseek-plugin: apply(ctx) -> ctx.tools.register(...) ]  <-- 本包
      |
[ DeepSeek Harness ]  <-- 通过 cordis.yml 加载的智能体

Cordis 插件模型:apply 与可回滚的注册

一个 Cordis 插件是导出 apply(ctx, config) 的模块。本插件声明 inject = ["tools"],表示它要等 Harness 的工具注册表就绪后再挂载,随后通过 ctx.tools.register(defineTool(...)) 注册两个工具;当插件卸载时,注册的工具会被自动移除——这是 Cordis 的 revertible effects(可回滚副作用)机制:

// integrations/deepseek-plugin/src/index.ts
export const name = "mem0";
export const inject = ["tools"];

export function apply(ctx: Context, config: Config): void {
  // ...校验配置、创建 MemoryClient 后
  ctx.tools.register(defineTool({ name: "search_memory", /* ... */ }));
  ctx.tools.register(defineTool({ name: "add_memory", /* ... */ }));
}

apply 入口处有两道硬校验(src/index.ts):apiKey 缺省时回退到 MEM0_API_KEY 环境变量,仍无则抛错;userId 为必填项,缺失直接抛错。单测 tests/apply.test.ts 精确验证了这两条失败路径以及"成功挂载后恰好注册 add_memorysearch_memory 两个工具"这一契约。

本地构建与加载

前置条件

  • 一个 Mem0 Platform 账号及 API key(key 以 m0- 开头);
  • 已安装 DeepSeek Harness;
  • 在 shell 中导出 MEM0_API_KEY

四步跑通

  1. 构建插件(构建脚本为 tsup,产物在 dist/):

    cd integrations/deepseek-plugin
    pnpm install
    pnpm build
    
  2. 设置 Mem0 密钥:

    export MEM0_API_KEY=...
    
  3. 让 Harness 指向插件:复制 cordis.example.yml,把 name 字段改为 dist/index.js绝对路径、填上你的 userId,然后加载:

    pnpm dsh web --patch ./integrations/deepseek-plugin/cordis.example.yml
    
  4. 打开 http://127.0.0.1:3080,先让 Agent 记住一件事,再在后续轮次要求它回忆,验证记忆跨轮次持久化。

示例配置 cordis.example.yml 的完整内容(注释说明其加载方式):

# Example DeepSeek Harness config that loads deepseek-plugin alongside the built-in
# tools plugin. Load with:
#
#   pnpm dsh web --patch ./integrations/deepseek-plugin/cordis.example.yml
#
# The tools plugin (and its system-prompt dependency) must be present, because
# the memory tools contribute schemas the system prompt renders.

- name: "@deepseek-ai/dsh-system-prompt"
- name: "@deepseek-ai/dsh-tools"
- insert:
   - id: mem0
     # Absolute path to the built plugin (run `pnpm build` first), or point
     # at src/index.ts when running through tsx during development.
     name: "/absolute/path/to/mem0/integrations/deepseek-plugin/dist/index.js"
     config:
       # apiKey is read from the MEM0_API_KEY env var when omitted here.
       userId: "your-user-id"
       # host: "https://your-onprem.mem0.ai"   # optional: Platform on-prem / dedicated base URL

注释中有一个容易踩坑的提示:@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-tools 两个内置插件必须同时存在,因为记忆工具会向系统提示词注入 schema 供渲染。开发阶段也可以不构建,直接把 name 指向 src/index.ts 用 tsx 运行(示例文件注释中已注明)。

关于 host 的重要澄清

对于 Mem0 Platform 的 on-prem 或专属部署,把 config.host 指向对应 base URL(默认 api.mem0.ai)。需要特别注意:host 只是 Platform 的 base-URL 覆盖,并不是切换到自托管 Mem0 OSS 的开关——自托管 OSS 的 server 暴露的是另一套 API 面,二者不兼容。这一点在 src/index.tsConfig.host 注释和 docs/integrations/deepseek-plugin.mdx 中均有明确说明。

配置项速查

字段 必填 默认值 说明
apiKey $MEM0_API_KEY Mem0 Platform API key
userId 记忆所属的实体(Mem0 用户作用域)
host api.mem0.ai Platform base URL(on-prem / 专属部署)

对应源码中的 Config 接口(src/index.ts):apiKeyhost 均为可选,userId 必填。MemoryClient 的构造为 new MemoryClient({ apiKey, ...(config.host ? { host: config.host } : {}) }),即仅当显式配置 host 时才传递给 SDK。

工具实现细节:作用域、格式与截断

三级作用域与"刻意的键名不对称"

插件挂载时绑定一个默认 userId,但一次 Harness 安装可能服务多个实体,因此两个工具都接受可选的 userId / agentId / runId 参数,在单次调用级别覆盖挂载时的默认值。参数描述直接写给模型看(src/index.ts):

  • userId:"只在你确实要读写另一个用户的记忆时才设置";
  • agentId:按 Agent 分区记忆;
  • runId:按会话分区记忆。

真正的实现集中在 src/scoping.ts,其中有一段值得细读的设计说明:两个调用点的键名大小写差异是刻意的

  • search 把作用域放在 filters 里并原样发给平台,因此必须 snake_caseuser_id / agent_id / run_id)——因为 Platform 的 search 接口不接受顶层实体参数;
  • add 的实体参数走顶层,经过 SDK 的 camel→snake 转换器,因此必须 camelCaseuserId / agentId / runId)。
// search:snake_case,spread 进 filters 后原样发给平台
export function resolveSearchFilters(params, defaultUserId) {
  const filters = { user_id: clean(params.userId) ?? defaultUserId };
  if (agentId) filters.agent_id = agentId;
  if (runId) filters.run_id = runId;
  return filters;
}
// add:camelCase,顶层参数经 SDK camel->snake 转换
export function resolveAddParams(params, defaultUserId) {
  const out = { userId: clean(params.userId) ?? defaultUserId };
  if (agentId) out.agentId = agentId;
  if (runId) out.runId = runId;
  return out;
}

空串或空白参数会被 clean() 归一为 undefined 并回退到配置的默认用户。tests/apply.test.ts 用断言固化了这一行为:调用时传 userId: "alice", limit: 3,SDK 收到的必须是 client.search("x", { filters: { user_id: "alice" }, topK: 3 })

输出格式化:为 token 而生的紧凑渲染

src/formatting.ts 把 Mem0 结果渲染成 token 友好的单行格式,与 Mem0 生态中其他插件保持一致,保证同一条记忆在每个 Harness 里读起来相同:

[分类] 记忆文本 (距今时间) [mem0:记忆ID]

例如 1. [preference] Likes tea (2h ago) [mem0:m1]。若直接倾倒原始 search 响应 envelope,大部分 token 会浪费在 JSON 骨架上。空结果渲染为 No memories found.,距今时间按分钟/小时/天三档降级(5m ago / 3h ago / 12d ago)。

add_memory 的异步写入与 PENDING 响应

client.add 打的是异步的 /v3/memories/add/ 端点,返回 { event_id, status: "PENDING" }——抽取在调用返回之后才在服务端执行,所以响应里不含抽取出的记忆。formatAddResult 因此做双分支处理:

  • 检测到 PENDING:输出 Memory queued for background extraction (event evt-123); it will be searchable shortly.(注意 SDK 会把响应键 camel-case 化,所以代码同时接受 eventIdevent_id);
  • 后端真的返回了记忆列表(旧版 / OSS 形状):输出 Stored N memories: 加紧凑列表。

测试用例 test "reports the write as queued on the async PENDING response" 同时验证了写入时的完整参数形态:client.add([{ role: "user", content: text }], { userId: "u", source: "DEEPSEEK_HARNESS" })

输出截断:防止上下文被单次工具调用打爆

src/output.ts 对工具输出施加硬性上限:200 行或 50KB(与兄弟插件 pi-agent-plugin 相同的守卫值)。超限时先截行、再按字节截断,并追加说明尾注 [Output truncated: showing 200 of N lines, cut at 50KB],让模型知道输出不完整。

错误处理:返回失败行而非 reject

两个工具都不向 Harness 抛异常:失败时返回 search_memory failed: <message> / add_memory failed: <message> 这样的可读失败行(src/index.ts),由模型自行决定后续动作。对应的单测断言输出包含 network down / boom 等错误信息(tests/apply.test.ts)。

遥测:source 归因与匿名用量事件

写入归因:DEEPSEEK_HARNESS

所有写入都带上 source="DEEPSEEK_HARNESS" 标签,使 Mem0 后端能把用量归因到本集成。源码 src/index.ts 解释了配套要求:后端按 KNOWN_EVENT_SOURCES 白名单保留已识别的取值,未知值会归入 OTHERS 桶;因此要让 DEEPSEEK_HARNESS 按名称出现,需要在后端白名单中加入这一行——与已有的 ZAPIER / STRANDS 源完全相同的模式,是一个一行改动的平台侧变更。

匿名用量遥测

src/telemetry.ts 实现了一套 fire-and-forget 的 PostHog 事件上报:

  • 事件内容:仅包含工具名、耗时、结果计数与粗粒度失败类别(timeout / auth / rate-limited / server-error / bad-request / network 等,由 errorKind 归类);查询文本、记忆内容、实体 ID 与 API key 从不发送
  • 发送策略:每 5 秒、队列满 10 条、或进程 beforeExit 时批量 flush,单批超时 3 秒,且"从不抛错、从不记日志"——遥测自身故障不得影响工具调用;
  • 身份:未识别账号前使用持久化在 ~/.mem0/deepseek-plugin-telemetry.json 的匿名 ID;SDK 的 ping() 落地账号邮箱后,通过一个 $identify 事件把匿名历史并入账号;
  • 开关MEM0_TELEMETRY=false(也接受 0 / no / off)即可完全关闭,见 isTelemetryEnabled

插件挂载时也会发送一条 deepseek.plugin.mounted 事件(携带是否配置了 host 的布尔量),便于统计 on-prem 部署占比。

测试基线与版本状态

离线单测

tests/ 目录下的 vitest 用例全部离线运行:mem0aiMemoryClient@deepseek-ai/dsh-toolsdefineTool 均被 mock(后者被简化为"原样返回定义",从而直接驱动注册工具的 execute)。五个测试文件分别覆盖挂载与校验(apply)、格式化(formatting)、截断(output)、作用域解析(scoping)、遥测(telemetry)。测试运行方式:

pnpm test          # vitest run
pnpm typecheck     # tsc --noEmit

版本与状态

当前 package.json 声明 @mem0/deepseek-plugin v0.1.1,依赖 mem0ai ^3.0.7,并将 @deepseek-ai/cordis(dev 版本 4.0.1)与 @deepseek-ai/dsh-tools 作为 peerDependencies 开放为 *。README 将插件标记为 Developer preview:它跟随 DeepSeek Harness v0.1 的插件 API,该 API 年轻且变动较快,建议待其稳定后再锁死版本。自动捕获(无需显式工具调用即存储对话轮次)与自动召回(在提示词组装时注入记忆)两项能力处于规划中,待 Harness 的 session/assembly 事件 API 确认后落地。

小结

deepseek-plugin 展示了把托管记忆接入第三方 Agent 运行时的一条完整路径:Cordis 插件模型负责生命周期(inject 等待工具注册表、卸载自动回滚),scoping.ts 处理三级实体作用域与两条 API 的键名不对称,formatting.ts + output.ts 保证返回给模型的文本紧凑且不失真,telemetry.ts 在无隐私泄露的前提下提供使用洞察。对使用者而言,核心动作只有四步——构建、导出 MEM0_API_KEY、用 cordis.example.yml 补丁加载 dist/index.js、在 Web UI 中验证跨轮次记忆;对维护者而言,tests/apply.test.ts 中的 SDK 调用断言是理解两条调用链最可靠的参照物。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384