DeepSeek Harness 接入 Mem0 长期记忆:deepseek-plugin 双工具实现全解析
DeepSeek Harness(Cordis)智能体在会话之间不保留任何状态,而 @mem0/deepseek-plugin 通过注册 search_memory 与 add_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.ts 中 add_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_memory、search_memory 两个工具"这一契约。
本地构建与加载
前置条件
- 一个 Mem0 Platform 账号及 API key(key 以
m0-开头); - 已安装 DeepSeek Harness;
- 在 shell 中导出
MEM0_API_KEY。
四步跑通
-
构建插件(构建脚本为
tsup,产物在dist/):cd integrations/deepseek-plugin pnpm install pnpm build -
设置 Mem0 密钥:
export MEM0_API_KEY=... -
让 Harness 指向插件:复制 cordis.example.yml,把
name字段改为dist/index.js的绝对路径、填上你的userId,然后加载:pnpm dsh web --patch ./integrations/deepseek-plugin/cordis.example.yml -
打开 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.ts 的 Config.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):apiKey 与 host 均为可选,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_case(user_id/agent_id/run_id)——因为 Platform 的 search 接口不接受顶层实体参数;add的实体参数走顶层,经过 SDK 的 camel→snake 转换器,因此必须 camelCase(userId/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 化,所以代码同时接受eventId与event_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 用例全部离线运行:mem0ai 的 MemoryClient 与 @deepseek-ai/dsh-tools 的 defineTool 均被 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 调用断言是理解两条调用链最可靠的参照物。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00