第 19 章:游戏——Gaming
第 19 章:游戏——Gaming
游戏是决策层最自然的试验场:规则写在代码里、状态能被读出来、可选动作是有限集合,而且每一步都要在肉眼可见的时间里给出判断。本章看 Jev 如何在不生成一行文本的前提下驱动角色、裁决对局,并支撑成千上万次模拟。
游戏里最难写死的判断
游戏开发者对"判定"这件事并不陌生,难的地方在于,很多判定没法用规则优雅地写出来。一个 AI 队友该不该开团?这句话在数值上可以拆成血量、位置、技能冷却,但真正决定它的是"当前这个局面像不像一个值得打的团"——这是一句语义判断,代码判不了,而传统做法是堆一堆启发式权重,调起来痛苦,上线后还常常"看着就不对"。
常见的补救办法是把判断丢给大模型:"根据以下状态,输出一个动作。"问题是游戏对输出有硬约束。大模型可能返回一段解释、一个不存在的动作,或者一个已经冷却中的技能。你必须在外面写解析器和校验器,把一个开放文本硬塞回封闭的动作空间。这和我们在第 1 章看到的错配是同一个。
Jev 的切法很直接:动作空间本来就是有限的,那就把它声明成一道 Choice 题的选项。模型不生成动作名,它只能从你给出的选项里选一个。于是"选出一个不存在的动作"这件事从"不太可能"变成"不可能"——这是一个类型系统的性质,不是调参的结果。
看一个 NPC 该做决策的最简形态:
def npc_turn(state_text: str, legal_actions: list[str]) -> str:
d = client.systemone(
state=state_text, # 血量、位置、敌人距离、队友状态……
questions={
"action": {
"type": "choice",
"question": "这一回合,这个角色最该做什么?",
"choices": legal_actions, # 只可能是代码认为合法的动作
}
},
instructions="你是这个角色的战术决策器,优先保证存活,其次推进目标。",
)
return d["action"].choice控制流仍然在代码里:legal_actions 由引擎按规则算出来,Jev 只负责在合法集合里选出当下最合理的一个。角色的"性格""战术偏好"写在 instructions 里,"能不能做"由代码保证,"该不该做"由决策层判断。这三件事第一次被干净地分开了。
详解一:jev-torneo-animales——一场 0.01 美元的锦标赛
jev-torneo-animales(hectorlcastro09)把"守擂制锦标赛"做成了一个近乎纯粹的决策循环。参赛者是最多 2,569 种动物,每一场对决都是 Jev 的一道 Choice:在两个名字之间选一个赢家,而胜负规则(陆地、水域、空中三套)就写在 state 里。
它有一个值得学的工程技巧:不逐场调用,而是一次性把冠军和接下来 K 个挑战者全问出来。一旦冠军在某一轮落败,后面那些"推测性"的回答直接丢弃。
def run_fight(champion: str, challengers: list[str], arena: str) -> str:
# 一次请求把冠军与接下来 K 个挑战者全部问完
questions = {
f"vs_{i}": {
"type": "choice",
"question": f"按{arena}规则,{champion} 对 {challenger},谁赢?",
"choices": [champion, challenger],
}
for i, challenger in enumerate(challengers)
}
d = client.systemone(
state=f"守擂制锦标赛。规则场地:{arena}。当前冠军:{champion}。",
questions=questions,
instructions="只依据两种动物在给定场地下的实际对抗能力判断胜负。",
)
for i, challenger in enumerate(challengers):
if d[f"vs_{i}"].choice == challenger: # 冠军落败
return challenger # 丢弃后面所有推测回答
return champion # 冠军守擂到底实测结果是1,999 场对决在约 16 秒内跑完,总成本约 0.01 美元。这个数字比任何论述都更能说明问题:当你把"每一步都要做一次判断"的任务交给决策层,单次成本低到可以让"成千上万次模拟"变成一件随手可做的事。如果用生成模型逐场写一段"XX 因为……所以获胜"的解释,成本会是另一个量级。
详解二:Jev Chess——让非法着法变成不可能
Jev Chess 是一个很有趣的玩法:一张共享棋盘,整个互联网集体轮流执子,对手是 Jev。它把"类型安全"这件事展示得最直白——每一个合法着法都是同一道 Choice 题的一个选项,因此"走一步非法棋"在结构上就是不可能的,而不是"被拒绝"。
legal = board.legal_moves() # 引擎算出的全部合法着法
d = client.systemone(
state=board.to_fen(),
questions={
"move": {
"type": "choice",
"question": "这一局面走哪一步?",
"choices": [m.uci() for m in legal], # 选项就是着法空间本身
}
},
instructions="下出当前局面最合理的一步,评估子力与局面得失。",
)
move = board.parse_uci(d["move"].choice) # 返回值必然落在合法集合里它还做了一件值得称道的事:把概率画在棋盘上。返回的概率不是被藏起来,而是直接用来给棋子着色,让你一眼看到模型对这步棋有多笃定。更关键的是页面上的一个实时校准面板——它拿每一次声明的置信度去对照一个"一层的子力检查"(one-ply material check),也就是活生生地把"你说你有 80% 把握,结果真的对吗"这件事摆在用户面前。
这恰好呼应了决策层的一个核心理念:概率是可用信息,别丢掉。选中的那一项只是一个点,整个分布才是判断的完整形态。
详解三:kNES——900 次决策,一次都没越出菜单
kNES(ArturSkowronski)是一个用 Kotlin 写的 NES 模拟器,它的 agent 通过 SemIf(Jev 接口的开源实现)在本地 Qwen3.5-4B 上直接读屏,玩《超级马里奥兄弟》和《最终幻想》。要点同样是"选项即动作空间":这一回合适用的目标,直接成为那道类型化 Choice 题的声明选项,于是"按下游戏根本没提供的键"是不可能的,而不只是"概率很低"。
它的实测数据很有说服力:900 次记录的决策,每次约 400 毫秒(在 M5 Pro 上),从头到尾没有一次回答了菜单之外的选项。
# 本回合适用的按钮,就是这道题的选项集
options = env.legal_buttons()
d = client.systemone(
state=env.screen_text(), # 本地模型自己看画面
questions={
"button": {
"type": "choice",
"question": "这一步按哪个键?",
"choices": options, # 游戏当前真正可用的按键
}
},
instructions="选择能推进当前目标的操作。",
)
env.press(d["button"].choice) # 菜单外的按键不可能被选中把这三个项目放在一起,游戏的决策层有一个共性:合法动作集由引擎保证,Jev 只在其中做语义选择。这也是为什么在游戏这个场景里,"类型安全"不只是工程洁癖,而是直接对应着"不会卡在无效输入上"的可靠性。
更多项目一瞥
同一分类下还有一批项目,各自把决策用在不同环节:
- PlayJev(OmniJev):一个开源 0.8B 视觉语言模型,读入一张 448px 的游戏画面,在一次前向里返回游戏所列动作上的概率分布,不生成任何文本;遇到低置信度的步骤,就把它交给一个搜索程序兜底,覆盖十款浏览器游戏。
- jev-plays-pokemon(milanboers):把《宝可梦 红》的游戏状态读成文本,每一回合回答类型化问题,再由确定性代码把答案翻译成动作——注意这里控制流依然在代码手里。
- JEV-Star(sc2musa):在《星际争霸 II》的 35 张 SMAC-Hard 地图上用 Jev
Choice做宏观与微观控制,选出的动作要对着"当前可用动作"校验,并可选地遵循 GPT-6 写的长线计划。 - typesafe-mario(fhshaik)与 tsai-sc(phyous):前者从结构化模拟器状态里选动作,后者用键盘鼠标驱动原版《星际争霸》,并把每次决策的动作概率都记录下来。
- Jevtown(gaborishka):严格说是"观众模拟"——从 id 生成 10,000 个角色读一条动态,一次请求里问 60 道
Score题(谁会在意)加 7 道Noul审核题(0.5 就把内容挡在公开流外,0.85 直接拦截),再用分批Choice按 600 / 1,500 / 3,000 三波返回每个角色的反应,只有当"叫好"至少比"唱衰"多出这一波 0.1 的比例时,才把内容送入下一波。 - Jev Driver(reinhard-z):浏览器里的俯视驾驶游戏,Florence-2 给路面图像写标题,Cloudflare Worker 就着这条标题问 Jev 三道
Choice(动作、类别、限速),没有任何规则表去覆盖模型答案;线上 median 约 330 毫秒、每次决策 0.00004 美元。 - 还有 2048 × Jev(每步在四个方向上做
Choice,无启发式兜底,靠用户设定的置信阈值暂停交给人工)、AI Hold'em(完整合法走法与下注额构成Choice选项,扑克引擎校验后再落子)、Magic Jev Ball(20 个经典答案里Choice)等一堆例子。
公平性与一致性
游戏里的"公平"有两层。一层是规则公平——谁能格挡、伤害怎么结算,这些由引擎代码保证,和决策层无关。另一层是判断一致性:同一个局面,这一次和下一次、这个 NPC 和那个 NPC,应该得到相近的决策。如果同一个血量的守卫这次死守、下次掉头就跑,玩家会立刻感到"这个 AI 在乱来"。生成模型做这种判断时抖动很大——温度、措辞、上下文的细微差别都会影响它给出的动作;而决策层返回的是选项上的概率分布,同一局面下分布通常高度稳定,这一步天然更利于一致性。
Jev Chess 的实时校准面板把一致性摆到了明面上:它拿每一次声明的置信度去对照一个"一层的子力检查",长期公开地展示"你说你有这么多把握,到底准不准"。这种把声明的置信度与实际结果对照的做法,是维持一个可信对手的关键——它让玩家对 AI 的强度有稳定的预期,而不是面对一个时而天才时而失误的黑箱。想调难度也简单:不是换模型,而是调阈值和指令,把决策往保守或激进的一侧推,同时保持判断分布的稳定性。
大规模模拟里的成本账
游戏场景最能体现第 2 章那句"决策成本极低"的实际意义。当你要跑的是几千场、上万次的对局,或者上万个人物的反应,逐次调用生成模型的成本会直接爆炸;而决策调用的成本低到让"多跑几百次以求稳健"成为可行策略。
flowchart LR
S["引擎状态
(规则/血量/位置)"] --> L["代码算出
合法动作集"]
L --> J["Jev:在合法集上
给出带概率的 Choice"]
J --> A["代码执行
(确定、可校验)"]
A --> S
J -.低置信.-> H["交给搜索程序
或暂停人工复核"]上图是游戏决策层的通用骨架,也是本章三个详解项目的共同形状:状态喂进来,合法集由代码算,Jev 只在集合内做语义选择,低置信度再走一条兜底分支。它把"聪明"用一个软件工程能接住的容器装了起来。
小结
- 游戏里大量判断是"像个值得做的决定吗"这类语义题,写不成规则,而生成模型又会返回越界输出,正好落在决策层的主场。
- 把合法动作集直接声明成
Choice的选项,能让"选中不存在的动作"从"不太可能"变为"不可能",这是类型系统的保证,不是调参。 jev-torneo-animales用"一次请求问完 K 个挑战者、冠军落败即丢弃后续"的技巧,跑出 1,999 场约 16 秒、约 0.01 美元的成绩。Jev Chess把每个合法着法做成单选题选项,并用实时校准面板把每次声明的置信度对照一层的子力检查;kNES用本地模型跑了 900 次决策、约 400 毫秒/次、0 次越出菜单。PlayJev(0.8B VLM,448px 画面,无生成文本)、Jevtown(10,000 角色,60Score+ 7Noul)等项目说明:游戏与模拟天然适合"代码掌控制,模型只判定"。- 决策层极低的单次成本,让"成千上万次对局/模拟"的批量做法在经济上成立——这正是下一章实时应用要延续的成本优势。
下一章把镜头转向实时应用,看看当预算被压缩到 150 毫秒时,决策层要怎么组织本地模型、并行与批处理。