各位读者朋友大家好。最近 Hacker News 上有一个很有意思的项目“Show HN: Doom Compiled into an LLM”。这个项目把经典游戏 Doom “塞进”了大语言模型里让模型不仅仅用来聊天、写代码而是直接充当一个游戏运行环境。Hacker News 上对它的讨论非常热烈很多开发者在思考一个问题LLM 是否真的可以“运行”一个确定性程序而不是仅仅“生成”一些看起来合理的文本这篇文章会围绕这个爆炸性项目展开梳理它背后的原理、实现路径以及社区争论的核心点。文章面向对 LLM 技术有兴趣的开发者、AI 算法工程师以及游戏开发者不需要你有非常深的强化学习背景但了解 Transformer 和神经网络的基本概念会更顺畅。读完本文你可以理解 Doom 被编译进 LLM 的思路框架知道这类实验的局限与潜力也能在自己的项目中找到一些可迁移的工程经验。1. 背景什么是“把 Doom 编译进 LLM”1.1 从 HN 热帖说起“Show HN: Doom Compiled into an LLM”这个标题乍一听很像是玩笑。Doom 是 id Software 在上世纪 90 年代推出的第一人称射击游戏它依赖精准的 CPU 指令、内存地址、图形渲染管线来运行。LLMLarge Language Model大语言模型本质上是一个基于海量文本训练的概率模型它根据输入的 Token 序列预测下一个 Token。把 Doom 编译进 LLM表面意思是让 LLM 不靠传统的游戏引擎而是通过“预测”来生成游戏的状态和画面。但这个项目并不是在说“把游戏画面字符串硬编码在模型参数里”。社区里讨论的核心是用一个神经网络模型去学习或者拟合 Doom 游戏逻辑的状态转移函数。换句话说当你向模型输入一个游戏状态和玩家操作时模型的参数内部发生了一次“计算”然后输出下一个游戏状态。如果这个学习过程能从游戏引擎的海量状态转移数据中抓到规律那么模型便成为了一台“神经模拟器”。1.2 这个实验到底做了什么从 Hacker News 上有限的讨论和类似开源项目的情况来看作者大概率采用了类似“状态 → Token 序列 → 游戏画面/逻辑状态 → 下一帧”的研究思路。项目没有使用传统的暴力图像比对也不是把游戏 ROM 打包进 Prompt而是致力于让 Transformer 网络在隐含空间里重构游戏逻辑。这个例子恰好触及了一个最近两年很火的学术问题LLM 的“世界模型”能力有多强我们让模型玩文字冒险游戏时它可以完成一些推理但让它模拟一个受物理规则、碰撞检测、敌人 AI 共同支配的复杂 3D 游戏难度就完全不一样了。1.3 核心价值它不是游戏破解而是编程范式实验把 Doom 编译进 LLM本质上不是在“优化游戏加载速度”而是在验证一个更大的想法我们能否不再按照传统方式编写指令而是把状态转移过程直接“压进”神经网络权重中如果可行未来很多复杂的嵌入式系统、仿真环境、虚拟世界生成或许都可以在 LLM 上以另一种方式运行。这个方向目前属于极客实验阶段但它把大语言模型的边界从“语言”拓展到了“计算引擎”。2. 技术原理LLM 如何充当状态转移引擎2.1 什么是状态转移函数为了理解这个项目我们需要把问题抽象化。任何确定性游戏都可以描述为一个状态机当前游戏状态玩家坐标、朝向、生命值、弹药、地图中的怪物位置、动画帧等。玩家输入键盘或鼠标事件。状态转移函数给定当前状态和输入输出下一状态。在传统程序中状态转移函数由 C/C 代码编译成机器码执行。而在 Doom 这类游戏中游戏状态还包括复杂的物理碰撞检测和渲染状态但底层逻辑依然高度确定。“把游戏编译进 LLM”的核心是训练一个神经网络 F(state, input) next_state让模型逼近游戏的状态转移函数。这样只要输入当前游戏状态模型就能预测后续状态。如果游戏画面的每一帧都能拿到用一个“下一帧预测”的损失函数去训练模型理论上模型可以自己“想象”出游戏画面。2.2 Transformer 如何“运行”程序从表面看Transformer 只是一个根据上下文预测下一个 Token 的工具。但在足够的数据和参数规模下它能够学习到一定的规则。如果把状态量全部序列化例如把地图数据、玩家血量、怪物的 AI 状态等变成 Token 序列那么 Transformer 就可能学会状态之间的映射关系。这里的关键问题是Transformer 有能力表示任意可计算函数吗理论上Transformer 具有图灵完备性的潜力只要给足够的层数和宽度它可以模拟计算机的任意状态转移逻辑。不过在现实中参数规模有限、训练难度极高学习一个完整的游戏引擎仍然非常困难。2.3 为什么选择 DoomDoom 是一个极其适合此类研究的对象原因很明确确定性较强Doom 的逻辑在单机模式下几乎没有随机性适合让模型学习固定的状态转移关系。状态复杂度适中相比于《GTA 5》或《赛博朋克 2077》Doom 的场景结构、怪物行为模式更加简洁便于序列化。社区基础好几十年来有大量 Doom 引擎分析文档、开源版本例如 Chocolate Doom使得数据提取、状态标注变得方便。视觉辨识度高即使模型生成的画面有噪点读者也能立刻明白这是在模仿 Doom实验展示效果好。2.4 “编译”和“训练”并不是一回事需要澄清一个概念。严格意义上的“编译”是把高级语言转换成机器码而 LLM 训练通常是通过梯度下降优化网络权重。在“Doom Compiled into an LLM”这类项目中“编译”更多是一种修辞实际指的是“通过训练把状态转移逻辑编码进权重”。社区中不少人也因此产生了争论与其说“编译”不如说是“蒸馏”或“拟合”。这个区分不是吹毛求疵。因为真正的程序编译可以保证代码逻辑全部保留而神经网络拟合则只能尽可能接近原函数一定会存在误差。搞清楚这一点才能理解为什么当前实验会产生画面撕裂、状态丢失等问题。3. 项目拆解如何把游戏逻辑塞进模型如果我们想自己复刻一个简化版的 “Game into LLM” 实验应该怎么设计这里提供一个没有依赖官方代码的基础思路严格按照工程步骤讲解。3.1 准备游戏数据集首先需要准备训练数据。我们需要海量的“状态-操作-下一状态”三元组。常见做法是用模拟器记录游戏运行帧数据包括屏幕图像、玩家的操作状态以及关键游戏变量。对于 Doom 这类游戏可以使用类似 VizDoom 这样的研究平台。VizDoom 提供了 API允许逐帧获取游戏画面、玩家状态和游戏变量并支持让 AI 发出动作指令。如果没有办法跑 VizDoom也可以退一步先用一个简化的 Grid World 游戏进行实验。数据形式可以如下示意{ frame: 100, screen: rgb_array_160x120, player: { x: 12.5, y: 33.2, angle: 180, health: 100, weapon: 2 }, enemies: [ {type: imp, x: 45.1, y: 80.0, health: 20} ], action: turn_leftmove_forward, next_screen: rgb_array_160x120, next_player: { x: 13.2, y: 35.1, angle: 175, health: 100, weapon: 2 } }需要强调的是直接把原始像素输入给 LLM 并不是好方法因为画面数据量过大、Token 序列太长。更好的办法是把屏幕图像先通过自编码器压缩成离散视觉 Token再把状态数据单独编码成数值 Token。3.2 状态 Token 化的设计LLM 无法直接处理连续浮点数所以需要把游戏状态离散化。我们可以设计一套与游戏状态对应的词汇表位置 Token把地图坐标离散成网格编号。朝向 Token把 0-360 度划分成 36 个区间。血量 Token把 0-100 的血量映射到 0-20 档位。敌人状态 Token敌人的类型、位置、血量离散组合。示例 Token 序列可以设计成[BOSS] [POS_X12] [POS_Y33] [ANGLE180] [HEALTH100] [ACTFORWARD] [NEXT]在训练阶段我们会把当前状态与动作拼接成输入序列把真实下一帧状态作为预测目标。如果你使用视觉 Token可以把当前帧的离散图像 Token 也放在序列中让模型预测下一帧离散图像 Token。3.3 构建一个小型神经网络状态模拟器在真正使用超大规模的 LLM 之前建议先用一个小型 GPT 模型做验证。下面是一个基于 PyTorch 的示例代码它演示了核心思想把状态 Token 序列输入一个仅有几层 Transformer 的模型预测下一个状态 Token。# 文件路径tiny_game_gpt.py import torch import torch.nn as nn class TinyGameGPT(nn.Module): def __init__(self, vocab_size, d_model128, nhead4, num_layers3, max_len256): super().__init__() self.token_embedding nn.Embedding(vocab_size, d_model) self.pos_embedding nn.Embedding(max_len, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, batch_firstTrue ) self.transformer nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.lm_head nn.Linear(d_model, vocab_size) def forward(self, x): B, T x.shape positions torch.arange(T, devicex.device).unsqueeze(0) x self.token_embedding(x) self.pos_embedding(positions) x self.transformer(x) logits self.lm_head(x) return logits在这个示例中模型输入是 Token 序列输出每个位置的下一个 Token 概率分布。我们可以把真实的下一个状态 Token 作为监督标签使用交叉熵损失进行训练。3.4 训练循环与数据批量加载为了让模型学会状态转移训练时应当把数据整理成如下格式# 输入玩家当前位置、朝向、生命值、输入动作 # 标签玩家下一步的位置、朝向、生命值这里给出一段训练循环的核心代码# 文件路径train_loop.py import torch import torch.nn.functional as F from torch.utils.data import DataLoader, Dataset class GameStateDataset(Dataset): def __init__(self, token_sequences): self.token_sequences token_sequences def __len__(self): return len(self.token_sequences) def __getitem__(self, idx): seq self.token_sequences[idx] x seq[:-1] y seq[1:] return torch.tensor(x, dtypetorch.long), torch.tensor(y, dtypetorch.long) def train_one_epoch(model, dataloader, optimizer, device): model.train() total_loss 0.0 for x, y in dataloader: x, y x.to(device), y.to(device) logits model(x) loss F.cross_entropy(logits.reshape(-1, logits.size(-1)), y.reshape(-1)) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(dataloader)这种设计与训练 GPT 做文本生成没有本质区别唯一区别是数据从自然语言换成了游戏状态序列。训练完成后模型应能在输入当前状态和动作时输出可能性最大的下一个状态。3.5 推理与画面重建在推理阶段我们不再需要执行游戏引擎的物理逻辑只需要把上一帧的状态 Token 输入模型选择概率最高的下一个状态 Token然后把它解码成画面。整个流程可以描述为一个循环初始化游戏状态 Token 序列。把序列输入到训练好的模型中。模型输出下一个状态 Token。将 Token 解码成可读状态和屏幕画面。把新状态接在序列后面继续预测下一帧。如果模型训练得足够好我们就能看到一个“脑补”出来的 Doom 画面序列。不过说实话以当前小型模型的能力生成的画面大概率会出现局部模糊或逻辑错误。作者在 HN 上展示的效果也只是一种弱化版画面但并不妨碍项目本身的价值。3.6 完整运行流程示例为了便于读者理解下面把整个流程写成一个简单的伪代码脚本# 文件路径run_doom_llm.py import cv2 import numpy as np import torch from tiny_game_gpt import TinyGameGPT def encode_state_to_tokens(state): # 伪代码将状态向量转成固定长度的 token id 列表 return [1, 2, 3, 4] def decode_tokens_to_screen(tokens): # 伪代码将输出 token 转成一帧图像 return np.zeros((120, 160, 3), dtypenp.uint8) def main(): model TinyGameGPT(vocab_size512) model.load_state_dict(torch.load(game_model.pt)) model.eval() state initial_state() with torch.no_grad(): for frame_id in range(100): tokens encode_state_to_tokens(state) input_ids torch.tensor([tokens], dtypetorch.long) logits model(input_ids) next_token_id logits[0, -1].argmax().item() state update_state_by_token(state, next_token_id) frame decode_tokens_to_screen(state) cv2.imshow(Doom in LLM, frame) cv2.waitKey(30) if __name__ __main__: main()这段代码展示了画面循环的基本骨架。实际项目里的状态编解码器会非常复杂但宏观思路是一致的。4. 社区的争议与冷静思考4.1 这真的是“编译”吗在 HN 评论区争议最大的问题就是“编译”这个词的使用。不少开发者认为神经网络训练更像是“拟合”或者“压缩”而不是编译。编译具有严谨的等价性保证但神经网络权重是连续参数空间上的数值优化结果没有谁能保证某些边界情况不会出错。因此项目标题有一定夸张成分。但反过来看也有开发者认为这种“语义扩展”是合理的。编译的本质是把高级描述转换成低层执行指令而把游戏逻辑编码进网络权重也可以看作是一种把“游戏的高级逻辑描述”转换成“神经网络数值计算指令”的过程。只是这种指令的表示方式不再是 CPU 能直接执行的机器码而是 GPU 上的矩阵乘加运算。4.2 模型是真正理解了游戏还是记住了路径这是社区关注的第二个核心问题模型是在泛化游戏规则还是单纯记住了训练集里的状态如果模型遇到训练数据里没有出现过的状态组合能否正确预测下一状态如果无法做到那模型不过是一个巨大的查表器。目前这类实验通常只是“记忆”加“局部插值”距离真正的规则泛化还有很大差距。从工程角度看完整记录所有 Doom 状态组合是不可行的但如果模型能推断出“玩家向左转后视角变窄”这类局部规则就已经具备了一定的世界模型雏形。4.3 实验价值为“模型即引擎”提供证据尽管存在争议这款实验无疑推动了社区对 LLM 能力的重新认识。传统观点认为神经网络擅长处理模糊问题不擅长精确计算。但这个实验说明如果有合适的 Token 化方案和训练策略神经网络也能逼近一部分精确逻辑规则。在未来的实际应用中这种技术思路有可能被用于以下方向游戏 AI 测试快速生成模拟玩家与游戏环境交互的状态路径。仿真环境压缩在模型参数中压缩复杂的环境模拟器替代昂贵的实时仿真计算。可微游戏引擎传统游戏引擎无法直接反向传播如果使用神经网络状态转移模型整个游戏循环变得可微AI 可以通过梯度信息训练。5. 常见问题与排错排查很多读者如果照着类似思路做实验很可能会遇到下面这些问题。这里整理成表格方便快速排查。问题现象常见原因解决思路训练 Loss 不下降状态 Token 化设计不合理序列学习目标不明确检查状态编码空间是否把连续值离散得过细或过粗建议先采用分类交叉熵验证模型预测画面模糊视觉 Token 压缩损失过大替换更好的自编码器或 VQ-VAE减少压缩倍数增加视觉 Token 数量游戏逻辑错误例如穿墙模型没有学习到地图碰撞规则在 Token 序列中加入地图网格信息增大模型容量增加碰撞相关样本权重预测下一帧时出现重复状态死循环模型陷入局部高概率路径产生模式坍塌推理时引入温度采样或 Top-k 采样或者在训练数据中增加随机动作来抹平分布训练数据太大内存爆掉帧数据 60 FPS 存储过大降低采样频率只保存关键帧先使用 15 FPS 或 10 FPS模型推理速度太慢Transformer 自回归解码逐 Token 生成序列过长限制输出 Token 序列长度使用更小的模型在推理时缓存历史 Key/Value如果你在复现中遇到了具体报错建议按以下步骤排查先确认训练数据和标签是否对齐。很多情况下 Loss 不降是因为输入与标签错位一个时间步。测试模型是否具备基本记忆能力从训练集里抽取一条序列看模型能否完整复现如果连训练集都拟合不了说明模型容量不足或 Token 化方式有严重问题。再测试泛化能力从训练集中移除若干状态观察模型在这些状态附近的表现如果完全无法预测说明模型没有学到规则只是在记忆数据。6. 工程建议与可落地的启示6.1 控制状态空间降低任务难度如果你准备复刻类似实验不要一开始就挑战完整版 Doom。强烈建议先使用一个简单环境比如 2D 迷宫、Grid World 或者经典游戏《吃豆人》。将地图大小降低到 20×20 以内动作空间压缩到上下左右四个方向。这样可以在半天内训练出一个小型演示模型建立对状态 Token 化的直觉。6.2 输入特征要混合视觉与符号状态研究项目的核心难点在于如何结合像素信息和符号状态。建议实践路线如下先只用符号状态做训练例如坐标、生命值、敌人类型验证 Transformer 能否学会基本状态转移。再加入视觉 Token用 VQ-VAE 或 VQGAN 把画面压缩成离散编码。最后再把符号状态和视觉 Token 拼接在同一个序列中让模型同时预测下一帧的视觉内容与符号状态。这种分阶段实验可以帮你判断瓶颈到底在哪里避免从一开始就陷入“画面模糊 逻辑错误”两难困境。6.3 日志和可视化是调试的关键训练这类状态模拟器模型结构和 Loss 数值只是冰山一角。大部分时间你会花费在检查具体输出上。因此务必在训练过程中定时输出模型预测的游戏画面或状态序列。推荐做法是每隔 500 步保存一组预测结果直接渲染成视频文件这样你能直观地看到问题。例如下面这段代码可以把模型输出保存为视频# 文件路径save_video.py import cv2 import numpy as np def save_frames_as_video(frames, video_pathoutput.avi, fps20): if len(frames) 0: return h, w, c frames[0].shape writer cv2.VideoWriter(video_path, cv2.VideoWriter_fourcc(*XVID), fps, (w, h)) for frame in frames: writer.write(frame) writer.release()6.4 序列长度不宜过长Transformer 对长序列的建模代价很高。游戏状态序列如果每次推理都持续增长最终会因为内存和计算量过大而不可行。工程上应当限制上下文长度例如只把最近 16 帧到 32 帧放入输入让模型基于有限的最近上下文预测下一帧这与视频预测模型的设计思路一致。6.5 记录实验配置避免无意义调参推荐使用统一的 YAML 配置管理训练参数包括模型层数、注意力头数、批量大小、学习率、Token 词汇表大小、训练帧率等。便于复现和回溯。model: d_model: 256 nhead: 8 num_layers: 6 data: map_name: map01 frame_skip: 4 image_size: [120, 160] token_visual_size: 256 train: batch_size: 32 learning_rate: 0.0002 epochs: 50 gradient_clip: 1.0在研究前期就建立完善的实验记录体系能节省大量踩坑时间。6.6 关于算力与时间成本的提醒训练“游戏逻辑进 LLM”对算力有一定要求。如果只有普通消费级显卡建议把地图尺寸和状态空间尽量缩小。不要试图一开始就跑完整版 Doom 画面。社区中很多成功的复现实验使用的都是极低分辨率和简化动作集。毕竟这个方向的重点是验证“神经网络可以掌握游戏状态转移规律”而不是在视觉清晰度上逼近原生游戏。7. 结语与实践方向“Doom Compiled into an LLM”这个项目给我们的最大启发不是模型能否真的替代游戏引擎而是它打开了另一种思考方式。当前的大语言模型训练方法本质上是在“预测下一个 Token”但预测的对象并不局限于自然语言。只要设计出合适的 Token 化协议我们可以把像素、状态、物理规则、甚至程序本身的执行痕迹都编码成可预测的序列。这样一来原本被认为是“模糊工具”的神经网络开始具备一种程序化的可能性。如果你对“LLM 世界模型”这类话题感兴趣接下来可以关注几个方向VQ-GAN 与离散视觉 Token 的进展、基于扩散模型的世界模型、以及微软、谷歌研究团队发布的通用智能体环境模拟器论文。这些研究本质上都与“把程序编译进模型”的理念高度相关。老实说当前这个领域仍处于非常早期的阶段距离用神经网络完整模拟一个现代 3A 游戏还有很长的路要走。但从开发者的视角看我们正站在一个重要拐点上过去我们通过写代码告诉计算机怎么做现在我们在学习如何把“怎么做”直接变成一组权重参数。让 Doom 跑在大模型里至少说明方向是存在的。如果你对本文讨论的项目有自己的看法或者想要尝试复现一个简化版本欢迎在评论区分享。遇到报错和实验设计问题也欢迎一起讨论。另外提醒一句这类实验会消耗不少电费和时间跑之前记得规划好训练节奏别让显存先“阵亡”。