一个 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 是按能力查 catalog
(reference: 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 引导创建(真实世界检索必定执行一次),随后被每个构建
阶段查阅与扩充。每个条目要么是带来源的事实,要么是建模参考,要么是
显式声明的假设(basis:sourced / proxy / assumed)。规则是
先锚定、再发明:承重的 E、P 数值必须引用一个事实 id 或假设 id。
它刻意不是 Pydantic 交接产物,而是一份扁平、易合并的 JSON:每个阶段
往里追加,dashboard 只读渲染。文档形状是约定,由
skills/sv_grounding.py 保证,而不是由 schema 强制:
| 字段 | 形状 |
|---|---|
study_id、query、updated_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_network 带 edges,spatial_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_ref、collector_ref、store_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
记录父子树——包括从旧版本分支的情形。见
迭代、版本与报告。
目录的规矩¶
两条不变式
- 永不原地修改参考研究(
reference: true)或任何既有研究—— 复用靠 fork,修改走/sv-iterate。 - 运行输出是本地的:
versions/、runs/、*.duckdb都是 gitignore 的历史,不进仓库。