作品见证 · 技术文章

一张牌如何变成一条可追溯的因果链

CardWorld 为什么要保存根卡、来源实体与生成回合,以及这些字段如何把 LLM 叙事变成可检查的世界历史。

内容核验:2026-08-01

问题不是生成,而是来路

让 LLM 写出“雾港出现一座潮汐塔”并不难。真正困难的是下一回合:玩家把另一张牌放到潮汐塔上时,系统能否回答它属于哪个地域、由哪张牌触发、在哪一回合出现,又为什么会影响新的事件。

如果只保存最终文本,世界看起来在变化,却没有可计算的历史。CardWorld 因此把每次生成拆成规则事实、因果引用和叙事表达三层。叙事可以重写,事实和引用不能凭空漂移。

三类对象构成世界

世界根与地域

世界根是第一次落牌时的锚点。根卡可以创建初始地域;后续地域保留 origin_root_card_id,因此地图能画出“这片地域从哪张根卡长出来”的第一层关系。

地域实体

实体存在于地域中,可以是人物、组织、建筑、资源或事件。实体记录创建回合、生命周期和剩余回合;短期事件会自然衰减,新实体不会在诞生的同一回合被错误老化。

卡牌放置

一次放置不仅包含卡牌,还包含目标类型与目标 ID。目标可能是世界根、地域或已有实体。这个选择决定模拟上下文,也为后续保存 source_card_idparent_entity_id 提供来源。

从动作到历史的路径

text
玩家选择卡牌与目标
        │
        ▼
服务端校验身份、所有权、房间阶段
        │
        ▼
原子认领 room + turn 的模拟权
        │
        ▼
规则层收集地域、实体、事件种子与历史
        │
        ▼
LLM 返回受约束的结构化变化
        │
        ▼
一个事务写入地域、实体、种子、奖励卡、回合历史
        │
        ▼
地图与叙事面板展示“发生了什么”及“从哪里来”

关键点在“一个事务”。如果叙事已经生成,但回合历史写入失败,系统不能留下半个新地域或几张孤立奖励卡。提交失败时,所有本回合写入一起回滚,内存中的临时 ID 也恢复原状。

为什么根边还不够

只有 origin_root_card_id 时,地图最多能解释地域与世界根的关系。实体之间更细的链路还需要两个引用:

  • source_card_id:这次变化由哪张卡直接触发。
  • parent_entity_id:这次变化基于哪个已有实体继续生长。

二者不能互相替代。卡牌回答“玩家施加了什么意图”,父实体回答“世界中的哪个对象承接了意图”。当一张卡直接作用于地域时,父实体可以为空;当它作用于人物或建筑时,父实体就成为链上的上一个节点。

可视化不应重新发明事实

因果图应当消费与世界状态相同的存储字段,而不是在浏览器里根据名称或叙事猜关系。一个可靠的视图至少要区分:

  1. 根卡到地域的起源边。
  2. 来源卡到新实体的触发边。
  3. 父实体到子实体的延续边。
  4. 缺少引用的历史数据,用明确的“来源未记录”状态表示。

选择地域时,地图和详情面板可以只突出相关链路,避免把完整世界历史一次性堆到屏幕上。移动端则应把图和详情拆成可切换视图,并保留同一组选中状态。

LLM 在链条中的正确位置

LLM 可以建议新地域、实体、事件种子和叙事,但它不直接决定数据库主键,也不能自行提交房间阶段。服务端先验证结构,再在事务中分配真实 ID 和建立引用。

这条边界带来一个有用性质:即使更换模型、重试叙事或以后重新生成文案,已经保存的因果关系仍然稳定。模型负责让历史值得读,数据模型负责让历史能够被证明。

如何验证这不是一张静态示意图

验证应从一条真实动作链开始:根卡创建地域,第二回合把卡放到地域生成实体,第三回合再把卡放到该实体。随后同时检查数据库引用、世界状态接口和前端选中链路。

单元测试负责字段与回滚,集成测试负责公开的结束回合接口,端到端测试负责用户能否从卡牌一路看到地图变化。只有三层对上,因果图才是产品能力,而不是一张和运行时脱节的插图。