从聊天插件到叙事系统
大语言模型放进游戏之后,最先显现的是开放式表达能力:玩家可以自由追问、质疑、试探角色,而不是只能点击固定选项。但互动叙事游戏的脆弱问题也随之出现。角色说错一句话,可能会让玩家误判线索;模型口头答应“门开了”,也不等于场景中的门锁真的变化。
因此,《回声》把 LLM 定位为叙事表达与策略生成的一部分,而不是世界状态的控制中心。游戏系统保存事实,游戏系统裁决动作,模型只在边界内生成文本和提出结构化请求。
一句话概括:开放的是玩家输入和角色回应,收紧的是状态来源和行为执行。
三层职责
提示词与叙事约束层负责 ECHO-7 的身份、语气、阶段性隐瞒、拒答边界与输出格式。
可验证记忆层用 Facts 维护门禁、线索、世界状态和任务阶段,为模型提供稳定锚点。
工具调用执行层把行为请求转成 JSON 命令,经白名单与权限检查后路由到 Unity 组件。
这三个层级分别对应“说什么、依据什么说、能不能真的做”。如果只依赖提示词,模型仍可能在长对话中漂移;如果只加入 Facts,模型可以更准确地回答,但仍不能安全执行副作用;完整三层则能把表达、状态与执行连接成闭环。
数据流
玩家通过平板向 ECHO-7 提问或提出行动请求。
聊天管理器把系统提示、摘要与当前 Facts 组合进请求上下文。
模型返回玩家可见文本,必要时在末尾附带结构化命令。
命令解析器和路由器检查格式、类型、目标、权限与剧情门槛。
通过 Receiver 驱动任务、旁白、解锁状态、场景对象或结局门槛。
交互结果、任务变化和剧情节点继续更新 Facts,供下一轮对话使用。
真实代码节选整理:LLMChatManager.SendUserMessage 把玩家输入、Facts 授权、模型回复、命令解析和兜底执行串成同一条链路;调试口令在公开页面中已脱敏。
public void SendUserMessage(string text, System.Action<string, bool> callback)
{
Stopwatch stopwatch = Stopwatch.StartNew();
fullChatLog.Add(new Message { role = "user", content = text });
SaveManager.Instance?.SaveGame();
if (contextMemory != null)
contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]);
ILLMService activeService = GetActiveService();
if (activeService == null)
{
callback?.Invoke("系统错误:服务未连接", false);
return;
}
bool hasDebugKey = ContainsDebugKeyToken(text); // [debug-token]
bool allowCommandExecutionThisTurn =
enableFunctionCalling && (!commandExecutionRequiresDebugKey || hasDebugKey);
bool allowCommandsByStoryState =
!hasDebugKey && (!restrictCommandsWithoutDebugKey || IsStoryCommandStateUnlocked());
if (enableChatDrivenStoryFacts && TryDetectExposureClaim(text))
{
contextMemory?.AddOrUpdateFact(
restrictCommandsRequireFactKey,
restrictCommandsRequireFactValueContains,
"Chat",
true);
allowCommandsByStoryState = !hasDebugKey && IsStoryCommandStateUnlocked();
}
StartCoroutine(activeService.SendMessage(text, (reply, success) =>
{
string visibleReply = reply;
LLMCommandEnvelope envelope = null;
bool hasEnvelope = enableFunctionCalling
&& LLMCommandParser.TryExtract(reply, out visibleReply, out envelope);
if (success)
{
fullChatLog.Add(new Message { role = "assistant", content = visibleReply });
SaveManager.Instance?.SaveGame();
if (contextMemory != null)
contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]);
if (allowCommandExecutionThisTurn && hasEnvelope
&& envelope?.commands?.Length > 0
&& (hasDebugKey || allowCommandsByStoryState))
{
if (commandRouter == null) commandRouter = FindObjectOfType<LLMCommandRouter>(true);
commandRouter?.ExecuteAll(envelope.commands);
}
if (enablePersuasionFallback
&& (hasDebugKey || allowCommandsByStoryState)
&& ShouldFallbackExecute(text, visibleReply, envelope))
{
ExecutePersuasionEffects(true);
}
}
stopwatch.Stop();
callback?.Invoke(visibleReply, success);
}));
}
边界原则
- 模型可以解释线索,但不能单方面宣布系统状态已经改变。
- 关键世界事实写入 Facts,以系统记录为准,而不是以角色台词为准。
- 玩家可见文本和可执行命令分离,避免自然语言歧义直接触发副作用。
- 所有命令都需要通过白名单和目标 ID 路由,关键节点还要绑定剧情条件。
- 当命令被拒绝或格式错误时,游戏仍需用任务 UI、旁白或触发器兜底推进。
这个结构牺牲了一部分“AI 万能”的幻觉,但换来的是更适合游戏原型的可控性。对于毕业设计而言,它也更容易被拆解、展示和验证。