1. 项目缘起:一个反直觉的AI编码实验
1.1 80万行Rust代码是怎么来的
先说清楚这个项目的背景。有一个团队做了一次相当激进的尝试:让AI Agent从零开始,把一个中等规模的TypeScript/Node.js后端服务完整迁移成Rust。不是写几个函数、补几个测试那种小打小闹,而是整个服务——包括HTTP路由层、业务逻辑、数据库访问、异步任务调度、错误处理、配置管理——全部重写。最终产出大约80万行Rust代码,覆盖了原本几十万行TypeScript所承载的全部功能。
这个数字听起来很唬人,但真正让我感兴趣的,不是“AI能写多少代码”,而是这个团队在复盘时反复强调的一件事:AI花在“读代码”上的精力,是花在“写代码”上的十倍以上。他们原本以为瓶颈在生成速度,结果发现瓶颈在理解——理解旧代码的意图、理解类型系统的约束、理解异步模型在两种语言之间的语义鸿沟。
这个结论对做AI Agent开发、做代码迁移工具、甚至只是日常用AI辅助编程的人,都有非常直接的参考价值。我把它拆开来讲,尽量还原这个项目里那些“文档不会写、但踩过才知道”的细节。
1.2 为什么是Rust,为什么是TypeScript迁移
选Rust作为目标语言,不是随便挑的。TypeScript到Rust的迁移,恰好踩中了几个最难的交叉点:
- 类型系统不对等:TypeScript是结构化类型,Rust是名义化类型加所有权系统。一个
interface在TS里可以随便扩展,在Rust里你得想清楚是trait、enum还是struct,还要处理生命周期。 - 异步模型差异:Node.js是单线程事件循环,Rust的
async是基于Future和运行时(比如tokio)的。Promise.all在Rust里对应join!还是try_join!,错误传播路径完全不同。 - 内存管理哲学:TS靠GC,Rust靠所有权和借用检查。AI在生成Rust代码时,最容易犯的错就是“按TS的思路写Rust”,结果编译器直接教做人。
这个项目之所以值得学,是因为它把“AI Agent在真实工程约束下如何工作”这件事暴露得很彻底。下面我按几个核心维度来拆。
2. 核心思路拆解:AI Agent的“读”与“写”比例为什么是10:1
2.1 读代码到底在读什么
很多人以为AI读代码就是“把源文件塞进上下文窗口”。实际远不止。在这个迁移项目里,AI Agent的“读”至少包含四个层次:
第一层:语法结构解析。把TypeScript的AST解析出来,识别出函数、类、接口、类型别名、导入导出关系。这一步相对机械,但量大。一个几十万行的TS项目,AST节点数量是千万级的。
第二层:语义依赖追踪。一个函数调用了另一个模块的导出,那个模块又依赖了第三个模块的类型定义。这种跨文件的依赖图,AI必须自己构建。否则它写出来的Rust代码会出现“引用了不存在的类型”或者“循环依赖”这类问题。
第三层:意图推断。这是最难的。TS代码里一个any类型,AI得猜它实际是什么。一个Promise链,AI得判断它是串行还是并行语义。一个try-catch,AI得决定在Rust里是返回Result还是直接panic。这些都不是语法问题,是工程判断。
第四层:约束提取。旧代码里隐含的业务规则——比如“这个字段不能为空”、“这个操作必须在事务里”、“这个超时是30秒”——AI得从代码、注释、测试用例里把这些约束挖出来,然后在Rust里用类型系统或运行时检查重新表达。
提示:如果你也在做类似的代码迁移,不要指望AI一次读一个文件就能写好。它需要反复读、交叉读、带着问题读。这个“读”的token消耗,往往是“写”的很多倍。
2.2 为什么“写”反而相对简单
一旦AI把上面四层“读”的工作做扎实了,写Rust代码反而变成了相对确定性的任务。因为Rust编译器本身就是一个极其严格的“审稿人”。你写错了,它不让你过。所有权问题、生命周期问题、类型不匹配问题,编译器会一条条指出来。
这个项目里有个细节很关键:AI Agent在生成Rust代码后,会进入一个“编译-报错-修正”的循环。平均每个函数要经历3到5轮编译修正才能通过。但每一轮修正,AI都在学习——它会把编译器的错误信息当作反馈信号,调整自己的生成策略。
这其实揭示了一个更普适的规律:在强类型、强约束的语言里,AI的“写”可以被编译器兜底;但在弱约束的环境里,AI的“写”就是灾难。这也是为什么这个项目选Rust反而比选Python或JavaScript更容易成功——Rust的编译器替人类做了大量验证工作。
2.3 十倍精力读代码的工程含义
“十倍”这个数字不是精确统计,而是一个量级判断。它意味着:如果你打算用AI做代码迁移,你的成本预算、时间预算、token预算,都要按“读为主、写为辅”来分配。
具体来说,一个典型的迁移任务里,AI的时间大概这样分配:
| 阶段 | 占比 | 主要工作 |
|---|---|---|
| 代码解析与依赖构建 | 30% | AST解析、模块图、类型映射 |
| 语义理解与意图推断 | 40% | 业务逻辑提取、约束识别、边界条件 |
| Rust代码生成 | 15% | 结构设计、函数实现、错误处理 |
| 编译修正与测试 | 15% | 编译循环、单元测试、集成验证 |
这个分布和很多人直觉相反。大家通常以为“生成”是大头,实际上“理解”才是。
3. 核心细节解析:AI Agent在迁移中到底怎么做
3.1 类型系统的映射策略
TypeScript到Rust的类型映射,是这个项目里最耗精力的部分之一。我整理了几种典型情况和对应的处理方式:
基础类型映射。string对应String或&str,number对应i32/i64/f64,boolean对应bool。这些看起来简单,但number在TS里是浮点数,在Rust里你得判断它到底是整数还是浮点。AI的做法是:先看这个值在代码里怎么用的,如果只做整数运算,就映射成i64;如果涉及除法或小数,就映射成f64。
联合类型映射。TS的string | number在Rust里对应enum。AI会生成一个枚举类型,每个变体对应一种可能。这比用serde_json::Value或者Box<dyn Any>要安全得多,但代码量会膨胀。
可选类型映射。TS的foo?: string对应Rust的Option<String>。这个映射很直接,但难点在于:TS里undefined和null有时候是混用的,AI得判断到底是“不存在”还是“存在但为空”。
接口与trait。TS的interface如果只是数据结构,映射成Rust的struct;如果有方法,映射成trait。但TS的接口可以被任意对象实现,Rust的trait需要显式impl。AI在这里经常需要生成额外的适配层。
泛型约束。TS的泛型约束比较宽松,Rust的trait bound更严格。AI需要把TS里的extends转换成Rust的where子句,有时候还要引入额外的trait来满足约束。
注意:类型映射不是一一对应的。有些TS类型在Rust里没有直接对应,AI需要做“类型重构”——把原来的类型拆成多个Rust类型,或者引入新的抽象。这部分工作很难自动化,需要人工review。
3.2 异步代码的迁移难点
Node.js的异步模型和Rust的异步模型,表面上看都是async/await,但底层语义差别很大。这个项目里,AI在处理异步代码时主要遇到几个问题:
运行时选择。Node.js自带事件循环,Rust需要选一个异步运行时。项目里用的是tokio,因为生态最成熟。AI在生成代码时,需要确保所有异步函数都在tokio的运行时里执行,不能混用其他运行时的API。
错误传播。Node.js里async函数抛出的异常会被Promise捕获,Rust里async函数返回Result。AI需要把TS里的throw转换成Rust的return Err(...),并且确保调用链上的每一层都正确处理了错误。
并发控制。TS里的Promise.all对应Rust的futures::join!或tokio::join!,但Rust的join!要求所有future都是Unpin的,有时候需要Box::pin。AI在生成这类代码时,经常需要额外处理Pin的问题。
取消语义。Node.js的AbortController和Rust的CancellationToken语义不同。AI需要把取消逻辑重新表达,确保在Rust里能正确传播取消信号。
阻塞操作。Node.js里可以用worker_threads做CPU密集任务,Rust里对应tokio::task::spawn_blocking。AI需要识别哪些操作是阻塞的,然后放到正确的执行上下文里。
这些细节,如果AI没有“读”透原来的异步逻辑,写出来的Rust代码要么编译不过,要么运行时有微妙的bug。
3.3 数据库访问层的重写
项目里原本用的是Node.js的ORM(类似Prisma或TypeORM),迁移到Rust后用的是sqlx。这部分的工作量很大,因为ORM的抽象层在Rust里没有完全对等的替代品。
AI的做法是:先把TS里的实体定义、关系映射、查询逻辑全部提取出来,然后在Rust里用sqlx的宏和查询构建器重新实现。sqlx支持编译时检查SQL,这对AI来说其实是好事——写错了SQL,编译就报错。
但难点在于:TS的ORM往往有懒加载、级联删除、事务嵌套这些高级特性,sqlx需要手动实现。AI在这里需要生成大量的辅助代码,比如连接池管理、事务封装、错误映射。
提示:如果你也在做Node.js到Rust的数据库迁移,建议先把ORM的查询日志打开,把实际执行的SQL全部抓出来。AI基于真实SQL来生成
sqlx代码,比基于ORM的抽象API要准确得多。
4. 实操过程:一个完整迁移任务的拆解
4.1 环境准备与工具链搭建
这个项目的基础设施不算复杂,但每一步都有讲究。我按实际顺序列一下:
Rust工具链。用rustup安装稳定版工具链,cargo作为构建工具。项目里用到了tokio、sqlx、serde、axum(HTTP框架)、tracing(日志)这几个核心crate。版本要锁定,避免AI生成的代码因为依赖版本变化而编译失败。
TypeScript解析工具。用ts-morph或@typescript-eslint/parser来解析TS代码,生成AST。AI Agent通过这个AST来理解代码结构。项目里还用了madge来生成模块依赖图。
AI Agent框架。项目里用的是一种“多轮对话+工具调用”的Agent架构。Agent可以调用“读文件”、“解析AST”、“搜索符号”、“生成代码”、“运行编译器”这些工具。每一轮对话,Agent根据当前状态决定下一步做什么。
编译反馈循环。这是关键。Agent生成Rust代码后,自动运行cargo check,把错误信息喂回给Agent,Agent修正后再检查。这个循环一直持续到编译通过。
测试框架。迁移后的Rust代码需要跑单元测试和集成测试。项目里用cargo test,并且把原来的TS测试用例也迁移过来,作为验证标准。
4.2 迁移流程的六个阶段
整个迁移不是一次性完成的,而是分阶段推进。我把它归纳成六个阶段:
阶段一:代码扫描与清单生成。AI先扫描整个TS项目,生成一份“迁移清单”——哪些文件要迁移、哪些模块有依赖关系、哪些类型需要映射。这份清单是后续所有工作的基础。
阶段二:类型定义迁移。先把所有的interface、type、enum迁移成Rust的struct、enum、trait。这一步不涉及业务逻辑,但决定了后续代码的类型基础。
阶段三:工具函数与基础设施迁移。把日志、配置、错误处理、HTTP客户端这些基础设施先迁移过去。这些模块被业务代码广泛依赖,先迁移可以减少后续的阻塞。
阶段四:业务逻辑迁移。按模块逐个迁移业务逻辑。每个模块迁移完后,立即跑测试验证。AI在这里的工作量最大,因为它需要理解每个函数的业务意图。
阶段五:数据库层迁移。把ORM相关的代码迁移到sqlx。这一步需要特别小心,因为SQL的正确性直接影响数据。
阶段六:集成与端到端测试。所有模块迁移完后,跑完整的集成测试,确保新旧系统的行为一致。
每个阶段都有AI的参与,但参与程度不同。阶段一和阶段二AI可以高度自动化,阶段四和阶段五需要大量人工review。
4.3 关键参数与配置示例
项目里有一些配置细节值得记录。比如Cargo.toml里的依赖配置:
[dependencies] tokio = { version = "1", features = ["full"] } sqlx = { version = "0.7", features = ["runtime-tokio", "mysql", "macros"] } serde = { version = "1", features = ["derive"] } axum = "0.6" tracing = "0.1" tracing-subscriber = "0.3" thiserror = "1" anyhow = "1"sqlx的数据库连接池配置:
let pool = MySqlPoolOptions::new() .max_connections(20) .min_connections(5) .acquire_timeout(Duration::from_secs(10)) .idle_timeout(Duration::from_secs(600)) .connect(&database_url) .await?;这些参数不是随便定的。max_connections根据数据库的实际承载能力来定,acquire_timeout要略大于业务的最长查询时间,idle_timeout要小于数据库的wait_timeout,避免连接被数据库端断开。
AI在生成这些配置时,需要参考原来的Node.js连接池配置,以及数据库的实际参数。如果AI没有读到这些信息,它生成的配置可能就是拍脑袋的。
4.4 编译修正循环的实操记录
这是整个项目里最有意思的部分。AI生成Rust代码后,编译报错是常态。我记录了几类典型错误和AI的修正方式:
所有权错误。AI经常写出“移动后使用”的代码。比如把一个String传给函数后,又试图使用它。编译器的错误信息会指出具体位置,AI的修正方式通常是加.clone()或者改成传引用。
生命周期错误。AI生成的函数签名有时候缺少生命周期标注,或者标注不正确。编译器会提示“missing lifetime specifier”,AI需要根据函数体的实际使用情况来补全。
trait bound错误。AI调用的方法在某些类型上不存在,因为缺少trait实现。编译器会提示“the trait bound is not satisfied”,AI需要引入对应的trait或者换一种实现方式。
异步错误。AI有时候会在非异步函数里调用异步函数,或者忘记.await。编译器会提示“future cannot be awaited”,AI需要调整函数签名或调用方式。
类型不匹配。AI生成的类型和期望的类型不一致,比如i32和i64混用。编译器会提示“expected i64, found i32”,AI需要加类型转换。
每一类错误,AI都会在修正后重新编译,直到通过。这个循环平均每个函数要跑3到5轮,复杂的函数可能要10轮以上。
提示:编译修正循环的token消耗很大。如果你在做类似项目,建议把编译器的错误信息做一下过滤和摘要,只把关键部分喂给AI,避免上下文被错误信息撑爆。
5. 常见问题与排查技巧实录
5.1 AI生成代码的典型问题速查表
| 问题类型 | 表现 | 排查思路 | 解决方式 |
|---|---|---|---|
| 所有权误用 | 移动后使用、借用冲突 | 看编译器错误位置 | 加clone、改引用、调整作用域 |
| 生命周期缺失 | 函数签名报错 | 看函数体的实际借用关系 | 补生命周期标注、改所有权 |
| 异步语义错误 | future未await、运行时混用 | 检查函数签名和调用链 | 统一运行时、补await |
| 类型映射错误 | i32/i64混用、String/&str混用 | 看类型定义和调用点 | 加转换、统一类型 |
| 错误处理不一致 | Result/panic混用 | 看错误传播路径 | 统一用Result、定义错误类型 |
| 数据库查询错误 | SQL语法或类型不匹配 | 看sqlx编译错误 | 修正SQL、调整参数类型 |
| 并发安全问题 | Send/Sync不满足 | 看跨线程传递的类型 | 加Arc/Mutex、调整设计 |
5.2 几个踩过的坑
坑一:AI会“幻觉”出不存在的API。比如它可能生成tokio::spawn_blocking的某个不存在的参数,或者sqlx的某个不存在的宏。这类错误编译器会直接报“no function found”,AI修正时通常会换成正确的API。但如果AI连续几次都幻觉同一个API,就需要人工介入,给它一个正确的示例。
坑二:AI对TS的隐式类型转换理解不足。TS里1 + "2"会得到"12",Rust里直接编译不过。AI在迁移这类代码时,有时候会忽略类型转换,导致编译错误。解决方式是:在迁移前,先把TS代码里的隐式转换显式化。
坑三:AI生成的错误类型过于复杂。Rust的错误处理可以用thiserror定义枚举,也可以用anyhow做动态错误。AI有时候会生成一个巨大的错误枚举,包含几十个变体,维护起来很痛苦。建议在项目初期就定好错误处理策略,让AI遵循。
坑四:AI对测试用例的迁移不够重视。项目里发现,AI倾向于把测试用例也“翻译”成Rust,但翻译后的测试有时候失去了原来的验证意图。建议测试用例的迁移要人工review,确保验证的是行为而不是实现。
坑五:AI在长上下文里会“遗忘”早期约束。当迁移任务持续很长时间,AI的上下文里积累了太多信息,它可能会忘记最初定下的类型映射规则。解决方式是:把关键约束写成“系统提示”或者“项目规范”,在每一轮对话里都带上。
5.3 人工review的重点
AI生成的代码,哪些必须人工看?我总结了几条:
- 涉及资金、权限、数据删除的逻辑:这些地方AI出错代价太大,必须人工逐行确认。
- 并发和锁的使用:AI对Rust的并发安全理解有时候不到位,容易写出死锁或数据竞争的代码。
- 错误处理的边界:AI倾向于“能编译过就行”,但错误处理是否合理,需要人工判断。
- 性能敏感路径:AI生成的代码可能功能正确但性能差,比如不必要的clone、低效的循环。
- 数据库查询:SQL的正确性和性能,必须人工验证。
6. 这个项目对AI Agent开发的启示
6.1 “读”的能力比“写”的能力更重要
这个项目最核心的启示就是:AI Agent的价值,不在于它能生成多少代码,而在于它能理解多少代码。生成代码这件事,随着模型能力提升,会越来越便宜。但理解代码——理解意图、理解约束、理解上下文——才是真正的瓶颈。
对做AI Agent开发的人来说,这意味着:你的Agent需要强大的“读”工具。AST解析、依赖分析、符号搜索、类型推断,这些能力比“生成”能力更值得投入。
6.2 强约束环境是AI的好朋友
Rust的编译器是一个极其严格的“审稿人”。它不接受模糊、不接受歧义、不接受“大概能跑”。这种强约束环境,反而让AI的工作变得可验证、可迭代。
相比之下,在JavaScript或Python这种弱约束环境里,AI生成的代码可能“看起来对”,但运行时才暴露问题。这种反馈循环太慢,AI很难从中学习。
所以,如果你在做AI辅助编程的工具,优先选择那些有强类型、强约束的语言和框架。编译器会帮你做大量的质量把关。
6.3 迁移是AI Agent的最佳练兵场
代码迁移这个场景,天然适合AI Agent:有明确的输入(旧代码)、有明确的输出(新代码)、有明确的验证标准(编译通过+测试通过)。而且迁移过程中,AI需要大量“读”的操作,正好锻炼它的理解能力。
如果你在学习AI Agent开发,我建议从一个小型迁移任务开始——比如把一个Python脚本迁移成Rust,或者把一个JavaScript工具函数迁移成TypeScript。不要一上来就搞几十万行的项目,先从几百行开始,把“读-写-编译-修正”这个循环跑通。
6.4 工具链的配套比模型本身更关键
这个项目里,AI模型本身的能力当然重要,但更关键的是围绕它的工具链:AST解析器、依赖图生成器、编译器反馈循环、测试框架。这些工具让AI的“读”和“写”都有了落脚点。
如果你在搭建自己的AI Agent,不要只关注模型选型。花更多时间在工具链上——怎么让Agent高效地读文件、怎么把编译错误结构化地喂回去、怎么管理长上下文里的关键约束。这些工程细节,往往决定了项目的成败。
7. 我个人的几点实操体会
做类似项目的时候,我踩过几个坑,也总结了几条经验,不一定对,但都是真金白银换来的。
第一,不要追求“全自动”。一开始我也想着让AI一口气把整个项目迁移完,结果发现上下文根本撑不住,AI到后面就开始胡言乱语。后来改成“分模块、分阶段、人工review”,效率反而高得多。AI适合做“批量理解”和“初稿生成”,但关键决策和边界情况,还是得人来。
第二,把约束写成文档。项目里定下的类型映射规则、错误处理策略、命名规范,我都写成了一个MIGRATION_GUIDE.md,每次让AI生成代码前,先把这份文档塞进上下文。这样AI就不会“忘记”之前的约定。这个习惯看起来笨,但省了很多返工。
第三,编译错误要“摘要”后再喂给AI。一开始我把完整的cargo check输出直接给AI,结果错误信息太长,AI的注意力被分散了。后来我写了个小脚本,把错误按文件、按类型聚合,只把最关键的信息喂回去。AI的修正效率明显提升。
第四,测试用例要“行为化”。原来的TS测试里有很多“实现细节”的断言,比如“这个函数被调用了3次”。迁移到Rust后,这些断言往往不适用。我后来把测试用例改写成“行为断言”——只验证输入输出,不验证内部实现。这样AI迁移起来更容易,测试也更稳定。
第五,留出“人工兜底”的时间。不管AI多强,总有一些代码它搞不定——复杂的生命周期、微妙的并发逻辑、业务规则密集的模块。这些地方,我都是自己上手写。把AI当成一个“高级助手”,而不是“替代者”,心态会好很多,项目也更容易成功。
最后再分享一个小技巧:如果你也在做代码迁移,建议先把旧代码里的“坏味道”清理一遍——重复代码、死代码、过度抽象——再让AI迁移。AI会忠实地把坏味道也迁移过去,甚至放大。先清理,再迁移,事半功倍。