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_aStep 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、GenerationSettingsproviders/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.mdnow frames the simulator as “the user in this session,” not as a stage actor playing a fictional character. Person-specific naturalness should primarily come fromuser_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.py—Persona含interaction_preferences: dict[str, InteractionPreference],每项有 setting/confidence/behaviors/asymmetrydata/personas/sheldon_cooper.yaml— 15 个 IPaS 维度全部填写完成(11 个从 Evidence 提取 + 4 个手动 author)data/scripts/sheldon_cooper/session_test.yaml— Data 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 产出,含sourceprovenance)。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/SessionScriptschemas 已存在;generate_beat_turn()单 beat 入口在simulator/loop.py)。Step 2 真正待做 = 4 / 5 / 6(外加 5b:脚本脱敏迁移)。
新建:✅(且 2026-05-01 起 canonical 切到data/personas/sheldon_identity.yamluser_a_identity.yaml,sheldon 版本保留作源参考)删除旧:✅sheldon_cooper.yaml更新:✅simulator/schemas.py(PersonaIdentity / PreferenceMatrix / Session.context / Beat.active_skills)构建:✅ 当前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。
- 脚本脱敏迁移:把
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/不删,保留作脱敏前源。 - 写
data/scripts/user_a/session_01.yaml的最终 review 版(已迁移即可,必要时调整 active_skills / beat 内容) - 构建
harness/transcript.py— 跨 simulator + PA 的对话记录器(perpa/ ↔ 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 行,先做最小集合
- 写
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_MODELenv 覆盖默认 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.py—InternalStateTracker类 - 写
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.py—SimulatorCheckpointManager - 边界情况处理:
- 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.pytests/test_simulator/test_checkpoint.py
你做:
- 跑 forced events 测试,确认固定台词正确插入
- 跑 checkpoint 测试:
- 正常保存恢复
- 手动中断后恢复
- 回滚重新开始
- 反馈是否有遗漏的边界情况
测试:
PYTHONPATH=. python tests/test_simulator/test_forced_events.py
PYTHONPATH=. python tests/test_simulator/test_checkpoint.pyComments on Step 5
关键文件清单
| 文件 | 操作 | Step | 用途 |
|---|---|---|---|
simulator/providers/*.py | 从 nanobot 复制 | 1a | LLM provider 支持 |
simulator/memory/__init__.py | 新建 | 1a | memory 模块入口 |
simulator/memory/state.py | 新建(空壳到Step4填充) | 1a/4 | InternalStateTracker |
simulator/config.py | 新建 | 1a | 精简版配置(temperature=0) |
simulator/schemas.py | 新建 | 1b | 所有 Pydantic 数据模型 |
simulator/loop.py | 新建 | 2 | SimulatorLoop 核心引擎 |
simulator/checkpoint.py | 新建 | 5 | 状态保存/恢复 |
simulator/prompts/generate_message.md | 新建 | 1c | 消息生成 prompt |
simulator/prompts/beat_judge.md | 新建 | 3 | Beat 转换 prompt |
simulator/prompts/state_reaction.md | 新建 | 4 | 状态反应 prompt |
data/personas/sheldon_cooper.yaml | Step 1b 建,Step 2 归档 | 1b→2 | 旧单文件卡;已拆为 identity + preferences 两文件 |
data/personas/sheldon_identity.yaml | 新建(已 ✅) | 2 | pre-anonymization 源 identity |
data/personas/sheldon_s01-s03_preferences.yaml | extractor 生成(已 ✅) | 2 | pre-anonymization 源 2 × 15 matrix |
data/personas/user_a_identity.yaml | canonical(已 ✅,2026-05-01) | 2 | 脱敏 identity — Step 2 之后由 runtime 加载 |
data/personas/user_a_s01-s03_preferences.yaml | canonical(已 ✅,2026-05-01) | 2 | 脱敏 2 × 15 matrix |
data/scripts/sheldon_cooper/session_{01,02,03}.yaml | 已 ✅ | 1b/2 | pre-anonymization 源剧本,保留参考 |
data/scripts/user_a/session_{01,02,03}.yaml | 新建(迁移并脱敏) | 2 | runtime 使用的剧本 |
tests/test_simulator/test_schemas.py | 新建 | 1b | YAML 加载验证 |
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 | 新建 | 5 | forced events 测试 |
tests/test_simulator/test_checkpoint.py | 新建 | 5 | checkpoint 测试 |
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。无脑合并可能稀释这条信号。等真出问题再有针对性处理。