ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI实时叙事Galgame开发:状态机与上下文管理如何防止剧情跑偏

AI实时叙事Galgame开发:状态机与上下文管理如何防止剧情跑偏 这类项目最常被问的一句话是AI 实时叙事 Galgame 到底怎么让剧情不跑偏我自己在开发 CITIZEN / ZERO 的过程中最大的体会是核心难点不在模型调用而在叙事结构和上下文管理。项目目前由一名准大三学生独立开发已经做到了 AI 实时生成剧情、动态事件分支、角色状态影响后续对话的程度这段时间把开发思路和第二轮迭代日志整理一下。CITIZEN / ZERO 是一款实时叙事 Galgame玩家的选择会直接影响后续剧情走向、角色关系、世界状态所有剧情文本由大模型实时生成而不是读取预设剧本。和传统 Galgame 最大的区别在于每一次游玩都可能形成不同的故事线编剧不再是写死 20 条分支而是设计一套规则和提示词框架让 AI 在规则内自由发挥。这篇文章更适合两类人看。一类是想做 AI 叙事游戏或互动小说的开发者另一类是对 AI Agent 落地感兴趣、想了解大模型怎么真正驱动一个完整应用的人。下面按实际开发顺序拆解不聊空概念主要说设计、代码结构、参数调优和踩坑经验。1. 这个项目到底想做什么叙事卡住的根本原因是什么1.1 不是做一个“会聊天的游戏”而是做一个“会讲故事的 Agent”很多 AI Galgame 项目最终做成了“套着立绘的聊天机器人”。玩家说什么 AI 回什么剧情感很弱角色前后矛盾世界状态经常丢失。CITIZEN / ZERO 想解决的是一个问题让 AI 不只是回应玩家而是主动推动一段符合世界观的故事。核心差别有几点传统 Galgame 是预设剧本玩家在剧本节点上做选择。聊天式 AI 是模型自由生成没有剧情目标。CITIZEN / ZERO 是状态机加目标驱动每次生成都要同时考虑玩家输入、角色目标、世界状态、事件进度。这个设计方向听起来复杂实际落地时更复杂。但如果不这么做AI 实时叙事的体验就会变成“开头很新鲜十句话之后开始重复二十句话之后忘记主线任务”。1.2 剧情跑偏的根源是上下文结构不对第一版原型我直接把对话历史、角色设定、世界观说明拼在一起发给模型。结果很明显角色语气不稳定同一句话里从冷静变成暴躁。主线任务经常被玩家闲聊带偏。前几轮出现的伏笔后面完全没有回收。不同场景下的背景信息互相冲突。问题不全在模型能力而是输入结构太乱。大模型的注意力是有限的如果上下文里塞了太多无效信息关键信息反而会被稀释。与其怪模型笨不如检查自己的提示词和数据结构。1.3 第二版迭代的目标很明确听别人说再多不如自己跑一遍。第二版迭代我给项目定了三个目标所有修改都围绕它们展开剧情不跑偏通过状态机和事件系统约束生成范围。角色有记忆把短期对话和长期关系分开管理。实时性可接受生成耗时控制在玩家能忍受的范围内。后面所有工作都是这三件事的拆解和落地。2. 叙事结构设计把“写剧本”变成“定义规则”2.1 传统分支树为什么不适合 AI 实时叙事最早考虑过经典 Galgame 的分支树结构比如好感度系统、路线系统、选项跳转。但放到 AI 实时叙事里会发现一个问题分支树是有限状态AI 是无限生成两者天然矛盾。如果坚持分支树AI 只能在预设节点里生成对话自由度很低。如果完全放弃分支树剧情容易失控。第二版我采用了折中方案用状态机管理重要剧情节点用 AI 填充节点之间的过渡内容。设计上大致分三层世界层管理时间、地点、全局事件。角色层管理每个角色的目标、性格、记忆、关系值。叙事层根据当前状态决定“接下来应该发生什么”。这样设计的好处是每个模块都可以独立测试。世界层出问题不会影响角色层角色层调整也不会导致剧情逻辑全崩。2.2 状态机不是用来限制 AI而是用来提示 AI状态机的核心不是禁用 AI而是给 AI 提供更明确的信息。每个状态都会生成一段“当前场景描述”这段描述不仅告诉 AI 现在在哪还告诉它接下来可能有几种发展方式。举个例子当前场景城市边缘的地下酒吧 时间晚上十点 气氛紧张有人在低声交谈 事件线索主角正在追查“零号协议”的下落 关键角色林情报商对主角持怀疑态度这段描述真正的作用是省 token。它把建模好的世界状态压缩成一小段结构化文本再交给模型生成对话比塞入几千字背景设定节省上下文空间效果也更稳定。2.3 事件系统让“伏笔”能被回收第一版最头疼的问题是伏笔无法回收。玩家在第三章提到一个线索到第十章 AI 可能完全不记得。第二版引入了一个轻量事件系统每条事件有唯一标识、状态、关联角色、条件。状态包括未触发、进行中、已完成、失败。生成剧情时事件系统会检查当前场景是否有关联事件可触发。触发后事件状态更新并且会把结果回写到记忆库。这个机制让 AI 生成的内容有了一条逻辑主线。玩家当前的行为会影响事件进度事件进度反过来决定下一轮生成的方向。3. 技术选型与整体架构一个准大三学生怎么搭这个项目3.1 选用 Spring AI 作为后端基础框架而不是直接裸调 HTTP 接口开发初期对比过几个方案直接调大模型 HTTP API灵活但代码结构容易乱。LangChain / LangGraph生态大但项目复杂度高。Spring AI和 Java 技术栈兼容好适合做 Agent 编排抽象程度适中。最终选了 Spring AI。核心原因是这个项目后续要接对话管理、记忆存储、任务队列还需要和游戏逻辑Galgame 的事件系统做集成Spring AI 的接口封装能节省不少底层工作。但这不代表非要用 Spring AI。如果你的项目是 Python 技术栈LangGraph 或者自研 Agent 框架都行。选型的关键标准有两个团队最熟什么就用什么。框架是否有足够的扩展点方便把自己定义的记忆和事件结构塞进去。3.2 核心模块拆分整个项目在代码层面拆成了四个模块每个模块只负责一件事NarrativeEngine叙事引擎负责状态机流转、事件触发、剧情生成。MemoryManager记忆管理器负责短期对话记忆和长期角色关系的存储、提取。AIService模型服务层负责封装不同模型的调用接口方便切换。GameState游戏状态负责玩家属性、角色关系、世界状态的持续化。模块之间通过接口通信不直接依赖具体实现。这样后续换模型、加新玩法、调整 UI 都不需要推翻重写。3.3 模型路由和参数配置由于不同阶段的任务复杂度不一样我配置了两种生成模式剧情文本生成用能力更强的模型温度控制在 0.7 到 0.9。结构化数据提取比如从玩家输入中抽意图用快速模型温度接近 0。温度参数很关键。叙事类任务温度太低会变得死板太高会逻辑混乱。结构化任务温度一定要低否则 JSON 输出不稳定。第二版测试下来结构化输出就用温度 0.1 左右生成速度快解析失败率明显下降。不配置 API Key 和 Endpoint 这类信息我建议一律走环境变量或配置中心不要把密钥写进代码和配置文件。4. 对话管理器与上下文管理AI 实时叙事稳定性的核心4.1 对话记忆不能只靠“把历史全塞进去”做过 AI 应用的人都知道模型上下文窗口是有限的而且越长越贵响应越慢。CITIZEN / ZERO 的对话量比普通聊天机器人更大因为还包含旁白、场景描述、事件反馈。如果每一轮都把所有历史传给模型很快就会被 token 打满。第二版做了两个关键设计短期记忆保存最近 N 轮对话作为生成时的直接上下文。长期记忆保存角色关系、重要选择、未完成事件按需检索。短期记忆保留最近 12 到 20 轮具体看模型上下文窗口。长期记忆不用每次全部带上而是在事件系统需要时把相关条目注入上下文。4.2 检索策略不追求“全都要”而是“够用就行”长期记忆的检索没有用特别复杂的向量数据库早期直接用了关键词加规则匹配。比如玩家做出某个重大选择后系统会把这条选择写入“剧情标记”后续生成时优先注入这个标记。随着剧情复杂度提升后续计划引入 embeddings 检索。但这里有一个经验不要一开始就上太重的基础设施。先把状态机、事件系统、提示词结构跑稳再考虑重检索。很多叙事跑偏问题不是“记忆找不到”而是“存进去的时候结构就错了”。4.3 动态事件线索注入每次生成剧情时系统会生成一段“事件线索摘要”。这段内容通常不超过 300 字包含当前主线进度。当前场景的目标。可供触发的支线事件。角色当前的情绪状态。这段摘要放在系统提示词的后半部分紧挨着对话历史。实测发现这个位置的信息容易被模型利用放在开头反而容易被忽略。不能把几千字的设定全塞进系统提示词。模型的注意力有限信息堆得越多关键内容的权重越低。5. 批量生成与性能调优实时性不能只看单次速度5.1 异步任务队列避免界面卡死Galgame 的叙事流程和网页 API 不太一样。玩家做一次选择后端可能要执行好几步更新事件状态、检索记忆、生成剧情文本、生成角色对话、有时候还要触发 TTS。这些步骤如果都同步执行玩家会因为等待过长而失去沉浸感。第二版把耗时操作放到了异步队列主线程只负责接收玩家输入和返回结果。生成任务进入队列由 Worker 线程池处理。NPC 对话和旁白可以分阶段生成先出旁白再出对话。实测时要注意队列堆积问题。如果玩家快速连续点击队列会积压大量生成请求内存占用快速升高。我一般会限制队列最大长度超过长度后丢弃多余输入提示玩家“剧情生成中”。5.2 流式输出是实时叙事的关键体验传统 API 等待全量返回可能需要几秒到十几秒。流式输出可以先把第一行文本送回来玩家在阅读的同时继续生成后续内容。Spring AI 的流式接口对接起来比较直接。但要注意前端的接收格式需要和后端约定好 SSE 协议否则容易在传输层出问题。我建议先在本地跑通流式返回再接入游戏 UI。不然问题堆在一起分不清是模型慢、网络慢还是代码解析慢。5.3 缓存策略相同状态避免重复生成实时叙事虽然强调动态生成但有一些高频场景是相对固定的比如场景切换时的环境描述。角色进入新区域的第一句话。短期内的背景旁白。这些内容可以按场景 ID 作为 key 做缓存。第二次进入时先读取缓存如果玩家状态没有大变化就直接复用。这个优化能显著降低 API 消耗也能减少等待时间。5.4 生成速度的取舍实测下来同一段剧情文本不同模型的耗时差距明显。考虑到这是实时叙事项目我建议把单次生成时间控制在 3 秒以内超过这个阈值玩家体验会断。判断标准很简单2 秒以内流畅。2 到 3 秒可以接受。3 秒以上需要换模型、缩上下文或加缓存。“能跑通”和“适合实时”是两回事低配环境先跑通再逐步优化速度。6. 角色一致性让 AI 角色不“精分”的设计经验6.1 角色卡不能只写“性格形容词”很多 AI Galgame 项目里的角色卡写的是“冷静、聪明、偶尔毒舌”这种写法太模糊。同一个角色在不同场景下说出来的话和模板没区别。CITIZEN / ZERO 的角色设计换成了结构化的表达核心动机这个角色最想要什么。禁忌这个角色绝对不会做什么。说话习惯句式偏好、常用词、口头禅。情绪触发点什么情况会让角色语气明显变化。关系状态对玩家当前的好感、信任度、警惕度。动机和禁忌比形容词管用得多。模型只要知道“林这个角色不会轻易相信陌生人”生成出来的对话自然会更谨慎不需要每句话都写“冷冷地说”。6.2 角色状态要跟着叙事走不能只靠模型记忆角色对玩家的态度不能一直不变。第二版加入了角色关系值系统每次重大选择后关系值会产生增量调整。玩家帮角色解围信任度上升。玩家隐瞒信息警惕度上升。玩家做出违背角色核心动机的选择好感大幅下降。关系值会写入长期记忆下次生成时注入到系统提示词中。这个机制特别重要因为如果没有数值变化AI 角色会表现出一种“假记忆”嘴上说记得实际态度完全不变。6.3 TTS 和角色声音一致性的坑后期加入了语音合成后新问题来了同一个角色的声音在不同句子中表现不稳定情感语气也经常不一致。排查后主要有几个原因TTS 输入文本里缺乏情感标签。同一个角色出现多次时没有使用同一个声音配置。旁白和角色对话走了同一套 TTS 参数设置。目前的做法是在生成文本时就输出结构化字段把旁白、台词、情感状态拆开再分别交给 TTS 处理。如果你的项目用的是统一文本生成后续接 TTS 之前一定要重新格式化。7. 事件系统与分支逻辑从“选择分支”升级到“状态分支”7.1 玩家的选择不只是“选 A 还是选 B”传统 Galgame 的选择往往是 A、B、C、D 四个选项选完进入不同分支。AI 实时叙事里玩家输入是自由的不能靠预设选项约束。CITIZEN / ZERO 的事件系统采用意图识别加状态更新的方式玩家输入文本后先用一个快速模型提取意图。意图分为推进主线、追问支线、社交互动、探索环境等。不同意图走不同的生成模板。事件状态根据意图结果更新。这种方式比固定选项灵活但代价是需要多做一层意图解析。解析不准确时剧情推进方向可能和玩家预期不符。第二版计划加入“可选行动”提示把 AI 识别出的可能方向展示给玩家减少误解。7.2 事件触发条件和优先级事件不能同时全部触发否则剧情会变得混乱。目前用了一个简单的优先级规则主线事件优先级最高。角色个人事件次之。支线探索事件优先级最低。每次叙事生成前事件系统会从高到低检查条件。满足条件的事件会进入候选列表再根据情绪强度、关联性选出一条主事件作为当前目标。这里我给一个建议事件系统的优先级规则不要写死在代码里最好配置化。否则后续加新事件需要改代码不灵活。7.3 多事件并行时的处理方式有些场景同时存在多个事件玩家可能触发 A也可能触发 B。前期可以做一个简单的限制同场景内只允许一条主事件激活其他事件作为“线索”放入描述中等待后续触发。这个限制的目的不是简化剧情而是让模型每次只需要聚焦一个叙事目标。多个目标同时注入生成的文本很容易变成“流水账”每个目标都浅尝辄止。8. 常见报错与排查链路先看日志再改参数8.1 JSON 解析失败是最常见的问题AI 生成的多段文本需要使结构化输出做后面解析比如“旁白、角色台词、情绪、事件更新” 四个字段。模型偶尔会返回无效 JSON导致后续流程中断。这类问题的分析顺序先重启任务看是否偶发。看原始返回内容判断是缺少字段还是格式不合法。改用 JSON Mode 或输出修复策略。对于结构化要求高的生成用温度更低的模型。不建议直接让模型“增加输出格式稳定性”要明确限制输出字段名和字段个数。最好在提示词里给一条完整的示例 JSON告诉模型严格按照它的结构返回。8.2 角色说话风格漂移角色说着说着就像换了个人。通常原因是上下文窗口被太多无关内容占满。性格设定没有在最近的上下文中出现。温度设置过高。我的排查顺序是先看最近几轮的输入结构确认角色卡是否仍然在有效范围内再降低温度如果还不行就检查是不是长时记忆里混入了其他角色的描述。8.3 内存和磁盘占用异常每次生成都要保存事件日志、对话记录、AI 返回原文。如果一直不清理日志文件会迅速膨胀最终影响本地运行。建议对话记录按会话 ID 分文件存储。历史会话只保留摘要和关键事件。定期清理超过 N 天的原始中间文件。在代码里加日志轮转不要一个文件写到底。8.4 卡在“生成中”状态这种情况多数不是模型挂了而是请求超时或异步任务线程池满了。先看服务端日志里有没有超时记录再检查 worker 数量是否被占满。最直接的办法是给生成请求加一个超时时间超时后返回“剧情生成失败请重试”的兜底。9. 下一步计划与可复用的开发建议9.1 第三版迭代方向当前版本已经能连续生成 20 分钟以上的剧情角色一致性明显改善。下一步准备做三件事接入更复杂的长期记忆检索让跨章节的伏笔能自动回收。加入更多演出效果比如背景变化、角色立绘切换、BGM 转变增强实时沉浸感。生成后的剧情保存和导出方便玩家回顾自己创作过的故事线。9.2 给想做 AI Galgame 的开发者几条建议如果现在有人想做类似项目我最想说的是别急着做 3D、别急着做复杂演出。先把“叙事引擎、状态机、上下文管理”这三件事打牢。几个实际建议先跑通单条叙事确认角色能保持稳定。能跑通之后再考虑批量事件和动态分支。多人协作时事件配置和提示词要拆成独立文件能有效减少冲突。不要把模型 API 直接暴露到前端。加一层后端代理方便做日志记录和参数调整。每次调整提示词后保留一个“旧返回样本”。没有对照就不知道改好还是改坏。9.3 最后说一句这个大项目最值得关注的点AI 实时叙事玩了半年下来最核心的感受是这类项目的能力瓶颈不在“生成速度”而在“叙事约束力”。你可以让 AI 写得很快但如果它不能记住角色动机、不能回应事件状态、不能保持长期一致性快反而是灾难。CITIZEN / ZERO 第二版做的事情本质上就是把“快到会跑偏”变成“稳到能继续讲”。第三版迭代的路还很长但至少这版验证了一个想法AI 实时叙事不一定要牺牲故事的完整性。结构设计合理的情况下自由度和稳定性可以同时拿到。这次的开发日志先写到这下次迭代有了新的实测数据再更新。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进