1. 当AI把80万行Rust代码交到你手上真正的功课才刚开始前阵子有个事在圈子里讨论得挺热一个团队让AI Agent把一整个中型项目的代码库从一种语言迁移到Rust最后产出了大约80万行代码。消息传开之后大部分人的第一反应是AI写代码的速度已经到这个程度了第二反应是这代码能跑吗、能维护吗。但真正让我停下来想了很久的是另一个细节——这个项目里团队花在让AI读懂现有代码上的精力大约是花在让AI写新代码上的十倍。这个比例非常反直觉。我们平时聊AI Agent聊的都是生成、补全、自动修复好像AI的价值就在于往外吐代码。但任何一个真正做过大型代码迁移的人都知道迁移这件事的难点从来不在写而在读。你得先搞清楚旧代码里每一个模块在干什么、依赖关系长什么样、哪些是历史包袱、哪些是隐藏的契约然后才能决定新代码该怎么写。AI再能写如果读不懂上下文写出来的东西就是一堆看起来很像样、实际上到处漏风的空壳。这篇东西我想聊的就是这个为什么在AI辅助的代码迁移里读的投入应该远大于写以及具体怎么把这件事做扎实。关键词里提到了Rust、AI Agent、TypeScript、Node.js、代码迁移我会围绕这几条线展开但重点不在某个具体工具的用法而在整套让AI读懂代码库的方法论。如果你正在做或者准备做跨语言迁移或者你只是好奇AI Agent在真实工程里到底怎么用才不翻车这篇应该能给你一些能直接抄作业的东西。先说清楚适用人群如果你是完全没碰过代码迁移的新手这篇会让你少走至少半年的弯路如果你已经做过几次迁移但每次都被改完之后到处是bug折磨这篇里的排查链路和验证方法应该能帮到你如果你只是对AI Agent的能力边界感兴趣那你可以把这篇当成一个真实工程案例来看看看AI在理解这件事上到底能做到什么程度、又在哪里必须靠人兜底。2. 为什么读代码在AI迁移里是十倍投入的那一环2.1 迁移的本质是语义翻译不是语法替换很多人对代码迁移的想象是这样的把Java的for循环换成Rust的for循环把TypeScript的interface换成Rust的struct把Node.js的require换成Rust的use。如果迁移真的只是这种级别的替换那AI确实可以一天干完80万行。但真实的迁移完全不是这么回事。我举个最简单的例子TypeScript里一个函数返回PromiseUser | null你在Rust里对应写什么OptionUser那异步呢async fn返回ResultOptionUser, Error错误类型用什么原来的null在业务语义上到底代表没找到还是出错了还是未初始化这些问题语法替换解决不了必须回到代码的语义层面去理解。AI Agent在这件事上的价值恰恰是它能同时看很多文件把散落在各处的语义线索拼起来。但前提是你得给它拼的素材也就是让它先把代码读进去。这就是为什么读的投入是写的十倍——写只是最后一步的输出读才是整个迁移的认知基础。2.2 旧代码里的隐性契约AI不读就一定会漏我做过一个从Node.js迁移到Rust的项目规模不算大大概十几万行。迁移到一半的时候发现一个诡异的问题某个接口在旧代码里偶尔会返回一个空数组新代码里对应返回了None结果下游一个统计模块直接panic了。查了半天才发现旧代码里那个空数组其实是一个隐性契约——下游模块约定空数组代表这个时间段没有数据但统计逻辑仍然要跑一遍初始化。这个约定没有写在任何文档里也没有类型能表达它只存在于两处代码的配合关系里。AI如果只读单个文件永远发现不了这种契约只有让它把调用方和被调用方一起读把数据流串起来它才能识别出这里返回空数组不是bug是feature。这就是读的核心价值把隐性契约显性化。而隐性契约的数量在任何一个存活超过两年的代码库里都远远超过你的想象。我粗略估过一个中等复杂度的业务系统显性接口契约可能有几百个隐性契约至少是这个数的三到五倍。AI写代码的时候如果不知道这些契约写出来的东西就是定时炸弹。2.3 AI Agent的读和人的读不是一回事这里要澄清一个误区。很多人觉得让AI读代码就是把代码喂给模型然后问它这段代码干什么的。这是最浅层的读效果也最差。AI Agent的读应该是一个主动的、结构化的过程我把它拆成三层第一层是结构层模块划分、依赖关系、调用链路。这一层AI做得比人快因为它可以瞬间遍历几千个文件画出完整的依赖图。但你要给它正确的工具比如让它用cargo metadata或者自己写脚本解析AST而不是让它凭记忆瞎猜。第二层是语义层每个函数、每个类型到底在业务上代表什么。这一层AI需要人的引导因为业务语义往往不在代码里而在需求文档、注释、甚至提交历史里。你得把这些上下文一起喂给它。第三层是意图层这段代码为什么这么写是为了性能为了兼容某个历史版本还是单纯因为当年写的人偷懒这一层最难AI基本只能靠推断而且经常推断错。我的做法是意图层的东西必须由人来标注AI只负责把标注和代码对应起来。三层读下来你会发现读的工作量确实远大于写。但正是这三层读透了后面写的时候才能一气呵成不用反复返工。3. 把代码库喂给AI之前先做这四步预处理3.1 第一步用依赖分析工具画出真实的模块边界在让AI读任何代码之前我做的第一件事永远是跑一遍依赖分析。以Node.js项目为例我会用madge或者dependency-cruiser生成完整的依赖图然后重点看三样东西循环依赖、跨层依赖、以及那些被大量引用的事实上的核心模块。为什么这一步不能省因为AI读代码是按文件读的它不知道哪些文件重要、哪些是边缘的。如果你直接把几万个文件丢给它它会平均用力结果就是核心模块读得不够深边缘模块浪费了大量token。有了依赖图你就可以按重要性排序让AI优先读那些被引用最多的模块。我通常会把依赖分析的结果整理成一张表标注每个模块的入度被多少模块依赖、出度依赖多少模块、以及是否在循环依赖里。入度高的模块就是迁移的重中之重必须让AI读透出度高的模块往往是工具类迁移相对独立循环依赖则是必须优先打破的否则迁移顺序没法排。3.2 第二步用类型信息给AI搭骨架TypeScript项目有一个天然优势类型信息丰富。但很多人不知道怎么用。我的做法是先用tsc --declaration把所有类型声明导出来然后让AI基于这些声明去理解每个模块的对外契约。这里有个技巧不要一次性把所有类型都喂给AI而是按模块分组。每个模块的类型声明单独成组让AI先读这个模块的类型再读实现。这样它读实现的时候脑子里已经有了这个模块对外承诺了什么的框架理解起来会准得多。对于没有类型信息的旧代码比如纯JavaScript我会先让AI根据使用情况反推类型。具体做法是找出所有调用这个函数的地方看调用方传了什么参数、怎么用返回值然后让AI归纳出一个类型签名。这个过程很费时间但一旦做完后面迁移的时候就是照着类型写效率极高。3.3 第三步把测试用例当成行为规格说明书旧代码里的测试用例是AI理解代码行为最宝贵的素材。因为测试用例是唯一一种既描述了输入输出、又描述了边界条件的文档。我通常会让AI先读测试再读实现而且要求它回答一个问题这个测试到底在验证什么行为举个例子一个函数有十个测试用例其中八个测正常路径两个测异常路径。AI读完这十个用例之后应该能总结出这个函数在什么情况下返回什么、在什么情况下抛什么错。这个总结就是迁移时的行为规格。新代码写完之后把旧测试改写成Rust测试跑一遍行为对上了迁移才算基本成功。注意如果旧代码没有测试或者测试覆盖率很低那你在迁移前必须先补测试。没有测试的迁移就是盲人摸象AI写得再快也没用。3.4 第四步标注不要动的区域这一步最容易被忽略但极其重要。任何存活一段时间的代码库里都有一部分是看起来可以优化、但实际上不能动的。可能是为了兼容某个老版本客户端可能是绕过了某个底层库的bug可能是性能敏感的热点路径。我的做法是在预处理阶段就让团队里的人把这些区域标出来写成一份禁区清单。然后让AI读代码的时候明确告诉它哪些区域是禁区迁移时保持原样或者只做最小改动。没有这份清单AI会好心办坏事把那些看似冗余的代码优化掉结果引入一堆难以排查的问题。4. 让AI真正读懂Rust目标形态的三个关键动作4.1 先让AI读目标语言的惯用法再让它写这是我在踩了无数次坑之后总结出来的最重要的一条。如果你直接让AI把TypeScript翻译成Rust它会写出用Rust语法写的TypeScript——到处是RcRefCellT、到处是clone()、错误处理用unwrap()、异步用block_on。代码能跑但完全不是Rust的味道性能和维护性都很差。正确的做法是在迁移开始之前先让AI读一批高质量的Rust代码建立对Rust惯用法的认知。读什么我通常选三类标准库的核心模块源码、一个同领域的知名开源项目、以及团队自己写的Rust代码如果有的话。让AI读完之后总结出这个领域的Rust代码通常怎么组织、怎么处理错误、怎么管理生命周期。这个读目标语言的步骤我一般会花整个迁移周期的百分之十左右。看起来是额外投入但它能让后面百分之九十的写代码工作质量提升一个档次。AI有了Rust的语感之后写出来的代码会自然地用Result而不是panic用迭代器而不是手写循环用所有权而不是到处clone。4.2 用对照阅读建立迁移映射表具体到每个模块的迁移我用的方法是对照阅读让AI同时读旧代码和新代码的骨架然后生成一张映射表。这张表里每一行是旧代码里的一个概念对应新代码里的一个概念以及迁移时需要注意的事项。比如旧代码里有一个UserService类有getUser、updateUser、deleteUser三个方法。映射表里就会写UserService对应Rust里的UserRepositorytraitgetUser对应async fn get_user(self, id: UserId) - ResultOptionUser, UserError注意事项是旧代码里getUser返回null表示未找到新代码用Option表达但调用方需要区分未找到和出错。这张映射表是迁移的施工图。有了它写代码的时候就是按图索骥不用每次重新思考。而且这张表本身就是最好的文档迁移完成后留给团队后面维护的人一看就懂。4.3 让AI自己解释为什么这样迁移每迁移完一个模块我会让AI写一段说明这个模块在旧代码里是怎么工作的、新代码里是怎么实现的、为什么选择这种实现方式、有哪些地方和旧代码行为不完全一致。这个动作有两个好处。一是强迫AI把理解显性化如果它解释不清楚说明它没真读懂那就得回去重读。二是这些说明积累起来就是一份高质量的迁移文档比事后补写的文档准确得多。我印象很深的一次AI在解释一个模块时写道旧代码里这个缓存没有过期时间是因为上游数据每天凌晨全量刷新所以缓存可以一直用到第二天。新代码里我加了过期时间因为Rust的缓存库默认需要TTL但我把TTL设成了24小时行为和旧代码一致。这段解释让我发现了一个我原本没注意到的隐性契约也让我确认了AI确实读懂了。5. 迁移过程中最容易翻车的五个地方5.1 错误处理的语义漂移这是最高频的问题。旧语言里的错误处理往往很随意比如JavaScript里一个函数可能返回null、可能抛异常、可能返回undefined调用方靠if (!result)一把抓。迁移到Rust之后Option和Result是严格区分的如果你不仔细对应就会出现该报错的地方返回了None该返回None的地方panic了。我的应对方法是在映射表里专门加一列错误语义逐个函数标注旧代码里的错误情况对应新代码里的哪种类型。这个工作很枯燥但省不得。我见过一个项目因为没做这一步迁移后线上出了十几次空指针panic最后不得不回滚重做。5.2 异步运行时的行为差异Node.js的事件循环和Rust的async运行时比如tokio在行为上有微妙但重要的差异。比如Node.js里一个没被await的Promise会继续执行而Rust里一个没被spawn的future根本不会跑。再比如Node.js的setTimeout和tokio的sleep在精度和调度顺序上也不一样。这些差异在单元测试里往往看不出来一到生产环境高并发的时候就暴露。我的做法是在迁移异步代码时专门写一批并发测试模拟高负载场景对比新旧代码的行为。另外所有涉及超时、重试、取消的逻辑都要单独review确保语义一致。5.3 字符串和编码的坑TypeScript的字符串是UTF-16Rust的String是UTF-8。大部分时候没问题但一旦涉及字符串索引、切片、长度计算就会出问题。比如旧代码里str.length返回的是UTF-16码元数Rust里str.len()返回的是字节数对于一个包含中文的字符串这两个值完全不同。我在迁移时养成了一个习惯所有涉及字符串长度、索引、切片的代码全部标红逐个检查。能用chars().count()的地方就用不能用地方就明确注释这里依赖字节长度。这个习惯帮我避免了好几个隐蔽的bug。5.4 依赖库的能力不对等旧项目用的某个npm包在Rust里可能没有完全对应的crate。这时候AI往往会找一个差不多的库替代但差不多往往意味着某些边界行为不一致。比如日期时间处理JavaScript的Date和Rust的chrono在时区、夏令时、闰秒上的处理就不完全一样。我的做法是对于每一个替换的依赖库都让AI列出一份行为差异清单然后针对这些差异写测试。如果差异太大宁可自己写一个薄封装也不要用一个行为不一致的库硬凑。5.5 AI的自信错误这是最危险的一类问题。AI有时候会非常自信地写出一个错误的迁移而且解释得头头是道。比如它可能把旧代码里一个有副作用的函数当成纯函数迁移或者在处理生命周期时想当然地加了一个static编译能过但语义错了。对付这个问题我的经验是永远不要相信AI对这段代码干什么的解释只相信它给出的证据。它说这个函数是纯函数那就让它指出所有调用点确认没有副作用它说这个生命周期没问题那就让它写出具体的借用场景。没有证据的解释一律当作猜测处理。6. 迁移完成后的验证怎么确认80万行代码真的能用6.1 行为对拍新旧代码跑同一批输入迁移完成后我做的第一件事是行为对拍。具体做法是把旧代码和新代码都跑起来喂同一批输入对比输出。输入要覆盖正常路径、边界条件、异常情况。输出要逐字节对比不能只看大概对。对于有状态的服务对拍会复杂一些需要把状态也纳入对比。我的做法是在关键状态变更点打日志对比新旧代码的状态变更序列。如果序列一致基本可以确认行为一致。这个对拍过程我通常会跑至少三轮第一轮用单元测试的输入第二轮用生产环境的采样数据第三轮用专门构造的极端输入。三轮都过了才敢说迁移基本成功。6.2 性能基准Rust不该比Node.js慢迁移到Rust的一个主要动机就是性能。如果迁移后性能反而下降了那说明迁移方式有问题。我会在迁移前后各跑一套基准测试对比关键路径的延迟和吞吐。这里要注意Rust的性能优势不是自动获得的。如果AI写出来的代码到处clone、到处ArcMutex、异步用得一塌糊涂性能可能还不如Node.js。所以基准测试不只是验证也是发现迁移质量问题的手段。哪个模块性能不达标就回去看那个模块的迁移代码通常能找到明显的优化点。6.3 内存和并发的压力测试Rust的内存安全是编译期保证的但内存泄漏和并发死锁仍然是运行期问题。我会用valgrind或者heaptrack做内存分析用loom做并发模型的验证再配合长时间的压力测试观察内存增长和线程状态。这一步在Node.js迁移里尤其重要因为Node.js是单线程事件循环很多并发问题在旧代码里根本不存在迁移到Rust的多线程模型之后才会暴露。我见过一个项目迁移后出现死锁查了一周才发现是旧代码里一个反正单线程不会出问题的共享状态在多线程下成了死锁源头。6.4 灰度发布和回滚预案不管验证做得多充分上线时我都会走灰度。先切百分之一的流量到新代码观察错误率、延迟、资源使用确认没问题再逐步放大。同时准备好回滚预案一旦出问题能快速切回旧代码。灰度期间要重点监控那些行为对拍没覆盖到的场景比如真实的用户输入、真实的网络条件、真实的并发模式。这些场景往往能暴露出测试环境发现不了的问题。7. 这套方法迁移到其他语言对也成立吗7.1 从Java到Go读的重点在并发模型Java到Go的迁移语法层面的差异不大真正的难点在并发模型。Java的线程池、Future、CompletableFuture对应到Go的goroutine、channel、context语义差异很大。这时候读代码的重点就要放在并发逻辑上哪些操作是原子的、哪些共享状态需要保护、哪些阻塞调用需要改成非阻塞。我的做法是在预处理阶段专门做一份并发地图标注每个模块的并发模型、共享状态、同步机制。然后让AI基于这份地图去读代码理解每个并发原语在业务上的作用。7.2 从Python到Rust读的重点在动态特性Python到Rust的迁移最大的挑战是Python的动态特性鸭子类型、猴子补丁、元类、运行时反射。这些在Rust里都没有直接对应。AI读代码的时候必须识别出哪些地方依赖了动态特性然后决定怎么在Rust里用trait、泛型、宏来替代。我通常会先让AI做一份动态特性清单列出所有依赖动态行为的地方然后逐个设计替代方案。这个过程很费脑子但比迁移到一半发现某个核心模块没法用静态类型表达要好得多。7.3 从C到Rust读的重点在所有权C到Rust的迁移表面上看最接近实际上最微妙。因为C的程序员习惯了手动管理内存而Rust的所有权系统要求你重新思考每一个资源的生命周期。AI读C代码的时候必须理解每个指针的所有权语义谁拥有、谁借用、什么时候释放。我的经验是C迁移到Rust读代码的时间要占到整个项目的百分之七十以上。因为所有权设计一旦错了后面全是返工。我通常会让AI先画出一份所有权图标注每个资源的生命周期然后再开始写Rust代码。8. 我在这个项目里踩过的三个具体坑第一个坑是过度信任AI的依赖分析。项目初期我让AI自己分析模块依赖它给出的依赖图和实际差了很多因为它只看了import语句没看动态require和运行时注入。后来我改用工具生成依赖图再让AI基于工具的输出做语义分析准确率才上来。这件事的教训是AI擅长语义理解不擅长精确的静态分析两者要分工。第二个坑是一次性迁移太大。我们一开始想按模块迁移但模块之间的耦合比预想的大迁到一半发现新旧代码必须共存接口对不上。后来改成按垂直切片迁移每次迁移一个完整的业务功能从入口到存储全部换掉新旧代码通过API边界隔离。这样虽然单次迁移量大但边界清晰不容易出问题。第三个坑是忽略了构建系统。Node.js的构建和Rust的构建差异很大我们迁移到后期才发现CI流程要重写依赖缓存策略要调整交叉编译要配置。这些工作如果提前规划可以并行做不占关键路径。后来我把构建系统的迁移提前到项目初期整个节奏就顺了。9. 给准备做AI辅助迁移的人几句实在话如果你问我这个项目最大的收获是什么我会说AI在代码迁移里的价值不在于它能写多少行而在于它能读多少上下文。80万行代码听起来很吓人但如果这80万行是在充分理解旧代码的基础上写出来的那它就是资产如果是在没读懂的情况下硬生成的那它就是负债。具体到操作上我的建议是把整个项目的时间预算重新分配一下。如果你原本打算花百分之二十的时间读代码、百分之八十的时间写代码那把它反过来。读代码的那百分之八十里又有一半要花在让AI把读懂的东西表达出来上——写映射表、写迁移说明、写行为规格。这些文档看起来是额外工作实际上是迁移质量的保证。还有一点不要指望AI一次读对。我通常会让AI对同一个模块读三遍第一遍读结构第二遍读语义第三遍读意图。每遍读完之后让它写总结三遍总结对上了才算真读懂。这个过程很慢但比写完再返工快得多。最后分享一个我一直在用的小技巧让AI在迁移每个模块之前先写一段如果我是旧代码的作者我为什么这么写的推测。这段推测不需要准确它的作用是强迫AI从作者视角去理解代码而不是从翻译视角去替换代码。实测下来这个动作能让迁移质量明显提升尤其是对那些写得很绕的历史代码。代码迁移这件事说到底是一场理解力的较量。AI给了我们前所未有的理解工具但理解本身仍然需要投入。那十倍的精力花得值。
