前面的博客里,我提出 Agent Memory 系统在发版前要先做离线评估。把一次评估写成最简单的公式:
text
Score = f(Memory, Harness, Model, Task, Environment)
前文已经固定了 Memory、Model、Task 和 Environment。这一篇只看剩下的 Harness 变量:不同 Harness 在哪里调用 Memory,以及怎样选择,才能尽量减少它对 Memory 评估结果的干扰。
1 Agent Memory 调用链与 Harness 介入点
1.1 Agent lifecycle 与读取调用点
带 Tool Loop 的 Agent 在一个用户请求内可能多次调用模型。第一次模型响应如果要求执行 Tool,Harness 会运行 Tool,把结果加入状态,再发起下一次模型请求,直到模型给出最终答案。把这段循环展开,大致是下面这样:
text
Session bootstrap
→ Turn admission
→ Pre-model step
→ 模型响应
├─ 返回最终答案 → Turn Stop
└─ 请求 Tool → Tool 执行 → Post-tool observation
→ Pre-model step → 模型响应 ...
→ 下一 Turn
→ Session End
条件分支:
step → Subagent / handoff
step → Compact / resume
Session 覆盖一次完整任务或会话,其中可以包含多个 Turn。Turn 从一条用户 Prompt 开始,到 Agent 返回一次最终答案结束。Step 则对应一次模型请求;只要中间发生 Tool 调用,一个 Turn 就会有多个 Step。
Hook 是 Harness 在这些状态转换处暴露的扩展点。PostToolUse 属于 Tool 返回后的 lifecycle 事件,pre-model middleware 拦截每一次模型请求,MCP 则把 Memory 暴露成 Agent Loop 内可调用的 Tool。三者都可能让模型看到同一条 Memory,调用频率和决策者却不同,这正是下面需要对齐的变量。
沿着这条 lifecycle,对离线评估,我认为五个核心边界加两个条件边界已经够用:
| 生命周期边界 | 触发频率 | Memory 在这里做什么 | 额外进入分数的 Harness 能力 |
|---|---|---|---|
| Session bootstrap | 每个新建或恢复的 Session 一次 | 加载项目规则、全局 Memory、上一轮摘要或索引 | 启动时延、陈旧内容、首个请求是否等待 Hook |
| Turn admission | 每条用户 Prompt 一次 | 用当前 Prompt 构造 query,检索后加入本轮首个 model request | Prompt 解析、超时策略、首轮注入 schema |
| Pre-model step | 每次模型请求前一次,包括 Tool continuation | 根据当前完整状态重新检索、去重并组装 model input | 调用频率、Top-K、token budget、重复注入控制 |
| Agent Loop 内的 Memory Tool | 由模型按需调用 | 通过 MCP、普通 Tool 或文件执行 search、get、add |
是否触发、query generation、Tool choice 与错误恢复 |
| Post-tool observation | 每次 Tool 或每个 Tool batch 后 | 用错误栈、文件路径和命令输出补查 Memory,结果在下一次 model request 可见 | 事件选择、并行 Tool 聚合、延迟一个 step 的可见性 |
| Subagent / handoff(条件边界) | 每次 Agent 切换或派生时 | 决定向子 Agent 传哪些 namespace、摘要与任务相关 Memory | Agent 间隔离、继承策略与跨 Agent 泄漏 |
| Compact / resume(条件边界) | 上下文压缩或恢复时 | 在旧上下文丢失前保存状态,在新上下文组装后重新注入 | 摘要损失、异步一致性与恢复时点 |
这七个边界不是七次顺序调用。Agent Loop 会在 pre-model、Memory Tool 和 post-tool 之间循环;短单 Agent Benchmark 只需要前五项,多 Agent 任务再打开 handoff,长上下文任务再打开 compact/resume。Session 或 Turn 级注入主要测检索与消费;MCP 模式还测模型能否意识到需要历史信息;pre-model step 再叠加一次频率策略。调用点本身已经是被测系统的一部分。
1.2 Memory 构造方式与可见时间
这里把 Memory 构造定义为从原始 message、Tool Result 或任务事件中生成候选 Memory。
候选 Memory 可以在三个不同位置构造。
-
模型主动调用
memory_add。 模型在当前 Step 判断一段信息值得保留,生成memory_add(content, scope, ...)的 Tool 参数。写入意图和候选内容在 Tool call 时形成,Memory 服务随后完成规范化、去重与持久化。模型没有调用,Memory 就不会产生;这条路径把是否值得记、写什么以及 Tool-use 能力一起放进评估结果。 -
Harness 根据 lifecycle 触发。 Harness 在 User Prompt、Tool 返回、Turn Stop、PreCompact 或 Session End 等边界调用 writer,把当时的 message 和状态交给抽取器。模型不负责决定是否写入,构造时点由 Hook 配置决定。Writer 可以阻塞当前流程,也可以由 Hook 启动异步任务;异步只改变完成时间,触发来源仍然是对应的 lifecycle 事件。
-
独立的 message 抽取服务。 Writer 消费已经落盘的 transcript 或 message stream,在 Agent 运行之外构造 Memory,再向 Harness 提供冻结 snapshot 或只读的
search/get接口。此时 Harness 只负责读取,Memory 构造发生在 message 提交之后、下一项任务读取之前。它可以实时消费,也可以批量处理,边界由 message offset、watermark 和 snapshot version 决定,不再依赖某个 Agent Hook。
Harness 触发的 writer 可以走 hot path,也可以转入 background。LangGraph 的文档直接区分这两种写法:hot path 让新 Memory 更早可见,同时增加当前请求延迟;background 不阻塞主链路,下一项任务开始时却可能还没有写完。1 独立 message 抽取服务始终位于 Agent Loop 之外,它自己的区别是实时消费还是批量处理。
2 业界 Harness 的 Memory 调用方式
我把几套常见实现放到同一口径下,资料核验截至 2026 年 8 月 17 日。三张表的纵轴都只放 Harness,不再把 Mem0 + Claude Code 这类集成组合当成一种 Harness。Mem0 决定 Memory 怎样接进宿主,表格这里先回答更基础的问题:宿主本身有没有这个入口。
2.1 读取调用点矩阵
| Harness | Session bootstrap | Turn admission | Pre-model step | Memory Tool | Post-tool | Subagent / handoff | Compact / resume |
|---|---|---|---|---|---|---|---|
| Claude Code | ✅23 | ✅2 | ❌2 | ✅34 | ✅2 | ✅23 | ✅2 |
| Cursor | ✅56 | ❌6 | ❌6 | ✅7 | ✅6 | ❌6 | ❌6 |
| DeepSeek Harness | ✅8 | ✅8 | ✅8 | ✅9 | ✅8 | ✅8 | ✅8 |
| AutoGen | ❌1011 | ✅1011 | ❌11 | ✅1011 | ❌11 | ✅1011 | ❌1011 |
| LangGraph | ❌12 | ✅12 | ✅1213 | ✅13 | ✅12 | ✅14 | ✅15 |
| OpenAI Agents SDK | ❌1617 | ✅16 | ✅17 | ✅18 | ✅1718 | ✅17 | ✅16 |
| Codex | ✅1920 | ✅19 | ❌19 | ✅19 | ✅19 | ✅19 | ✅19 |
这里判断的是入口,不是默认行为。例如 Cursor 有 beforeSubmitPrompt,但该 Hook 只能校验 Prompt,没有 model-visible context 输出,所以 Turn admission 仍是 ❌;它的 postToolUse 明确支持 additional_context,Post-tool 才是 ✅。6 我拉取 DeepSeek Harness 47f9438 反查事件表后,把 Session bootstrap 从 ❌ 改成了 ✅:代码中确实有 agent/session-start,其 source 还区分 startup、resume、clear 和 compact。8
2.2 写入调用点矩阵
| Harness | 模型主动调用 memory_add |
Harness 根据 lifecycle 触发 | 独立 message 抽取服务 |
|---|---|---|---|
| Claude Code | ✅34 | ✅2 | ❌3 |
| Cursor | ✅57 | ✅6 | ✅521 |
| DeepSeek Harness | ✅9 | ✅8 | ❌89 |
| AutoGen | ✅1011 | ❌1011 | ❌10 |
| LangGraph | ✅13 | ✅112 | ✅1 |
| OpenAI Agents SDK | ✅18 | ✅1618 | ❌1618 |
| Codex | ✅19 | ✅19 | ✅20 |
这张表只回答构造路径能不能成立。模型主动写入既可以是原生 Memory Tool,也可以是 Harness 暴露的 MCP 或普通 Tool;lifecycle 写入表示 Hook、middleware、node 或 Runner 在边界调用 writer;独立抽取则要求 writer 在 Agent Loop 之外消费 transcript 或 message stream。一个 lifecycle Hook 即使异步启动 writer,仍属于第二列,不会因为跑在后台就自动变成第三列。
2.3 调用决策者与离线评估影响
| Harness | 读取决策者 | 写入决策者 | 主要评估影响 |
|---|---|---|---|
| Claude Code | Hook / 模型23 | Hook / 模型23 | 默认 Memory 需清场3 |
| Cursor | Harness / 模型567 | Hook / 模型 / 后台服务5621 | 异步可见性621 |
| DeepSeek Harness | Plugin / 模型89 | Plugin / 模型89 | Step 调用频率8 |
| AutoGen | AssistantAgent / 模型1011 | 应用 / 模型1011 | Memory 刷新频率11 |
| LangGraph | Middleware / node / 模型1213 | Middleware / node / 模型11213 | Graph 拓扑1214 |
| OpenAI Agents SDK | Runner / input filter / 模型161718 | Runner / 应用 / 模型1618 | Session history 混入16 |
| Codex | Hook / 模型19 | Hook / 模型 / 后台服务1920 | 后台写入延迟20 |
我按来源反查后还保留了两个不太对称、但对评估重要的边界。Cursor 的 beforeSubmitPrompt、subagentStart 和 preCompact 都没有 Memory context 输出,所以不能因为 Hook 名字存在就标成 ✅;AutoGen 的 update_context() 不会在 Tool continuation 前再次执行,所以 Turn admission 是 ✅,Pre-model step 仍是 ❌。611 OpenAI Agents SDK 的 on_tool_end 本身只是 observer,但可以把查询结果写入 run context,再由下一次 call_model_input_filter 注入,因此 Post-tool 作为组合路径标成 ✅。1718
3 Harness 对 Memory 任务验证的影响变量
3.1 任务验证与对照实验
这里的任务验证,是让 Agent 在固定环境中完成一项可判定的任务,再由 verifier 检查最终结果。编码任务可以运行隐藏测试,RCA 任务可以检查根因和证据链,Tool-use 任务可以检查环境中的最终状态。离线评估用不同实验组之间的任务结果差估计 Memory 的价值;单条召回是否相关属于组件指标。
一套最小实验可以包含下面五组。代号用于与这个系列的上一篇文章对齐,单独读本文时看实验组名称就够了。
| 实验组 | 模型可以看到的长期 Memory | 它回答的问题 |
|---|---|---|
无长期 Memory(B0) |
不提供长期 Memory | 没有 Memory 时能完成多少任务 |
当前版本(C0) |
线上版本的检索结果 | 当前 Memory 系统的任务表现 |
候选版本(C1) |
待发布版本的检索结果 | 新版本是否带来稳定增量 |
Oracle evidence(O2) |
预先标注的最小充分证据 | 检索完全正确时模型能做到什么程度 |
错误 Memory(N2) |
陈旧或相似但错误的证据 | 错误 Memory 是否让结果比不用 Memory 更差 |
主比较发生在当前版本和候选版本之间。无长期 Memory 组给出 Agent 自己完成任务的基线,Oracle evidence 把检索问题拿掉,用来观察模型消费 Memory 的上限;错误 Memory 组低于无长期 Memory 组,就出现了 Negative Transfer。
完整矩阵没有必要跟着每次开发迭代重跑。Agent 任务验证需要实际调用模型并执行 Tool Loop,成本比单纯的检索评估高很多。日常开发只跑候选版本(C1),每次保留任务级结果和运行指纹,观察它相对上一版候选的变化;B0、C0、O2 和 N2 只要 Harness、模型、任务、环境和 Memory snapshot 没变,就复用最近一次结果。准备发版时,再把五组在同一配置下全量重跑一次,发版结论只使用这批同时生成的结果。这样开发阶段控制成本,发版阶段仍保留严格的横向可比性。
举个例子,同一个跨租户缓存错误可以在五组里各跑一遍,隐藏测试统一检查 tenant isolation。当前版本每个 Turn 只注入一次,候选版本却在每个 Step 都重新检索,即使候选版本通过了更多测试,也无法确认增量来自 Memory 算法。Harness 已经改变了检索频率、可用证据和上下文预算。
3.2 Harness 影响变量
换一个调用点,就换了被测系统。MemoHarness 把 context、tools、orchestration、memory、decoding 和 output 都列为 Harness 配置;Anthropic 也把运行 Agent 的 Harness 与负责出题、隔离和判分的 Evaluation Harness 分开讨论。2223 为了让实验组之间的分数可以归因到 Memory,下面这些 Harness 变量必须固定,或者至少进入实验记录。
| 影响变量 | 它怎样改变 Memory 对任务结果的贡献 | 离线评估中固定或记录什么 |
|---|---|---|
| 读取调用点 | 决定 Memory 在 Session、Turn、每个 Step 还是 Tool 后进入模型268121719 | trigger_point |
| 读取决策者 | 模型自主调用会额外测试触发判断与 Tool-use 能力391318 | harness 或 model |
| 查询输入 | 只用 User Prompt 与使用完整 state、Tool Result 会产生不同召回81013 | 原始 query 与 query builder |
| 模型在哪里看到检索结果 | 检索结果可以进入下一次请求的 developer message、由 state 渲染进 Prompt,或在模型主动查询后作为 Memory Tool Result 返回261719 | developer_message / state / tool_result |
| 调用频率 | Turn 级一次与每个 Step 一次会改变命中率、成本和重复内容8111217 | 每 Turn 调用次数与去重规则 |
| 上下文预算 | Top-K、token cap 和截断位置会改变模型实际看到的证据31922 | Top-K、token cap 与截断规则 |
| 谁负责生成候选 Memory | 模型调用 memory_add、Harness 在 lifecycle 事件后调用 writer、独立抽取服务消费 message stream,三条路径会产生不同的 Memory1320 |
model_tool / harness_writer / message_extractor |
| 写入可见时间 | 异步 Hook 或后台抽取可能赶不上下一项任务读取162021 | captured_at、committed_at、visible_at |
| 状态边界 | 原生 Memory、Session history、Subagent 继承与 compact summary 会污染实验组314151620 | Session、namespace、workspace 与 snapshot |
| 失败语义 | Timeout、重试和静默降级会把 Memory 故障伪装成正常的 no-memory 样本2681923 | 状态码、timeout、retry 与 fallback |
模型看到检索结果的位置,以实际发给模型的 request 为准。Memory 已经写入 Agent state,但 Prompt 模板没有读取,模型仍然看不到;Memory 作为 Tool Result 返回,则意味着模型已经先做出了一次调用决定。候选 Memory 的生成方,要看写入意图从哪里发起:memory_add 来自模型,lifecycle writer 来自 Harness,message extractor 位于 Agent Loop 之外。三种方式需要分别评估。
发版前的主结果应该让当前版本和候选版本使用完全相同的 Harness 配置,Memory 实现是唯一变量。模型自主调用 MCP 也值得测,但它回答的是另一件事:Memory 与模型、Skill、Tool schema 和 Harness 组合以后,真实 Agent Loop 还能保留多少收益。两个结果都有效,不能混成一个分数。
4 离线评估 Harness 的选择
到这里,Harness 的选择可以收得很窄。第 3 节列出这些影响变量,目的是把 Harness 对 Memory 分数的贡献显式化。只要其中的读取、写入、状态边界和失败处理在实验组之间固定,任何harness原则上都能承担同一种归因实验。Harness 在这里是一组实验条件。
落到实践,我会看四个条件。
-
Memory 调用链可以按业务配置。 很多上层业务 Agent 本来就是团队自己搓的,有的在 Turn 开始时自动检索,有的等 Tool 返回后再查,还有的只把 Memory 暴露成 MCP。底层评估平台需要把读取调用点、查询输入、模型在哪里看到检索结果、调用频率、候选 Memory 由谁生成以及写入可见时间都做成配置。每个大业务建立一份独立 Profile,按需开启对应的评估;同一业务的实验组继续固定这份 Profile。这样才能让一个评估平台兼容五花八门的业务调用方式。
-
模型可以配置。 Memory 的收益会随模型变化,Harness 应该通过配置切换 provider 和 model,不需要重写 Agent Loop。每次运行仍要固定模型版本、推理参数和 adapter。
-
原生 Memory 可以关闭。 评估时只保留被测 Memory,宿主自动读写的长期 Memory 必须关闭。Session history 和 compact summary 虽然不等于长期 Memory,也要固定边界,避免它们替实验组保存历史信息。
-
代码可以审计。 开源最好。这样才能确认 lifecycle 的真实顺序、最终 model request、失败后的 fallback,并把代码固定到 commit 和 lockfile。业务内部代码只要能够查看和固定,也满足同一个要求。
按这个口径,DeepSeek Harness 适合作为公开参考。它的 Plugin 和 Profile 可以承载不同业务的 Memory 调用链,provider 和 model 可以通过配置选择;官方提供的第三方 Memory 只是默认关闭的 MCP overlay,默认组合里没有 Memory Server。8924 代码采用 MIT License,也可以固定到具体 commit。四个条件都能覆盖。它仍处于 Developer Preview,实际评估还需要锁住 lockfile。24
DeepSeek Harness 的优势落在可配置和可审计,不在某种特殊的 Memory 调用方式。实际落地时,可以为每个大业务把自己的 Memory 调用链固化成一个 Profile,评估按业务加载对应 Profile;新增业务只增加配置,不修改评估 Runner。同一业务的实验组始终使用同一份 Profile,Harness 带来的变量才不会混进 Memory 版本差异。
5 参考资料
1 LangChain Docs: Memory overview
2 Claude Code Docs: Hooks reference
3 Claude Code Docs: How Claude remembers your project
4 Claude Code Docs: Extend Claude Code
5 Cursor Docs: Memories(旧版详情页,当前重定向到 Rules)
7 Cursor Docs: Model Context Protocol
8 DeepSeek Harness, commit 47f9438: Architecture, Core lifecycle, Plugin tutorial, Tool runtime
9 DeepSeek Harness: MCP Memory Examples, commit 47f9438
10 Microsoft AutoGen: Memory and RAG
11 AutoGen AssistantAgent, commit 027ecf0
12 LangChain Docs: Custom middleware
13 LangChain Docs: Long-term memory
14 LangGraph Docs: Use subgraphs
15 LangGraph Docs: Persistence
16 OpenAI Agents SDK: Sessions
17 OpenAI Agents SDK: Running agents
18 OpenAI Agents SDK: Agents and lifecycle hooks
20 OpenAI Codex Docs: Memories
21 Cursor Changelog 1.2: Memories GA
22 Huang et al., MemoHarness: Agent Harnesses That Learn from Experience, 2026
23 Anthropic: Demystifying evals for AI agents
24 DeepSeek Harness, commit 47f9438: README, Plugin Config Catalog