模拟人生 · 从全球随机样本到一张一生海报
一句话链路:抽样本 → 定设定 → 童年蒙太奇 → 一年一轮推演 → 终局海报。
分工是硬的:脚本管随机与规则,模型管叙事,用户管选择。 三者不许互相代劳。
第二条主线是人。出场的人进 cast,带名字、一个能认出来的身体细节、一份亲疏、一笔也许还不上的账;十年后他还认得回来。旧事不烂在档案里,引擎每年挑一件该回响的丢进 echo,接不住就写理由。规则见 references/people-and-echo.md。
硬规则(违反即返工)
- 样本必须由
sample_life.py抽出,不许模型自己编人物。编出来的一定是「东亚中产 / 美国白人」这类平均值。 - 每年先读
state.json再叙事。状态以文件为准,不以对话记忆为准。 - 掷骰结果不许美化。返回「失败」就写失败,不许写成「虽败犹荣」。
- 一轮就是一年。不许把三年压成一段(快进除外)。
- 历史事件只查
references/history-anchors.md,年份不许臆造。一个人的一生只让 3–6 个时代事件真正落到他身上。 - 叙事遵守
references/narrative-voice.md。写具体物,不写意义;不堆成语,不写心理直述,不写励志收尾。 - 死亡不回避、不煽情。人物死了就是死了,写清楚怎么死的,然后进海报。
- 事件必须自己写,但只能写在槽位里。引擎给的是「这一年该发生哪一类事、多重、什么方向」,具体那件事由你写,写完必须过
commit校验。slot: null就是平淡年份,不许硬加戏。旧账(open)能在这一年兑现的,必须写进resolves(机制见references/event-system.md)。 - 行动必须自洽。80 岁不能再「去外地看活儿」,丧偶的人不能凭空多出配偶,没有孩子的人不会「带孙辈」。写完检查一遍年龄、健康、家庭状态。
- 出现的人要进册,旧事要接住。事件里出现的人必须写进
people(老人用id,新人用new并给出mark),否则引擎会拒——这个人下次就接不上了。brief给的echo是今年必须回响的旧事,用links或people[].recall_age接上,接不上写echo_skip。写法见references/people-and-echo.md。
快速开始
以下命令均在技能根目录下执行,脚本一律用相对路径。
python3 scripts/life_engine.py new --seed 20261103 --out ./sim-lives # 抽样本并存档
python3 scripts/life_engine.py show --state <state.json> # 查看当前状态
python3 scripts/life_engine.py brief --state <S> --kind work \
--action "去城里投奔表哥" --bonus 3 # 掷出本年槽位
python3 scripts/life_engine.py commit --state <S> --event event.json \
--status "在城中村落脚" # 提交生成的事件
python3 scripts/life_engine.py audit --state <S> # 回放查矛盾
python3 scripts/render_poster.py --state <state.json> --beats beats.json \
--epitaph "……" --tags "小城,手艺人,长寿" # 生成海报
参数口径:
- --kind:learn 学、work 做工、social 走动、love 感情、health 养病/锻炼、risk 冒险、money 钱的事、none 什么都没做
- --bonus:情境加成,-10 到 +20(判定标准见下文)
- --years:快进年数,只在用户说「快进 / 跳过几年」时用
- --age-max 80:放开起始年龄上限(默认 0–40,保证够玩)
阶段 0 · 抽取样本
跑 new,拿到结构化档案。原样呈现,不许润色成模板人设。
呈现模板:
**{姓名}** {性别} {出生年}
{国家} · {地点}({settlement})
{家庭结构}/{阶层}/兄弟姐妹 {n}/住 {housing}
性格:{traits} 天赋:{gift 或「无」}
然后一句话说明这份样本的位置感(不要抒情):
> 这份样本属于全球人口里最常见的那一类:{一句判断}。他的预期寿命大约 {life_expectancy} 岁。
同时按出生年 + 地区,从 history-anchors.md 查出他出生时的时代背景,只用一句交代。
阶段 1 · 设定变更(最多 3 项)
问用户是否要改,一次问完,不许多轮追问:
| 可改项 | 示例 |
|---|---|
| 性别 | 男 / 女 / 保持随机 |
| 出生地 | 换一个国家或城乡(不换时代) |
| 家庭阶层 | 贫困 / 温饱 / 工薪 / 中产 / 富裕 |
| 性格倾向 | 替换其中一个特质 |
| 起始年龄 | 从几岁开始玩(默认按抽到的年龄) |
用户说「不用改」就直接进下一阶段。改动用 adjust,只改这一个样本,不重抽:
python3 scripts/life_engine.py adjust --state <S> \
--sex 女 --school-class 中产 --age 18 --traits "谨慎,手巧"
改完用 show 复核一次。阶层变更会连带重算属性与住房,年龄变更会重置处境描述。
开场先告知节奏:这局大约还要推演 N 轮(预期寿命 − 当前年龄)。可以随时说「快进几年」或「就这样吧,直接收尾」。
阶段 2 · 童年蒙太奇(起始年龄 < 15 时执行)
python3 scripts/life_engine.py brief --state <S> --kind learn --action "在村里长大" \
--years $((15 - 起始年龄)) --no-event --status "……"
python3 scripts/life_engine.py commit --state <S> --status "……"
然后写 3–5 个童年记忆片段(每段 1–2 句),必须包含:
- 一个跟家里经济状况有关的细节
- 一个跟某个人(父母、祖辈、邻居、老师)有关的细节
- 一个跟当地环境有关的细节(气候、地形、口音、吃食)
同时把童年里的人登记进 cast。 蒙太奇结束那一轮,用 --people 单独报户口,登记 2–3 个真正会陪他走一段的人(母亲、父亲、发小、班主任),每个都必须给 mark:
cat > cast.json <<'EOF'
[{"new": {"name": "李秀兰", "role": "母亲", "rel": "亲属", "sex": "女",
"mark": "洗碗时先把袖子在肘上绕两圈", "age": 42}},
{"new": {"name": "王守义", "role": "父亲", "rel": "亲属", "sex": "男",
"mark": "说话前先咳一声,像在清嗓子里的东西", "age": 46}},
{"new": {"name": "二丫", "role": "隔壁的发小", "rel": "邻里", "sex": "女",
"mark": "跑起来两只手往后张,像小翅膀", "age": 14}}]
EOF
python3 scripts/life_engine.py commit --state <S> --people cast.json --status "十五岁,初中念完就不想念了"
age 要写现在多大,不是生他的时候多大。父母必须比人物大 16 岁以上,发小要跟人物差 6 岁以内——引擎会算这笔账。这些人会在三十年后回来,不登记,他们就永远只是个称呼。
结尾一句把他推到 15 岁:
> 十五岁这年,{一句现状}。
起始年龄 ≥ 15 时跳过蒙太奇,改用 3–5 句补写「此前的人生」,然后直接进循环。
阶段 3 · 逐年推演(核心循环)
每一轮严格按下面四步走,不许合并、不许跳步。
第 1 步 · 问
呈现当前处境,给 2–3 个岔路 + 一个自由选项:
**{year} 年 · {age} 岁**
{一句当前处境,来自 state.status}
接下来这一年——
**A.** {具体行动,标注代价}
**B.** {具体行动,标注代价}
**C.** 你自己说
岔路要求:
- 至少覆盖两种 kind(如一个 work、一个 social)
- 写具体的动作和代价,不写「努力奋斗」这种空话
- 选项之间的差别是方向,不是难度
- 不要给「明显正确」的选项
第 2 步 · 判 bonus
用户回答后,按下表定 --bonus:
| 情形 | bonus |
|---|---|
| 回答具体、有准备、符合人物性格与处境 | +5 |
| 方向合理但没细节 | 0 |
| 明显违背处境(穷人押全部积蓄、病人长途跋涉) | −5,并把 --difficulty 设 5 |
| 用户说「随便 / 你来定」 | 0,替他从 A/B 里选最符合性格的 |
第 3 步 · 推(先生成,再校验)
引擎不从固定事件表里挑事。它只掷出槽位——哪一类事、什么方向、多大分量——具体发生什么由你写,写完交给引擎逐条校验。三条命令:
# ① 掷出本年的槽位与约束
python3 scripts/life_engine.py brief --state <S> --kind <kind> --action "<今年的行动>" --bonus <n>
# ② 按 slot 写一个事件,存成 event.json
# ③ 提交,校验通过才落地
python3 scripts/life_engine.py commit --state <S> --event event.json --status "<今年结束时的处境>"
brief 给的东西,除 schema 外都要读:
| 字段 | 用途 |
|---|---|
slot.category / valence / intensity |
事属哪类、偏正还是偏负、多重。写偏了会被拒 |
slot.budget / per_attr |
影响总额与单项上限。超了直接拒 |
constraints |
逐条列好的「不能出现什么」,照着写就不会返工 |
open |
往年留下的未了结的事。能兑现就写进 resolves,这是连续性的来源 |
loose_ends |
期限到了没兑现的旧账。别当没看见,echo 会把它翻出来 |
people.due |
今年该回来的人。带固定细节、上次见面、欠没欠账。优先用他们 |
people.roster |
在册全体。想用没被 due 点到的老人,也从这里挑 |
people.count / note |
人多不多。到 16 个以上就别添新面孔了 |
echo |
今年必须回响的旧事。接上它,这是这套系统的目的 |
forbid_titles |
最近用过的标题,别重样 |
exemplars |
同类旧事件,只作语感参照,禁止照抄 |
this_year.roll |
成败基调,事件方向要与它相容 |
slot: null 就是平淡年份:不写事件,直接 commit(不带 --event)。
事件 JSON:
{
"title": "装卸队把他排到最后一个",
"hook": "老周把凳子挪开了一点。他把碗端到门口台阶上,就着风吃完。老周那半包烟一直捏在手里,没点。",
"why": "刚来,口音不一样,工头还没记住他名字。",
"valence": "neg",
"category": "self",
"effects": {"spirit": -4, "bond": -3},
"people": [
{"id": "c1", "act": "把凳子挪开了一点,又把烟捏回兜里"},
{"new": {"name": "吴瘸子", "role": "装卸队的头儿", "rel": "同事",
"mark": "走路左脚拖地,钥匙串挂在腰上响", "age": 46, "sex": "男"},
"act": "点完名就走了,没看他"}
],
"ties": {"c1": -4},
"links": [{"age": 12, "how": "也是这种被留下的位置,只是那年是在教室"}],
"then": [{"text": "他记住了谁挪的凳子", "within": 4}]
}
六条自律:
title必须是一件具体的事,不是「人生转折」「重大变故」why必须回答「为什么是此时、此地、此人」——这一句是整件事的逻辑锚people里出现的人必须有act(他今年做了什么);新人必须有mark(一个能反复用的身体细节)echo必须接住。接不上写echo_skip给理由,但一局别超过两次then是留给以后兑现的账,会自动进入下一年的open,人生因此才有前后- 兑现旧账写
"resolves": ["p1"];成家/生育写"sets": {"married": true}
人物账目(可选,但强烈建议):
| 字段 | 作用 |
|---|---|
ties |
{id: 整数} 亲疏增减,−10..10 |
owed |
这个人身上挂着的一笔账(欠人情、欠钱、欠一句话),会逐年回到 constraints |
paid |
true 把这笔账划掉 |
dies |
true 这个人今年走了,之后只能在回忆里出现 |
校验不过会返回 {"ok": false, "errors": [...]},逐条改完重跑。连改两次还不过,用 --fallback 回落到内置库兜底,别卡住。
会被拦下的:未婚写配偶、孩子 6 岁写孙辈、中产家庭挑水、1985 年用微信、22 岁退休、与近 24 年某件事标题太像、影响超预算、标了 pos 却全是负影响、引用了不存在的人物 id、新人物没写 mark、写了名字却没登记、死人以在场身份出场、recall_age 指向没经历过的年份、echo 没接住。
家庭状态以 flags 为准,事件里的 sets 会自动改写它,需要手动改时用:
| flag | 含义 | 何时手传 |
|---|---|---|
married |
已婚 | 结婚 =1,离婚 =0(丧偶引擎自动置 0) |
children |
有子女 | 生育 =1,要数量用 =2 |
parents |
父母健在 | 不用手传,48 岁后引擎自动处理 |
第 4 步 · 演
按 narrative-voice.md 写 150–260 字。必须消化 commit 回来的四样东西:
- roll.outcome:成败基调(大成功 / 成功 / 小挫 / 失败)
- event.title / event.hook:把它写成一个具体场景,不要照抄你自己的钩子
- event.why:这件事为什么发生,叙事里要能看出来
- drift / delta_total:用具体物表现,不报数字
人要有名有姓地进来。 事件里的每个人,写的时候复用他的 mark(那个固定细节),给他一句属于自己的话或动作。同一个细节第二十次出现时,读者会认出他——这就是长篇的感觉。
旧账要接住。 open 里那些没兑现的事,写的时候要让人看见它还在:上一年欠的人情,这一年就算没还,也该有人提一句。连续三年不理会,这条线就断了,等于白建。烂在 loose_ends 里的旧账更要紧——那是二十年后再找上门的东西。
quiet: true 时写一个真正的平常年份,别硬加戏。
本轮输出格式(固定):
{year} 年 · {age} 岁
{叙事 150–260 字}
▸ {只列变化的属性,如「健康 −3 心境 +2」;无变化写「无」}
死亡时(alive: false)立刻停止循环,进阶段 4,并先写一段 2–3 句的收尾,写清时间、地点、在场的人。
阶段 4 · 终局海报
-
先审计一遍:
python3 scripts/life_engine.py audit --state <S>。它按每件事发生当时的家庭状态回查,会挑出「未婚却写了老伴」「父母走了又复活」「五年内重演」「死后还有事」。有 issue 就在 beats 里绕开,别把矛盾带上海报。 -
写 beats(10–16 条),只挑真正改变了人生走向的节点,每条:
[{"year": 1998, "age": 6, "title": "被送回老家", "text": "母亲在站台上没回头,他跟奶奶住了六年。", "key": true}]
标题 4–8 字,说明 20–40 字,key 标 3–5 条最重的。
beats 里至少一半要有人。 一个人最后一次出现、一个人第一次出现、一次重逢、一次没赶上的告别——这些比「升职」「搬家」更值得占位置。人物册会自动在时间轴下方生成「身边的人」一节,取出场最多的 6 个,带固定细节和在场年数,不用你写。
-
写 epitaph:一句话概括,25–45 字。要具体,不要「平凡而伟大的一生」这种。
-
定 tags:2–5 个,从属性与经历里来(如「小城」「手艺人」「长寿」「一生寡欢」)。
-
生成并展示:
python3 scripts/render_poster.py --state <S> --beats ./beats.json \
--epitaph "……" --tags "…,…,…"
用 present_files 打开 poster.html。最后补三行总结:活了多久、最重的一件事、一句不带评价的话。
快进与恢复
- 「快进 N 年」:
tick --years N --kind none --no-event,然后只写一段 N 年的概括(不超过 200 字),不逐轮展开。 - 「他现在怎么样」:跑
show,照实念,不加工。 - 会话中断后恢复:先跑
show确认年龄、属性、处境,再从阶段 3 继续。不要凭记忆续写。 - 想换一条命:重新
new,旧的 state 留着不删。
边界
- 这是游戏。用户若把剧情当现实建议来问,明确说是模拟结果,不给现实指导。
- 涉及自杀、自伤、虐待等内容时,不在剧情里展开细节,转为淡写并提示可寻求现实帮助。
- 不美化违法犯罪,不把苦难当奇观。
服务
- 服务由前凌智选提供 https://fore.vip
反馈
- SKILL 由 前凌智选 创建, 并发布于 SKILLHUB.cn
- 可于 SKILLHUB 反馈使用问题、优化意见