Simulator MVP: Incremental Build Plan

项目: MemPABench Simulator
日期: 2026-04-05(2026-05-01 最新:persona 切到 user_a,sheldon_* 仅作 pre-anonymization 源保留)
状态: v7 — Step 1 全部 ✅;Step 2 进行中,persona 数据已脱敏切换到 user_a
目录: /Users/JL/Desktop/MemPA/simulator/

2026-05-01 Persona 数据切换到脱敏版 user_a

Step 1 的产物(sheldon_identity.yaml / sheldon_s01-s03_preferences.yaml)保留作为 pre-anonymization 源参考,不再被 Step 2 之后的代码直接使用。MVP 当前 canonical persona = user_a(脱敏自 Sheldon,IPaS matrix 内容等价)。所有新代码、prompt 注入、test fixture 一律走 data/personas/user_a_*。脚本目录 data/scripts/sheldon_cooper/ 中三个 session YAML 仍含 “Sheldon” / “Caltech” 等具名字段,需要在 Step 2 内迁移并脱敏到 data/scripts/user_a/

Context

MemPABench simulator 需要扮演 TV 角色,通过 process_direct() 与 PA 交互。设计方案(Character Card + Beat System + MRPrompt + EmotionTracker)已在 MemPABench_Simulator_Design 中确定。

现有代码:forked PA (pa/)、memory backends (memory/)、harness (harness/)。simulator/ 目录目前只有 __init__.py 和空的 prompts/ 目录。

核心原则:Simulator 和 PA 完全独立。 Simulator 从 nanobot 精简 fork,保留 provider 系统,删除所有不需要的模块。两者唯一交互点是 pa_loop.process_direct()

对话历史机制: Simulator LLM 每次生成消息时,会看到当前 session 内的完整对话历史(自己说的 + PA 回的),作为 prompt 的一部分注入。这确保角色行为在 session 内连贯一致。

关于 PA: pa/ 已经是完整的 nanobot fork,有 API key 就能跑。Step 2 测试时用最简配置(一个 SOUL.md + API key),不需要提前做完整 PA。

Comments


Simulator 架构(从 nanobot 精简 fork)

nanobot-ref/ 复制 providers,其余全部重写或删除:

保留(几乎不改):

  • providers/base.py — LLMProvider 抽象基类、LLMResponse、GenerationSettings
  • providers/anthropic_provider.py — Claude 直连
  • providers/openai_compat_provider.py — OpenRouter / OpenAI / DeepSeek 等
  • providers/registry.py — provider 元数据

删除(simulator 不需要):

  • agent/tools/ — 全部(simulator 不调用工具)
  • agent/runner.py — AgentRunner 是 tool loop,simulator 不需要
  • agent/subagent.py, agent/hook.py, agent/skills.py — 全部
  • bus/, channels/, command/, cli/ — 全部
  • nanobot 原有的 memory/context 系统 — 用 simulator 自己的 memory/ 模块替代

最终目录结构:

simulator/
├── __init__.py
├── providers/            # 从 nanobot 保留,支持 OpenRouter 等
│   ├── __init__.py
│   ├── base.py
│   ├── anthropic_provider.py
│   ├── openai_compat_provider.py
│   └── registry.py
├── memory/               # simulator 内部记忆(预留扩展)
│   ├── __init__.py
│   └── state.py          # InternalStateTracker
├── config.py             # 精简版:provider + model 配置
├── schemas.py            # Persona, Beat, SessionScript, LifeContext 等
├── loop.py               # SimulatorLoop 核心引擎
├── checkpoint.py         # 断点续传
└── prompts/
    ├── generate_message.md
    ├── beat_judge.md
    └── state_reaction.md

data/
├── personas/                     # 角色卡(identity + preferences 分两个文件,canonical = user_a)
│   ├── user_a_identity.yaml             # 脱敏版 identity:personality / speech_style / act_repertoire + interaction dynamics
│   ├── user_a_s01-s03_preferences.yaml  # 脱敏版 2 × 15 matrix,含 source provenance
│   ├── sheldon_identity.yaml            # 保留作为 pre-anonymization 源参考(不被 runtime 使用)
│   └── sheldon_s01-s03_preferences.yaml # 保留作为源参考
└── scripts/                      # 剧本(按角色分目录)
    ├── user_a/                       # canonical runtime scripts
    │   ├── session_01.yaml               # 脱敏:去掉 "Sheldon" 等具名字段
    │   ├── session_02.yaml
    │   └── session_03.yaml
    └── sheldon_cooper/               # 保留作为脱敏前源参考
        ├── session_01.yaml
        ├── session_02.yaml
        └── session_03.yaml

关于 actor skill specs:旧的 preference_skills/ 目录设计已废弃;当前实现使用 data/actor_skill_specs/{attribute}/{setting}.md。Runtime 先用 session.context + beat.active_skills 在 2 × 15 PreferenceMatrix 中解析每个 active attribute 的 setting,再加载对应的 character-agnostic actor skill spec 注入 prompt。Matrix 负责”这个角色在这个 context 的 setting 是什么”,actor skill spec 负责”这个 setting 应该怎么演”。

数据文件格式约定:

  • Identity: data/personas/{user_id}_identity.yaml
  • Preference matrix: data/personas/{user_id}_{source_span}_preferences.yaml(2 × 15 cells)
  • Actor skill specs: data/actor_skill_specs/{attribute}/{setting}.md
  • Scripts: data/scripts/{user_id}/session_{nn}.yaml
  • 所有 YAML 通过 simulator/schemas.py 的 Pydantic models 校验加载
  • MVP canonical persona 是 user_a;Sheldon 文件只保留作 pre-anonymization 源参考

Simulator LLM Prompt 结构(每次生成消息时):

[System] Identity(personality / speech_style / catchphrases / act_repertoire / person-specific interaction dynamics)
[System] Active Skills(beat.active_skills 声明的 2-3 个 cell,从 matrix[session.context] 切出)
[System] 当前 beat 的 goal / constraint / life_context_hint
[System] life_context(心情、时间压力、精力等)
[System] InternalState 最近事件(Step 4 加入)

[对话历史] simulator: "上一轮说的话"
[对话历史] PA: "PA 的回复"
[对话历史] simulator: "再上一轮说的话"
...

[User] 请根据以上信息,生成角色的下一句话。

2026-05-11 note: generate_message.md now frames the simulator as “the user in this session,” not as a stage actor playing a fictional character. Person-specific naturalness should primarily come from user_a_identity.yaml; the generic prompt preserves beat routing and eval-only XML block requirements.
2026-05-11 refinement: generic prompt must preserve the identity card’s conversational rhythm. User A should remain somewhat long-winded, picky, and mildly written; avoid compressing him into terse efficient commands or report-style transitions such as “On the recommendation itself.”

Comments on Architecture


Step 1: 项目骨架 + Providers + Schemas + Single Beat 生成

Goal: 搭好 simulator 完整目录结构,确保 import 通畅,验证 LLM 能生成一条角色消息。不接 PA,只测 simulator 自身。

Step 1a: 项目骨架(确保 import 通畅) ✅ Done 2026-04-05 23:50

Claude 做:

  • 创建完整目录结构(simulator/providers/, simulator/memory/, simulator/prompts/
  • nanobot-ref/nanobot/providers/ 复制 4 个文件到 simulator/providers/,修正 import 路径
  • simulator/memory/__init__.py + simulator/memory/state.py(InternalStateTracker 空壳,Step 4 填充)
  • simulator/config.py — 精简配置(provider api_key/base、model name、temperature=0

结果: import 验证通过。registry 只保留了 OpenRouter / Anthropic / OpenAI / DeepSeek 四个 provider(删掉了 Azure、Codex 等不需要的)。SimulatorConfig 默认 OpenRouter + temperature=0。

你做:

  • 跑验证命令,确认 import 通畅:
PYTHONPATH=. python -c "from simulator.providers.base import LLMProvider; from simulator.memory.state import InternalStateTracker; print('imports OK')"
  • 如果报错,告诉我错误信息

Step 1b: Schemas + YAML 加载 ✅ Done 2026-04-05 23:48

设计变更 1 (20:10): 角色卡的 preferences{favorite_drink: ...} 泛用 dict 改为 interaction_preferences — 直接对应 IPaS 偏好维度。每个维度有 setting(偏好值)、confidence(质量门控)、behaviors(隐性表达方式列表)、asymmetry(生产/接收不对称)。角色卡和 benchmark 评测维度直接挂钩。参考:Preference_Extraction_Hybrid_Evidence.md

设计变更 2 (23:30): 从 11 维扩展到 15 维,加入 4 个 manually-authored 属性(#7 Process Visibility, #8 Memory & Privacy, #12 Solution Breadth, #13 Capability Boundary)。这 4 个在角色对话中无直接对应,需根据人设推断后人工填写。

confidence 字段的作用: 质量门控标记。High = transcript evidence 充分,可信;Medium / Medium-High = 需要人工复核。批量生产角色卡时,筛 confidence != "High" 就能快速定位需要人介入的维度。当前 Sheldon 卡中 4 个 manually-authored 维度标为 Medium,其余 11 个均为 High(1 个 Medium-High: proactive_outreach)。

Claude 做:

  • simulator/schemas.pyPersonainteraction_preferences: dict[str, InteractionPreference],每项有 setting/confidence/behaviors/asymmetry
  • data/personas/sheldon_cooper.yaml — 15 个 IPaS 维度全部填写完成(11 个从 Evidence 提取 + 4 个手动 author)
  • data/scripts/sheldon_cooper/session_test.yamlData Analysis 场景 3-beat(读CSV→画散点图→整理组会材料)
  • tests/test_simulator/test_schemas.py — 已通过,验证了 15 个维度都加载正确

4 个 manually-authored 属性定稿:

  • Process Visibility → Full narration(Sheldon 自己就爱 narrate 过程)
  • Memory & Privacy → Full(记住一切,只要效果好)
  • Solution Breadth → Low(给 optimal,不要一堆选项)
  • Capability Boundary → Suggest alternatives(PA 做不到时给替代方案,不要 hand off)

Step 1c: Single Beat 生成

Claude 做:

  • simulator/prompts/generate_message.md — 消息生成 prompt 模板
  • tests/test_simulator/test_single_beat.py — 调 LLM 生成一条 Sheldon 消息

你做:

  • 确保 OPENROUTER_API_KEY 环境变量已设置
  • 跑测试:
PYTHONPATH=. OPENROUTER_API_KEY=sk-... python tests/test_simulator/test_single_beat.py
  • 看打印出的消息是否像 Sheldon,反馈质量

成功标准:

  • 所有 import 通畅
  • YAML 通过 Pydantic 校验
  • 打印出一条 Sheldon 风格的消息(temperature=0),语气和内容符合 beat goal

Comments on Step 1

Step 1b 你只需要给一个文档,里面是一堆待填入的空格就可以,我会试着填一下看看都要填什么
beat 和 constraint 你可以给我拟定一个 Data analysis 的场景
别的没问题


Step 2: SimulatorLoop + 接入 PA

Goal: 写 SimulatorLoop,遍历 beats 生成消息,通过 process_direct() 发给 PA,获取回复。

设计变更 3 (2026-04-22 最终):Active-Skills-Only + per-context matrix 注入。

  • Identity 与 preferences 分文件sheldon_identity.yaml(personality / speech_style / act_repertoire)+ sheldon_s01-s03_preferences.yaml(2 × 15 matrix,extractor 产出,含 source provenance)。Simulator loop 加载时合并
  • Per-context matrix:preferences 是 2 context × 15 attr = 30 cells。session YAML 头部声明 context: <work|personal>,runtime 切出该 context 的 15 cell
  • Active skills only:不再注入 “15 × 1-line default snapshot”,只注入 beat.active_skills 声明的 2-3 个 full skill spec。其余维度靠 Character Card 涌现。节省 ~73% preference tokens(1500 → 400)
  • Matrix + actor skill spec 两层:matrix cell 提供当前 context 下该 attribute 的 setting / source / confidence;data/actor_skill_specs/{attribute}/{setting}.md 提供 character-agnostic full skill spec。Runtime 由 simulator.skills.resolve_active_skills() 把二者连接起来。

完整设计见 MemPABench_Simulator_Design > Active Skills Only(2026-04-22 简化)MemPABench_Simulator_Design > Session 声明 context(per-context matrix 注入)

硬性约束:arc 设计要保证每个维度至少被 K ≥ 3 个 session 列入 active_skills

Claude 做:

:项目 1–3 ✅ 已在 Step 1 期间完成(PersonaIdentity / PreferenceMatrix / SessionScript schemas 已存在;generate_beat_turn() 单 beat 入口在 simulator/loop.py)。Step 2 真正待做 = 4 / 5 / 6(外加 5b:脚本脱敏迁移)。

  1. 新建 data/personas/sheldon_identity.yaml:✅(且 2026-05-01 起 canonical 切到 user_a_identity.yaml,sheldon 版本保留作源参考)
  2. 删除旧 sheldon_cooper.yaml:✅
  3. 更新 simulator/schemas.py(PersonaIdentity / PreferenceMatrix / Session.context / Beat.active_skills):✅
  4. 构建 SimulatorLoop:✅ 当前 simulator/loop.py 已包含跨 beat session driver。
    • SimulatorLoop.run_session() 遍历 beats。
    • 每个 beat 用 session.context + beat.active_skills 从 matrix 解析 setting,并加载 data/actor_skill_specs/{attribute}/{setting}.md 作为 full skill spec。
    • 组装 prompt(identity + active skills + beat + life_context + session history)→ 调 simulator LLM。
    • 只把 <message> 发给 pa_loop.process_direct()<factual_check> / <turn_assessment> / <emotion_event> 只写 transcript eval-only 通道。
    • 每个 turn 的 simulator output + PA outbound 都写入 transcript;PA 回复追加到 session history,进入下一个 beat。
  5. 脚本脱敏迁移:把 data/scripts/sheldon_cooper/{session_01,02,03}.yaml 拷贝到 data/scripts/user_a/,scrub 掉 “Sheldon” / “Caltech” / “Pasadena” 等具名字段(替换为 user_a_identity.yaml 里的 fictional 替代:Westlake Institute / Glenmont, CA 等),保持 IPaS-relevant 内容不变。原 sheldon_cooper/ 不删,保留作脱敏前源。
  6. data/scripts/user_a/session_01.yaml 的最终 review 版(已迁移即可,必要时调整 active_skills / beat 内容)
  7. 构建 harness/transcript.py — 跨 simulator + PA 的对话记录器(per pa/ ↔ simulator/ 不共享代码,harness 承接整合):
    • TranscriptWriter__init__(run_dir) 创建 data/runs/<run_id>/<persona>/<session_id>/log_event(dict) 追加一行到 transcript.jsonl(append-only,崩了不丢已写 turn);close() flush
    • 事件 schema:{"t": int, "kind": str, ...}kind 取值 session_start | beat_enter | simulator_turn | pa_turn | session_end_reflection
    • render_markdown(jsonl_path) -> str:把 jsonl 渲染成人读的 md(每 turn 两块:simulator → PA、PA → simulator;eval-only 块用 <details> 折叠并标注 “PA 不可见”)
    • 写出 transcript.md(次生产物)+ meta.yaml(config snapshot:persona / script / PA backend / model / 时间)
    • 大约 80–120 行,先做最小集合
  8. tests/test_simulator/test_session_with_pa.py — 完整 session 测试:
    • 自动创建最简 PA(临时 workspace + 一句话 SOUL.md + API key)+ 实例化 TranscriptWriter
    • 跑 3-beat simulator 与 PA 的对话
    • 断言 prompt 里确实按 session.context + beat.active_skills 只注入了对应的 2-3 个 cell
    • 断言 transcript.jsonl 行数 = session_start + beat_enter * N + simulator_turn * N + pa_turn * N + session_end_reflection
    • test 结束打印 transcript.md 路径,user 直接去那查看完整对话

你做:

  • 审核 session_01.yaml — 3-beat 剧本是否合理
  • 确保 OPENROUTER_API_KEY 已设置(simulator 和 PA 都走 OpenRouter,单 key)
  • 可选:PA_MODEL env 覆盖默认 PA 模型(通过 OpenRouter 模型名配置)
  • 跑测试:
PYTHONPATH=. OPENROUTER_API_KEY=sk-... python tests/test_simulator/test_session_with_pa.py
  • 看对话是否自然,反馈质量

关于 PA: 测试脚本会自动创建一个最简 PA(临时目录 + 基础 SOUL.md),你不需要提前配置 PA 全套。PA 在 Step 2 只是一个”会回话的对象”——验证 simulator → PA → simulator 的环路通即可。PA 真正的研究价值(memory backends 对比、actor skill specs、配置)是 benchmark variable,应当在环路验证后再分层接入,否则会过早优化一个 API surface 还没被端到端验证过的东西。Memory backend 比较等内容由 harness/ 层介入,与 simulator 完全解耦。

provider 路由约束(lab 报销 only OpenRouter,2026-05-01):simulator + PA 全部走 OpenRouter(pa.providers.openai_compat_provider.OpenAICompatProvider + find_by_name("openrouter"))。PA 不再走 Anthropic 直连。Simulator 是 fixed user persona 不需要换模型;PA 是 system under test,模型经 PA_MODEL env 灵活切换(不同 memory backend × 不同 PA 模型 = 后续 benchmark 矩阵的两个轴)。

成功标准: 3 轮 simulator 到 PA 对话完整打印,每条 simulator 消息都符合 beat goal,对话历史跨 beat 连贯。

Comments on Step 2


Step 3: Beat Transition Judge

Goal: LLM judge 决定当前 beat 是 stay / advance / branch。

Claude 做:

  • simulator/prompts/beat_judge.md — Beat 转换判断 prompt
  • 更新 simulator/loop.py_judge_beat_transition() 方法
  • 支持 beat 中的 trigger 字段(推进条件)
  • 更新测试

你做:

  • 重跑 session 测试,观察 beat 转换是否合理
  • 反馈 judge 的决策质量

测试: 重跑 Step 2 的 session,观察 beat 根据 PA 回复推进。

Comments on Step 3


Step 4: InternalStateTracker

Goal: Append-only 状态事件日志,覆盖情绪、满意度、correction fatigue 等,注入到消息生成 prompt。

Claude 做:

  • 填充 simulator/memory/state.pyInternalStateTracker
  • simulator/prompts/state_reaction.md — 状态反应 prompt
  • 接入 loop.py — PA 回复后生成 state event,注入后续 prompt

你做:

  • 跑 session,检查 state event log 输出
  • 对比有/无 state tracker 的对话差异
  • 反馈状态追踪是否合理

测试: 跑 session,检查 state event log。验证事件影响后续 simulator 消息风格。

Comments on Step 4


Step 5: Forced Event Nodes + Checkpoint + 状态恢复

Goal: 支持固定台词 beat(forced events)和完整的状态保存/恢复机制。

Claude 做:

5a. Forced Events:

  • 更新 loop.py — 如果 beat.line 存在,跳过 LLM 生成,直接用固定台词

5b. Checkpoint 保存/恢复:

  • simulator/checkpoint.pySimulatorCheckpointManager
  • 边界情况处理:
    • mid-session resume: 从 checkpoint 恢复,接着当前 beat 继续
    • full rollback: 清空所有状态,重新开始 session
    • cross-session continuity: state events 跨 session 保留
    • partial state corruption: 优雅降级
    • concurrent session isolation: 不同 persona 互不干扰

5c. 测试脚本:

  • tests/test_simulator/test_forced_events.py
  • tests/test_simulator/test_checkpoint.py

你做:

  • 跑 forced events 测试,确认固定台词正确插入
  • 跑 checkpoint 测试:
    1. 正常保存恢复
    2. 手动中断后恢复
    3. 回滚重新开始
  • 反馈是否有遗漏的边界情况

测试:

PYTHONPATH=. python tests/test_simulator/test_forced_events.py
PYTHONPATH=. python tests/test_simulator/test_checkpoint.py

Comments on Step 5


关键文件清单

文件操作Step用途
simulator/providers/*.py从 nanobot 复制1aLLM provider 支持
simulator/memory/__init__.py新建1amemory 模块入口
simulator/memory/state.py新建(空壳到Step4填充)1a/4InternalStateTracker
simulator/config.py新建1a精简版配置(temperature=0)
simulator/schemas.py新建1b所有 Pydantic 数据模型
simulator/loop.py新建2SimulatorLoop 核心引擎
simulator/checkpoint.py新建5状态保存/恢复
simulator/prompts/generate_message.md新建1c消息生成 prompt
simulator/prompts/beat_judge.md新建3Beat 转换 prompt
simulator/prompts/state_reaction.md新建4状态反应 prompt
data/personas/sheldon_cooper.yamlStep 1b 建,Step 2 归档1b→2旧单文件卡;已拆为 identity + preferences 两文件
data/personas/sheldon_identity.yaml新建(已 ✅)2pre-anonymization 源 identity
data/personas/sheldon_s01-s03_preferences.yamlextractor 生成(已 ✅)2pre-anonymization 源 2 × 15 matrix
data/personas/user_a_identity.yamlcanonical(已 ✅,2026-05-01)2脱敏 identity — Step 2 之后由 runtime 加载
data/personas/user_a_s01-s03_preferences.yamlcanonical(已 ✅,2026-05-01)2脱敏 2 × 15 matrix
data/scripts/sheldon_cooper/session_{01,02,03}.yaml已 ✅1b/2pre-anonymization 源剧本,保留参考
data/scripts/user_a/session_{01,02,03}.yaml新建(迁移并脱敏)2runtime 使用的剧本
tests/test_simulator/test_schemas.py新建1bYAML 加载验证
tests/test_simulator/test_single_beat.py新建1c单 beat 生成测试
tests/test_simulator/test_session_with_pa.py新建2完整 session 测试
tests/test_simulator/test_forced_events.py新建5forced events 测试
tests/test_simulator/test_checkpoint.py新建5checkpoint 测试

Simulator 与 PA 交互方式

Simulator (OpenRouter, temp=0)      PA (OpenRouter provider, PA_MODEL selects model)
    |                                   |
    |  simulator LLM 生成角色消息         |
    |  (prompt 含 session 内完整对话历史)  |
    |---- process_direct(msg) --------->|
    |                                   |  PA 处理 + 回复
    |<--- OutboundMessage --------------|
    |                                   |
    |  beat judge + state update         |
    |  对话历史追加本轮                    |
    |  生成下一条消息...                  |

两者完全独立:

  • Simulator: 自己的 providers/、自己的 config、自己的 model(走 OpenRouter)
  • PA: pa/ 目录下的完整 nanobot fork(走 Anthropic 或任意 provider)
  • 唯一接触点: pa_loop.process_direct(content, session_key)

整体 Comments


验证方式

每一步完成后跑对应的测试脚本。每个测试独立运行,stdout 输出:

  • Step 1a: import 验证通过
  • Step 1b: YAML 文件 Pydantic 校验通过
  • Step 1c: 单条 Sheldon 风格角色消息
  • Step 2: 完整 simulator 与 PA 对话(含 session 内对话历史)
  • Step 3: 对话 + beat 转换决策
  • Step 4: 对话 + state event log
  • Step 5: forced events + checkpoint 保存/恢复/回滚

其他想法


已知问题 / Open items

[PENDING-1] History 末尾 [user, user] 邻接(2026-05-01)

现象SimulatorLoop 累积的 history 在 beat 2+ 时末尾是 PA 回复(role=user)。assemble_prompt 会再追加一条 user trigger(“Continue the dialogue. Produce your next message in character.”),形成 […, user: pa_reply, user: trigger]。

风险:Anthropic 原生 API 严格要求 user/assistant 交替,会拒绝。OpenAI-compat(含 OpenRouter 默认链路)一般容忍,多数 provider 隐式合并。

当前策略不预防性修。Simulator 默认走 OpenRouter(temperature=0),先等 task #7 端到端跑出来再看:

  • 若 OpenRouter 链路通且对话质量正常 → 不动
  • 若 provider 报错或质量异常 → 改 simulator/loop.py:_render_user_trigger + assemble_prompt:当 history 末尾是 user 时,把 trigger 合并进那条 user 的尾部,不再单独追加

改动入口simulator/loop.py:155-189_render_user_trigger + assemble_prompt)。最小改动 ~10 行,SimulatorLoop 那边一行不用动。

为什么不立刻修:trigger 文本本身(“Continue the dialogue, produce your next message in character”)有引导价值,告诉 LLM 推进 beat goal。无脑合并可能稀释这条信号。等真出问题再有针对性处理。