// Created At 2026-07-20// P3
// DSL · JSON · LLM · Engineering

叙事引擎与大模型:从自由叙事到契约约束

我用大模型做叙事的第一天,就遇到了一个问题。

我定义了一个角色:贝利亚奥特曼,性格狂傲、蔑视光之国、崇尚力量。前三轮对话表现完美——每次回答都带着那种令人满意的反派质感。第五轮开始,他开始跟我讲人生哲理,像个温和的哲学家。第十轮,他自称“光之国的守护者”,完全忘记了自己是谁。

这不是模型不够好。这是“自由生成”的固有缺陷——大模型没有“必须遵守规则”的内置机制。你可以在系统提示里写“你是一个狂傲的反派”,但模型对“狂傲”的理解是概率性的,会在长上下文中逐渐衰减,被用户的输入、模型的输出、甚至 token 顺序漂移所覆盖。

我需要一种方式,把规则写下来,让模型必须遵循。于是我开始写 Mephisto。

但在写引擎代码之前,我先要解决一个更基础的问题:这些规则应该用什么格式来写?

它需要承载的内容包括角色名(单行)、世界观(多行)、状态变量(键值对)、行为规则(条件-动作)。每种内容的结构和解析方式都不同,这个格式直接决定了创作者是把时间花在写故事上,还是花在调试语法上。

这篇文章记录了我在这件事上的取舍和最终设计。

一、先看目标:一份真实的 .meph 契约

在讨论任何设计之前,先看成品。下面是一份完整的 .meph 契约文件,来自项目中的 data/sample.meph

【角色名】
贝利亚奥特曼

【锚点】
- 核心信念:力量就是一切
- 说话风格:狂傲、嘲讽、不容置疑

【世界观】
光之国是宇宙中最强大的文明,也是贝利亚的故乡。
但他早已被驱逐,如今他带着对光之国的憎恨归来。

【状态】
- 堕落指数:50
- 情绪:暴怒
- 位置:宇宙空间站

【规则】
[攻击] if 包含 "攻击" -> 注入 "贝利亚发动了猛烈的攻击"
[防御] if 包含 "防御" || 包含 "防守" -> 注入 "贝利亚摆出了防御姿态"
[光之国] if 包含 "光之国" -> 注入 "{角色名}的故乡是光之国,也是他最大的仇恨来源"
[高堕落] if 状态.堕落指数 > 80 -> 状态.情绪 = "癫狂"

这就是一份契约的全部。创作者看到的不是 {},不是转义引号,不是缩进层级,而是:

  • 【角色名】 下面直接写名字
  • 【锚点】 下面用 - 键: 值 列出核心人格
  • 【规则】 下面用 [名] if 条件 -> 动作 定义行为

注意规则中的 {角色名}——这是插值语法,运行时会被替换为当前状态中的实际角色名。它让规则可以复用,不必在每个动作里硬编码角色名字。

看起来像文档。但程序可以精确地解析它,并且当它出错时,能告诉创作者“第 12 行(区块「状态」):列表项必须以 ‘-’ 开头”,而不是 unexpected token at position 246

接下来的问题就是:怎么走到这一步的?

二、JSON:对程序友好,对人残忍

JSON 是最自然的第一选择。结构清晰、解析简单、任何语言都有现成库。我最初也确实用 JSON 写过原型,但很快发现了一个致命问题。

上面那条规则在 JSON 里长这样:

{
  "rules": [
    {
      "name": "攻击",
      "condition": "包含 \"攻击\"",
      "action": "注入 \"贝利亚发动了猛烈的攻击\""
    }
  ]
}

创作者的意图是:

包含 "攻击"

在 JSON 里必须写成:

"condition": "包含 \"攻击\""

问题不在于语法“有多难”,而在于心智切换成本。创作者在书写时不能直接表达自己的意图,必须时刻思考“我是在写 JSON 还是在写规则”。在大型契约中,这种成本是持续累积的——你阅读的不是内容,而是在不断核对“这一行有多少个反斜杠”。

更糟糕的是错误信息。一个常见的错误:在 "rules" 数组的最后一个元素后面多加了一个逗号。JSON 解析器报错:

Unexpected token } in JSON at position 246

创作者需要复制粘贴去数“position 246”在哪。这个体验对于非技术用户几乎是毁灭性的。

JSON 对程序友好,但对人不友好。 而这份文件的作者是创作者,不是程序员。

三、YAML:简单的幻觉

YAML 看起来解决了 JSON 的可读性问题:

role_name: 贝利亚奥特曼
rules:
  - name: 攻击
    condition: 包含 "攻击"
    action: 注入 "贝利亚发动了猛烈的攻击"

缩进取代了括号和逗号,确实更像“写文档”了。

但 YAML 的问题比 JSON 更隐蔽。它不是“语法错误”的问题,而是“逻辑错误”的问题——文件能读,但行为完全不对。

想象一下:你在 YAML 中定义一个多行文本块(比如世界观),用 | 标记:

worldview: |
  光之国是宇宙中最强大的文明。
  贝利亚被驱逐后,一直在寻找复仇的机会。

然后在下面继续定义一个列表。缩进稍微偏差一个空格,解析器就可能把多行文本块的后续行当作列表的子元素。结果就是:世界观内容被截断,列表结构被破坏,没有任何报错,引擎运行时产生非预期的行为。

更隐蔽的是,中文全角空格和英文半角空格在视觉上几乎无法分辨。创作者在编辑器中敲了一个全角空格( )而非半角空格(),YAML 解析器不会把它当作缩进的一部分,而是抛出语法错误,或者诡异地将整段文本识别为键名。没有语法高亮的情况下,这种错误几乎无法肉眼排查。

创作者不是在阅读内容,而是在不断核对“这一行到底缩进了几个空格”。对于上百行的契约文件,这种认知负担是持续累积的。

YAML 的“简单”是一个错觉。 它对人友好,但对程序来说,它的规则比 JSON 更复杂、更难以预测。

四、纯文本:自由但模糊

既然结构化格式都有问题,那能不能干脆不用结构,直接用纯文本写?

比如这样:

角色名是贝利亚奥特曼。
世界观是光之国。
规则是如果用户提到攻击,就执行攻击。

对创作者来说,这是最自然的方式——没有任何学习成本。

但问题是:程序看不懂。它不知道“角色名是贝利亚奥特曼”这句话里的“是”是声明还是叙述,不知道“世界观是光之国”和下一句“规则是……”之间是什么关系。纯文本的自然语言对人类来说是清晰的,但对程序来说,它是模糊的、歧义的、无法精确解析的。

如果我用纯文本,就需要设计一套隐式约定——比如靠关键词匹配来识别区块,靠换行来区分条目。这比显式语法更难保证正确性。

纯文本对创作者最友好,但对程序几乎不友好。

五、三个方案的对比

格式 对创作者友好 对程序友好 核心问题
JSON 转义地狱、报错信息不可读
YAML ⚠️ 缩进敏感,多行文本与列表易混淆
纯文本 结构模糊,程序无法精确解析

我需要一种格式:写起来像文档,读起来有结构。 它必须同时满足两个条件:

  1. 创作者可以自然地书写,不需要学习 JSON 转义或 YAML 缩进规则
  2. 程序可以精确地解析,并且报错时能告诉创作者具体位置和原因

这意味着我无法使用任何现成的通用格式——我需要设计一种专门针对“叙事契约”这个场景的格式。

六、.meph 的设计原则

回到开篇那份 .meph 契约。它的设计基于三条原则:

1. 用人类语言做边界,消除括号恐惧

创作者看到的不是 {},而是 【角色名】。中文书名号对中文创作者来说比花括号自然得多。【角色名】 本身说明了区块的内容是什么,不需要额外注释。

更重要的是,区块标题被限定在白名单内(角色名锚点规则状态 等)。如果创作者写了 【脚色名】(错别字),解析器不会把它当作区块开始,而是报错“第 1 行:内容出现在任何区块之外”——创作者立刻就能发现并修复。

2. 区分“语义区块”而非“数据结构”

在 JSON 中,创作者需要自己决定用对象还是数组,这属于实现细节。在 .meph 中,创作者只需要知道“这是一个列表”或“这是一段话”。“角色名是单行文本”和“规则是列表”由解析器根据区块名识别,不由创作者声明。

3. 语法贴近自然逻辑

规则采用 [规则名] if 条件 -> 动作 的直观写法。条件中的逻辑运算符用 包含状态.键 > 值 这类可读性强的表达,而不是纯符号。

七、代价

这套设计不是没有代价的:

  • 解析器需要手写:我不能用 json.Unmarshalyaml.Unmarshal,需要自己写 Lexer 和 Parser
  • 需要维护白名单:新增区块时要同步更新 knownBlocks 列表
  • 需要文档:创作者需要学习这个格式的写法——虽然学习成本比 JSON 低,但毕竟需要学习
  • 工具链缺失:没有现成的语法高亮、格式化、校验工具

但这个取舍的衡量标准很简单:这份文件的作者是谁?

如果是程序员写、程序读,JSON 够用了。如果是创作者写、程序读,就需要一种“以人为中心”的格式。而叙事引擎的目标用户正是创作者——写故事的人。

这个取舍,我认为是值得的。

八、小结

这篇文章回答了“用什么格式”这个问题。答案是 .meph——一种专门为叙事契约设计的、对创作者友好的文本格式。

但“设计”只解决了一半问题。下一篇文章要回答的是:怎么让程序精确地读取这个格式,并在出错时报出“第 12 行(区块「状态」):列表项必须以 ‘-’ 开头”,而不是 position 246

答案是手写区块扫描器——一个逐行扫描、精确绑定行号、白名单前置的轻量级 Lexer。我们下一篇见。

项目地址:https://github.com/yuelinghuashu/mephisto

如果这篇文档对你有帮助,可以请我喝杯咖啡 ☕️
Ali PayWechat Pay
© 2026 MOONGATE