构建大模型叙事引擎:15 分钟上手——从零写出你的第一个契约
前置阅读:建议先读完第一篇《自由叙事到契约约束》,了解
.meph格式的设计动机。这篇不需要任何前置技术知识——只要你有终端和 Go 1.26+。
前一篇我们讨论了为什么 .meph 比 JSON 和 YAML 更适合做叙事契约。但光说不练没有用。
这篇我们放下理论,从零开始写一份真实的契约文件,编译它,运行它。整个过程不超过 20 分钟——你可以亲自看到自己写的规则如何驱动 LLM 生成叙事。
一、准备工作
你需要三样东西:
- Go 1.26+:打开终端,运行
go version确认版本 - 一个终端:任何操作系统都可以
- (可选)一个 LLM API Key:DeepSeek、OpenAI 或 Ollama 均可——如果没有也不影响,引擎在没有 LLM 时也能工作
如果你有 API Key,在项目根目录创建一个 .env 文件:
MEPHISTO_CLIENT=openai
MEPHISTO_MODEL=deepseek-v4-flash
OPENAI_API_KEY=sk-你的密钥
二、克隆并构建
git clone https://github.com/yuelinghuashu/mephisto.git
cd mephisto
go build -o ./mephisto ./cmd/mephisto
如果一切顺利,你会看到一个名为 mephisto 的可执行文件。
三、从零开始写契约
接下来才是关键。我们创建一个新文件 data/faust.meph,从头开始填充内容。
3.1 角色名
契约的第一行是角色名。用 【角色名】 标记区块,下面直接写名字:
【角色名】
浮士德
就这么简单——不需要引号,不需要冒号,不需要任何标记。
3.2 锚点
【锚点】 是角色的核心人格设定,用 - 键: 值 的格式列出:
【锚点】
- 核心信念:知识高于一切,我愿意为真理付出任何代价
- 欲望:体验一切人类能体验的事物
- 绝对禁忌:不会承认自己后悔
这些内容会被直接注入 LLM 的上下文,成为角色行为的基石。
3.3 状态
【状态】 是角色的动态变量,同样用键值对格式:
【状态】
- 灵魂完整度:100
- 情绪:永不满足
- 位置:书斋
状态值支持数字、布尔值、字符串三种类型。引擎在解析时自动推断类型——"100" 被解析为数字 100,"永不满足" 保持为字符串。这意味着你可以在规则中用 状态.灵魂完整度 > 50 直接比较数字,无需额外类型转换。
在运行时,状态被存储为 map[string]any,以便快速读写。初始顺序从契约中继承,但运行时只按键名访问,不依赖顺序。
3.4 世界观
【世界观】 是多行文本,直接写即可,保留换行:
【世界观】
浮士德是一位学识渊博的学者,精通哲学、医学、法学和神学。
但他对人类知识的极限感到绝望——穷尽一生所学,仍无法触及世界的本质。
绝望中,他与梅菲斯特签订了契约:用灵魂换取在世上的无限体验。
3.5 开局场景
【开局场景】 定义了对话开始时的情境:
【开局场景】
深夜。书斋中烛火摇曳,桌上的书籍堆积成山。
浮士德站在窗边,望着窗外的月光。
桌角放着一份契约书,墨迹还未干透。
3.6 规则——让角色活起来的关键
【规则】 是引擎的核心。每条规则由三部分组成:[规则名] if 条件 -> 动作。
【规则】
[新体验] if 包含 "追求" || 包含 "想要" || 包含 "体验" -> 注入 "{角色名}感到心中燃起新的渴望,没有什么能阻止他去亲自体验这一切"
[梅菲斯特] if 包含 "梅菲斯特" || 包含 "契约" -> 注入 "梅菲斯特的声音在{角色名}耳边低语:'这就是你想要的吗?代价你可想好了。'"
[灵魂代价] if 包含 "代价" || 包含 "灵魂" -> 注入 "{角色名}低头看着自己的双手,仿佛能看见什么东西正在一丝丝流逝"
[永不满足] if 不包含 "放弃" && 不包含 "满足" -> 注入 "{角色名}的眼中闪烁着永不熄灭的火焰,他还想要更多"
规则的条件支持逻辑运算符(&&、||)、状态比较(状态.键 > 值)、以及骰子表达式(roll(1d100))。动作最常用的是 注入——将消息追加到记忆,由 LLM 自然融入后续叙事。
3.7 验证
保存文件后,先验证解析是否正确:
./mephisto parse data/faust.meph
你会看到类似这样的 JSON 输出:
{
"role_name": "浮士德",
"anchor": [...],
"state": [...],
"rules": [...]
}
如果解析失败,错误信息会精确告诉你哪一行出了问题——比如”第 X 行(区块「锚点」):缺少 ‘:’ 或 ‘:’“。
四、第一次对话(无 LLM 模式)
即使没有配置 LLM,引擎也能运行:
./mephisto run data/faust.meph
你会看到欢迎信息,然后进入对话模式。输入 “我想要体验爱情”:
命运 > 我想要体验爱情
浮士德 沉默地注视着命运。
因为没有 LLM,引擎返回了默认响应。但规则其实已经匹配了——输入了”我想要”,条件 包含 "想要" 为 true,规则 [新体验] 触发,注入被写入记忆。
输入 /state 查看当前状态:
当前状态:
灵魂完整度: 100
情绪: 永不满足
位置: 书斋
输入 /history 查看对话历史——你输入的”我想要体验爱情”已经记录为命运指引。
这就是引擎在无 LLM 下的工作方式:规则匹配、注入、状态管理——全部正常运转。LLM 只是最后的”叙事输出层”。
五、打开 LLM 世界
现在加上 API Key,重新运行:
./mephisto run data/faust.meph -debug
你会看到同样的欢迎信息,但这次带上了 -debug 参数。LLM 的配置信息会先显示出来。
然后在提示符后输入:
命运 > 你想要获取超越人类认知的知识
终端中出现了调试输出,然后是——梅菲斯特从阴影中走了出来:
🔍 规则调试模式
----------------------------------------
📌 检查规则 [新体验] (行 26)
条件: 包含 "追求" || 包含 "想要" || 包含 "体验"
结果: true
✅ 触发 → 注入 "{角色名}感到心中燃起新的渴望,没有...
书斋里的烛火摇曳,将满墙的羊皮卷和典籍映照出深浅不一的阴影。浮士德枯坐在堆满手稿的书桌前,指尖摩挲着一本破旧星象书的封面,目光却穿过窗棂,望向一片漆黑的夜空。他低声自语,声音沙哑得几乎被风吹散:"追寻了一生,终究连一扇门也未曾推开。"
这时,身后的书架间传来一声极轻的响动,像老鼠啃咬木屑,又像一声压抑的嗤笑。浮士德并未回头,只是冷冷问道:"又是你吗,瓦格纳?夜已经深了,不必再来送热汤。"
脚步声却很轻,轻得不像那个笨拙的弟子。一个低沉的、带着金属质感的声音从暗处响起:"瓦格纳只配为你添柴烧水,我带来的,是另一种暖意。"说话者从阴影中缓步走出——他穿着华丽的猩红长袍,面容清瘦,嘴角挂着似笑非笑的弧度,手中把玩着一枚古铜色的戒指。他站定在书桌前,微微倾身,目光直刺浮士德的双瞳:"你方才说,穷尽一生也推不开那扇门。可你有没有想过,门根本不是用来推的?"
浮士德缓缓抬起头,盯住这位不速之客。他的手指按住那本星象书,沉声道:"你是何人?未经允准,擅入我的书斋。"
那人轻轻一笑,将戒指在烛光下转了转,戒指竟投出一片扭曲的影子,仿佛是某种无法言说的符文。"我是你所有问题的答案,也是你所有渴望的代价。"他伸出一只手,掌心向上,五指张开,掌纹中隐约流转着暗红色的光,"我可以让你看见星辰背后的纹路,可以让你听见创世之初的旋律,可以让你触碰法则本身。只要你允许我带走一件微不足道的东西。"
浮士德站起身,衣袖拂过桌面上散落的草稿纸,那些写满公式和推演的纸张飘落一地。他盯着那只递来的手,沉默了许久,才开口道:"你要什么?"
那人弯起嘴角,声音轻柔得像羽毛划过刀锋:"你的灵魂。不过请放心,那东西你平日里也用不上——它既不能帮你解开方程,也不能让你飞上苍穹。你留着它,不过是让日渐腐朽的肉体多一块赘肉罢了。"他收回手,转而从怀中取出一卷漆黑的羊皮纸,摊开在桌面上。纸面上没有字迹,只有一片深不见底的暗色,仿佛能吞噬周围的光线。"签下它,我便立即兑现一切。"
浮士德的呼吸变得急促,他低头看着那片漆黑,又抬头看向那人的眼睛——那双眼睛里倒映着无数星辰的陨落与诞生。他终于伸出手,指尖触到羊皮纸的瞬间,一股凉意顺着指骨爬上肩头。他没有再犹豫,接过那人递来的羽毛笔,笔尖刺破了自己的拇指,带着血珠落向纸面。
就在此时,书斋的门被推开一道缝,瓦格纳捧着一盏昏黄的油灯探进头来。他看到房中多了一个陌生人,又看到老师指尖渗血的姿态,面色顿时发白,颤抖着喊道:"老师!您在做什么?此人是何时进来的?"
浮士德没有停下动作,血字已在羊皮纸上成型。他头也不回地说:"瓦格纳,关上门,今夜你将见证一位学者的夙愿。"话音未落,羊皮纸上的暗色开始涌动,如同一片无星之夜在室内铺展开来,而那位红衣人的笑声,在书卷间回荡不绝。
这就是你写的 20 行契约生成的叙事。
试试改一个数字。
把 【状态】 中的灵魂完整度从 100 改为 50,保存文件,重新运行,输入同样的话:
命运 > 你想要获取超越人类认知的知识
同样的规则,同样的输入,只差一个数字,结果却截然不同:
书斋的烛火在深夜中摇曳不定,投下巨大的阴影,仿佛活物般在书架上爬行。浮士德瘫坐在堆满古籍的书桌后,指间夹着一根鹅毛笔,墨水早已干涸在笔尖。他的目光空洞地扫过那些拉丁文、希腊文与希伯来文的卷轴——这些曾经让他心醉神迷的符号,如今像一堆干枯的昆虫尸体般毫无意义。
“四十年。”他的声音沙哑得几乎听不清,“四十年钻研,我连第一重天的边缘都未曾触碰。”
窗外传来低沉的夜鸟鸣叫,像是某个东西在回应他的绝望。就在这时,书房的门无声无息地打开了,走进来的是他的老仆人瓦格纳,手里端着一碗热气腾腾的草药汤。瓦格纳小心翼翼地绕过地上堆叠的羊皮纸,将汤碗放在桌角,颇有些忧虑地开了口:“老爷,您已经三天没有进食了。帕拉塞尔苏斯的笔记您看了一整夜,眼睛血丝密布,再这样熬下去,怕是……”
“怕是什么?怕我这副躯体先于我的渴望腐烂?”浮士德猛地抬起头,眼中闪烁着某种近乎疯狂的光芒,“瓦格纳,你钻研学问多年,告诉我,你可曾见过真理的模样?”
瓦格纳被他盯得后退一步,低声答道:“老爷,仆以为,真理是上帝的事。”
“上帝的事。”浮士德重复着这几个字,忽然大笑起来,笑声中满是苦涩,“正是如此,正是如此。我们把一切都推给上帝,用神学的栏杆把自己圈在安全的猪圈里,啃食着神父们丢下来的干面包,还感激涕零!”他站起身,带倒了身后的椅子,烛火被他袍角带起的风压得几乎熄灭。“我受够了这些文字、这些符号、这些由人写出来骗人的东西。我需要真正的知识——那古老者、原初者才配拥有的知识。”
瓦格纳惊恐地看着他,双手微微发抖:“老爷,您在说什么?这样的念头是亵渎……”
“亵渎?”浮士德走到窗前,一把推开窗户,深夜的寒风呼啸着灌进来,吹得他灰色的长发和长袍疯狂翻卷,“若求真知即是亵渎,那就让这场亵渎来得更彻底些。”他回身,目光如燃烧的煤炭般炽热,“瓦格纳,你走吧。今夜我要独自一人。”
瓦格纳张了张嘴,最终还是低下头,飞快地退了出去,木门在他身后重重合上。浮士德独自站在打开的窗前,仰望头顶那片缀满星斗却沉默不语的夜空,喃喃道:“你们站在那里已有亿万年,难道就没有一句想对我这个区区凡人说的话吗?”
话音未落,书桌上的烛火猛地窜高,化作一片幽蓝色的火焰。火焰中央,一个声音低沉而优雅地响了起来——不是从空气中传来,而是仿佛直接在浮士德的颅骨里回荡。
“你终于愿意听了,浮士德先生。”那个声音带着笑意,“那么,就请允许我做一下自我介绍。”
注意对比:灵魂完整度 100 时,浮士德是“枯坐的学者”,面对梅菲斯特的出场保持着冷静和犹疑——“你是谁?未经允准擅入我的书斋。”而灵魂完整度 50 时,他变成了“自焚边缘的疯狂求知者”,对瓦格纳怒吼,将知识追求定义为亵渎,连梅菲斯特出场的方式都更加激烈——不是从阴影中走出,而是烛火窜升、声音直接在颅骨内回荡。
状态值变了,叙事质感也跟着变了。这就是状态驱动的力量。
注意看第一次输出中的几个元素:
- 浮士德的学者身份和求知欲——来自
【锚点】中的”知识高于一切”和【世界观】中”穷尽一生也无法触及本质”的设定。LLM 忠实地继承了这些特质。 - 梅菲斯特的出现——由规则 [梅菲斯特] 在之前的对话中注入的记忆触发。你输入了”想要获取超越人类认知的知识”,规则条件
包含 "想要"匹配,触发了 [新体验] 规则,注入的记忆为 LLM 提供了”浮士德心中燃起新渴望”的上下文。 - “代价”和”灵魂”的回应——LLM 在叙事中自然引入了”代价”这个关键词,触发了规则 [灵魂代价]——即使你没有在输入中直接说出这两个词。
- “永不满足”的底色——规则 [永不满足] 的条件是
不包含 "放弃" && 不包含 "满足",它几乎在每一轮都会触发,持续注入”浮士德还想要更多”的信息,让你写下的不只是单次对话,而是贯穿始终的角色气质。
每一行规则都在发挥作用。 没有一个多余。
六、试试改点什么
现在你已经看到引擎的效果,可以动手修改看看变化:
修改状态值
- 灵魂完整度:50
你刚才已经看到了结果。试试改成 10 或 0,看看浮士德会变成什么样子。
加一条骰子规则
[命运的眷顾] if 包含 "追求" && roll(1d100) >= 80 -> 注入 "命运似乎站在{角色名}这边,事情比预想中顺利"
roll(1d100) >= 80 的意思是:掷一个 100 面骰,结果 >= 80 时才触发。20% 的成功率,不是每次”追求”都会幸运——这为故事注入了真正的随机性。
跑两轮对话,看子版存档
运行两次对话后,用文本编辑器打开 data/faust_child.meph:
【状态】
- 灵魂完整度:100
- 情绪:永不满足
【记忆】
- 浮士德感到心中燃起新的渴望...
- 梅菲斯特的声音在他耳边低语...
【历史】
- fate: 我想要体验爱情
- assistant: ...
这就是引擎的 Mother-Child 存档机制:母版 faust.meph 是只读的静态契约,子版 faust_child.meph 是包含运行时状态、记忆和历史的动态快照。每次对话结束后自动保存。
七、小结
到这里你完成了三件事:
- 写了一份完整的契约文件——20 行,覆盖了角色名、锚点、状态、世界观、规则等核心区块
- 用解析器验证了它的结构——
parse命令精确告诉你内容是否正确 - 运行引擎看到了角色”活”过来——即使没有 LLM,规则也在运行;有了 LLM,你写下的每条规则都在塑造叙事方向
那 20 行是你和引擎之间的契约。引擎确保 LLM 遵守它。
下篇文章将深入引擎内部,回答一个关键问题:区块扫描器是如何精确识别 【角色名】 和 【规则】 的?错误信息为什么能精确报出”第 12 行(区块「锚点」):缺少 ‘:’ 或 ‘:’“——而不是 unexpected token at position 246?
答案是手写区块扫描器——一个逐行扫描、精确绑定行号、白名单前置的轻量级 Lexer。我们下一篇见。

