架构分层¶
基础设施的三层¶
两个循环 —— 一次运行内部的纵向模拟循环、跨版本的可控研究循环 —— 都跑在 同一套基础设施上,这套基础设施分三层。每层占有不同的文件;判断一个问题 属于哪一层,通常是最快的定位代码的办法。
| 层 | 做什么 | 在哪里 |
|---|---|---|
| 工作流层 | sv-* 这组 skill。每个 skill 读前面阶段的产物,只写一份属于自己的、经校验的产物,然后停在一个检查点等你决定 |
.claude/skills/sv-*/SKILL.md;共享辅助库在 skills/sv_workspace.py(study 目录、版本、分支)与 skills/sv_grounding.py |
| 服务层 | 在构建期把 P 与 E 的外生部分解析成真实数据,以类型化 MCP 工具的形式提供 —— 人群池服务(sv-user-pool)与事件信号服务(sv-event-tool) |
服务端部署,经 MCP 访问;结果落盘进 population/ 与 environment/,运行时本身不发起任何外部请求。见数据服务 |
| 治理层 | 记录每个阶段做了什么、用了哪些数据、研究者批准了什么 | grounding/grounding.json、resources.json、versions.json,以及 trajectory/study.duckdb 里的 events 表 |
本页讲的是这三层站立其上的东西:运行时。
Core 运行时与 study 层¶
运行时分为两层:你复用的 Core 运行时,和你编写的 study 层。 Core 定义契约并驱动循环;study 提供实现。新增研究永远不需要改引擎。
socioverse/ CORE 运行时 —— 复用,不要动
├── schemas/ 类型化契约(StudySpec、EnvironmentBundle、PopulationBundle、
│ SimulationConfig、Observation、Action、TrajectoryRecord……)
├── abc/ 研究要实现的接口(EnvironmentProvider、
│ PopulationProvider、DecisionModel、MetricCollector)
│ + Simulator / TrajectoryStore / Reporter / MessageBus
├── engine/ LongitudinalSimulator(主循环)、AgentMemory、registry、
│ InMemoryMessageBus、build_simulator(装配)
├── env_layers/ 信息轴助手(广播投递、邻居信息流)
├── io/duckdb_store.py 可查询的 panel / metrics / events / messages 存储
├── providers.py 通用 File / MCP 人群 provider
└── validation.py validate_handoff —— 严格的阶段边界校验
studies/<id>/ STUDY 层 —— 你写(或由工作流生成)
└── model.py + 产物 四个接口实现 + study.yaml / env / pop / sim
四个接口¶
实现 socioverse/abc/ 里的四个抽象基类,一个研究就完整了:
| 接口 | 职责 | 关键方法 |
|---|---|---|
EnvironmentProvider |
持有世界状态 E | reset、advance_to(t)(外生)、observe_batch()(四象限视图)、apply(actions)(内生) |
PopulationProvider |
构建固定人群 P | build() → 带确定性持久 id 的 Persona[] + 交互结构 |
DecisionModel |
计算 B = f(P, E) | decide_batch(observations) → Action[] —— 整步输入、类型化 |
MetricCollector |
度量每一步 | collect() → 该步的聚合指标 |
其余由 Core 提供:LongitudinalSimulator 循环、轨迹存储、报告器、
消息总线、agent 记忆。
运行时动线¶
LongitudinalSimulator.run()(socioverse/engine/loop.py)先建一次 P、重置 E,
随后每一步都跑同样的五个阶段,顺序固定:
build() → Persona[] # P:只建一次,持久 id,此后固定
reset() # E_0 —— 作为第 0 步的面板行落盘
loop t = 1..N:
① 外生更新 advance_to(t) # 定时事件 + 广播改变 E
② 观察 observe_batch() → Observation[] # 每个 agent 的四象限视图
③ 决策 decide_batch() → Action[] # = B_t = f(P, E_t, memory)
④ 应用 apply(actions) # 内生反馈:E_{t+1} = g(E_t, B_t)
⑤ 记录 collect() + store.record() # 面板行 + 指标 + 本步触发的事件
| 阶段 | 调用 | 为什么排在这里 |
|---|---|---|
| ① 外生更新 | env.advance_to(t) → 触发的事件 |
干预在任何人观察之前进入环境,所以同一轮里所有 agent 面对的是同一个状态 |
| ② 观察 | env.observe_batch(ids, t, round) |
E 抵达 f 的唯一通道 —— 宏观/局部 × 物理/信息 |
| ③ 决策 | decision.decide_batch(obs, memories) |
整步输入、类型化输出:每个 agent 一条 Action,LLM 或规则都走这里 |
| ④ 应用 | env.apply(actions) |
在从零构建的研究里它是传入 actions 的纯函数,这正是分支重放能精确复现父版本的前提 |
| ⑤ 记录 | collector.collect()、store.record(...) |
先推记忆窗口,再写每个 agent 一行面板、一行指标,以及 ① 中触发的事件 |
同样五个阶段的两种变体:
interaction_rounds = K > 1时,②–④ 在一步之内重复 K 次;只有最后一轮的 action 进入记忆窗口和面板行。见步内多轮交互。- 分支用父版本已存的 action 重放 ①、④、⑤(
_replay_inherited), 到分叉步才开始真正调用决策模型。见干预与分支。
这些调用之间的交接是类型化的
(Persona[] → Observation[] → Action[] → metrics),所有类型都定义在
socioverse/schemas/。Schemas 是所有组件共同引用的契约层——
不是数据「流经」的一个阶段。
Core 内置的场景钩子¶
三类常见需求已在 Core 解决,研究只需声明、无需实现:
- 按受众投放信息 ——
Broadcast/InformationProgram(env_layers/information.py):audience: "all"抵达每个 agent 的 宏观信息象限;选择器 dict 命中匹配 agent 的局部信息象限。 - 大规模 agent 交互 —— 中介化的
MessageBus加interaction_rounds(不是 O(N²) 的两两聊天):发言落到总线上,邻居通过研究声明的InteractionStructure读取(engine/messaging.py、env_layers/neighbor_feed.py)。 - 自带数据 ——
ResourceManifest加通用的FilePopulationProvider/McpPopulationProvider(socioverse/providers.py),人群可来自文件或外部服务。
一个值得内化的命名区分¶
- Core 引擎是被复用的
LongitudinalSimulator——主循环本身。 - engine-seam(引擎缝合层)是研究内部的胶水,用来包装遗留 模拟器(见包装既有模拟器)。 Core 永远不调用它;只有研究自己的 provider 引用它。
注册与装配¶
研究用 @register(kind, key) 装饰器(socioverse.engine.registry)
发布实现,产物里按 key 引用它们(provider_ref、decision_ref、
collector_ref……)。此后装配是通用的:
它把每个 *_ref 在 registry 中解析、实例化 provider、接好 DuckDB 存储,
返回就绪的 LongitudinalSimulator。这就是全部集成面——不改引擎,
也没有要学的插件系统。