小时候玩魂斗罗,我最熟练的不是枪法,是那条秘籍:上上下下左右左右 BA。按出来 30 条命,然后带着 30 条命,在一个瀑布前面死光。

三十年后,我又把那盘卡带翻了出来——这次它是个压缩包。而我没有再练枪法,换了个思路:让 AI 替我打。

一、小时候打不过的那一关,三十年后我想改掉它

小时候的《魂斗罗》是借来的卡带,能玩多久取决于别人什么时候来要。那会儿最流行的说法是:只要按出 30 命,就没有过不去的关。事实证明是有的,瀑布那段,我三十条命全交在那儿,连为什么死都没看明白。

所以拿到压缩包的第一反应特别朴素:改它。改什么呢?

  • 无限血,别再被一枪打死
  • 子弹超级猛,一路平推
  • 顺手把人物改成生化危机的样子

折腾一圈之后,版本收敛成了三个:原版、30 命版、无敌不掉命版。这是小时候最想要的东西,一次给足。

意外收获是另一个:这套老游戏能被整个塞进一个网页里跑。浏览器里挂一个 NES 模拟器,把 ROM 用 base64 内联进去,再加一层键盘映射,产出一个 600 多 KB 的 HTML 文件,双击就能玩,能全屏,键位按游戏机的来——右边那两排 BABA 我映射成了 UIJK,选择和开始是 FG。

改完那天我坐在电脑前玩了半小时,然后承认了一件事:我打不过,跟难度关系不大。给我 30 条命,我还是死在那道瀑布上。

二、自己手还是不行,那就让 AI 上

承认手残之后,想法反而通了:既然我自己打不过,那找个不手残的来打——之前不是接过一个决策模型吗。

于是新开了一个页面。不是我玩的那个试玩页,而是一个"观战页":左边是游戏,右边是一块 AI 面板,只做两件事——把游戏状态发给模型,把模型的决定变成按键。

那一瞬间的感觉挺奇怪的:我小时候求的是 30 条命,现在我求的是一个人替我按方向键。

三、先说清楚:决策模型到底是个什么东西

在动手之前,得先说明白它不是"和 AI 聊天"。

平时用的对话模型,是你一句它一句,输出长度随便,还得你自己去理解。决策模型是另外一种用法:你把当前局面丢给它,它一次前向直接吐回一份结构化的答卷——每道题只给一个答案,可能是一个选择、一个概率,或者一个分数。

TypeSafe 给这类东西起了个名字,叫 System One 模型:借的是"系统一/系统二"的比喻——聊天、写代码、做推理是慢思考,那是 System Two;游戏里每半秒要做的"往左还是往右",是快思考,是 System One。它家的第一个模型就叫 Jev,观点很直接:软件自动化要的可能不是聊天,是判断。

决策模型不是"决策模型"这个叫法好听。它的意思是:这个模型的输出天然就是用来落地的。你不用问它"你觉得该怎么办呀",直接问它"往左还是往右",它就得选一个。放到游戏里,这份答卷能一行代码翻译成手柄按键——这才是它和聊天模型最大的区别。

它和聊天模型的三点不同

第一,输出不是文章,是答案。聊天模型是一个字一个字往外写的,后一个字要等前一个字,这叫自回归。你让它做判断,它也得先写一段话,程序还得从话里把结论抠出来。决策模型把写话这步砍了:你提前定好要哪几个字段,它直接给结构化结果,不会多写一句解释。

第二,多个问题一次问完、并行返回。传统模型问五个问题要写五段话;Jev 这类模型是五个答案同时出来,上下文只组装一遍,延迟只付一次。这就是为什么它能做到端到端 70 到 500 毫秒,输入 每百万 Token 0.042 美元、输出目前免费——它省掉的恰恰是最贵的那部分:逐字生成的输出 Token。

第三,每个答案都带概率,而且概率是练过的。比如"投诉 92%、退款 5%、咨询 3%"。Jev 背后的方法叫 RLCD(面向校准决策的强化学习),就是冲着 RLHF 去的:RLHF 是让人投票,训练模型生成更讨人喜欢的答案;RLCD 不讨好人,要诚实——如果它对一类判断长期说有 90% 把握,那这批判断最好真的对九成左右。打个比方就像天气预报说明天 90% 下雨,报 10 次得有 9 次真下,才叫准。这样程序才能写死逻辑:95% 以上直接执行,70% 左右交给更强模型再看一眼,40% 左右直接转人工。AI 就变成程序里的一个模糊 if 语句,随叫随到。

顺带澄清一个最容易误解的宣传:"零幻觉"。此幻觉非彼幻觉。我们常说的幻觉,是编出不存在的人名论文;TypeSafe 说的,是模型生成了程序没定义过的输出——比如只允许选"投诉、退款、咨询",普通模型突然回一个"严重投诉"还加一大段解释,人看着只是啰嗦,自动化系统直接不知道怎么处理。Jev 从源头砍掉这种可能:所有输出和类型提前定好,只能三选一,不会临时创造第四个。官方注明,0% 不是测出来的,是靠 schema 匹配保证的。真正的"答错"它照样会,只是会诚实地告诉你"我只有 40% 把握"。

Request:你是怎么"问"它的

Jev 的接口小到离谱,整个 API 就一个端点:

POST https://api.typesafe.ai/v1/systemone
Authorization: Bearer <API_KEY>
Content-Type: application/json

请求体只有三个字段:model、state、questions。拿魂斗罗举例,大概长这样(字段是我按游戏简化过的,结构和官方完全一致):

{
  "model": "jev-latest",
  "state": {
    "lives": 3,
    "x": 1820, "y": 144,
    "dx_last_30f": 24, "stuck": false,
    "weapon": "M",
    "bullets_on_screen": 2,
    "enemies": [
      {"dx": 40, "dy": -8, "kind": "soldier"},
      {"dx": 96, "dy": 0, "kind": "turret"}
    ]
  },
  "questions": {
    "move":   {"type": "choice", "instructions": "往哪走?",
               "criteria": {"left": "向左移动", "right": "向右推进", "stay": "原地不动"}},
    "jump":   {"type": "noul", "instructions": "现在该不该跳?",
               "criteria": {"true": "前方需要起跳", "false": "继续跑就行"}},
    "up":     {"type": "noul", "instructions": "要不要抬枪口向上射击?"},
    "down":   {"type": "noul", "instructions": "要不要下蹲躲子弹?"},
    "danger": {"type": "score", "instructions": "当前有多危险?",
               "criteria": ["安全", "有点怪", "危险", "马上要死"]}
  }
}

这里有三个细节值得品一下:

  • state 就是模型的"眼睛",可以是字符串,也可以是对象/数组。客服场景里它是一封邮件正文;游戏里它是一坨结构化数字。它看不见游戏画面,只能通过这些数字理解世界——后面坑一会证明,这句话每个字都是代价。
  • questions 是你起的名字,答案会原样挂回来。key 本身不送进模型、不影响推理,只是为了让你在代码里好对号入座。
  • 三种题型正好对应我的五个问题:choice 是三选一(往左/往右/原地,最多支持 255 个选项);noul 是 calibrated yes/no,返回 0 到 1 的概率(跳不跳、抬不抬枪口);score 是有序打分,传 2 到 10 个等级(危险度 0–3 分)。五个问题一次问完,是这个模型最舒服的用法——上下文只组装一遍,决策一次拿全。
问题Jev 题型回答形式它要说清的事我怎么用
move 往哪走choice三选一往左 / 往右 / 原地按住对应方向键
jump 跳不跳noul概率现在该不该跳概率过 0.45 就按 A
up 上瞄noul概率要不要抬枪口按住上
down 下蹲noul概率要不要趴下按住下
danger 危险度score0–3 分现在多危险只用来展示,不按键

Response:它是怎么"回"你的

返回同样简单:model、answers、usage。继续上面的例子,一次真实的返回大概长这样:

{
  "model": "jev-1.13.0",
  "answers": {
    "move":   {"type": "choice", "choice": "right",
               "probabilities": {"right": 0.81, "stay": 0.12, "left": 0.07},
               "confidence": 0.74},
    "jump":   {"type": "noul", "noul": 0.43},
    "up":     {"type": "noul", "noul": 0.22},
    "down":   {"type": "noul", "noul": 0.08},
    "danger": {"type": "score", "score": 2.6,
               "legend": {"0": "安全", "1": "有点怪", "2": "危险", "3": "马上要死"},
               "probabilities": {"0": 0.02, "1": 0.08, "2": 0.25, "3": 0.65},
               "confidence": 0.58}
  },
  "usage": {"input_tokens": 312, "output_tokens": 18}
}

程序拿到这个,直接写分支就行,不用解析、不用正则、不用等它"说完":

ans = r.json()["answers"]
hold("right" if ans["move"]["choice"] == "right" else ...)
if ans["jump"]["noul"] > 0.45: press_A(frames=22)

注意 jump 那行:0.43,那就是坑二里坑边的那个数字——模型其实觉得"有点该跳",只是没过阈值,代码就当"不跳"处理了。校准概率的好处在这里体现得最清楚:它不替你拍板,它把把握交出来,拍板的阈值(0.5 还是 0.45)留给你调。而 danger 的 2.6 分虽然只用来展示,却是全文最诚实的一行:它知道情况不对(3 分拉满 0.65),但不知道该干嘛。

如果我想自己做一个"决策模型",有思路吗

Jev 的 RLCD 训练细节、奖励函数、系统性校准指标都没公开,架构和并行采样器也只给了名字。但这事有趣的地方在于:质疑它的人,反而把路指出来了。结合社区已有的复刻和论文,大概有三条路,从易到难:

思路一:拿小模型做"并行 JSON 分类器",不做任何新训练。 发布不到一天,开发者 Harsha Gundala 就用 Qwen2.5-1B 做了个类似版本:多个 JSON 字段并行判断、直接算概率,原话很挑衅——"他们隐身两年,我隐身两小时"。原理不神秘:冻结一个 1B 左右的小模型,每个问题接一个独立的输出头(choice 接分类头、noul 接 sigmoid、score 接有序回归),一次前向同时算所有头,再用 softmax 归一出概率。这条路做不出 Jev 的校准,但能做出它的"形状":结构化、并行、便宜,延迟一样能进几百毫秒。

思路二:监督微调 + 温度缩放,先解决"敢不敢信概率"。 开源复刻里最常见的做法:拿历史数据(比如我攒的那 67 组请求返回,外加人工标注的"这里真该跳")做监督微调,让模型先答对;再用温度缩放(temperature scaling)做后处理——在验证集上只调一个温度参数,把过分自信的概率压回来。这一步不涨准确率,只涨诚实度,但对自动化恰恰是最值钱的一步:阈值 0.45 到底该设多少,只有校准过的概率才配谈。

思路三:照着 OpenJev-RLCD 的论文,做"先校准、再强化"。 有篇复现论文把 RLCD 拆成了两步,名字就叫 calibrate-then-reinforce:第一步让模型在自己的推理后面接一个"读数头",用严格真值评分规则(比如 log loss、Brier 分数)训练这个头——答对且自信给高分,答错还嘴硬给重罚;第二步再用同样的分数做奖励,去优化前面的推理过程,同时用 KL 约束锚住第一步学到的诚实,别让它为了答对又开始吹牛。标准 RL(只奖对错)会把模型训得极度自信,这篇论文的价值就是证明了:校准是可以被当成奖励函数直接训出来的,代价是你得准备大量带 outcomes 的判断题,而不是聊天记录。

一句话总结这三条路:形状(并行多头)最好抄,诚实(校准)最难训。Jev 真正想聊的也不是某个训练 trick,而是提醒我们:过去几年什么分诊、打标签、路由的小事全塞给通用大模型,有点杀鸡用牛刀——那批数量最大、最无聊、但对延迟和成本最敏感的活,值得一个"闭嘴只打勾"的模型。

四、把它请进游戏:一次前向,五个问题

光有模型不够,接口两边都得有人干活。

左边:怎么把游戏讲给它听。 每 30 帧(大概半秒),我从模拟器内存里读一份状态打包发过去:还有几条命、死过几次、人在哪、上一段往哪边挪了、是不是原地卡住了、手里什么枪、屏幕上有几发自己的子弹、附近有哪些敌人(这个后面会出大事)。这些就是模型的眼睛——它看不见游戏画面,只能通过这些数字理解世界。

右边:怎么把答卷变成按键。 模型的回答是一份数据,得翻译成"按住右键、这 22 帧里按着 A"。跳到顶用完整弧线,半截的跳跳不过坑,这点吃过亏。

有一件事我刻意没有交给模型:开火。

这游戏里 B 键不自动连发,想连射得自己按节奏。而敌人是 60fps 动的,用半秒一拍的节奏去决定"要不要开枪",等于拿算盘追高铁。实测更直接——模型给出的开火概率长期停在 0.3 上下,连阈值都够不着,于是从头到尾一枪不放,站着挨打。干脆下放给页面:每 10 帧自动连点两下,问题当场消失。

于是一整套被拆成了两个环:

慢环每半秒问一次模型,管"往哪走、那边危不危险"这种需要判断的事;快环每一帧跑一次规则,管"脚下要空了""卡住了""掉下去"这种需要反应的事。分工之后,模型专心做判断,反应交给代码——这是我在这件事上学到的最实用的一条经验。

五、AI 的战报:它玩得怎么样

先说结论:比我强,但也只是强一段。

开局特别顺。它能自己按 START、跳过开场动画、清掉桥上的敌人,边走边打,一路推过第一段桥区,进到瀑布区。那段路我看着还挺感动——三十年前我在这儿死了一百次,它像散步一样走过去了。

然后它到了瀑布。

接下来 90 秒里,它死了 17 次,其中 15 次是摔死的;每一次都摔在同一个地方附近。面板上能清清楚楚看到它在"想":跳的概率在 0.3 到 0.49 之间来回晃,危险度打到 3 分,方向不变——它知道情况不对,但不知道该干嘛。

所以战报可以写得很短:能走、能打、能自己清路,但只要离开平地,就废了。

六、坑,一个接一个

真正花时间的都在这里。三个坑,一个比一个深。

坑一:它是个瞎子

跑起来之后我发现它有个很怪的毛病:完全不躲坑。

于是我把每一次请求和返回都存盘,攒了 67 组真实往返,一组组看。答案在数据里躺着:发给它的状态里有坐标、位移、命数、子弹,唯独没有任何地形信息。

为什么会这样?因为我们扫屏幕用的是模拟器的精灵层——敌人、子弹、道具都在那里,而地面、平台、坑全在背景层。所以模型能说出"右边 40 像素有两个敌人",却看不见"你脚前面就是悬崖"。

处理: 把精灵层的实体簇也扫出来,按相对位置塞进状态里,让它至少知道身边有什么。效果: 它开始会绕开敌人、会对着敌人开火了,但对悬崖依然一无所知——这一坑只能算填了一半。

坑二:犹豫的阈值

翻数据时看到一个巧合:坑边的跳,概率几乎都是 0.41、0.43、0.47 这种数字。而阈值是 0.5,差一点点,就是"不跳"。

它其实判断对了——那地方确实该跳——只是概率给得保守,卡在线上就没执行。

处理: 阈值从 0.5 降到 0.45。效果: 平地上明显更敢跳了,坑边还是犹豫——因为它犹豫的不是"该不该跳",而是"我根本不知道前面有坑"。

坑三:给它看清地形,它就能过吗?

到这一步,直觉告诉我很简单:既然它瞎,那就把地形写进状态里。比如加一个"前方脚下有没有地"。

但也有人提了个更狠的思路:这活儿干脆别问模型,规则层直接执行"前方没地 + 正在向右走 = 立即大跳"。

两边都有道理,那就做实验——这也是整件事里最值得的一步。

先解决公平性:真接口每次返回都不一样,跑两遍的差异到底来自我的改动还是它的脾气,说不清。所以拔掉真接口,换成一个确定性的假模型:永远选向右、从不主动跳,完美复现那个走进坑的盲模型;再让受测组做一个理想化的假定——只要状态里写着"前方无地",就以 0.9 的概率立刻跳(这是"写进状态"这条路的理论天花板,真模型只会更差)。

四组各跑 90 秒,跑三轮,结果一致:

组别90 秒死亡其中摔死规则层跳了几次
基线(无地形信息)17150
写进状态17150
规则层直接跳14122
两个都开17143

"写进状态"那组,90 秒里它一共看到 24 次「前方无地」,也老实跳了 24 次——死亡数和基线一分不差,一次都没救回来。

为什么会这样?两个原因,第二个才是要害。

一、时间根本来不及。 从"看得见缺口"到"掉下去",只有 11 帧左右的滑跑距离;而模型 30 帧才拍一次脑袋。就算它 100% 判断正确,指令送到时人也已经飞出去了——这游戏空中按跳无效。慢环再聪明,也追不上快环的意外。

二、这段路的杀招根本不是平地挖坑,是平台之间的落差。 探测往下扫,一看更深处有地板,就判"前方有地",不跳,继续走——然后掉进致命的深水。只看一层的探测,对这种地形等于没装。

处理与效果: 规则层直接跳留下来(默认开着),死亡从 17 降到 14;"写进状态"这条路留着但不指望。真正要过瀑布,需要的是一张平台高度图——每层平台在哪、哪个落点安全,才谈得上规划。这一条我记在了下一步。

七、总结:AI 已经很强了,但还差一张地图

这件事做下来,我的感受挺复杂的。

先说强的地方,而且是真强:它能读懂五个问题,能给出概率,能权衡危险度,能自己按开始键、自己清路、自己过桥。一个游戏,我们没有给它写一行"该怎么玩"的代码,它是自己看着状态决定往哪走的。 这在几年前是论文里的东西,现在是我在浏览器里跑着玩的东西。

但也必须说实话:它过不了那道瀑布。 不是因为它笨,而是因为它看不见——它拿到的世界里没有地形这一维,你在状态里漏掉的每一维,都会在游戏里变成一个坑,而且它会以你最不容易察觉的方式反复死给你看。

这大概就是现在 AI 的真实位置:能力已经很强,但 corner case 还在。强的部分让人兴奋,弱的部分非常具体——具体到你能量出它死在哪个像素、差几帧、缺了哪一维信息。

所以下一步我很清楚:不是换一个更聪明的模型,而是给它一张地图。这其实是从"让模型猜"变成"给模型看"——左边那半张地图(地面、平台、落点)补上,右边的判断才有意义。

小时候的我是靠 30 条命硬闯,现在的 AI 有 30 条命也过不去。不过它比我强的一点是:它每死一次,都留下一份数据。而数据攒够了,那道瀑布迟早会变成一串坐标。