包装既有模拟器(Path C)¶
你已经有一个能跑的模拟器——Mesa 模型、自研 ABM、领域专用引擎——想让它 作为 SocioVerse 研究运行:纵向循环、面板存储、版本化、报告,全套能力。 Path C 把它包装到四个核心接口后面,对原始源码零改动。
黄金法则:永不修改遗留代码
原模拟器的源码逐字节保持原样。所有胶水都住在你的研究目录里。 这让被包装的项目保持可升级,也让适配保持诚实——与原版的一致性 (parity)始终可证。
一次适配长什么样¶
studies/<your_study>/
├── adapter/
│ ├── engine_seam.py # 缝合层(SEAM)—— 定位并构造遗留引擎
│ └── providers.py # 四个接口实现,经由缝合层调用
├── study.yaml # 发现字段:legacy_simulator: "<name>"、……
└── environment/ population/ simulation/ # 标准产物
1. 立起 engine-seam(缝合层)¶
缝合层是研究内部的一个上下文管理器:注入配置(路径、工作区覆写), 并在其内部构造遗留引擎——通过 import,或者当遗留代码读取模块级状态时 通过 monkey-patch。Core 永远不调用缝合层;只有你的 provider 用它。
别混淆两个「引擎」:
- Core 引擎 =
LongitudinalSimulator,原样复用; - engine-seam = 你包在遗留引擎外面的胶水。
2. 把四个接口映射到遗留引擎上¶
| 接口 | 典型映射 |
|---|---|
EnvironmentProvider |
投影遗留世界状态;把观察内容标注进四个象限(宏观/局部 × 物理/信息);定时事件与广播路由进 advance_to(t);内生更新进 apply |
PopulationProvider |
遗留 agent → Persona,确定性持久 id;group_key 作为批处理分组;邻居/网络关系编码为 InteractionStructure |
DecisionModel |
调用遗留引擎的(批式)决策步;结果投影为类型化 Action——保持批式,不做逐 agent LLM 循环 |
MetricCollector |
包装既有指标;DuckDB 存储直接复用 Core 的 |
3. 编写产物并注册¶
与任何研究相同:四份产物,加上 provider 类上的 @register。填好
study.yaml 的发现字段——尤其是 legacy_simulator: "<name>"——让研究
加入 catalog,未来匹配的问题就能路由到它的 fork(Path A)而不是重建。
4. 证明一致性(parity)¶
信任这层包装之前,先断言它复现遗留轨迹:
- 原版与包装版用同一个种子各跑一遍,
- 所有 LLM 调用由确定性客户端替身,
- 逐步对比两条轨迹。
仓库的测试套件里有一个可参照的一致性测试范例。
来自真实迁移的坑¶
步数计数器耦合
遗留引擎常携带隐式的步数状态——例如内部读取
len(metrics_history),而 __init__ 预置了一条 step-0 记录。你的
collector 必须严格贴合这个契约(t ≥ 1 追加、t = 0 返回预置行),
否则遗留逻辑会悄悄失步。
一个进程一个研究
monkey-patch 模块全局量的缝合层不是并行安全的。一个进程只跑一个 研究;批量实验用进程池,不用线程。
钉死遗留依赖
被包装的研究继承遗留项目的依赖约束(例如某个钉死的 Mesa 版本)。 把它们声明为研究专属的 extra,而不是放宽 Core 的依赖——Core 本身 保持精简。
如果遗留项目在仓库之外,用环境变量把研究指向它(随仓库提供的参考迁移
用的是 SV_ABM_ROOT),不要硬编码路径。