Hacker News AI · 2026/10/22 10:18:41
WorldTUI: 360M 模型模拟终端交互
开发者微调了一个 360M 参数语言模型,使其能预测终端屏幕状态变化而非执行真实命令。该模型支持创建文件、运行 Vim/Tmux 及游戏,并通过并行解码与自我修复机制提升稳定性。目前提供基于 WebGPU 的本地浏览器演示,权重即将开源。
报道全文原始报道全文
本文目录18 个章节
TL;DR:我微调了一个拥有 3.6 亿参数的语言模型,使其能够充当终端。其背后并没有 Shell 或文件系统:每一次按键输入都会进入模型,由模型预测屏幕如何变化。它可以创建文件、运行 vim 和 tmux、玩贪吃蛇游戏,并且通常能在几条命令之后仍然记住你之前输入的内容。最大的突破在于教会它自我修复错误,以及并行解码屏幕行。在浏览器中试用(208 MB),并输入 pythoon 以验证这并非真正的 Shell。
该演示专为配备 WebGPU 和物理键盘的桌面浏览器设计。Chrome 或 Edge 是最佳体验平台。它在现代手机上也能运行,但速度会非常慢。
去年我第一次看到 Genie 3 时,开始畅想一个未来:应用程序不再由代码编写,而是通过“学习”获得。场景渲染不再依赖游戏引擎,而是由模型根据用户输入预测下一个最可能的帧。
自然而然地,我想训练自己的世界模型。但我没有庞大的 GPU 集群,也没有海量的视频数据,因此我缩小了野心:与其构建游戏世界,不如构建一个终端。
事实证明,终端是一个理想的选择。它只是一个字符网格而非像素阵列,而且我可以通过运行真实的 Shell 生成任意数量的完美标注的训练数据。预训练的语言模型本身也已经对 ls 或 cat 应该输出什么内容有了相当好的概念。
我真正想知道的是,它能否维持一个一致的世界,还是仅仅能生成看似逼真的终端输出。
最终模型基于 Qwen2.5-Coder-0.5B,并在从 Docker 内运行的真实程序录制的终端会话上进行了微调。跳转至示例视频
它是如何看待终端的
标准的 LLM 接收提示词后,每次向末尾追加一个 token。这种机制并不适合映射到终端。单次按键可能独立产生效果,例如 vim 中的 esc;而输出也不一定总是出现在底部:clear 会清除屏幕并将光标移回顶部。因此,我不需要预测文本流,而是要预测屏幕的变化。
我将终端视为一个字符网格,每个单元格对应一个字符。1 模型并不直接观察这个网格,而是观察描述其内容及变化的 token 序列。
一个会话
每次会话都从一张快照开始:屏幕尺寸、屏幕内容以及光标位置。
尺寸是输入的一部分,并非固定为 32×96。在演示中,你可以在创建自定义起始屏幕时设置行数和列数。
BOS SCREEN_START 32 96
ROW_0 0 'simedw@laptop:~$ '
CURSOR 0 17 1
SCREEN_END这也让我可以从应用程序的中间状态开始。给它一个 bash 提示符、vim 缓冲区或一局贪吃蛇游戏,模型就会尝试从那里继续。(它在 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 列。最后的 1 表示光标可见,因为全屏应用有时会隐藏光标。
不会先清除任何内容。新字符只是直接覆盖在旧字符之上。
我不需要为字符创建新的 token。Qwen 已经拥有所有 256 个字节值的 token,我将其复用于字符、坐标和其他小数字。因此,像 17 这样的坐标只占用一个 token,而不是两个。
时间也是输入的一部分。如果没有按键,模型会收到一个带有持续时间的 WAIT token,而不是键值,这就是贪吃蛇能够自行移动的原因。
复制行
当你到达终端窗口底部并按下 Enter 键时,每一行都必须向上移动一行。我可以简单地要求模型在新位置重新输出每一行,但这对于单次按键来说大约需要 3,000 个 token。
所以我添加了一个 COPY 操作来代替:
ROW_0 COPY 1
ROW_1 COPY 2
ROW_2 COPY 3
...每个操作都从前一屏复制一行。在这个例子中,旧的 row 1 变成 row 0,row 2 变成 row 1,依此类推。
现在滚动每行只需三个 token,而不是大约 96 个。
同样的技巧也适用于清屏、在 vim 中插入和删除行,以及浏览 man 页面。你还可以复制一行然后覆盖其中的一部分。
总而言之,模型使用了一个仅包含 333 个 token 的小型词汇表:256 个字节 token、64 个行 token 以及少量特殊操作。解码过程受语法约束,因此每个预测都是有效的屏幕更新。
这个小型词汇表在我将模型压缩到浏览器中时会再次发挥作用。
生成训练数据
我最初寻找现成的终端录制数据集,包括 asciinema,但没能找到符合我所需的精确“按键-屏幕”对齐要求的数据集。
幸运的是,自己创建一个并不太难。
基本思路是通过在 Docker 容器中运行真实命令来生成逼真的终端会话,并记录每一次按键以及每次按键后的屏幕状态。
合成数据的方案如下:
- 随机化用户名、主机名、提示符样式和终端尺寸。
- 基于虚构的人物角色(如开发者、系统管理员等)随机化目录和文件。
- 选取 6–24 个加权活动(修改文件、vim、python、tmux 等)。
- 跟踪状态,确保会话中打开的文件大多确实存在。
- 模拟人类打字方式:一次一个按键,包含 Tab 补全等功能。我将错误率设定为 1.2%,这对我来说已经相当宽容了。
- 35% 的会话从中间开始,迫使模型学会从任意状态入手工作。
终端尺寸主要为 32×96,同时也包含一些 24×80 和 40×120 的尺寸,以防止模型过拟合于单一尺寸。
随着模型能力的提升,我不断扩充数据集,最终达到了 33.6 万个 episode,大约对应 20 亿 tokens。作为对比,基础模型在训练过程中看到了约 18 万亿 tokens。
训练
我以 Qwen2.5-Coder-0.5B 作为起点。我需要一个小模型来接近实时性能表现。作为一个代码模型,它此前也已经见过大量的 CLI 使用场景。
起初,我进行了直接的监督微调(SFT):给定屏幕历史和一次按键,预测由真实终端产生的屏幕变化量(screen delta)。
普通的打字操作很快就被学会了:字母出现、退格删除字符、光标移动。
然而,在更长的会话中,错误开始累积:文件名中的一个错误会传播到整个会话的后续部分。
从自身的错误中学习
为了解决这个问题,我借鉴了 DAgger 的思路,让模型从其实际访问的状态中学习,而不仅仅是理想化的演示数据。2
对于每个训练样本:
- 在录制的会话中选择一个点,通常靠近产生输出的命令。
- 让模型预测接下来的 1-4 个事件。
- 将生成的屏幕与真实录制的屏幕进行比较。
- 如果差异足够小,则构造一个修正项,在下一次按键时修复模型的屏幕状态。
- 继续在修正后的帧以及随后的几个真实事件上进行训练。
这种 delta 将模型错误的屏幕状态修正为正确的下一个屏幕状态。本质上,它学会了“修复”自己的错误。
有一个重要的限制:模型必须拥有足够的信息来推断出修正内容。任何由修正引入的词汇,都必须出现在之前的真实屏幕或用户的输入中,而不能仅仅出现在模型自身的预测里。
例如,如果模型拼错了之前出现过的文件名,我可以教它进行修正。但如果它列出了一个从未见过的文件名,我就不能指望它能正确生成。
在这些样本上进行训练实际上会奖励幻觉行为,而我最不需要的就是更多的幻觉了。
大约三分之一的采样会话产生了有用的修正样本。我重复进行了三轮,每轮都从最新的模型中收集错误。
自我纠正是较为有效的训练改进之一。结合一些额外的训练数据,它在 int4 精度下将 PresetBench(定义见下文)的成绩从 24/47 提升到了 38/47。
总体而言,训练该模型及其后续优化在 RTX 4090 上总共耗时约 75 GPU 小时。
测试
我使用了三项测试。ShellBench 包含来自 24 个手写 bash 会话的 106 条命令,按输出完全匹配评分。PresetBench 包含 47 个脚本化案例,源自我在演示中遇到的失败情况。这两项测试均未出现在训练数据中。
记忆测试包含 32 个世界。每个世界向两个文件写入两个随机值,然后在独立的会话中读回这些值。只有当两次读回都完全准确时,该世界才算通过,因此总共有 64 次读回操作。名称和值从未出现在训练中。3
微调究竟有多大帮助?
| Model4 | ShellBench /106 | File readbacks /13 |
|---|---|---|
| Qwen2.5-Coder-0.5B, prompted | 52 | 2 |
| Qwen3-0.6B, prompted | 60 | 3 |
| WorldTUI | 80 | 9 |
最大的差异体现在记忆方面:WorldTUI 在 13 次读回中正确完成了 9 次,而提示基线模型仅为 2/13 和 3/13。
它距离一个可靠的文件系统仍然很远。它对熟悉命令的处理相当不错,但以新方式组合这些命令可能会迅速导致问题。例如,在执行 echo green >> colors 后,运行 cat colors 通常不会显示追加的那一行。
让它更快
有一段时间,我完全避开了自回归模型。一次完整重绘大约涉及 3,000 个 token,逐个生成这些 token 对于本应感觉像终端的应用来说似乎毫无希望。
但它们的表现确实远优于我尝试过的其他方案,所以我妥协了。在逐 token 生成的情况下,打字时的延迟约为每个按键 38 ms,体验尚可;但任何需要全屏重绘的操作(如 vim test.txt、man、clear)在我的 RTX 4090 上耗时高达 12 秒,正如我所担心的那样。这足够让人重新考虑是否真的需要查看手册页了。而且我希望它能在我的 Mac 上以接近实时的速度运行。
并行处理行
我之前忽略的一点是,帧中的大多数行彼此之间并没有依赖关系,因此没有理由按顺序逐行生成。我为每一行运行一个 batch element。每一行都接收相同的历史上下文(包括 KV Cache),但只生成其自身的更新内容。
这将解码成本从所有行的总和(最多 3,000 步)降低到了最长行的长度(约 100 步)。
随后,光标成为最后的一个独立步骤,它可以查看所有行更新的集合。
这在训练期间需要配合相应的 attention mask,但带来的速度提升是值得的。大帧的 p95 延迟从 6.7 秒降至 0.87 秒。
大帧 p95 延迟 (RTX 4090) ↓ 串行模式 6.7 s | 并行行模式 0.87 s
无法相互通信的行
这听起来可能好得难以置信,事实也确实如此。问题在于,由于各行是独立的,它们无法进行协调,这导致 vim 在第 3 行而不是第 0 行打开,或者贪吃蛇游戏中的多行同时添加食物。
解决方案是投机解码(Speculative Decoding)。
通常,投机解码使用一个小模型来起草 token,并由一个大模型来验证它们。得益于因果注意力机制(causal attention),可以在单次前向传播中验证多个 token。
在这里,我对两者使用同一个模型:并行的行产生一个草稿,然后模型再按顺序对其进行验证。
验证器保留直到第一个分歧点之前的 token。从该点开始,它生成自己的修正内容,并重新提出剩余的草稿。
在我的 M4 Mac 浏览器中,输入单个字符大约需要 60ms。像 man test 这样较大的更新,串行执行需要 2.5s,而采用并行起草和验证则只需 0.9s。5
使其更小
我最初使用的是 Qwen2.5-Coder-0.5B,该模型大约有 494M 参数。我非常希望做一个 Web 演示,因此需要对其进行压缩。其 151,936 个 token 词表的嵌入表(embedding table)占了 136M 参数。我只需要其中的 333 个 token,6 因此可以移除几乎整个嵌入表,这将模型从 494M 参数缩减到约 360M 参数(文件大小从 990MB 降至 716MB)。
随后,我将其量化为 int4,使模型大小进一步降至 208 MB。
浏览器演示使用了一个自定义的 JavaScript 实现的 Qwen 解码器,其中计算着色器(compute shaders)采用 WGSL 编写,并通过 WebGPU 运行。这些着色器在推理过程中解包 4-bit 权重。激活值和注意力缓存(attention cache)使用 32-bit 浮点数。所有推理均在本地标签页中运行。
蒸馏(Distilling)
将模型蒸馏以减少层数让我了解到哪些层对记忆功能至关重要。教师模型是一个较早的检查点,在 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 量化下(大小约为 138MB),它依然通过了全部 32 个世界的测试。不过,在其他方面存在代价:它在 ShellBench 上正确执行了 65 条命令,而教师模型为 78 条。这意味着学生模型可能看起来表现良好,却悄然丢失了其记忆能力。
最终,我放弃了蒸馏方案。
它能做什么
记忆
这是我最感兴趣的部分。生成一个令人信服的目录列表很容易。我想要的是创建一个文件,编辑它,离开一会儿,回来时发现它未被更改。
第一次启动 tmux,在 vim 中打开一个文件,修改并保存它,然后在另一个 tmux 窗口中使用 cat 读取更新后的内容时,我感觉自己就像弗兰肯斯坦博士:它活了!
其他应用
这一点对我至关重要:它不能只在 bash 提示符下工作,因此我在 vim、python、man pages、贪吃蛇游戏甚至 tmux 上进行了训练。
在 vim 中,它能处理 esc 键的模式切换,让我可以保存、退出或重新进入插入模式。
这大概是世界上计算量最大的贪吃蛇游戏了。
真正让我震惊的是 tmux:我可以创建多个窗格(panes),在每个窗格中执行各种操作,并在它们之间跳转。虽然并不完美,但这毕竟是一个小模型。
它甚至能管理环境变量和变量展开。
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 参数模型来说,它的表现远超我的预期。
最让我感兴趣的是,它在没有实现任何底层规则的情况下,设法维持了一个相对一致的世界。
这显然是一个非常弱的世界模型,我很想看看如果提高一致性并采用不同的屏幕更新生成方式(例如使用扩散模型而非自回归解码),这种方法能走多远。
接下来我想尝试的一个实验是,在数千个随机生成的应用程序上进行训练,每个应用都有各自的控制方式和规则。这个想法旨在观察模型是否能够在提供足够上下文线索的情况下,学会预测它从未遇到过的应用程序的行为。
在此处体验。这是一个 208 MB 的下载包,完全在本地运行,且在 Chrome 浏览器中效果最佳。请尝试找出它的漏洞,并告诉我你发现了什么。
我很快会发布模型权重,并将下载链接添加到这里。