Natural Memory Agent Lab
为 Natural Memory v2(Qwen3.5-4B + 模型内记忆层) 量身定制的测试 Agent 框架。
基座是 Pi Agent Harness(MIT)——用它的
pi-agent-core(带状态管理与事件流的 agent runtime)与 pi-ai(统一多 provider LLM API),
而不是自己重写一个 agent 循环。
为什么用 Pi 而不是别的
| 理由 | 事实 |
|---|---|
| provider 接线是现成的 | pi-ai 已有 openai-completions 适配器,provider 配置原生支持 baseUrl;本机 DSH 的 Grok/GPT provider 就是这么接的。不需要改 Pi 的代码 |
| 插桩是一等公民 | agent.subscribe(event => …) 提供 `agent_start / turn_start / message_* / tool_execution_start |
| 多会话天然支持 | stateful agent + 独立 SQLite session backend;本框架每个会话新建 Agent(空上下文),而模型记忆持续存在——这正是被测能力 |
| 许可 | MIT(© 2025 Mario Zechner),改/fork 无约束 |
架构:进程与语言双重隔离
┌──────────────────────────────┐ HTTP (OpenAI protocol) ┌────────────────────────────┐
│ model_server/server.py │ ◄───────────────────────────────────► │ agent/ (Node + Bun) │
│ 【LLM conda 环境】 │ /v1/chat/completions (tools) │ pi-agent-core + pi-ai │
│ torch 2.9/cu128 原样不动 │ /v1/memory/* /v1/diagnostics │ 自己的 node_modules │
│ 复用 natural_memory_service │ │ 工具 / 场景 / 记录 │
└──────────────────────────────┘ └────────────────────────────┘
模型侧留在原有的 LLM conda 环境(torch 2.9 + bitsandbytes 不能被 agent 依赖污染),
Agent 侧是独立的 Node 工程——两边只通过 HTTP 说话。
完整接管 NM2:观测面 + 控制面 + 路由即工具
项目的 natural_memory_service.py 只给了一个单条消息的 /v1/chat 和少量记录管理接口。
要「接管」NM2,agent 需要三样它给不了的东西,本 server 全部补齐:
1. 观测面(NM2 内部其实早就记录了,只是没人暴露)
| 暴露出来的状态 | 含义 |
|---|---|
v2_last_decisions |
每轮路由决策的全部字段:need_memory、页/记录 id、页/记录分数、hop_count、hop_trace、stop_reason、confidence、score_margin、evidence_score |
text_read_seconds |
路由+重排耗时(与解码耗时分离,便于成本归因) |
text_prefix_tokens / text_prefix_used |
实际注入的前缀 token 数 |
auto_memory_probability / auto_memory_forget_probability |
自动记忆策略头的写入/遗忘概率 |
context_compaction |
KV 自动压缩报告 |
v2_no_evidence |
路由器是否判定无证据 |
每次调用后 server 会 snapshot 这些状态并保留逐轮 trace 历史(GET /v1/nm2/trace)。
2. 控制面(15 个可运行时改写的旋钮 + 治理操作 + 用户隔离)
| 类别 | 接口 |
|---|---|
| 旋钮读写 | GET/POST /v1/nm2/config —— top_k_pages top_k_records max_hops hot_pages read_threshold write_threshold min_read_margin require_evidence auto_memory_threshold auto_forget_threshold gpu_cache_records gpu_cache_tokens context_chunk_tokens kv_budget_tokens kv_keep_recent_tokens(白名单 + 类型/范围校验,拒绝非法键) |
| 记录治理 | POST /v1/nm2/write(可信显式写入,带 entity/attribute/value/importance/confidence)、/correct(生成新版本)、/retract、/approve(quarantine → active,此前模型没有公开包装)、GET /v1/nm2/records、/audit、/export |
| 会话/用户隔离 | POST /v1/nm2/session —— save/load/list/delete/reset,走 MemoryOSV2.export_payload() / from_payload(),完整搬移记录、页、quarantine、检索阈值与 KV 预算(阈值随会话走,不随进程走) |
| KV 预算 | POST /v1/nm2/compact —— 把长上下文压缩成 NM2 记录并返回热窗口报告 |
| 请求级覆盖 | chat 请求体里的 nm2: { top_k_pages, top_k_records, max_hops, read_threshold, ... },只在该次请求内生效(进入前保存旧值、退出时恢复) |
3. 路由即工具:POST /v1/nm2/probe
只跑「粗索引 → 候选页 → 页内重排 → Top-K」这条有界路径,不生成任何 token,直接返回:
决策(need/hop/stop_reason/margin/evidence)、命中的记录及其分数、以及 encode_seconds / route_seconds 分解。
这让「这里记忆会给我什么」变成 agent 可调用、harness 可评分的一等操作。
Agent 侧对应的工具
| 工具 | action | 用途 |
|---|---|---|
nm2_read |
probe / search / trace / audit / stats |
探测路由、检索记录、回看逐轮决策与延迟 |
nm2_write |
write / correct / retract / approve |
记忆治理 |
nm2_admin |
config_get / config_set / compact / session / reset |
调参、压缩、切换用户 |
只用三个工具而不是二十个:4B 模型对「少量工具 + 明确 action 枚举」的可靠性远高于长工具表。
仍然保留的(原有能力,未删)
- 用模型自己的 chat template 渲染完整消息列表,原生支持 Qwen 的
<tool_call>/<tool_response>,是真正的 function calling; - 保持项目记忆语义:用户轮触发写入、生成只读、reset token 生效、写入自动持久化;
memory_mode = on | read_only | off,即无记忆对照组的开关。
Agent 侧做了什么
| 文件 | 作用 |
|---|---|
src/provider.ts |
把本机模型注册成 pi-ai 的一个 provider(createProvider + OpenAICompletions) |
src/memory.ts |
模型 server 的记忆/诊断客户端 + MemoryRecorder(逐轮记录,忠实于「每次用户消息 = 一条记录」而不是内部循环次数) |
src/tools.ts |
工具集:run_shell / read_file / write_file / http_get / notes / memory_admin |
src/agent.ts |
createTestAgent():Pi Agent + 事件插桩 + ask() 一次用户往返 |
src/run_scenario.ts |
多会话场景编排、双组(记忆开/关)、断言评分、写出 JSONL 轨迹与 summary.json |
scenarios/*.json |
场景套件 |
两个刻意的设计
notes工具(文件便签)是故意的。 对照组若没有任何外部记忆,比较的就不是「模型内记忆 vs 无记忆」而是 「有记忆 vs 什么都没有」。两组都给同一份文件便签,才能真正隔离被测变量。memory_admin把记忆治理变成可评测的 Agent 能力(列出/审计/撤回),而不是看不见的旁路。
场景套件
| 场景 | 考的是什么 |
|---|---|
cross_session_recall |
会话 1 写入 → 会话 2/3(全新上下文、无历史文本)询问 |
conflict_update |
同属性旧值→新值,跨会话必须返回新值且旧值不得活跃 |
unknown_abstention |
从未提及的属性必须明确说不知道,不许编造或硬套相似记忆 |
multi_hop_chain |
项目→负责人→工号,两跳串联 |
forget_retraction |
明确要求遗忘后,跨会话不得再使用该事实 |
运行
# 1) 启动模型 server(在 LLM conda 环境,占用 GPU)
$env:PYTHONPATH = 'H:\Memory'
& C:\Users\Administrator\miniconda3\envs\LLM\python.exe H:\Memory\agent_lab\model_server\server.py `
--model-path qwen3_5_4b_natural_memory_v2 --port 8766
# 2) 安装 agent 侧依赖(独立于模型环境)
cd H:\Memory\agent_lab\agent
bun install
bun run typecheck
# 3) 跑单个场景(双组对照)
$env:NATURAL_MEMORY_API_KEY = 'local'
bun run scenario --scenario scenarios/cross_session_recall.json --arm both
# 4) 跑全套
bun run suite
产物:runs/<timestamp>/<scenario>.<arm>.jsonl(逐轮轨迹:用户消息、助手回答、工具调用与结果、记忆诊断)
与 runs/<timestamp>/summary.json(每场景每组的断言通过率、活跃记录数、每轮耗时)。
当前状态(重要,勿误读)
- 模型 server:已成功启动过一次(
{"event":"ready", ...},memory_mode: on),但因用户正在满载训练路由器、 需要让出 GPU,在 tool call 往返验证之前就主动停掉了。 - Agent 侧:代码按
[email protected]与pi-agent-core的实测 API 面编写(createProvider/Model字段 /AgentTool/ 事件名均已核对本机安装与官方 README),但尚未bun install、尚未 typecheck、尚未真正跑过。 首次运行预计需要少量类型修正。 - 因此:目前没有任何关于该模型 Agent 能力的实测结论。
后续可加
pass^k式重复采样稳定性(参考 τ-bench)- 用户模拟器(多轮协商类场景)
- 记忆容量压力场景(逼近
max_pages/ 淘汰行为) - 多用户隔离场景(两个 user id 交叉询问,验证互不污染)
- 工具输出是否被误写入长期记忆(记忆污染)