开发者近期微调了一个拥有 360M 参数的语言模型,使其能够模拟终端环境。该模型背后并不依赖任何外部文件系统或真实 Shell,而是通过预测每次按键后的屏幕变化来运作。它能够创建文件、运行 vim 和 tmux、玩贪吃蛇游戏,并通常能记住用户输入的命令。其核心突破在于教会模型修复自身错误,并实现屏幕行的并行解码。目前该项目已提供浏览器演示版本(约 208 MB),用户可尝试输入 python 等命令,体验非真实 Shell 的模拟效果。

该演示主要面向支持 WebGPU 且配备物理键盘的桌面浏览器,推荐在 Chrome 或 Edge 中运行。虽然现代手机也能使用,但性能表现较为缓慢。
受“Genie 3”等世界模型启发,开发者设想未来的应用程序可能不再由代码编写,而是由模型根据用户输入预测下一个最可能的状态。由于缺乏大规模 GPU 集群和视频数据,开发者将目标缩小至终端模拟:终端仅是一个字符网格而非像素阵列,且可以通过运行真实程序生成大量完美的标记化训练数据。预训练的语言模型本身对 ls 或 cat 等命令的输出已有较好认知。
最终模型基于 Qwen2.5-Coder-0.5B,通过在 Docker 容器内运行真实程序记录的终端会话进行微调。
终端视角的建模方式
标准 LLM 采用提示符后逐个追加 token 的方式,这与终端行为不符。终端输出并非总是追加到底部,例如清屏操作会将光标移回顶部。因此,模型需要预测屏幕上的具体变化。
开发者将终端视为一个字符网格,每个单元格包含一个字符。模型不直接处理网格图像,而是处理描述网格内容及其变更的 token 序列。
会话结构
每个会话以快照开始,包含屏幕尺寸、内容及光标位置。
尺寸作为输入的一部分,并非固定为 32×96。在演示中,用户可在自定义起始屏幕时设置行数和列数。
BOS SCREEN_START 32 96
ROW_0 0 'simedw@laptop:~$ '
CURSOR 0 17 1
SCREEN_END
这种机制允许从任意状态启动应用。若在此模型中输入特定指令,其表现优于基础 bash 模型。
随后,每一次按键进入真实终端都会发送原始字节。字母保持原样,Enter 对应 \r,Escape 对应 0x1b,左箭头对应 ESC [D 等。
INPUT_START 'l' INPUT_END
模型需回答哪些行被重写以及光标的最终位置:
OUTPUT_START
ROW_0 17 'l'
CURSOR 0 18 1
OUTPUT_END
上述示例表示在第 0 行第 17 列写出字符 l,并将光标移至第 0 行第 18 列。全屏应用有时会隐藏光标。
无需先清除屏幕,新字符直接覆盖旧字符。
开发者未引入新的角色 token。Qwen 已为所有 256 个字节值分配了 code,这些 code 被复用于字符、坐标及其他小数字。因此,像 17 这样的坐标仅需一个 token,而非两个。
时间也是输入变量。若无按键操作,模型接收带有持续时间的 WAIT token,而非键值。
行复制优化
当用户在终端底部按下回车时,每一行需向上移动一格。若让模型重新输出每行的新位置,单次按键需消耗约 3,000 个 token。
为此,开发者引入了 COPY 操作:
ROW_0 COPY 1
ROW_1 COPY 2
ROW_2 COPY 3
...
每个操作复制前一屏幕的行。例如,旧的第 1 行变为第 0 行,第 2 行变为第 1 行,以此类推。
滚动操作现在每行仅需 3 个 token,而非 96 个。
类似技巧也适用于清屏、vim 中的行插入与删除以及手册页导航。可以复制一行后覆盖其部分区域。
总体而言,模型使用了仅 333 个 token 的微小词汇表:256 个字节 token、64 个行 token 及少量特殊操作。每个预测均代表有效的屏幕更新。
这一微小词汇表在后续将模型压缩至浏览器时发挥了关键作用。
训练数据生成
开发者最初寻找现有的终端记录数据集(如 asciinema),但未找到所需的按键到屏幕的精确对齐数据。
解决方案是通过在 Docker 容器中运行真实命令来合成可信的终端会话。
合成配方包括:
- 随机用户名、主机名、提示风格及终端大小。
- 根据虚构人物(开发者、系统管理员等)随机选择目录和文件。
- 选择 6–24 项加权活动(修改文件、vim、python、tmux 等)。
- 大多数会话旨在打开已存在的文件。
- 逐键输入,包含标签补全等。错误率设定为 1.2%。
- 35% 的课程从中途开始,迫使模型学习任意状态下的行为。
终端尺寸主要为 32×96,部分为 24×80 和 40×120,以防止模型过拟合单一尺寸。
随着模型能力提升,数据集不断扩展,最终达到 336 万集。相比之下,基础模型在训练期间看到了约 18T token。
训练过程
起点选用 Qwen2.5-Coder-0.5B,因其较小且适合实时性能,同时作为编码模型已见过大量 CLI 使用场景。
初期采用简单的监督微调:给定屏幕历史和按键,预测真实终端产生的屏幕值。
常规打字很快被学会:字母出现、退格删除、光标移动。
但在长会话中,错误开始累积:文件名中的一个错误会在剩余会话中传播。
从自身错误中学习
为解决此问题,开发者借鉴了 DAgger 算法的思想,让模型从其访问的状态中学习,而不仅仅是理想演示。
对于每个训练示例,流程如下:
- 在录制的会话中选择一个点,通常靠近产生输出的命令。
- 让模型预测接下来的 1-4 个事件。
- 比较结果屏幕与实际录制屏幕。
- 若差异足够小,在下一次按键时修正模型的屏幕。
- 继续训练正确的帧及随后的真实事件。
Delta 将模型的错误屏幕转移到正确的下一屏幕。本质上,模型学会了“修复”自己的错误。
存在一个重要限制:模型必须有足够信息推断修正。通过校正引入的任何单词必须出现在之前的真实屏幕或用户输入中,而不能仅存在于模型自身的预测中。
开发者无法指望模型正确列出从未见过的文件名,也不希望增加幻觉。
约三分之一的采样会议产生了有用的校正示例。每次迭代都从最新模型中收集错误。
自我校正是最有效的训练改进之一。结合额外训练数据,它将 PresetBench(下文定义)的成绩从 int4 精度下的 24/47 提升至 38/47。
整个模型训练及后续优化在 RTX 4090 上耗时约 75 GPU-hours。
测试评估
使用了三项测试:ShellBench 包含来自 24 个手写 bash 会话的 106 条命令,按输出精确度评分。PresetBench 包含 47 个脚本案例,源自演示中遇到的失败情况。这两项测试均未出现在训练数据中。
记忆测试 包含 32 个世界。每个世界向两个文件写入两个随机值,然后在不同会话中读回。只有两次读回完全准确才算通过,总计 64 次读回。名称和值均未出现在训练数据中。
微调带来的提升显著:
| Model | ShellBench /106 | File readbacks /13 |
|---|---|---|
| Qwen2.5-Coder-0.5B, prompted | 52 | 2 |
| Qwen3-0.6B, prompted | 60 | 3 |
| WorldTUI | 80 | 9 |
最大差异体现在记忆能力上:WorldTUI 正确完成了 9/13 的文件读回,而提示模型仅为 2/13 和 3/13。
尽管它仍远非可靠的文件系统,但对熟悉命令的处理相当不错。然而,以新方式组合命令容易出错。例如,执行 echo green >> colors 后,运行 cat colors 往往不会显示追加的行。
性能优化
起初开发者避开了自回归模型,因为完整重绘涉及约 3,000 个 token,逐个生成似乎无法满足终端的实时性要求。
但由于自回归模型性能远超其他方案,开发者最终妥协。单 token 生成时,打字速度约为 ~38 ms/key,尚可接受;但涉及全屏重绘的操作(如 vim test.txt, man, clear)在 RTX 4090 上耗时高达 12 秒。开发者希望能在 Mac 上实现近实时运行。
并行行解码
关键在于意识到一帧中的大多数行彼此独立,无需依次生成。开发者为每一行运行一个批次元素。每行获取相同的历史记录(包括 KV cache),但仅生成其自身的更新。
这将解码成本从所有行之和(最多 3,000 步)降低到最长行的长度(约 100 步)。
光标随后成为最后的一个独立步骤,可以看到所有行更新的集合。
这需要在训练期间匹配注意力掩码,但速度提升值得付出代价。大框架 p95 延迟从 6.7 秒降至 0.87 秒。
行间的协调问题
并行解码带来了新问题:由于行之间相互独立,它们无法协调,导致 vim 可能在第 3 行而非第 0 行打开,或多行同时添加贪吃蛇的食物。
解决方案是推测解码(Speculative Decoding)。
通常推测解码使用小模型起草 token,大模型验证。得益于因果注意力,多个 token 可在单次前向传递中验证。
此处使用同一模型完成两项任务:并行行生成草稿,随后模型顺序验证草稿。
验证器保留直到第一个分歧点的 token。从那里开始,它生成自己的修正并重新提出剩余的草稿。
在 M4 Mac 的浏览器中,输入单个字符约需 60ms。较大的更新如 man test 顺序执行需 2.5s,而使用并行起草和验证则降至 0.9s。
模型小型化
初始模型 Qwen2.5-Coder-0.5B 约有 494M 参数。为实现网页演示,需大幅缩减体积。其 151,936 个 token 词汇表的嵌入层占 136M 参数。由于仅需 333 个 token,几乎全部嵌入层均可移除,使模型从 494M 减至约 360M 参数(990MB → 716MB)。
随后量化至 int4,体积进一步降至 208 MB。
浏览器演示采用自定义 JavaScript 实现的 Qwen 解码器,计算着色器使用 WGSL 编写并通过 WebGPU 运行。着色器在推理期间解包 4-bit 权重。激活值和注意力缓存使用 32-bit 浮点数。所有推理均在本地标签页中运行。
知识蒸馏实验
蒸馏至更少层数的实验揭示了内存依赖的关键层。教师模型为早期检查点,ShellBench 得分 78/106。仅移除第 7 层且不重新训练,下一 token 准确率几乎不变:99.3% 降至 98.9%。但精确内存读回从 64/64 跌至 0/64。这些测试输入真实历史,因此失败并非由模型自身输出错误的累积引起。
首个蒸馏学生模型保留了所有偶数层,但使用第 23 层而非第 22 层作为最终层。它能写出合理的终端输出,但在 64 次内存读回中仅 31 次正确。它始终打印出写入的两个值之一,但约一半时间选错。这表明它丢失了记忆功能,且在 480M 训练 token 后仍未恢复。
该学生模型丢弃了第 1、7、15 和 22 层。另一个保留这四层的 16 层学生模型通过了所有 32 个记忆测试世界,实现 64/64 精确读回。它在 4-bit 精度下也通过了所有 32 个世界,体积约 138MB。但在其他方面存在代价:ShellBench 得分为 65,低于教师模型的 78。这说明学生模型可能看似正常,却悄悄失去了记忆能力。
最终,开发者放弃了蒸馏路线。
功能表现
记忆能力
这是开发者最关注的部分。生成令人信服的目录列表很容易,难点在于创建文件、编辑、离开后再回来发现文件未被改变。
首次成功启动 tmux,在 vim 中打开文件,修改并保存,然后在不同的 tmux 窗口中使用 cat 读取更新内容时,开发者感到兴奋:“它活了!”
其他应用
为确保超越 bash 提示符的功能,训练数据涵盖了 vim、python、man pages、snake 甚至 tmux。
在 vim 中,它处理 esc 的模式切换,并允许保存、退出或返回插入模式。
tmux 的表现尤为惊人:可以创建多个窗格,在每个窗格中执行各种操作并在其间跳转。虽不完美,但对于一个小模型而言已属难得。
它甚至管理环境变量和变量展开。
Python 支持
鉴于基础模型是编码模型,开发者好奇其对 Python REPL 的处理能力。它做出了不错的尝试:小加法通常正确,列表推导式返回列表,尽管内容常有误。它对拼写错误略宽容:pythoon 能启动 Python,但 ython 不能。
局限性
Docker 容器未允许互联网访问,因此 lynx 等工具表现平淡。
Python 能处理非常简单的操作,但更复杂的任务会产生类似 GPT-3 时代的幻觉答案。
有时在创建嵌套目录时会迷失方向,突然处于不同路径。一致性仍有很大提升空间。
测试显示,Firefox 中的演示比 Chrome 慢约 20 倍。两者均使用 WebGPU,原因尚不明确。
此类项目通常先自行解决,再参考他人方法。早在 2022 年,人们已通过提示 ChatGPT 模拟 Linux 终端。近期,GameNGen 使用扩散模型模拟 Doom,NeuralOS 则针对桌面 GUI。WorldTUI 更接近原始的 ChatGPT 终端技巧,但专门针对按键和屏幕更新进行了训练,包括 vim 和 tmux 等全屏应用。
总结
构建该项目充满乐趣。其实用性有限,但对于一个在浏览器标签页中运行的 360M 参数模型而言,其表现远超预期。





