ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM进入游戏主循环:收权与放权的架构设计与实操

LLM进入游戏主循环:收权与放权的架构设计与实操 1. 从“收权”到“放权”LLM 进入游戏主循环到底意味着什么第一次把大语言模型塞进游戏主循环的时候我踩的坑比想象中多得多。那时候我的想法很朴素既然 LLM 能理解自然语言、能做推理、能调用工具那让它直接决定 NPC 下一步干什么不就完事了吗结果跑起来才发现模型确实很聪明但它聪明得“不受控”——它会生成一个游戏里根本不存在的动作会在战斗中途突然开始吟诗会在需要严格回合制的场景里一次性输出五个动作。那一刻我才真正理解把 LLM 放进游戏玩法核心矛盾从来不是“模型够不够强”而是“控制权怎么分配”。这个项目标题里的“收权”和“放权”说的就是这件事。收权指的是把 LLM 的输出严格约束在游戏规则允许的范围内让它只能从合法动作集合里选放权指的是在规则框架内给模型足够的自由度让它去组合、去推理、去产生人类设计师没写死的玩法。这两者不是对立的而是一个动态调节的旋钮。旋得太紧LLM 退化成随机数生成器玩法毫无惊喜旋得太松游戏逻辑直接崩坏玩家体验一地鸡毛。我写这篇东西是想把这套“收放”机制讲透。适合谁来读如果你正在做 LLM 驱动的 NPC、AI 队友、动态剧情生成或者任何需要模型参与游戏决策的场景这里面的思路和代码结构你大概率能直接抄。如果你只是好奇“大模型打游戏”是怎么回事也能从里面看到真实工程里那些不浪漫但关键的细节。核心关键词我会反复提到LLM、游戏玩法、主循环、工具调用、合法动作掩码这五个词基本构成了整个系统的骨架。先说结论性的判断LLM 进入游戏玩法最稳的架构不是“让模型直接控制游戏”而是“让模型在一个受约束的动作空间里做选择由游戏主循环负责执行和校验”。模型负责“想”主循环负责“做”和“管”。这个分工听起来简单但落地时每一步都有讲究。2. 整体架构设计主循环、工具调用与动作掩码怎么配合2.1 为什么不能让 LLM 直接写游戏状态我见过不少早期尝试思路是让 LLM 输出一段类似“把玩家血量减 10然后移动到坐标 (3,4)”的指令游戏直接解析执行。这种做法在 demo 阶段很爽一旦进入真实玩法就会暴露三个致命问题。第一状态一致性无法保证。模型不知道当前游戏的真实状态它只能基于你喂给它的上下文做推断。上下文一旦有延迟或者遗漏它就会基于错误前提做决策。比如玩家其实已经死了模型还在规划下一步移动。第二动作空间是开放的。自然语言可以表达无穷多种动作但游戏引擎只认识有限的那几十个 API。让模型自由生成等于让它不断试探引擎的边界大部分输出都是无效的。第三无法做回合制或实时制的节奏控制。游戏主循环有自己的 tick 节奏模型推理有延迟两者直接耦合会导致卡顿或者逻辑错乱。所以我的方案是LLM 不直接写状态只输出“意图”由主循环翻译成合法动作并执行。这就是“收权”的第一层含义——把状态修改权收回到引擎手里。2.2 主循环里 LLM 应该待在哪个位置一个典型的游戏主循环大概是这样的输入处理 → 游戏逻辑更新 → 渲染 → 下一帧。LLM 应该插在“游戏逻辑更新”这一层但不是在每一帧都调用。我的做法是给 LLM 决策单独开一个“决策 tick”比如每 0.5 秒或者每个回合触发一次而不是每帧都问模型。这样做的好处是模型推理的延迟被隔离在决策层不会阻塞渲染和输入。玩家看到的是流畅的画面而 NPC 的“思考”在后台异步进行。等模型返回结果后主循环在下一个决策 tick 把它应用上去。具体到代码结构我会维护一个DecisionQueueLLM 的调用是异步的返回的意图先入队主循环在合适的时机出队并执行。这样即使模型响应慢游戏也不会卡。2.3 工具调用让模型“能做事”但“不能乱做事”工具调用Tool Calling / Function Calling是放权的关键机制。与其让模型输出一段自由文本再去解析不如直接给它一组定义好的工具让它选择调用哪个、传什么参数。比如{ name: move_to, description: 移动到指定坐标, parameters: { x: {type: integer, minimum: 0, maximum: 99}, y: {type: integer, minimum: 0, maximum: 99} } }模型返回的就不是“我想往右走”而是结构化的{name: move_to, arguments: {x: 3, y: 4}}。这一步把自然语言的模糊性消掉了大半。但工具调用本身还不够。模型仍然可能调用一个当前情境下不合法的工具比如在没有蓝量的时候调用cast_spell或者在已经被眩晕的状态下调用move_to。这就引出了下一个核心机制合法动作掩码。2.4 合法动作掩码收权的最后一道闸门合法动作掩码Legal Action Mask这个概念在强化学习里很常见但用在 LLM 游戏玩法里同样关键。它的逻辑是在每一次决策前游戏引擎根据当前状态计算出一个“当前允许的动作集合”然后把这个集合作为约束传给模型。实现上有两种粒度。粗粒度是只告诉模型“你现在能做哪些类别的动作”比如[move, attack, talk]。细粒度是连参数范围都限制好比如move只能选相邻格子attack只能选攻击范围内的目标。我实测下来细粒度掩码的效果明显更好因为模型不需要去猜哪些参数合法它只需要在给定选项里挑。代价是每次决策前要跑一遍掩码计算但这个计算量对现代游戏引擎来说可以忽略不计。把这三层叠起来——主循环隔离延迟、工具调用结构化输出、动作掩码约束范围——就构成了“收权”的完整链条。模型在这个框架里自由度被限制在“选择”而不是“创造”上。但别急放权的部分马上就来。3. 核心细节解析从 Prompt 设计到动作校验的实操要点3.1 Prompt 里必须写清楚的三件事给游戏里的 LLM 写 Prompt和写聊天机器人的 Prompt 完全是两码事。聊天场景下你可以让模型自由发挥游戏场景下你必须把边界画死。我的经验是一个合格的游戏决策 Prompt 必须包含三块内容。第一块是当前状态摘要。不要把所有游戏状态都塞进去那样 token 爆炸且模型抓不住重点。只给和当前决策相关的位置、血量、附近敌人、可用资源、当前目标。我一般会写一个summarize_state()函数把原始状态压缩成一段 100 到 200 字的自然语言描述。第二块是可用动作列表。这就是动作掩码的输出直接以工具定义的形式给模型。注意这里要给的是“当前合法”的动作不是全部动作。模型看到的就是它现在能选的。第三块是决策约束和输出格式。明确告诉模型你只能从给定动作里选一个必须用工具调用格式返回不要输出解释性文字。我甚至会在 Prompt 里加一句“如果你不确定选择 wait 动作”给模型一个安全的兜底选项。3.2 动作校验永远不要相信模型的输出即使你做了动作掩码也永远要在执行前再做一次校验。这不是不信任模型而是工程上的防御性编程。模型可能因为上下文污染、采样随机性或者 API 返回格式问题输出一个掩码之外的动。我的校验层分三步。第一步检查动作名是否在合法集合里第二步检查参数是否在允许范围内第三步检查执行前提是否满足比如目标是否还存在、资源是否还够。任何一步失败就丢弃这个动作让 NPC 执行默认的wait同时记录一条日志。这个日志很重要。我靠它发现过好几次模型“试图作弊”的情况——比如在掩码没更新及时的时候模型会尝试调用一个上一帧还合法但现在已经非法的动作。有了校验层这些都被拦下来了。3.3 上下文管理别让历史对话撑爆窗口游戏是持续运行的如果每一轮决策都把之前所有决策历史塞进上下文token 消耗会飞速增长而且模型会被久远的历史干扰。我的做法是只保留最近 N 轮决策加上一个“长期记忆摘要”。长期记忆摘要可以定期由模型自己生成比如每 20 轮让它总结一下“这段时间发生了什么、当前目标是什么”。这个摘要比原始历史短得多但保留了关键信息。实测下来这种“滑动窗口 周期摘要”的组合在保持决策连贯性的同时把 token 消耗控制在了可接受范围。3.4 温度参数和采样策略的取舍游戏决策场景下我一般把温度设在 0.3 到 0.7 之间。太低比如 0会让 NPC 行为极其可预测玩几次就腻了太高比如 1.0 以上会让 NPC 做出莫名其妙的选择破坏玩法体验。对于战斗这种需要精确决策的场景我会用低温度甚至贪心解码对于对话、探索这类需要多样性的场景温度可以调高一些。如果框架支持还可以用 top-p 采样配合温度进一步控制输出的稳定性。提示不同游戏类型对温度的要求差异很大。回合制策略游戏可以容忍较高温度因为玩家有充足时间理解 NPC 行为实时动作游戏则建议低温度避免 NPC 做出让玩家措手不及的怪动作。4. 实操过程搭一个最小可用的 LLM 游戏决策循环4.1 环境准备与依赖选型我用的技术栈比较朴素Python 做游戏逻辑和 LLM 调用一个简单的 2D 网格世界做测试环境。LLM 侧我用的是支持工具调用的模型 API本地部署的话可以用带 function calling 能力的开源模型。关键依赖就几个openai或兼容的客户端库、pydantic做参数校验、asyncio做异步调用。游戏引擎我用的是自己写的一个极简网格世界大概 200 行代码方便快速迭代。选这套的原因很简单决策逻辑和游戏逻辑要解耦。我不想在验证 LLM 玩法的时候还要处理复杂的引擎 API所以先用最小环境把机制跑通再往真实项目里移植。4.2 定义动作空间和工具 schema先定义游戏里 NPC 能做的动作。我的测试环境里就五个动作移动、攻击、拾取、对话、等待。每个动作对应一个工具定义。TOOLS [ { type: function, function: { name: move, description: 向指定方向移动一格, parameters: { type: object, properties: { direction: { type: string, enum: [up, down, left, right] } }, required: [direction] } } }, { type: function, function: { name: attack, description: 攻击指定目标, parameters: { type: object, properties: { target_id: {type: string} }, required: [target_id] } } }, # ... 其他动作 ]注意direction用了enum这就是最细粒度的掩码——模型只能从四个方向里选。target_id没法用 enum 写死因为目标是动态的所以要在运行时根据当前状态动态生成合法目标列表写进 Prompt 里。4.3 计算合法动作掩码每一轮决策前先根据当前状态算出哪些动作合法、哪些参数可选。def compute_legal_actions(state, npc_id): npc state.npcs[npc_id] legal [] # 移动检查四个方向是否可走 for direction in [up, down, left, right]: if can_move(state, npc, direction): legal.append((move, {direction: direction})) # 攻击检查攻击范围内是否有敌人 for enemy in get_enemies_in_range(state, npc): legal.append((attack, {target_id: enemy.id})) # 拾取检查当前格子是否有物品 if state.grid[npc.x][npc.y].item: legal.append((pickup, {item_id: state.grid[npc.x][npc.y].item.id})) # 等待永远合法 legal.append((wait, {})) return legal这个函数返回的就是当前所有合法的“动作 参数”组合。接下来把它格式化成 Prompt 的一部分。4.4 构造 Prompt 并调用模型Prompt 的构造我分成系统提示和用户消息两部分。系统提示定义角色和规则用户消息给当前状态和合法动作。def build_prompt(state, npc_id, legal_actions): npc state.npcs[npc_id] system_prompt 你是一个游戏中的NPC决策器。 你只能从给定的合法动作列表中选择一个动作。 必须使用工具调用格式返回不要输出任何解释文字。 如果无法决定选择 wait。 state_summary f当前状态 - 你的位置({npc.x}, {npc.y}) - 你的血量{npc.hp}/{npc.max_hp} - 附近敌人{format_enemies(state, npc)} - 当前目标{npc.goal} - 最近事件{npc.recent_events[-3:]} legal_desc 当前合法动作\n for action, params in legal_actions: legal_desc f- {action}: {params}\n user_message state_summary \n legal_desc return system_prompt, user_message调用模型时把TOOLS作为工具定义传进去模型就会返回结构化的工具调用结果。4.5 执行与校验拿到模型返回后先校验再执行。def execute_decision(state, npc_id, decision, legal_actions): action_name decision[name] args decision[arguments] # 校验动作是否在合法集合里 if (action_name, args) not in legal_actions: log_warning(f非法动作被拦截: {action_name} {args}) return execute_wait(state, npc_id) # 执行 if action_name move: return execute_move(state, npc_id, args[direction]) elif action_name attack: return execute_attack(state, npc_id, args[target_id]) # ...这套流程跑下来一个完整的决策循环就成型了。我实测在网格世界里NPC 的行为既不会越界又有一定的策略性——它会优先攻击血量低的敌人会在血量低时往后退这些都是模型自己推理出来的我没有写死任何策略。4.6 放权的体现让模型自己组合策略前面说的都是“收权”那“放权”体现在哪体现在模型可以在合法动作空间里自由组合出设计师没预设的策略。举个例子我在测试环境里放了一个“陷阱”机制某个格子踩上去会掉血。我没有在 Prompt 里告诉模型陷阱的存在但模型通过观察“某个方向移动后血量下降”的历史事件自己学会了避开那个格子。这就是放权的价值——模型基于规则内的动作推理出了规则外的策略。另一个例子是模型会“拉扯”它发现攻击后如果立刻后退敌人追不上来于是形成了“打一下退一步”的循环。这个策略我完全没写是模型在多次决策中自己涌现出来的。5. 常见问题与排查技巧实录5.1 模型不调用工具只输出文本怎么办这是最常见的问题。模型有时候会忽略工具定义直接输出一段自然语言比如“我应该往右走”。解决办法有三个层次。第一在系统提示里用强指令明确说“必须使用工具调用禁止输出文本”。第二如果模型仍然不听话可以在 API 调用时设置tool_choicerequired强制它必须调用一个工具。第三如果框架支持可以在返回后做一次解析如果发现是纯文本就重新调用一次并加强提示。我一般用前两个就够了。第三个作为兜底但会增加延迟所以只在必要时用。5.2 模型选了一个合法动作但参数不对比如动作是move但方向传了north而不是up。这种情况通常是模型对参数格式理解有偏差。解决办法是在工具定义的description里把参数格式写得更明确或者在 Prompt 里给一个示例。如果还是不行就在校验层做一次参数归一化比如把north映射成up。但我不建议长期依赖归一化最好还是让模型输出规范格式否则会积累技术债。5.3 决策延迟导致游戏卡顿LLM 推理有延迟尤其是大模型。如果主循环同步等待模型返回游戏帧率会直接崩。解决办法就是前面说的异步决策队列。具体实现是主循环每帧检查决策队列如果有新结果就应用同时检查是否有 NPC 需要新决策如果有就发起异步调用。这样模型推理和游戏运行是并行的玩家感知不到延迟。注意异步方案下要处理好“过期决策”的问题。如果一个决策返回时游戏状态已经变了这个决策可能就不再合法。所以执行前必须重新校验一次。5.4 模型行为重复、缺乏多样性如果发现 NPC 总是做同样的选择通常是温度太低或者 Prompt 太死。可以适当提高温度或者在 Prompt 里加入一些随机化的目标描述比如“你现在的情绪是谨慎的/激进的”让模型在不同情绪下做出不同选择。另一个技巧是给模型一个“性格”设定比如“你是一个鲁莽的战士”或者“你是一个谨慎的法师”。性格会影响模型的决策倾向增加行为的多样性。5.5 常见问题速查表问题现象可能原因排查方向解决手段模型输出纯文本工具定义不清晰或提示不够强检查系统提示和 tool_choice加强指令设置 required动作参数格式错误参数描述模糊检查工具 schema补充示例参数归一化游戏卡顿同步等待模型返回检查调用是否阻塞主循环改为异步决策队列行为重复温度过低或提示单一检查温度和 Prompt提高温度加入性格设定决策与状态不符上下文过期检查状态摘要时效性执行前重新校验Token 消耗过快历史上下文过长检查上下文长度滑动窗口 周期摘要5.6 几个我踩过的坑第一个坑是掩码更新不及时。有一次我在模型调用之后才更新状态导致模型基于旧状态做的决策在新状态下非法。后来我把掩码计算和状态快照绑定在一起确保模型看到的状态和掩码是同一时刻的。第二个坑是过度依赖模型推理。我一开始想让模型自己判断“现在该不该攻击”结果它经常在血量很低的时候还冲上去。后来我把“血量低于 30% 时攻击动作不合法”写进了掩码用规则兜底模型就再也没犯过这个错。这让我意识到能用规则解决的不要交给模型。第三个坑是忽略兜底动作。有几次模型返回了空结果或者格式错误导致 NPC 卡住不动。后来我强制要求任何异常情况下都执行wait保证游戏不会因为模型问题而挂起。6. 收放之间的平衡一些个人体会做这个项目最大的收获是理解了“收权”和“放权”不是二选一而是一个需要持续调节的过程。收得太紧LLM 就是个高级随机数生成器玩法没有惊喜放得太松游戏逻辑会崩玩家体验会碎。我的调节原则是规则层面收权策略层面放权。什么是规则层面动作的合法性、参数的取值范围、状态的修改权限这些必须收死。什么是策略层面在合法动作里选哪个、怎么组合、什么时候激进什么时候保守这些可以放给模型。这个原则落地到代码里就是动作掩码 工具调用 执行校验这三件套。它们保证了模型永远在安全边界内活动同时又有足够的空间去产生设计师没想到的玩法。还有一个体会是LLM 在游戏里的价值不在于替代规则而在于补充规则。传统游戏 AI 是行为树、状态机它们擅长处理明确的情况但遇到模糊的、需要推理的场景就捉襟见肘。LLM 正好补上这块——它能在规则框架内做出“像人”的决策让 NPC 的行为更有层次感。最后分享一个小技巧如果你想让 NPC 的行为更有“人味”可以在 Prompt 里加入一些模糊的目标描述比如“你隐约觉得应该先解决眼前的威胁”而不是“攻击最近的敌人”。模糊描述会促使模型做更多推理行为也会更自然。这个技巧我在对话和探索场景里用得最多效果比硬编码规则好得多。这套机制后续还可以往几个方向扩展。一是多 NPC 协同让多个模型驱动的 NPC 通过共享状态互相配合二是长期记忆让 NPC 记住和玩家的历史交互形成持续的关系三是动态难度根据玩家表现调节模型的放权程度。这些我还在摸索等跑通了再另开一篇聊。
返回列表