怎么让 AI 操控一个服务端玩家?
答案是可自由组合的原子操作
致谢
这一章也欠了不少人情,先在这里鞠躬谢过!
- @murasaki44:在 #109 里指出 0.1.3 的渐进式加载效果变差,这一声反馈,直接让我推翻了半个 0.1.4。
- Claude Code:确实启发了我 bash 这一个图灵完备的入口有多重要。
- Voyager、Mindcraft:早就选了用脚本操控,我绕了一大圈才明白前辈们的良苦用心。
- CC: Tweaked、超级工厂:两位用脚本驱动游戏的前辈,给了言出法随选择图灵完备脚本的启发。
为什么不能直接给 AI 一个键盘
很早就有朋友评论了我的demo视频提出建议,让 AI 直接操控 WASD 键盘。 且不说 token 会爆炸,按 AI 的反应速度,效果也肯定很差。想象一下你要从 A 走到 B 去挖矿,对 AI 来说,这就是一长串工具调用:W W W W W A A A A S S S D D D,再按住左键……大家的钱包承担不了,也接受不了 AI 那么慢的思考速度。
什么是"原子操作"
我们暂且把 WASD 这一类操作称为原子操作:在系统的定义下,不能再拆分的操作。
注意,是"在系统的定义下"不能再拆。你要是硬拆"按下 W"这个动作,当然也可以拆成:大脑神经元放电,信号传到中指的神经突触,中指响应,按下 W。(为什么是中指?因为我是 LOL 玩家,中指常年放在 W 上~)
所以,把这一类原子操作直接提供给 AI 调用,显然行不通:效果差,经济上也不划算。
把原子操作组合起来
那么,能不能把原子操作组合好了再交给 AI 调用呢?比如直接定义一个"挖矿"工具:AI 调用它之后,言出法随自动替 AI 执行"走到目标方块旁边、挖掉这个方块"这一整串动作。
多个原子操作组合成的一套动作序列,这里就称之为工作流。
这个办法显然非常棒。当然不全是我自己想出来的:我参考了 Voyager、Mindcraft 这些优秀的开源项目,大家都是这么做的,没有人会直接把一个键盘交给 AI。
到这里,给 AI 提供一串串的动作序列,一切看起来都非常非常美好。殊不知,命运的馈赠早已在暗中标好了价格,这也为言出法随后来的发展埋下了一笔技术债。
一把双刃剑
AI 操控服务端玩家的问题解决了,下一个问题随之而来:Minecraft 宇宙里庞大的模组生态,能不能适配?我们显然不能让言出法随成为一座孤岛。
鬼脑发力了:只需要抽出一层 API,所有工具都必须向这层 API 注册,哪怕是言出法随自己提供的工具,也一样要注册。人人平等嘛!
哇,多么优雅的架构,看起来言出法随很快就能适配各种模组了!然而,命运的馈赠又一次在暗中标好了价格,又为言出法随后来的发展埋下了一笔技术债(梅开二度)。
世界上没有绝对正确的架构,只有适配业务的架构。站在当时的角度看,这个架构无疑是成功的:
- 我们选择了服务端假人,获得了近乎原生的 Minecraft 兼容性,还能很方便地适配别的模组;
- 我们选择用工作流让 AI 操作,让 AI 真正能操纵模组。
没问题,这就是言出法随的早期版本。在 0.1.4 之前,言出法随用的都是工具注册架构:任何想让 AI 调用的工具,只要向 API 注册就行。
架构定好之后就开始写代码。很快,我和 Claude 就做完了早期的工作流:挖矿、建房子、战斗、合成……一系列给 AI 调用的工作流。
到这里,我以为言出法随已经有了一个优秀的架构,是一个合格的生产级模组。
工具越堆越多
先找上门的,是梅开二度的那一笔技术债。
很快,我发布了模组的第一个版本,并迅速开始适配模组生态。紧接着就发现了一个严重的问题:言出法随的工具定义正在不断堆积。
想加新功能,就只能不断注册新工具。而工具之间又有很强的组合关系:比如工具 A、B、C、D 可以组合成一套工作流,可我还是得额外注册一个工具 E = A + B + C + D,让模型知道 E 是一个专门的工作流;如果让模型分别去调用 A、B、C、D,效果会大打折扣。原子工具明明可以组合成工作流,我却不得不把组合出来的工作流再注册一遍,再写一遍参数和描述。
而每多一个工具,消耗的 token 也跟着水涨船高。虽然那时工具定义的 token 消耗还不算多,但面对 Minecraft 庞大的模组生态,每适配一个模组我都不得不加几个工具,言出法随的工具迟早会变得无比臃肿,可能出现上百个工具定义,这对模型的注意力是灾难性的。很快,言出法随的 30 多个工具就要把 AI 的注意力淹没了。
从渐进式加载到命令行
为了解决"每注册一个新工具就多一份 token"的问题,我在 0.1.3 版本引入了工具的渐进式加载:只给模型工具的名字,先不给定义,等模型需要的时候再去搜索定义。
但因为机制没有做好,很快就有玩家反馈效果特别差(#109),而且这种做法还容易破坏缓存。同时我自己也觉得渐进式加载还不够:如果将来有几百个工具,哪怕只给名字也够呛。
经过几天的思考,我鬼脑一动,选择了命令行:把所有工具都转换成命令,通过 --help 按命名空间加载工具的定义,比如 numen --help、ysm --help。这其实就是以模组为单位的渐进式加载,token 也的确省下来了。
工作流成了黑盒
第一笔技术债也找上门来了。
我重新想了一遍"原子工具"和"工作流"。原子工具是错的吗?不是,有时候 AI 也需要像"右键"这样的原子工具。工作流一定是对的吗?也不是,有时候模型也需要精细化地控制走路的路径,而不是一股脑走到底。这里面其实隐含着取舍。
比如早期经常被诟病的寻路:AI 有时会破坏玩家的房子,就算它知道玩家的房子在哪里,也照样会破坏。不是它想破坏,而是它掌控不了"走路"这个工作流:在它下达 goto xxx 的那一刻,言出法随就接管了一切,而程序是死板的。
所以我急需找到一个方案:模型既要具备原子粒度的操作,也要能自己把工具组合起来用,而不是靠我们再给它注册一个工作流。
命令行的死结
那时我正好在优化 AI 建筑这个功能,很快就发现了新的问题。
最开始,模型是用一次工具调用写完整个建造脚本。这里的脚本只是言出法随提供的一套 DSL,比如"这里铺一层石头"(只输入长宽)、"这里建一个球体"(只输入半径),它并不是图灵完备的。一口气输出整份建筑脚本,token 消耗太大了。
我希望模型能分步完成:比如先在地面上铺一层石头,看看情况,再继续。这就需要一个类似"对象"的东西:模型通过 numen create build --name A 得到一个名叫 A 的建筑对象,然后 A.addlayer(xxx),一步一步往上加。
可是命令行是无状态的,而 A 是一个有状态的对象。我试着硬在命令行上实现:numen create build 在磁盘上存一个 A.numen 文本文件,A.addlayer 就往这个文件里写一行,最后执行 numen build A,读取 A.numen 一行行执行。
这显然非常别扭。本质问题在于命令行的无状态和 A 这种有状态对象之间的冲突,而我选择把状态存在磁盘上。很快我就发现,磁盘上要存各种各样的文件,这显然不合理,治标不治本。于是我停了下来:0.1.4 版本改了一半,又被我自己推翻了。
图灵完备才是答案
命令行依旧做不到让模型控制工作流,因为命令行并不是图灵完备的。我只好停下来,重新思考整个架构。
这时我想起了 Claude:凭什么 Claude 靠十几个工具就能完成那么多工作?区别在哪?我把工具表拿来一对,显然,bash 是其中最特殊的一个。bash 能调用各种命令行,用一门图灵完备的语言组合任何操作。哪怕删掉 bash 之外的所有工具,Claude 依旧能完成大部分工作:删掉 Read,模型读不了文件,它可以用 bash 写一段 Python 脚本,借标准库去读;删掉 Edit,它也可以用 bash 执行 Python 去做字符串替换,照样能改文件。
显然,言出法随需要一门图灵完备的语言,把原子工具组合成工作流,并且让模型自己掌控工作流的细节,而不至于像以前那样,走着走着就硬生生挖掉玩家的房子。
也是在这一刻我才意识到,图灵完备确实不可或缺。前辈们(Voyager、Mindcraft)选择用脚本来控制,并非没有道理,我这才真正明白了前辈们的良苦用心。
我继续调研,发现了前辈 CC: Tweaked,还有我在 ATM9 整合包里玩过的超级工厂。我发现大部分需要脚本的模组都选择了图灵完备的语言。至于为什么是 Lua,这里还有一段故事,我们会在 Why Lua 这一章详细介绍。
现在的架构
选择了 Lua 之后,我们只需要把原子操作注册成 Lua API,模型就通过图灵完备的 Lua 脚本去操控这些原子操作。前面的两个目的都达到了:
- 不再无脑堆积工具:只需要提供 Lua API,模型需要的时候自己调用
numen.api.help("numen.move")这样的帮助获取说明; - 模型能自主掌控工作流:比如在寻路时避开某些区域。
配合技能(skill),这个方案无疑是成功的,甚至极大地减轻了适配别的模组的负担。
以车万女仆为例。如果用以前的工具注册方案,我大概会在技能里教模型"拿蛋糕对着女仆右键就能驯服",然后赌模型能连续成功调用两个工具:一个拿出蛋糕,一个右键驯服。两次工具调用之间还得等模型思考,我估计很快就会发现:刚拿出蛋糕,女仆就走开了,一右键直接把蛋糕放在了地上,一切都搞砸了。那就只能再注册一个"驯服女仆"工具,内容是拿出蛋糕、右键驯服,一气呵成,可这又得向模型描述工具和定义,token 又增加了。
而用 Lua 脚本,完全可以在技能里写清楚,然后在一段 Lua 脚本里连续调用两个 API。两个问题一起解决:既省了 token,又保住了效果,甚至效果更好。
于是,我们就得到了 AI 操控玩家的架构:
我们只需要注册 Lua API,模型就能自由组合使用。言出法随的 0.1.4 版本在拖了半个月之后(虽然 GitHub 每天都在更新),终于发布了!
