构建停止争吵的那个星期六
那些不会出现在截图里的修复,才是决定你的运行时能否发布的关键。RakuAI 用了一整周时间清除软肋,让接下来八个月的开发工作不必再和构建过程争吵。
有一类工作是不会产生截图的。它产生的是一个在曾经变红的机器上变绿的构建。它产生的是一个不再超时的 CI 运行。它产生的是一份不再出现的堆栈跟踪,因为导致它的空指针现在已经在边界处被捕获。这个刚结束的星期六所在的这一周,正是这样的一周。
十八个 DLL 在 Linux 上全部转绿
运行时以十八个原生 DLL 的形式发布。直到两周前,这些 DLL 在 Windows 上能干净地构建,在 macOS 上也能干净地构建,但在 Linux 上会因为一个符号可见性错误而失败。修复在 4 月 11 日的 PR #1463 中落地。
这是那种毫不起眼的故事。GCC 默认让共享库中的每一个符号都可见。MSVC 默认让每一个符号都隐藏,除非它被显式导出。运行时代码是按照 MSVC 的默认行为编写的,在需要跨越 DLL 边界的符号上使用了 __declspec(dllexport)。而在 Linux 上,这些声明是空操作,这意味着每一个符号都是可见的,这意味着链接器无法弄清楚正确的 DLL 内部绑定关系,这意味着十八个 DLL 中有一部分根本无法构建。
修复方法是给 GCC 构建加上编译器标志 -fvisibility=hidden,并在需要导出的符号上显式添加可见性属性。diff 很小,波及范围却很大。Linux 构建现在是一个一等公民目标。发布运行时改动的智能体现在可以让 CI 矩阵自动在 Linux 上进行测试。服务器部署、容器镜像、云端构建集群,所有这些现在都触手可及。
这是那种感觉像是税务减免的修复。它不会把产品向前推进,但它也不再阻碍产品前进。这就够了。
每一个 LLM 调用点上的提示词长度保护
四月份另一个值得记录的提交,是针对提示词长度保护的全面排查。运行时现在在好几个地方调用 Claude:NPC 行为大脑、对话模型、世界事件叙述者、程序化内容生成器。这些调用者中的每一个都是通过拼接上下文、指令和当前状态来构建提示词的。如果上下文增长得足够大,这些调用者中的任何一个都可能生成一个超出模型输入窗口的提示词。
朴素的失败模式是 LLM 调用返回一个错误。危险的失败模式是 LLM 调用返回一个被截断的响应,调用者没有检测到截断,引擎就依据一个畸形的答案采取行动。半句话说到一半就停下的 NPC。分叉到虚无之处的对话树。带着错误参数触发的世界事件。
PR #1465 给每一个 LLM 调用点加上了保护:在调用前估算 token 数量,如果数量超出预算就拒绝发送,并抛出一个调用者可以据此做出反应的结构化错误。PR #1466 审计了整个代码库,验证这个保护措施在每一个调用点都已就位。PR #1467 添加了文档,让下一个调用者知道这项纪律。
这件事之所以重要,是因为运行时内部的 AI 调用并非偶发事件。在峰值时它们每一帧都会发生很多次。一个让提示词每次调用多增长几个 token 的构建错误,在一百帧之内是不可见的,但在一千帧之内就是灾难性的。这个保护措施让失败变得响亮而有边界,而不是悄无声息、不受限制。
API 命名空间的稳定化
第三个提交是三者中最安静的一个,也可能是影响最深远的一个。运行时为外部工具、仪表盘和 SDK 提供了一个 REST API 供其调用。这个 API 在过去六个月里一直挂在 /api/v2/ 之下,因为它是早先一个仅供内部使用的 /api/v1/ 的继任者。
/api/v2/ 是一个错误的名字。它暗示 v1 曾经是公开的、现已被取代,但事实并非如此。它还暗示 v3 即将到来,但事实也并非如此。PR #1464 遍历了代码库中的每一处引用和文档中的每一处引用,把命名空间从 /api/v2/ 重命名为 /api/raku/。这个名字现在是稳定的。以后不再有版本号需要协商。命名空间内部的向后兼容性就是契约本身。
这是那种必须恰好发生一次、而且必须在任何外部开发者依据这个命名空间编写代码之前发生的重命名。我们及时发现了它。下一次团队之外的人写脚本调用这个 API 时,他们看到的会是 /api/raku/,而不必在六个月后重做一遍工作。
空指针安全作为一次全面排查
运行时上最近的一次提交,也就是两天前 4 月 30 日的 58c3a806,标题是「fix(build): resolve all build errors, warnings, and null-pointer safety」。标题中的「空指针安全」这个短语承载了大量信息。这个改动背后的实质,是对代码库进行一次全面排查,找出每一个可能为空却在没有检查的情况下被解引用的指针。
触发这次排查的是来自某个示例应用的一份崩溃报告。一个纹理加载器被传入了一个指向资源清单的空指针,对其解引用后崩溃了。修复方法是在入口点加上检查。而这次审计的问题是:代码库里还有多少地方存在同样的模式。
答案是有好几十处。并非所有这些都是可利用的漏洞。其中很多是从未被触发过的代码路径,因为调用代码碰巧从未传入过空值。但那不是一种防御。真正的防御是在边界处的检查。
这次排查添加了这些检查。它没有改变正常路径下的行为。它确实为失败路径添加了一个返回值(RAKU_ERR_INVALID_PARAM)和一行日志。运行时现在在那些以前会悄无声息崩溃的情况下变得更「吵」了。吵闹好过沉默。在一个 AR 运行时里,悄无声息的崩溃是最糟糕的一类 bug。
为什么这样的一周才是该有的一周
当你以周末节奏运营一个由智能体团队驱动的引擎项目时,诱惑在于不断发布新功能。每个星期六早上队列都会重新填满。每个星期六晚上的 diff 都比上一周更大。这种节奏奖励的是向前的动量。
这种节奏也容忍技术债。过去八个月里合并的每一个 PR,都对构建环境、API 表面、LLM 输入窗口或空指针安全契约做出了一个小小的假设。这些假设没有一个单独看是错的。但它们合在一起,就是一份迟早会出问题的软肋清单。
用一整周时间停下向前的动量去修复这些软肋,并不是浪费的时间。恰恰是这一周过后,接下来八个月的向前推进才能够不再和构建、API、LLM 输入或空指针争吵。争吵才是真正浪费时间的东西。而清理工作终结了这场争吵。
接下来是什么
智能体团队这个周末又回到了队列前。下一轮问题正是我这个星期六早上正在提交的那些。它们不再是清理类的问题了,而是功能类的问题。构建不会再和它们争吵了。
这就是一个干净的构建能换来的东西。不是一张截图,而是发布的许可。
在枯燥的事情上赢得信任的运行时
在每一个目标平台上都是绿色,API 保持稳定,失败响亮而不是悄无声息地崩溃。RakuAI 是一个以生产环境所要求的纪律构建而成的空间运行时。看看合作伙伴为什么选择在它之上构建。