跳转至

一个 Study 的解剖

study(研究)是 SocioVerse 的工作单元:一个研究问题、一个 studies/<study_id>/ 目录、一组固定的产物。工作流的每个阶段恰好写入 一份产物,所以这个目录就是研究的全部状态——可检查、可编辑、可 diff。

studies/<study_id>/
├── study.yaml                      # StudySpec —— 问题、步数、指标、发现字段
├── grounding/
│   └── grounding.json              # 事实 + 来源 + 声明的假设(溯源侧车)
├── model.py                        # (从零构建的研究)四个接口实现
├── environment/
│   └── environment.json            # E —— 层、定时干预、广播
├── population/
│   ├── population.json             # P —— personas、规模、交互结构、传播模式
│   └── roster.jsonl                # 实例化后的 agent 在 t=0 的状态
├── simulation/
│   └── simulation.json             # 运行配置 —— decision/collector/store 引用、轮数、热启动
├── trajectory/
│   ├── study.duckdb                # 持久化的 panel/metrics/events 存储
│   └── metrics_history.json
├── reports/
│   └── report.md                   # 渲染出的轨迹 + 图表
├── resources.json                  # 本研究实际使用的外部能力(按名字记录)
├── versions.json                   # 版本树(父子关系、注记)—— 由 /sv-iterate 写入
└── versions/                       # v1/、v2/…… 完整快照(本地历史,gitignore)

产物逐个说,按阶段

study.yaml —— StudySpec

/sv-init 写入,后续构建阶段继续补齐。它是唯一一份人会从头读到尾的产物: 问题、时钟、指标,以及让研究能被找到的元数据。Schema 在 socioverse/schemas/study.py

身份与问题

字段 类型 默认 含义
study_id str 必填 工作区目录名;会嵌进每一份产物和每一行 DuckDB 记录
title / research_question / hypothesis str "" 研究问题,用用户自己的语言写
title_i18n / research_question_i18n / hypothesis_i18n dict[str, str] {} {zh, en} 变体;dashboard 按访问者语言取值,取不到就回落到无后缀字段
study_type "longitudinal" | "cross_sectional" longitudinal 纵向面板 vs 单次横截面

时钟

字段 类型 默认 含义
n_steps int 5 时间跨度
seed int 42 运行种子
time_unit str "abstract" 一步在现实中推进多久:day / week / month / quarter / year;实在没有日历映射才用 abstract。自由字符串,不做校验
step_meaning str "" 一句人话,把「一步」和研究问题绑起来,例如 "每步=1个月的消费决策周期"

指标

字段 类型 默认 含义
metrics list[str] [] 运行必须产出的指标名(collector 的 columns() 必须覆盖它们)
metric_descriptions dict[str, str] {} 每个指标一行说明——量的是什么、单位与方向;渲染在 dashboard 卡片上,也作为时间线图例的 tooltip
display_metrics list[str] [] dashboard 时间线真正会画的 3–5 个指标,免得记账类指标淹没主线;留空则回落到 metrics 的前五个
run_note dict[str, str] {zh, en} {} 当这个研究的运行发生在外部/重型流水线里时,显示在 run 卡片上的提示——「请 clone 到本地跑,别在托管服务里跑」

产物引用 —— 工作区内的相对路径;默认值就是约定布局,通常不必改。

字段 默认
environment_ref environment/environment.json
population_ref population/population.json
simulation_ref simulation/simulation.json
resources_ref resources.json

发现 / catalog —— sv-init 拿新问题去匹配的就是这几项。

字段 类型 默认 含义
domain str "" 主路由键,例如 urban-segregation
tags list[str] [] 次级路由键,例如 ["ABM", "Schelling"]
legacy_simulator str "" 被包装的遗留引擎路径/名称;Core 原生研究写 "from_scratch"
provider_refs list[str] [] 本研究注册的 registry key
adjustable_params list[str] [] fork 出去只改产物、不动代码就能调整的量。新问题匹配的就是这个面
status str "draft" 约定取值:draft / demo-only / parity-tested
maintainer str "" 谁在维护这个研究

参考 / 教学

字段 类型 默认 含义
demonstrates list[str] [] 本研究示范了哪些可复用模式——受控 kebab-case 词表,按字符串精确匹配(见下)
teaches str "" 一句话:照抄这个研究是为了学什么
reference bool False 只读基线:可以被 fork、可以当模板读,永不原地修改
created_by str "sv-init" fork 出来的研究写成 "sv-init (fork of <src>)"

demonstrates 词表,扩展要审慎:

legacy-seam · from-scratch-core · external-events · real-user-pool
cross-sectional-survey · warm-start · multi-round-messaging
geo-spatial · policy-intervention · info-broadcast

正因为是精确字符串匹配,构建 skill 是按能力查 catalogreference: true from-scratch-core),而不是把模板名写死在 skill 文本里——随仓库提供的示例集换一批,skill 一个字都不用改。

发现字段与参考字段合起来,就是 sv-init 路由新问题所用的 catalog: 已有研究覆盖得了的问题,fork 并调整,而不是重建。

先把「一步」锚到真实时间

time_unit + step_meaning 是两个很小的字符串,却决定了一条轨迹读起来是 时间还是一串抽象 tick。读者看到 step 7,必须知道那是「第七周」还是 「第七年」——而模型里每一步的量级也必须和这个答案自洽。

先选真实行为发生的尺度,再让所有速率跟它对齐:

行为 合理的 time_unit 随之必须对齐的东西
食堂 / 外卖 / 做饭的就餐选择 month 月度预算、月度价格漂移
选举中的意见转变 week 周度媒体周期、周度衰减
住房搬迁 year 年度租金增长、年度搬迁概率

abstract 是正当选项——但仅当日历映射确实无意义时(这一步就是一个纯粹的 决策回合),并且必须在 step_meaning 里把这个判断写出来。

不要默默地默认成 abstract

time_unit 默认就是 "abstract" 且不做校验,所以「从没填过」和 「刻意选择」在文件里长得一模一样。/sv-build-model 现在会在写下任何 每步量级之前先定这个单位。这两个字段是新增的,随仓库提供的若干 研究早于它们——老研究里 step_meaning 为空应理解为「还没说明」, 而不是「刻意抽象」。

grounding/grounding.json —— 溯源侧车

/sv-init 引导创建(真实世界检索必定执行一次),随后被每个构建 阶段查阅与扩充。每个条目要么是带来源的事实,要么是建模参考,要么是 显式声明的假设basissourced / proxy / assumed)。规则是 先锚定、再发明:承重的 E、P 数值必须引用一个事实 id 或假设 id。

刻意不是 Pydantic 交接产物,而是一份扁平、易合并的 JSON:每个阶段 往里追加,dashboard 只读渲染。文档形状是约定,由 skills/sv_grounding.py 保证,而不是由 schema 强制:

字段 形状
study_idqueryupdated_at 研究、原始问题、最后一次合并时间
method_notes 自由文本;哨兵值 "stylized" 有特殊含义(见下)
implementation_refs[] {title, url, takeaway, accessed} —— 别人怎么给同类现象建模
facts[] {id, claim, value, unit, as_of, basis, source, applies_to, note}
assumptions[] {id, claim, rationale} —— 没有 basis:假设本身就是它的依据

一条 fact 内部:

字段 取值
basis sourced | proxy | assumed
source {title, url, via, accessed, local_path?} —— local_path 指向下载到 grounding/data/ 的参考表
source.via event_service | web_search | provider | user
applies_to 这条事实支撑的产物路径,例如 ["environment.provider_args.base"]

facts 与 assumptions 按 id upsert(缺 id 的自动补 f<N> / a<N>), 所以后一个阶段可以修订某条而不产生重复,模型里的常量也就能在行尾注释里 引用一个稳定 id。

stylized 是一个决定,不是缺失

method_notes: "stylized"facts 为空,是在明确记录这个研究 没有真实世界锚点——抽象/玩具模型,其常量不需要事实 id。这和「压根没做 grounding」是两种状态,工具的措辞也不同(「stylized model, no real-world anchors」 vs 「no grounding recorded」)。

environment/environment.json —— E

/sv-build-environment 写入。声明环境的(两轴状态)、 定时干预(指定步数的外生变化)、以及带受众的信息广播"all" → 所有人的宏观信息象限;选择器 → 命中 agent 的局部信息象限)。

population/ —— P

/sv-build-population 写入。population.json 描述 personas、池子规模、 交互结构(谁能看见谁)与传播模式。人群在构建时就实例化—— roster.jsonl 存有每个 agent 在 t=0 的完整状态,让你在花掉任何一步模拟 预算之前就能审阅确切的人群。

字段 取值 含义
personas[] {agent_id, attributes, group_key, weight, init_state} agent_id 是纵向主键且必须唯一——schema 会拒绝重复 id
materialized_count int \| None 真实 agent 数;personas 为大规模人群留空时,它才是权威值None = 尚未实例化
interaction.kind spatial_adjacency | explicit_network | none 谁能看见谁——explicit_networkedgesspatial_adjacency 带 provider 计算的 adjacency_ref
propagation independent | contagion | broadcast_then_local 影响怎么流:只看 E、受邻居状态影响、或宏观冲击后再局部扩散

大规模人群

人群很大时,population.json 里的 personas 列表保持为空, roster.jsonl 才是已实例化 agent 的事实来源——此时 materialized_count 是唯一可靠的计数。

propagation 是声明性标签

引擎不读它。它为研究自己的环境实现记录意图,并给 dashboard 的人群 卡片打标签;真正的影响路径取决于 observe_batch 往 agent 的信息象限里 塞了什么。见多轮交互

simulation/simulation.json —— 运行配置

/sv-run 在预检阶段写入。按 registry key 绑定各部件 (decision_refcollector_refstore_ref 及各自 *_args),重复 n_steps / seed,另外承载两件值得知道的事:

  • interaction_rounds(默认 1)—— 写出面板行之前,循环在一步之内 跑几轮交互。移动类研究用 1;讨论与传染类用 K > 1。见 多轮交互
  • warm_start —— 由 /sv-iterate 设置,继承父版本已经跑过的步数, 不再重复付费。要求 interaction_rounds == 1。见 迭代、版本与报告

trajectory/ —— 运行输出

/sv-run 写入。study.duckdb 是可查询的存储,包含面板行、聚合指标 与已触发事件:

SELECT step, state FROM panel WHERE agent_id = '…' ORDER BY step;
SELECT * FROM metrics ORDER BY step;
SELECT step, note FROM events ORDER BY step;

reports/ —— 渲染结果

/sv-report 写入:分步可视化、带干预标记的指标时间线、以及 report.md——末尾附有从 grounding 侧车渲染出的「数据基础与参考来源」。

versions.json + versions/ —— 历史

/sv-iterate(修改既有研究的唯一入口)写入。每次迭代都可以先把 现行目录快照到 versions/vN/(产物 + 代码 + 结果),versions.json 记录父子树——包括从旧版本分支的情形。见 迭代、版本与报告

目录的规矩

两条不变式

  1. 永不原地修改参考研究reference: true)或任何既有研究—— 复用靠 fork,修改走 /sv-iterate
  2. 运行输出是本地的versions/runs/*.duckdb 都是 gitignore 的历史,不进仓库。