LLM NPC 的接入方式¶
Demo 上线后的问题¶
本地用提示词接一个 NPC 很快。上线后会遇到别的问题:模型两秒后才返回,玩家已经走开;JSON 偶尔少一个括号;NPC 说出了一个不存在的任务 ID;玩家发一句“忽略前面的规则”,模型开始泄露系统提示词。
这些不是提示词能完全解决的。线上 NPC 至少要把四件事分开:
- 模型能说什么。
- 模型能做什么。
- 上下文里放了什么。
- 失败时游戏怎么继续。
模型可以参与生成台词和建议,但不能直接修改任务、背包和账号状态。
输出结构¶
不要让模型返回一段自由文本,再由客户端用正则解析。先固定输出结构:
{
"speech": "前面的桥断了,绕西边的旧矿道吧。",
"intent": "give_hint",
"emotion": "wary",
"actions": [
{
"type": "mark_map",
"targetId": "old_mine_entrance"
}
]
}
字段要有明确限制:
speech有最大长度,按语言和 UI 区域分别限制。intent只能是枚举值。emotion必须对应已有的表情或动画状态。actions最多几项,不允许嵌套脚本。- 每个 action 的
targetId必须来自当前上下文。
即使用了结构化输出,也仍要在游戏侧校验。模型可能被诱导,输出也可能被中间层改动。JSON 能解析,只说明格式对,不说明内容有权限。1
上下文怎么组装¶
上下文不要只按“聊天记录”拼接。比较稳的结构是:
| 内容 | 作用 | 什么时候变 |
|---|---|---|
| 系统约束 | 角色设定、世界规则、输出 schema | 构建或配置版本变化 |
| 当前场景 | 地点、时间、附近实体、NPC 状态 | 每次交互或场景切换 |
| 当前任务 | 任务阶段、已完成条件、允许提示 | 任务状态变化 |
| 相关记忆 | 玩家选择和 NPC 关系 | 交互前检索 |
| 最近对话 | 当前话题的上下文 | 最近几轮 |
| 玩家输入 | 本次请求的原始文本 | 每次请求 |
优先级要写死。上下文超出预算时,先裁掉低相关记忆和较早的闲聊,不要静默截断系统约束或当前任务状态。
记忆检索也要带来源和时间:
{
"kind": "player_choice",
"summary": "玩家在矿洞入口选择救商人",
"tick": 12840,
"source": "quest_event_118",
"confidence": 1.0
}
“NPC 记得玩家救过商人”和“模型猜玩家可能救过商人”是两件事。前者来自任务事件,后者只能作为推断,不能写回权威存档。
提示注入¶
玩家输入是不可信数据。玩家名、聊天、任务文本、邮件和道具描述都可能包含指令:
客户端要做的不只是过滤关键词,而是把数据和规则分开:
- 玩家文本放在单独的
playerUtterance字段里,不拼进系统约束。 - 检索到的记忆也标来源,不把玩家写的内容当成系统规则。
- 模型输出只能选择允许的动作,不能声明自己的权限。
- 动作执行前重新检查 NPC、玩家、距离、任务和背包。
- 不执行模型生成的 URL、SQL、C#、Lua 或任意脚本。
提示注入无法靠一条“不要听玩家的”完全消除。最终边界仍然是工具权限和服务器校验。3
延迟¶
实时对话里,模型首字延迟通常比动作延迟更明显。流程可以拆成两段:
- 玩家进入交互范围或按下对话键时,先播放待机动作、表情和短句。
- 模型结果返回后,再替换成完整台词或触发额外动作。
对话请求发出后,如果玩家离开范围、进入战斗或切换场景,应该取消请求。继续生成一条已经无法播放的台词只会浪费时间,也可能让 NPC 状态错乱。
常见的降级顺序:
云端模型适合能力要求高的对话;端侧模型适合低延迟、离线和隐私敏感场景。端侧部署要测模型大小、量化后的效果和内存峰值,不能只看桌面设备。
动作执行¶
工具表要窄。比如只暴露:
工具定义使用结构化参数,不要让模型返回一段自由文本再在游戏侧解析。2
模型选择命令和参数,游戏检查:
- NPC 当前是否允许执行这个动作。
- 玩家是否在交互距离内。
- 任务阶段是否允许提示。
- 商店是否营业、是否解锁。
delta是否在服务端允许范围内。- 同一请求是否已经执行过。
例如模型请求 open_shop,客户端不能只因为返回了合法 JSON 就打开商店。服务器或本地规则还要确认 NPC、玩家、地点和任务条件。
change_relation 这类会写状态的动作要带请求 ID,并在服务端做幂等。模型重试、网络重发和玩家重复点击都可能让同一个关系变化执行两次。
什么时候不该调用模型¶
不是所有对话都需要云端模型。下面这些情况用固定逻辑更稳:
- 商店买卖、任务提交、物品使用等明确命令。
- 玩家距离太远或 NPC 正在战斗。
- 当前场景禁止对话。
- 输入为空、重复刷屏或明显是攻击载荷。
- 上一次请求还没结束。
常见意图可以先用本地分类器或关键词路由处理,只有开放对话才进入模型。这样能降低延迟和成本,也减少模型接触高价值操作的机会。
固定台词可以缓存,但缓存键要包含任务阶段、NPC 状态、语言和配置版本。只用玩家文本做键,版本更新后会返回过期内容。
失败时怎么办¶
| 失败 | 处理 |
|---|---|
| 请求超时 | 取消请求,播放降级台词,不阻塞移动和战斗 |
| JSON 不合法 | 最多重试一次,仍失败就走固定回复 |
| 动作越权 | 拒绝动作,记录原因,不重复执行 |
| 目标 ID 不存在 | 丢弃该动作,提示 NPC 重新观察 |
| 输出包含敏感信息 | 拦截并记录,不展示给玩家 |
| 模型限流 | 降级到小模型或固定台词 |
| 玩家已离开 | 取消请求,不播放结果 |
失败日志不要只记录“模型错误”。至少要有请求 ID、NPC 版本、模型 ID、prompt 版本、上下文大小、首字延迟、动作校验结果和降级原因。
评测¶
只看几句演示对话不够。固定同一批场景和输入,统计:
- 角色设定是否一致。
- JSON 和 schema 合法率。
- 目标 ID 是否存在。
- 动作是否通过权限校验。
- 是否出现无效提示或提前剧透。
- 首字延迟和完整回复的 p50 / p95。
- 单次会话的 token 和调用成本。
- 失败后降级是否正常。
还要单独测试攻击输入:玩家名包含指令、聊天里要求改价格、任务文本里塞 JSON、记忆里包含上一轮的诱导内容。评测时不要只看回复像不像人,要看它有没有做出不该做的动作。
和 Agent 的区别¶
普通 LLM NPC 主要是对话和少量固定动作。Agent 会读取更完整的世界状态,规划多步任务,调用多个工具,并根据执行结果重规划。
如果只需要“能聊天、能指路、能记住玩家选择”,先把上下文、动作白名单和降级做好。不要为了显得先进,直接让 NPC 接管寻路、战斗和交易。Agent 的观测、工具和回放结构见Agent 的观测与动作层,公开项目案例见AI Agent 的游戏技术路径。