本文是关于一个开源的 Claude 能力(Claude skill)项目的跟进文章。这个项目最初是在几个月前在 r/ClaudeAI 上发布的,现已转变为一个托管的 SaaS 服务,位于 neuralinitiative.ai。考虑到这个社区更关心项目的架构变化而不是用户界面变化,因此本文将以此为主题。

是什么:一个持久的、多人AI 游戏主持人(Game Master)为5e 桌游campaign。有趣的问题不是D&D 的部分,而是当“我和我家人在一个笔记本电脑上”变成“互联网上的任意用户带有他们自己的浏览器、角色和预算”的时候。

编码规则集下面的叙述。架构选择是其他一切的基础:LLM叙述,而不是计算。生命值、能力值、状态、咒力槽、物品、派系声誉、NPC 记忆都存储在服务器端的代码中。掷骰子是在Python 中进行的(种子随机数,不描述)。战斗序列是真实数据结构。模型接收真实数据作为下一个提示的输入并产生叙述;它永远不会拥有状态。这就是为什么模型选择有趣——你在选择叙述者,而不是规则引擎。它也是系统保持生命值真实的原因,而不是让玩家自行叙述自己避开致命伤害。

模型路由。Vertex AI 是 Claude 家族的主要路由;OpenRouter 是fallback 路由,并且是其他所有情况下的主要路由。路由器将逻辑模型名称(如 sonnet-4.6)转换为优先顺序的(提供者、路由)元组链,然后遍历链条,跳过 unhealthy 提供者,根据滑动窗口健康跟踪器。这个链条是如何在Vertex 输入令牌每分钟配额事件(在五月发生)期间保持服务正常的,当叙述调用开始连续返回429 错误时。这条链就走向了OpenRouter,玩家们没有察觉。每个任务的默认等级选择一个便宜的分类器来处理NPC 名称重置和其他类似的事情,而高级等级则用于打开场景和DM 回应。每个campaign 和每个用户的模型偏好可以通过同样的链条覆盖默认等级。

多租户状态机。每个campaign 都有一个服务器端的引擎,拥有游戏状态——当前场景、党派状态、最近叙述、排队的玩家输入。玩家浏览器通过 SSE 连接并接收去重的事件流,使用 _seq 计数器对事件流进行去重。去重逻辑需要花费大量时间来解决:重新连接时,lastEventId 重置在 _seq 去重检查之前,这导致重新连接的浏览器丢弃所有正在直播的事件。四行的修复。这个教训更深远:当你在campaign 中广播到 N 个浏览器,并且在flaky 移动网络上进行重新连接时,你的去重逻辑和游标恢复的顺序就非常重要了,并且不会在单用户测试中暴露出来。

预付费模式。每个LLM 调用遵循相同的流程:估算成本从输入令牌数量、最大输出令牌数量和模型的费率中加起来,持有这个估算值与用户的余额进行比较(在用户行上执行SELECT FOR UPDATE),然后进行上游调用,最后进行实际成本的恢复(退还估算值中的超出部分,扣除估算值中的不足部分)。持有恢复的形状是防止两个并发调用从一个$0.50 余额中消耗$1 的原因。每天的花费限制检查是针对支付者(campaign 拥有者)而不是调用者。

开源兄弟姐妹。Claude 能力(claude-dnd-skill)和一个模型无关的通用化(open-tabletop-gm)仍然活跃并且使用 AGPL v3 协议。托管的 SaaS 与它们共享架构DNA,但在多租户和管理计费时,两条路径会分叉。

诚实的经历。我最初构建了这个技能是为了自我满足——我想和我家人一起玩D&D,但我无法获得我想要的体验。SaaS 的初始想法是“如果其他家庭也可以有这个体验而不需要Claude 订阅和终端”?现在有一个小规模的私人测试,支付LLM 的费用是自费的,学习在多租户规模下哪些东西会破裂而不是在单个运营商下会破裂。最有趣的bug 都出现在LLM、引擎和玩家之间的交界处。

如果感兴趣,可以深入讨论架构决策。