系列: 与 AI 一起学编程

引擎如何与你带来的任何模型对话

AI 层是一个接口:带上任何模型,而无需重新链接引擎。

AI 作为接口,而非依赖 调用方在前。模型在后。一份小巧、稳定、笨拙的契约。 行为树 导航 人群仿真 感知 决策树 模型管理接口 — 句柄与张量 设备端 SLM 云端 LLM 学习策略 启发式 换一个模型。换一个供应商。这条线以上的一切都不需要改变。
每一条接口边界,都是一个未来的变化可以发生而不破坏调用方的地方。

带上你想要的任何模型。RakuAI 的 AI 层是接口,不是导入——这样模型市场每个季度闹一次"谁才是最好的"的脾气时,你的引擎完全不用在意。

这个周末有人问了我一个尖锐的问题。”如果 AI 是你引擎里的一个运行时原语,那引擎就被锁死在你接入的那个模型上了。下一次模型供应商有变动,你就得重写引擎。”

合理的担忧。而且是错的,但合理。

运行时里的 AI 原语不是某个特定的模型。它是一个接口。五个独立的子系统,各自有自己的 C API,各自都能被引擎的其余部分寻址,而不需要任何人知道是哪个模型、哪些权重,或者是哪条推理路径在产出答案。模型活在接口之后。调用方活在接口之前。两者之间的契约小巧、稳定,而且刻意保持笨拙。

这篇文章讲的是这条边界是怎么画出来的,以及为什么我从画出它这件事上不断获得更多的价值。

“AI 原语”实际指的是什么

我们运行时里的 AI 层不是一个东西。它是五个。

  • 行为树。 确定性执行层。在意图已知之后告诉智能体该做什么。
  • 导航网格与寻路。 “我该如何在这个空间中移动”这一层。
  • 人群仿真。 “许多智能体如何互相避让并协调一致地行动”这一层。
  • 感知系统与知觉。 “智能体观察到了什么”这一层。
  • 决策树与复杂 AI 逻辑。 “基于我所知道的一切,我想做什么”这一层。

每一个都是它自己的子系统。每一个都以自己的 DLL 形式发布。每一个都有一个公共的 C API。它们都不导入某个特定的模型。它们都通过各自的接口与引擎的其余部分对话,彼此之间也用同样的方式对话。

当一个 .raku 体验文件写着 "ai_behavior": "strafe" 时,它命名的不是一个模型。它命名的是一个已注册的行为。运行时解析这个字符串。这个字符串映射到一个实现。这个实现可以是一棵行为树,一棵决策树,一个学习到的策略,或者一个手写的启发式规则。调用方不知道是哪个。文件也不知道是哪个。实现可以在不动这两者的情况下被替换。

这就是整个诀窍所在。

模型管理层是它自己的子系统

当我们需要在运行时内部进行真正的机器学习推理时,我们没有把它硬塞进上面五个子系统中的某一个里。我们加了第六个关注点,带有它自己的 API 表面:模型管理。

// Roughly what the surface looks like, simplified for the post.
RakuModelHandle raku_ai_load_model(const char* model_id, RakuModelOptions opts);
RakuInferenceResult raku_ai_infer(RakuModelHandle h, const RakuTensor* input);
void raku_ai_unload_model(RakuModelHandle h);

一个想调用模型的行为树节点,要通过这个 API。不是通过导入某个供应商的 SDK。不是通过链接到某个特定的运行时。而是通过向模型管理子系统请求一个句柄,并使用它。

这意味着模型管理子系统是代码库中唯一知道具体模型格式、供应商或推理框架的地方。引擎里其他任何地方看到的都只是句柄和张量。换一个模型。换一个运行时。换一个供应商。其他一切都不需要改变。

这是不起眼的基础设施工作。也正是它让我们在 AI 生态每个季度闹一次”哪个模型才是新的最佳”的脾气时,不至于恐慌。

这条边界也保护着文件格式

看看任何 .raku 体验文件顶部的 ai 块。它有像这样的键:

"ai": {
  "dda_enabled": true,
  "target_flow_state": 0.7,
  "profiler_mode": "active"
}

这些名字没有一个是在命名模型。它们命名的是能力。”动态难度调整已开启。运行时应该以 0.7 的心流状态为目标。性能分析器处于活动状态。”运行时决定由哪些子系统参与来交付这些能力。如果这个季度正确的答案是一棵行为树,那就运行行为树。如果下个季度它变成了一个小型的设备端模型,文件不需要改变。

你在一个系统中画出的每一条接口边界,都是一个未来变化可以发生而不破坏调用方的地方。我们提前大胆地画出了这些边界。我们现在正在花掉这份提前投入换来的收益。

为什么这一点只会变得越来越重要,而不是越来越不重要

三个原因。

第一,模型市场一直在变动。 去年最好的模型是今年昂贵的那个。今年最好的是明年过时的。那些把某个模型供应商硬编码进引擎的团队,到现在大概已经做过两三次这种集成了。那些在中间放了一个模型管理接口的团队,只做过一次。

第二,设备端的重要性比以前更高了。 这个接口让我们能够在开发环境中针对一个服务器端模型、在生产环境中针对一个设备端模型,运行同样的 ai_behavior: "strafe"。调用方并不知道是哪一种。这种灵活性正是设备端推理能够在不重写体验层的情况下变得可行的唯一原因。

第三,开发循环中的 AI 助手也能从干净的边界中受益。 当我要求一个助手加入一个新行为时,它必须遵守的契约是已注册行为接口。而不是一团供应商 SDK 的乱麻。接口越干净,助手产出正确代码的速度就越快,我这边的审查成本也就越小。

这件事难在哪里

诚实地说说代价。

接口设计比实现要花更长时间。 跳过接口设计这一步、直接写一个能跑的版本,这种诱惑是真实存在的。要抵制住。你在这里走的每一个捷径,都会在之后需要替换实现时付出代价。

你必须在接口里该放什么这件事上保持纪律。 每一个参数都是一份你之后不容易打破的契约。加的参数要比你以为需要的更少。等到第二个使用场景出现,再让它告诉你什么才是真正通用的。AI 接口的第一版有五个参数,后来发现它们根本不该放在那里。之后再移除它们非常痛苦。

已注册的行为需要版本管理。"strafe" 在运行时的一个构建版本里意味着一件事,而在下一个构建版本里意味着略有不同的另一件事时,调用方会通过游戏性回归发现这一点。我们为行为打上版本号,并把 .raku 文件固定到具体的运行时版本上。这很烦人。但这是必要的。

有时候你确实想要那种依赖。 这是最离经叛道的一条。有些情况下,一个特定模型有一种通用接口无法表达的特定能力。诚实的做法是扩展接口,让这种能力变得通用,而不是让模型泄漏到调用方里。我们不止一次抓到自己想抄这个近道。

这个架构论点很直接。AI 是一个运行时原语。驱动它的文件格式命名的是能力,而不是模型。运行时把能力解析为当前能够交付它们的任何子系统。这三层之间的连线就是这个接口。这个接口小巧、稳定,而且刻意保持笨拙。

这就是这篇文章的全部内容。

回去继续构建。

带上你的模型。接口在等着你。

RakuAI 通过句柄和张量与任何模型对话——开发环境中在服务器端,生产环境中在设备端,调用方永远不知道其中的区别。看看你的权重是如何接入一个为了活过整个模型市场而构建的空间运行时的。

← 所有文章