把Doom编译进LLM:用Transformer拟合游戏状态转移的世界模型实践 把 Doom 编译进一个 LLM这个标题看起来像是把两个不太相关的概念强行拼成了一个玩笑一边是 1993 年发布的经典第一人称射击游戏另一边是大语言模型的 token、权重和前向传播。这个演示真正想探索的问题不是“LLM 能不能编写一个 Doom 模拟器”而是“一个游戏的状态转移逻辑能不能被压缩进神经网络参数里让模型每次根据当前状态和玩家动作预测下一帧状态”。这个问题很有讨论价值因为它逼着我们把“编译器”的含义从传统编译器扩展到神经网络训练过程也逼着我们去想 LLM 的下限和上限到底在哪里。这种实验通常不会走完整个产品化流程但它非常适合用来检验模型对确定性规则、数值变化和长序列状态的处理能力。这篇文章会先把“编译进 LLM”这句话拆开再给出一条从数据采集、状态 Token 化、模型训练到结果验证的实现路径最后整理这类实验最容易踩的坑和工程化边界。1. 先拆开“编译进 LLM”这个标题1.1 传统编译器与神经网络“编译”的区别传统的源码编译过程是这样的Doom 的 C/C 源码 - 词法分析、语法分析 - 中间表示 IR - 机器码 / 字节码 - CPU 执行这个过程的输入是源码产物是二进制文件执行者是 CPU。二进制文件里的每一条指令都对应一个确定行为编译器产出的程序是可重复、可回溯、可单步调试的。而“把 Doom 编译进 LLM”的实验路径更接近下面这样Doom 游戏的状态轨迹 - 数据清洗和状态 Token 化 - 文本样本 - 梯度下降 - Transformer 权重 - GPU 执行前向推理这里的“编译”并不是传统意义上的静态翻译。训练过程中模型通过大量观测样本学习“当前状态 动作 - 下一状态”的映射最后把这些映射近似存入参数里。严格来说这更像训练一个“世界模型”或“神经模拟器”而不是把一个确定性的游戏引擎翻译成另一份可证明等价的代码。理解这个区别很重要。传统编译器的每一步都有明确语义而神经网络参数只是对训练数据的统计压缩。它可以表现得很像“运行游戏”但在边界情况、矩阵运算精度、异常输入等方面都会出现真正的 Doom 不会出现的偏差。因此看这类项目时不要被“compiled”这个词误导更应该关注模型到底学到了什么。1.2 三种容易混淆的技术路线围绕“Doom 与 LLM”可以做很多不同方向的工作标题里虽然都用“LLM”和“Doom”但内核完全不一样。路线做法是否能玩到原生 Doom主要难点世界模型 / 神经模拟器用 LLM 预测游戏状态当前状态加动作输出下一状态不能直接运行原二进制只能模拟状态变化状态表示、长序列稳定、规则一致性Agent 控制 / 工具调用LLM 只输出按键指令真正游戏由原生 source port 运行能玩LLM 只是玩家或裁判动作规划、长期记忆、环境反馈像素级帧生成用扩散模型或自回归模型直接预测屏幕帧能看到运行画面但非原生引擎渲染算力、时序一致、内存开销标题里的“compiled into an LLM”通常更容易让人联想到第一条路线把游戏的状态转移函数装进模型参数中。下面内容也主要围绕这条路展开。如果目标只是让 LLM 控制角色打 Doom那本质上还是一个 Agent 项目和“把游戏编译进模型”不是一回事。1.3 为什么选 Doom 作为实验对象选择 Doom 而不是某个简单的弹球游戏不是没有原因。Doom 的状态量足够复杂存在位置、朝向、血量、弹药、武器、敌人、地图、贴墙状态和连锁事件同时它又不像现代大型游戏那样需要规模庞大的物理引擎和图渲染管线。Doom 的游戏循环以 tick 为单位推进每个 tick 的输入输出相对固定非常适合被建模成“状态转移函数”。另一个原因是 Doom 的开源生态很成熟。学习实验通常可以借助 ViZDoom、DoomRL 或其他自定义场景采集数据也可以使用开源的 freedoom 资源替换非自由授权的游戏数据这让“在沙箱里做实验”成为可能。不过需要区分学习环境与生产环境。在 Doom 上跑通一个小规模“神经模拟器”只是第一步距离“让模型直接替代引擎”还有非常大的工程鸿沟。后面会专门讨论这件事。2. 训练前想清楚观察值、动作和状态 Token2.1 环境准备用最小可采集环境跑通数据链路这类训练实验的依赖如下python3.10 torch2.0 transformers4.30 datasets2.0 accelerate0.20 vizdoom1.1.9如果只是验证“LLM 能不能学会一个简单游戏的状态变化”可以先不从完整 Doom 开始。建议先用 ViZDoom 里很小的教学场景或者干脆用自己写的一个 2D 网格 Doom-like 环境。先跑通数据采集、Token 化、训练、验证四条链路再逐步替换成更接近真实 Doom 的场景。ViZDoom 的 Python 端通常这样初始化from vizdoom import DoomGame game DoomGame() game.load_config(scenarios/basic.cfg) game.set_doom_map(map01) game.init() # 拉取一帧状态 state game.get_state() # state.game_variables 里的字段顺序由 cfg 文件定义 variables state.game_variables # [health, ammo, ...] # 假设动作空间有 3 个离散动作前进、左转、右转 action [1, 0, 0] reward game.make_action(action) game.advance_action()这段代码只是示意。不同版本的 ViZDoom 和不同场景的变量顺序可能不同落地前要先确认配置文件中定义的变量顺序。如果你的项目只做状态预测实验不建议一上来依赖完整游戏引擎可以先保存轨迹数据再离线构建训练集。2.2 状态抽象不要一开始就让模型生成像素“让模型生成下一帧像素”听起来最震撼但它需要极大的参数量、算力和上下文窗口。即使模型能输出一帧差不多的画面画面之间的逻辑也不一定一致。更稳妥的做法是先定义“语义状态”。一个 Doom 世界模型需要关注的核心字段至少包括当前地图编号玩家坐标 x、y玩家朝向角度当前武器和弹药血量敌人编号、敌人位置、敌人血量已触发事件或已拾取物品标记把这组字段表示成一个结构化文本片段例如s m:01 h:100 a:020 x:0100 y:0064 angle:0015 w:0000 e:01/035/0024其中s表示 statem是地图编号h是 healtha是 ammox/y是坐标angle是朝向w是当前武器e表示第一个敌人的生命值。具体字段可以根据实验需求增减核心思路是把连续、稠密、视觉化的游戏状态先转成一块足够表达因果关系的文本 Token 序列。这种抽象有两个好处。第一模型不用学会绘制纹理和光照只需要关注状态量的变化训练量明显变小。第二验证更方便。你可以在训练集和测试集上直接比较模型生成的状态字段和真实状态的字段是否一致而不是人眼去比较两张图片相似度。2.3 状态 Token 的数值必须格式统一LLM 处理数字时比传统程序脆弱得多。同一个数字“100”写成100、0100或1e2在词表中可能对应完全不同的 token。更麻烦的是模型对数字词尾的小变化很容易产生“近似生成”例如把坐标0100写成0099这在自然语言任务里可以容忍但在游戏状态预测里就是致命的。因此Token 化时要做到三件事固定字段顺序。模型需要学习的是“位置性语言”不是靠字段名猜测含义。固定数值长度。例如坐标统一用 4 位整数血量统一用 3 位整数。不使用大量无意义的逗号和空格。状态文本设计得越短越有利于模型在有限上下文里记住长序列。示例格式def state_to_text(state): # 假设字段已经被清洗成整型 m str(state[map]).zfill(2) h str(state[health]).zfill(3) a str(state[ammo]).zfill(3) x str(state[x]).zfill(4) y str(state[y]).zfill(4) ang str(state[angle]).zfill(4) w str(state[weapon]).zfill(2) return fs m:{m} h:{h} a:{a} x:{x} y:{y} angle:{ang} w:{w}这段代码把状态统一成定长字符串能显著降低模型解析难度。对于学习环境来说数值范围有限定长格式足够用。对于更复杂的地图和更大的坐标范围需要单独设计离散化方法而不是直接让模型预测浮点数。2.4 数据来源与授权要注意Doom 源代码以 GPL 协议发布但游戏资源 WAD 文件的授权并不等价。如果只是学习研究最好使用 freedoom 这类自由资源或者在 ViZDoom 自带的迷你场景上做实验。不要在自己项目里默认捆绑原始 WAD 文件。还有一点如果从真实游戏过程中录制玩家数据需要考虑数据是否包含可辨识的用户信息公开数据集要检查许可不能直接拿来训练又拿去发布。3. 一条可实现的“LLM 编译”流水线3.1 构造“当前状态 动作 - 下一状态”样本把状态表示成文本后需要把交互记录变成自回归模型的训练样本。一个样本可以这样组织输入前缀: s m:01 h:100 a:020 x:0100 y:0064 angle:0000 w:0000 | op:move_forward | 期望输出: s m:01 h:100 a:020 x:0100 y:0063 angle:0000 w:0000 |end|这里模型需要做的是典型的“续写”给定前缀生成状态文本。实现时可以使用 decoder-only 架构例如distilgpt2或类似的小型预训练模型。构造数据集的代码伪码如下def make_pair(obs, action, next_obs): src state_to_text(obs) dst state_to_text(next_obs) text f{src} | op:{action} | label f{dst} |end| return {text: text, label: label}需要注意text和label需要作为一个连续文本喂给模型但计算 loss 时text部分的 token 应该被屏蔽掉只计算label部分。如果直接把整段文本作为标签模型只是学会了复制输入而不是学会从动作预测下一状态。3.2 用因果语言建模训练状态转移在 HuggingFacetransformers体系里可以用自定义Dataset构造样本。from torch.utils.data import Dataset class StateTransitionDataset(Dataset): def __init__(self, examples, tokenizer, max_len128): self.examples examples self.tokenizer tokenizer self.max_len max_len if tokenizer.pad_token_id is None: tokenizer.pad_token tokenizer.eos_token tokenizer.pad_token_id tokenizer.eos_token_id def __len__(self): return len(self.examples) def __getitem__(self, idx): text self.examples[idx][text] label self.examples[idx][label] src_ids self.tokenizer(text, add_special_tokensFalse).input_ids dst_ids self.tokenizer(label, add_special_tokensFalse).input_ids input_ids src_ids [self.tokenizer.eos_token_id] dst_ids [self.tokenizer.eos_token_id] labels [-100] * len(src_ids) [-100] dst_ids [self.tokenizer.eos_token_id] input_ids input_ids[: self.max_len] labels labels[: self.max_len] return {input_ids: input_ids, labels: labels}数据分批时需要对同 batch 内不同长度样本进行 padding。此时attention_mask也要构造出来padding 位置为 0。def collate_fn(batch): input_ids [item[input_ids] for item in batch] labels [item[labels] for item in batch] max_len max(len(ids) for ids in input_ids) padded_inputs [] padded_labels [] attention_masks [] for ids, label_ids in zip(input_ids, labels): pad_len max_len - len(ids) padded_inputs.append(ids [tokenizer.pad_token_id] * pad_len) padded_labels.append(label_ids [-100] * pad_len) attention_masks.append([1] * len(ids) [0] * pad_len) return { input_ids: torch.tensor(padded_inputs), labels: torch.tensor(padded_labels), attention_mask: torch.tensor(attention_masks), }这里的-100是 PyTorch 交叉熵损失函数约定的忽略位置。把输入前缀和eos设置为-100模型就不会去预测 prompt 内部内容只专注状态转移输出。然后就可以开始训练from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(distilgpt2) tokenizer.pad_token tokenizer.eos_token model AutoModelForCausalLM.from_pretrained(distilgpt2) training_args TrainingArguments( output_dir./doom_llm, num_train_epochs10, per_device_train_batch_size16, learning_rate5e-5, logging_steps50, save_steps500, evaluation_strategyepoch, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetvalid_dataset, data_collatorcollate_fn, ) trainer.train()对于一个小型教学演示distilgpt2已经够用。如果你的数据量较大希望模型学到更复杂的 Doom 关卡规则可以换更大模型但训练成本会直线上升。核心思路不是追求模型大小而是先建立一套可验证的状态转移能力。3.3 推理把模型当成自动回滚的游戏模拟器训练结束后推理方式仍然是续写def generate_next_state(prompt, action, model, tokenizer, max_new_tokens40): text f{prompt} | op:{action} | inputs tokenizer(text, return_tensorspt) outputs model.generate( inputs.input_ids, max_new_tokensmax_new_tokens, do_sampleFalse, num_beams1, ) generated tokenizer.decode(outputs[0], skip_special_tokensTrue) return generated生成结果需要从完整文本里截取s m:之后的部分再解析。建议在解析时使用与state_to_text相反的逻辑不要依赖肉眼。这段推理代码本身并不复杂真正的难点在于多步回滚时的累积偏差。模型虽然能学会一步转移但如果每一步都把上一轮输出作为下一轮输入生成误差会逐帧累积。十步之后模型可能把角色从坐标(100,64)慢慢“漂移”到地图中从不存在的坐标。因此验证环节不能只看单步损失还要做闭环回滚测试。3.4 真正要尝试完整 Doom 时建议从确定性事件开始如果想把实验从小场景扩展到完整 Doom不建议一开始就让模型模拟全部 AI、音效、随机数和渲染状态。建议分阶段增加能力先模拟玩家基础状态坐标、朝向、血量、弹药。再加入简单门锁和物品拾取。再加入有限敌人。最后才尝试长时序地图推进和完整胜负判断。每一步都必须保留一个“验证集”用来检查模型是否能从之前未见过的轨迹中泛化。如果模型只记住了训练轨迹中的状态值遇到新起点就崩溃那说明它并没有学到真正的转移规则。4. 运行验证怎么判断模型真的“运行”了 Doom4.1 单步验证不能只看 loss模型训练完很多人只看 eval loss 就判断好坏。这在这个场景里不够。状态文本里可能 90% 的字段在绝大多数动作下保持不变例如角色站着不动时 health、weapon、map 都不变。模型只要学会“输出和输入一样”loss 就可能很低但它完全没有学会动作效果。因此必须分别统计每种动作下的“正确转移率”。例如在测试集上固定动作类型逐一判断模型输出状态与真实状态是否完全一致。判断规则可以是字段完全一致的样本数 / 样本总数每个字段独立误差的绝对值坐标字段平均距离血量字段错一位的次数这个检查往往能暴露真实问题模型很擅长预测静止帧但不擅长预测转身、移动、开火导致的状态变化。4.2 多步回滚测试是绕不开的验证真正的“游戏循环”是闭合的当前状态 - 玩家动作 - 预测下一状态 - 再作为当前状态 - ...如果模型把上一步的输出作为下一步输入就会出现误差累积。验证时可以这样写state initial_state_text for step in range(100): action random_policy(step) state generate_next_state(state, action, model, tokenizer) state parse_state(state) if not state: print(step, step, 生成状态无法解析) break print(step, state)正常期望是在足够多的 steps 内模型不会生成非法坐标不会出现血量突然从 100 跳到 1000不会把地图编号从01变成不存在的地图。异常结果通常是这几类模型生成的状态文本缺少字段或字段顺序错乱坐标在一百步内滚出地图范围模型重复上一个状态动作没有产生任何影响血量在受伤后不下降反而上升4.3 与原生引擎对照验证为了下一判断模型是否真的学到了“Doom 规则”需要用原生引擎生成对照轨迹从同一初始状态出发执行同一动作序列。记录原生引擎的真实状态序列。让模型从同一初始状态出发预测状态序列。比较每个时间步的状态差异。比较结果可以汇总成一张表指标说明one-step exact match单步状态文本完全一致比例one-step field error每个数值字段的平均误差closed-loop survival模型在闭环回滚中走到非法状态前的步数action effect correctness每个动作是否产生了正确方向的改变map consistency是否始终停留在合法地图编号范围内如果模型在 closed-loop 测试里只能稳定几帧那就不能称为“编译进 LLM 的 Doom”最多算是“一个看过 Doom 轨迹的文本生成模型”。5. 这类实验最常见的五个坑5.1 模型学会了“复制当前状态”现象训练 loss 很低但 rollout 时无论给什么动作模型都输出和当前状态一样的内容。原因状态样本中大量字段在大多数动作下保持不变模型发现最优策略是复制。尤其当动作空间很大而每个动作只出现少量变化样本时这条捷径很严重。排查方式按动作类型统计正确转移率不要只统计整体正确率。处理建议在数据集中做动作均衡对会产生状态变化的样本做上采样。同时让状态文本只包含变化字段例如用dx:1 dy:0 angle:15作为输出目标逼模型学习差分而不是复制。5.2 loss 正常但 rollout 十几步后漂移现象单步验证结果不错一旦把模型输出作为下一轮输入状态就开始逐渐偏离最终越界。原因模型的状态分布是由“见过的真实状态”构成但预测会引入微小误差。误差进入下一轮输入后会让模型进入一个训练时很少见的状态区域后续预测质量快速下降。排查方式把闭环回滚的前 5 步、20 步、50 步分别打印出来比较误差增长曲线。处理建议增加边界约束和状态合法性校验。模型预测坐标后先检查坐标是否在合法范围再传给下一步。或者在训练时使用 scheduled sampling让模型部分时间看到上一轮模型预测结果而不是完美输入。5.3 同一个动作在不同位置产出完全错误的变化现象模型学会了向右转会改变角度但在某些坐标点上向右转会突然改变坐标。原因模型可能没有学到“地图边界”和“墙体碰撞”的真实规则。它只是看到某些区域数据量少于是用概率生成填补缺失。排查方式检查地图边缘、拐角、门口附近的样本覆盖率。低覆盖区域往往就是错误集中区。处理建议增加这些区域的训练数据或者显式把“该方向是否撞

相关新闻

最新新闻

单店务实挑选|2026 美业 saas 哪家做得好

单店务实挑选|2026 美业 saas 哪家做得好

小编发现,身边开美容院、理发店的朋友这两年聊得多的话题之一,就是“要不要上个系统”。以前手工记账、微信约客那一套,越来越跑不动了——客人多了记不住,卡项乱了算不清,预约冲突了还得挨个打电话道歉。市场数据也印…

2026/9/3 12:20:25
OpenClaw 2.0 Multiplayer:AI Agent 多人协作的边界设计与落地实践

OpenClaw 2.0 Multiplayer:AI Agent 多人协作的边界设计与落地实践

最近升级到 OpenClaw 2.0,看到 Multiplayer 多人协作功能出现在版本信息里,我的第一反应并不是“又多了一个可以把同事拉进来聊天的入口”,而是:这是 agent 使用方式从“私人助手”走向“团队公共设施”的一个关键变化。 怎么理解…

2026/9/3 12:20:25
Split Dance meme项目解析:从素材到AI视频生成的本地部署指南

Split Dance meme项目解析:从素材到AI视频生成的本地部署指南

这次要看的项目是 shtdn/meme ,标题里的核心词是 スプリットダンス / Split Dance ,翻译过来就是“劈叉舞”。如果你平时刷短视频和表情包,多半见过这类内容:人物在某一帧突然完成劈叉、起跳、再接一段卡点转场,或…

2026/9/3 12:20:25
裁判文书大数据分析:用Python挖掘二审改判关键因素

裁判文书大数据分析:用Python挖掘二审改判关键因素

感谢您的需求,但这个选题我无法按当前设定完成。您提供的项目标题是“二审改判的实战逻辑:法官的‘风险规避’与‘纠错动力’”,属于刑事辩护与法律实务分析类内容。而我当前的角色设定是长期撰写CSDN 技术教程的博主,专注的是软件…

2026/9/3 12:20:25
离线语音转文字如何靠模型量化瘦身八成:Handy 的完整实战指南

离线语音转文字如何靠模型量化瘦身八成:Handy 的完整实战指南

离线语音转文字如何靠模型量化瘦身八成:Handy 的完整实战指南 【免费下载链接】Handy A free, open source, and extensible speech-to-text application that works completely offline. 项目地址: https://gitcode.com/GitHub_Trending/handy11/Handy Hand…

2026/9/3 12:20:25
测试工程师两周速成:三极管驱动功率负载核心设计与验证

测试工程师两周速成:三极管驱动功率负载核心设计与验证

这次我们来看一个对测试工程师特别实用的技能点:如何用两周时间快速掌握三极管驱动功率负载的核心设计思想。很多测试工程师在工作中会遇到硬件相关的测试需求,比如要验证某个驱动电路是否正常工作、负载能力是否达标,但硬件知识储备不足往往…

2026/9/3 12:15:25