[控场AI]
· 6 分钟阅读· 3,224 字

Claude移植Rust代码实测:9628条结果对不上,问题出在哪?

Claude移植Rust代码实测:9628条结果对不上,问题出在哪?

AI移植代码零警告却九成六不一致,真相藏在字节级默认行为里。

Anthropic将API从Python迁到Rust、代码多由Claude编写,引发“AI写的代码如何证明没错”的疑问。B站UP主用Claude Code复现了一个小实验:把流式输出拼接函数从Python移植到Rust,编译零警告,但首轮一万条用例中九千六百多条字节不一致。问题不在Rust逻辑,而在Python未写出的默认行为——JSON转义开关、字符与字节长度口径差异,以及比对工具自身的缺陷。修复后十一万条用例零差异。实验还表明移植期旧系统加功能会掩盖错误,单函数快七倍不能外推整套API,核心功夫在于把“一样”定义到字节级别。

Anthropic团队在一段访谈中透露,他们的API正从Python迁移到Rust,且大部分代码由Claude编写,团队里甚至没人会Rust。这个说法引发了一个关键疑问:AI搬出来的代码,怎么证明它没写错?

B站UP主“1024工程笔记”用Claude Code做了一个小规模复现实验:把一个将模型流式输出拼接成网页响应的Python函数逐句移植到Rust。编译一次通过,零警告,但第一轮测试跑下来,一万条用例里有九千六百多条不一致。问题不在Rust,而在Python旧代码里“一个字都没写”的默认行为。

这个实验的价值在于,它把“AI写代码靠不靠谱”的抽象争论,拉回到了可验证的工程细节上。

实验设计:把比对缩到一个函数上

完整复刻Anthropic的“影子流量”验证并不现实——那需要把线上请求复制一份给新系统跑,再比对结果。UP主没有线上流量,但把比对范围缩小到了一个函数:它负责把模型一小段一小段吐出的字,拼接成网页上一个个蹦出来的流式响应。

输入用随机生成,刻意塞入中文、emoji、反斜杠,以及从终端复制来的控制字符。两边输出一个字节一个字节地比。

两边输出一个字节

这个设计的关键在于“按字节比”而非“解析后比”。UP主后来总结:如果先解析成JSON再比较,前两个坑会直接漏过去。解析后相等,不等于字节相等。

三个编译器看不见的坑

坑一:JSON转义的默认开关

第一轮九千六百多条不一致,Rust代码哪一句都没写错。问题出在Python生成JSON时的一个默认开关:它会把中文、emoji等字符全部转成反斜杠U加编号的写法。Claude Code搬得很忠实,但这个默认值根本没写在Python代码里,AI翻不到。

结果是两边解析出来是同一个字符串,字节却不一样。谁要是拿响应体做签名或当缓存的键,这里就会出错。修完这个坑再跑,错误只少了四十多条——一条用例里常常同时踩着好几个坑,剩一个没修它就照样算错。

JSON标准(RFC 8259)只要求对引号、反斜杠和 U+0000 至 U+001F 的控制字符做转义,其余字符(包括中文、emoji)可直接以 UTF-8 原样输出。但 Python 的 json.dumps 默认 ensure_ascii=True,会把所有非 ASCII 字符转成 \uXXXX 形式——这是为了兼容只能处理 ASCII 的传输通道而设的历史默认值,并非 JSON 规范要求。Rust 的 serde_json 则相反,默认输出原始 UTF-8。两者解析后得到同一个字符串,字节序列却不同。这也解释了为什么很多团队会在序列化层显式指定 ensure_ascii=False,以及为什么跨语言移植时序列化配置必须作为契约单独核对,而不能指望从代码逻辑里读出来。

坑二:短写法与长编码

第二轮的锅由退格符和换页符背。Python把它们写成两个字符的短写法,Rust写成一长串编码。第三轮还剩九千四百多条,这次错在最后那个用量数字上。

RUST算出一百四十二

同一条消息,Python算出一百零三,Rust算出一百四十二,多了快四成。原因是两门语言都有一个拼写一模一样的长度属性,编译器不会报错,但Python算的是字符个数,Rust算的是字节个数。好比点名,一边数来了几个人,一边数占了几个座位——一个中文字一个人占三个座,所以中文越多,Rust这边的用量就越容易算大。放在真实API里,用量是拿来计费的。

JSON 允许两种等价的转义写法:短写法如 \b(退格)、\f(换页)、\n、\r、\t,以及通用长写法 \u0008、\u000c 等。二者解析结果完全相同,但输出的字节数不同。不同语言的 JSON 库在选择哪种写法上各有默认策略,这属于规范允许的自由度,编译器自然不会报错。凡是把响应体字节直接用于哈希、签名、ETag 或缓存键的系统,这类差异都会导致新旧系统对同一逻辑内容产生不同的键值,从而出现缓存击穿或签名校验失败——而这类问题在解析层比对中完全不可见。

Python 的 len() 作用于 str 时返回 Unicode 码点个数,因为 Python 3 的字符串是码点序列;Rust 的 str::len() 返回 UTF-8 编码后的字节数。同名的“长度”语义完全不同。对于计费、限流、截断等场景,需要明确采用哪一种口径:OpenAI 等 API 的 token 计量基于分词器而非字符或字节,但若系统内部用长度做配额或日志统计,口径不一致就会造成可观测的偏差。移植时应统一封装一个显式的 char_count 或 byte_len 函数,避免直接调用语言内建的 length 属性。

坑三:测试工具自己埋的雷

前三个坑修完,第四轮还剩三十九条。这次错的是UP主自己的比对工具:一个空字符串混进了输入格式,成了空字段,Rust以为一个片段都没有。换到线上也一样,请求怎么复制、怎么配对,每一环都是新写的代码,它会冤枉新系统,也会替新系统放过真错误。

输入格式修好

输入格式修好后,第五轮零条不一致。UP主又换了五个随机种子,每个跑两万条,加起来十一万条,一个字节都不差。

移植期间别加新功能

实验还做了一个延伸验证:模拟旧系统继续演进,给Python那边加了一个字段,Rust一个字没动,结果一万条全红。

访谈里提到,Anthropic移植期间只搬代码、不加功能。UP主理解原因就在这:新功能一混进来,该抓的移植错误就埋在一片红里,根本分不清哪条失败是移植引入的,哪条是新功能造成的。如果你的旧系统还在加功能,比对就得挂进每次提交,一改就能看见。

影子流量(shadow traffic)验证是这类迁移的标准做法:将生产请求复制一份发给新系统,只记录其响应而不返回给用户,再与旧系统响应做逐字段或逐字节比对。其有效性依赖两个前提——新旧系统在同一时间窗口处理同一输入,以及比对基线在此期间保持稳定。若旧系统同期还在变更行为,影子比对的红灯就无法归因,实验设计本身失效。这也是为什么迁移窗口期通常要求旧系统进入功能冻结,并把比对作为 CI 关卡而非一次性验收。

性能:单个函数快七倍,不代表整套API

UP主顺手量了速度:同一批用例跑二十遍,单个函数Rust快了差不多七倍。但访谈里说的是整套API的CPU少用四到五成。

不全是这种活吧

这个函数只做字符串拼接,是Rust最占便宜的活。整套服务里显然不全是这类工作。UP主的建议很务实:换语言之前,先量一量CPU在你的请求里占多大比例,再动手。

定义“什么叫一样”才是真功夫

这份Rust代码Claude Code很快就写完了,UP主的时间全花在定义“一样”上。他整理出的几条经验值得记下:

  • 比对要按字节来,先解析成JSON再比,前两个坑会直接漏过去;
  • 解析后相等不算数,字节相等才算;
  • 输入池里要放中文、emoji、控制字符、空字符串,只放英文单词的话,前三个坑一个都抓不到;
  • 每个分歧先判是移植错了,还是测试自己错了。

这其实回到了访谈里那个最初的问题:AI写的代码怎么证明没错?答案不是“编译过了”“测试过了”,而是你能否把“一样”定义到字节级别,并且承认比对工具本身也会犯错。

#Claude Code#Rust移植#AI编程#代码迁移#Python转Rust#影子流量#字节比对
分享:

相关推荐