Initial commit: Natural Memory Agent Lab:模型服务 + TypeScript agent + 记忆能力场景评测(冲突更新、跨会话回忆、遗忘撤回、多跳、未知拒答)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# Natural Memory Agent Lab
|
||||
|
||||
为 **Natural Memory v2(Qwen3.5-4B + 模型内记忆层)** 量身定制的**测试 Agent 框架**。
|
||||
基座是 **[Pi Agent Harness](https://github.com/earendil-works/pi)**(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|end / turn_end / agent_end`;`beforeToolCall`/`afterToolCall` 可拦截与改写 |
|
||||
| **多会话天然支持** | 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` | 场景套件 |
|
||||
|
||||
### 两个刻意的设计
|
||||
|
||||
1. **`notes` 工具(文件便签)是故意的。** 对照组若没有任何外部记忆,比较的就不是「模型内记忆 vs 无记忆」而是
|
||||
「有记忆 vs 什么都没有」。两组都给同一份文件便签,才能真正隔离被测变量。
|
||||
2. **`memory_admin` 把记忆治理变成可评测的 Agent 能力**(列出/审计/撤回),而不是看不见的旁路。
|
||||
|
||||
## 场景套件
|
||||
|
||||
| 场景 | 考的是什么 |
|
||||
|---|---|
|
||||
| `cross_session_recall` | 会话 1 写入 → 会话 2/3(**全新上下文、无历史文本**)询问 |
|
||||
| `conflict_update` | 同属性旧值→新值,跨会话必须返回新值且旧值不得活跃 |
|
||||
| `unknown_abstention` | 从未提及的属性必须明确说不知道,不许编造或硬套相似记忆 |
|
||||
| `multi_hop_chain` | 项目→负责人→工号,两跳串联 |
|
||||
| `forget_retraction` | 明确要求遗忘后,跨会话不得再使用该事实 |
|
||||
|
||||
## 运行
|
||||
|
||||
```powershell
|
||||
# 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](https://github.com/sierra-research/tau-bench))
|
||||
- 用户模拟器(多轮协商类场景)
|
||||
- 记忆容量压力场景(逼近 `max_pages` / 淘汰行为)
|
||||
- 多用户隔离场景(两个 user id 交叉询问,验证互不污染)
|
||||
- 工具输出是否被误写入长期记忆(记忆污染)
|
||||
Reference in New Issue
Block a user