跳转至

RFC 0002:可编程工具调用(在游戏里运行智能体写的代码)

  • 状态:已推迟——这里没有任何东西被实现,而现在这是决定,不是缺口:第一版草案被判定为"为还没被证明的需求做太多设计"。见 整体结构 里的"故意不做的(至少现在)"。当同一个手写 helper 在不止一个实验里反复出现时重开;在那之前,从进程外做(看一眼约 35 毫秒)就够用
  • 范围:让智能体把代码注入到游戏进程内部运行
  • 相关:RFC 0001、智能体在游戏里是谁

问题,用实测数字说话

目前智能体做的每一件事都要跨过一次 socket 和一次 tick 边界。在活世界上实测(64 格内 521 个实体、256 KB JSON):

从游戏外部做一次操作 代价
一个微不足道的请求(state)往返 中位数 35 ms(请求要排队到下个客户端 tick)
实体快照,半径 64 521 个实体、256 KB、59 ms
一条命令加上读它的回答 34 ms,这还没算人为等待

一个 Minecraft tick 是 50 ms,而客户端线程同时还在渲染。所以外部探针大致被限制在每秒 10–20 次采样,每次都付几十 KB,并且不先把原始数据搬出去就算不出任何派生量。命令(/player、/data get、Scarpet)形状相同:最多每 tick 一条命令,没有数值库,没有"游戏里还没写好的触发逻辑"。

这个天花板就是本篇 RFC 的理由。智能体应该能说:"盯着这些实体 600 tick,记录每次超过 X 的冲量周围的 Δv 与碰撞箱尺寸,把汇总给我"——并且让这段以 tick 率运行,只有汇总穿过 socket。

智能体需要什么(需求)

  1. tick 率观测,派生量就地计算(Δv、冲量窗口、AABB 分组、计数、分位数)。
  2. 触发器与捕获窗口:"记录这个事件前后 ±N tick"。
  3. 带 tick 时序的脚本化动作:T 时刻生成、T+3 放置、T+5 点燃、采样到 T+40——确定性的,而不是每次往返一条命令。
  4. 紧凑的结果:回到智能体的是 KB,不是几百 KB。
  5. 可复现:一个脚本文件就是一个实验的定义。
  6. 一份契约、多个观察点(见下),于是同一个脚本今天能跑在客户端、将来能跑在服务端。

非目标:给人用的通用 modding API、防恶意代码的安全沙箱、取代数据包/命令方块。

观察点

代码跑在哪 能看到 tick 精确 备注
客户端(今天的 mod) 插值后的实体视图、界面、客户端命令、受渲染距离限制 是(客户端 tick) 单机和任何服务器都能用,不需要服务端安装
服务端(一个服务端 mod) 权威实体、所有已加载区块、不受渲染距离限制、真实的冲量 是(服务端 tick) 单机:集成服务端跑在同一进程里,所以服务端 mod 在那里同样可用
外部进程(Minescript 那一类) API 暴露什么就有什么 否 写起来最快,循环最慢;见"先例"

实测出来的差异是真实的:同一个起跳的假人,客户端插值视图读到 vy=0.333,服务端读到 -0.078。做物理,服务端观察点才是诚实的那一个。

运行时调研(查过,不是猜的)

游戏跑的是 Mojang 的 Java 25 运行时,对它执行 java --list-modules 能看到 jdk.compiler、jdk.jshell、java.scripting 都在。这意味着游戏可以在运行时编译 Java,且不需要任何额外依赖。

运行时 体积 许可 备注
用 jdk.compiler 编译 Java + 子类加载器 0(本来就有) 不适用 全速、可直接访问游戏类、编译错误以文本回传;没有沙箱
Rhino(JS,ES5+) ~1.4 MB MPL-2.0 可嵌入、有 Java 互操作、解释执行速度
LuaJ(Lua 5.2) ~250 KB MIT 极小、速度尚可、Java 互操作偏笨
Nashorn-core + ASM(ES5.1) ~2 MB GPLv2+CE 旧 JDK 引擎的独立维护版
GraalJS ~40 MB UPL 最快的脚本方案,依赖最重
Kotlin scripting ~50 MB Apache-2.0 语言最舒服,也最重

先例

项目 是什么 对我们的适配度
Minescript(Fabric/Forge/NeoForge 26.2,GPL-3.0) 外部进程里的 Python 脚本驱动客户端 好的人机工程学参考;外部进程 = 上面那个延迟问题
Neo Scripts Lua(Fabric/NeoForge 26.2,GPL-3.0) /lua(客户端)与 /slua(服务端)的进程内 Lua 现存最接近的东西;很适合做可选运行时
Allium(Fabric,MIT) Lua 加载器 只有 26.3+,对这台机器的 26.2 太新
Carpet Scarpet 服务端解释器、tick 精确、/script 证明了概念;它的语言与能力边界正是我们想抬高的
KubeJS 给整合包作者的 JS Fabric 26.2 上没有

许可说明:我们的仓库是 MIT。依赖一个 GPL 程序没问题,捆绑或链接 GPL 代码则不行,所以我们发布的东西必须是 MIT/Apache/MPL/UPL 兼容的。

设计提案(供讨论)

生命周期原语

script_start  { id, source, mode: once|tick|on_event, budget_ms, caps, vantage }
script_stop   { id }
script_list   {}
script_status { id }         -> 状态、已跑 tick 数、错误、结果
script_output { id, since }  -> 脚本写下的记录,以游标形式

脚本按 id 索引、可热替换;出错的脚本会被停用(不重试)并说明原因。

host API 就是契约

脚本默认拿不到裸内部。它拿到的是一个很小的 host 对象,同样的名字必须同时存在于客户端适配器和将来的服务端适配器上:

interface ScriptHost {
    long tick();                       // 游戏 tick 计数
    Self self();                       // 坐标、速度、血量、游戏模式
    List<Entity> entities(double radius, Filter filter);   // 有类型、开销低
    void record(String key, Object value);     // -> script-<id>.jsonl
    void result(Object value);                 // 最终结果,有上限
    void log(String message);                  // -> 事件流
    void schedule(long tick, Runnable action); // 定时到某个 tick 的动作
    void command(String command);              // 服务端命令,不走往返
    void stop();                               // 干净地结束
}

逃生舱:一个能力开关(allowGameInternals),额外暴露裸的游戏对象,用于 API 还没覆盖的实验——显式开启,也正是它在总体上无法被沙箱化的原因。

结果

record(...) 追加到 <gameDir>/mc-agent/script-<id>.jsonl(有上限、会轮转),并且每秒最多往普通事件流里推几行紧凑记录,这样智能体能看进度又不至于被数据淹死。result(...) 由 script_status 返回并做截断(64 KB)——聚合结论和判断应当放这里。

安全姿态(明确写清)

脚本是智能体为用户自己的游戏写的可信本地代码。目标是保护帧率和世界,不是防一个有恶意的作者:

  • 每 tick 的墙钟预算(默认约 2 ms),反复超限后自动停止并报告原因;
  • 异常按 tick 捕获,只停用那一个脚本;
  • 每个脚本都可列出、可记录、可 kill;
  • 可选的受限 import(拒绝 java.io、java.net、反射),由编译器的 file manager 执行——是护栏,不是沙箱;
  • 不做隐式的事:想写文件或开 socket 的脚本必须在能力列表里声明,并且用户得允许。

为什么先做 Java

零依赖、紧密的逐 tick 循环能拿到 JVM 的全速、访问游戏类没有互操作边界,而且编译错误对模型来说是很好的反馈。代价是每次改动约 100 ms 的编译时间,以及没有沙箱。JS 或 Lua 运行时以后可以作为额外的 language 字段加入——脚本是对着 host API 写的,所以运行时是可以替换的部件。

供讨论的开放问题

  1. v1 的语言:只做 Java,还是 Java + 一种脚本语言(Rhino/LuaJ)?
  2. 观察点:先客户端,还是客户端与服务端适配器一起?(物理测量要诚实,靠的是后者。)
  3. host API 的形状:在我们知道前三个实验之前,值得设计多少类型化 API;还是先用薄 API + gameInternals 逃生舱?
  4. 安全:接受"可信代码 + 预算 + 可 kill"吗,还是第一天就要受限 import?
  5. 外部 runner:无视 Minescript / Neo Scripts,还是把它们作为同一批工具名后面的可选运行时?
  6. 结果落在哪:只写文件、只走流,还是两者都做(提案)?