一个反直觉的发现
用 AI 写代码,选什么语言最高效?
直觉会说 Python——语法简单、AI 训练数据多、生成即运行。但在过去几个月用 Claude Code 开发了 https_proxy 和 trans_proxy 两个 Rust 项目之后,我的结论恰恰相反:
Rust 可能是最适合 AI 编程的语言。
不是因为 AI 写 Rust 从不犯错——它犯的错不少。而是因为 Rust 的编译器会在毫秒级内把错误精确地拍回来,形成一个极其高效的反馈循环。Python 代码生成后"看起来对了",但 bug 可能藏在运行时的某个角落;Rust 代码只要编译通过,一大类错误就已经被消灭了。
这篇文章以这两个项目的实际开发经历为线索,讨论为什么 Claude Code + Rust 会成为一种更好的编程范式。
编译器是 AI 最好的搭档
类型系统:自动验证 AI 的输出
先看 trans_proxy 中上游代理协议的定义:
enum ProxyProtocol {
HttpConnect,
Socks5(ProxyAuth),
}
enum ProxyAuth {
None,
UsernamePassword { username: String, password: String },
}
当 AI 生成处理上游连接的代码时,它必须覆盖 ProxyProtocol 的每个变体:
match &proxy.protocol {
ProxyProtocol::HttpConnect => { /* HTTP CONNECT 隧道 */ }
ProxyProtocol::Socks5(auth) => { /* SOCKS5 握手 */ }
}
如果 AI 遗漏了 Socks5 分支,或者在 Socks5 里忘记处理 UsernamePassword 认证——编译器直接报错,连运行的机会都没有。这不是 lint 警告,不是 best practice 建议,是硬性编译失败。
在 Python 中,同样的逻辑可能写成一串 if/elif,遗漏一个分支只会在运行时某个特定条件下触发 KeyError。AI 不会告诉你它漏了什么,因为它自己也不知道。
Borrow Checker:并发安全的自动证明
trans_proxy 的 DNS 模块大量使用跨任务共享状态:
// DNS 查询表:IP → 域名的映射
let dns_table: Arc<RwLock<HashMap<Ipv4Addr, String>>> = ...;
// 查询合并器:避免重复请求
let coalescer: Arc<QueryCoalescer> = ...;
AI 生成的异步代码经常涉及跨 tokio::spawn 边界的数据共享。在 Go 或 Python 中,数据竞争是运行时的偶发事件,可能在压力测试中才偶尔浮现。在 Rust 中:
- 忘记
Arc包装?编译失败——所有权无法跨线程转移 - 忘记
RwLock?编译失败——不能在多线程中可变借用 - 在持有锁的情况下
.await?Clippy 直接警告死锁风险
Borrow checker 把"代码审查中需要人类凭经验发现的并发 bug"变成了"编译器自动检测的类型错误"。对于 AI 生成的代码,这意味着无需信任 AI 的并发推理能力——编译器会替你验证。
穷举 Match:消灭遗漏
https_proxy 的隐身检测是一个典型例子:
pub fn is_proxy_request(req: &Request<Incoming>) -> bool {
if req.method() == Method::CONNECT {
return true;
}
if req.version() == Version::HTTP_2 {
return false;
}
req.uri().authority().is_some()
}
这段逻辑看似简单,但背后的判断链条非常精确:CONNECT 一定是代理请求;HTTP/2 非 CONNECT 一定不是(因为 :authority 伪头始终存在);HTTP/1.x 看 URI 是否包含 authority。
当 AI 生成这类分支逻辑时,Rust 的穷举 match 确保了每个 Method、每个 Version 变体都被考虑到。如果未来 hyper 库新增了 HTTP/3 的 Version 变体,所有未覆盖的 match 会自动变成编译错误——而不是静默地走进一个错误的 fallback 分支。
互补关系:AI 擅长什么,Rust 补足什么
AI 和 Rust 编译器各自有明显的强项和弱项,恰好形成互补:
| AI 擅长 | AI 不擅长 | |
|---|---|---|
| 模式识别 | 见过数百万个 HTTP 解析器实现,生成新的得心应手 | 判断某个特定实现在边界条件下是否正确 |
| API 记忆 | 精确记忆 tokio、hyper、reqwest 的 API 签名和用法 | 确保 API 调用的顺序和组合在所有执行路径上都正确 |
| 样板代码 | 秒级生成 serde 序列化、clap 参数定义、错误类型转换 | 保证生成的类型定义在整个项目中一致 |
Rust 编译器精确地补上了右列的每一项:
- 边界条件:
Option<T>强制处理空值,Result<T, E>强制处理错误 - 执行路径:穷举 match 确保所有分支被覆盖
- 全局一致性:类型系统确保修改一处接口后,所有调用方都必须适配
以 https_proxy 的配置系统为例。AI 生成了完整的配置结构体:
#[derive(Debug, Deserialize, Serialize, Clone)]
struct Config {
listen: String,
domain: String,
acme: AcmeConfig,
users: Vec<UserConfig>,
#[serde(default)]
stealth: StealthConfig,
#[serde(default)]
fast_open: bool,
}
#[derive(Deserialize)] 和 #[serde(default)] 这类注解,AI 比大多数人都记得准。但如果 AI 在某处把 users 的类型从 Vec<UserConfig> 改成了 HashMap<String, String>,所有使用 users 的代码会立即编译失败——不需要人去全局搜索哪里用了这个字段。
效率杠杆:秒级反馈循环
cargo check 的魔力
下图对比了两种反馈循环的差异:
左侧的传统工作流中,人是瓶颈——阅读、理解、评估一段代码可能需要几分钟。右侧的 Rust 工作流中,cargo check 秒级完成类型检查,Claude Code 直接读取编译器输出并自动修复,整个循环不需要人的介入。
一个典型场景:开发 trans_proxy 的 SOCKS5 支持时,AI 第一轮生成的代码在 ProxyAuth::UsernamePassword 分支中忘记了对 username 长度做 u8 范围检查(SOCKS5 RFC 1929 要求用户名不超过 255 字节)。cargo check 通过了,但 cargo test 中的边界测试捕获了问题。AI 看到测试输出后,第二轮就生成了正确的验证逻辑。从错误到修复,整个循环不到 30 秒。
对比:动态语言的反馈延迟
如果同样的项目用 Python 写:
- 类型错误?要等运行时触发,或者依赖 mypy(覆盖率通常不到 100%)
- 并发 bug?可能在压力测试中偶现,难以稳定复现
- API 不匹配?import 成功不代表调用正确,要等实际执行到那一行
这些延迟的反馈意味着 AI 的错误可能一路传播到后面的代码中,修复成本指数级增长。Rust 的编译器把尽可能多的验证前移到编译期,让 AI 的每一轮迭代都从一个已验证的基线出发。
实战案例:两个项目的开发历程
https_proxy:10 个模块的协作
https_proxy 的代码组织成 10 个模块:
src/
├── main.rs # 入口与 CLI
├── config.rs # YAML 配置解析
├── tls.rs # ACME 证书与 TLS
├── stealth.rs # 隐身伪装检测
├── auth.rs # Basic Auth 认证
├── proxy.rs # CONNECT 隧道与 HTTP 转发
├── net.rs # TCP 连接与 Fast Open
├── service.rs # hyper 服务层
├── setup.rs # TUI 配置向导
└── lib.rs # 模块导出
这个结构不是一开始就设计好的。开发过程大致是:
- 我描述需求(What):"我需要一个 HTTPS 正向代理,支持 ACME 自动证书、隐身伪装、多用户认证"
- AI 生成初始代码(How):根据需求生成模块结构和核心逻辑
- 编译器反馈(Correctness):类型不匹配、生命周期错误、未处理的 Result——逐一修复
- 我补充约束(Why):"隐身检测需要区分 HTTP/1.1 和 HTTP/2,因为 HTTP/2 的
:authority语义不同" - 迭代收敛:AI 修改 → 编译 → 测试 → 反馈 → AI 修改
整个过程中,我几乎不写具体的 Rust 代码。我的工作是定义需求、解释领域知识、评审架构决策。AI 负责把这些意图转化为类型安全的实现。编译器确保实现不会偏离类型系统定义的契约。
trans_proxy:系统编程的挑战
trans_proxy 更有挑战性,因为它涉及大量平台相关的系统编程:
- macOS 上通过
DIOCNATLOOKioctl 查询 pf NAT 表 - Linux 上通过
SO_ORIGINAL_DSTgetsockopt 获取原始目的地址 - DNS 协议的二进制解析(手动字节操作)
- SOCKS5 三步握手的状态机
这些场景下,AI 的 API 记忆能力大放异彩——ioctl 的参数结构、socket option 的常量值、DNS 报文的偏移量,这些细节 AI 比人记得准。而 Rust 的类型系统确保了:
// DNS 查询合并:broadcast channel 的类型签名
// 确保发送端和接收端传递的数据类型一致
let (tx, _) = broadcast::channel::<Vec<u8>>(1);
AI 生成的 DNS 线格式解析器(parse_query_name、parse_a_records、extract_min_ttl)使用了大量的字节索引操作。这类代码容易出现 off-by-one 错误,但 Rust 的数组边界检查在运行时会 panic 而不是静默越界——即使编译器没能在编译期捕获,运行时也不会产生内存安全问题。
新范式:人、AI、编译器的三角分工
传统编程是人对着编辑器,把脑中的逻辑翻译成代码。AI 编程助手出现后,很多人的使用方式是"AI 写初稿,人来改"——本质上还是人在做 Correctness 验证。
Claude Code + Rust 打开了一种不同的分工模式:
人负责 What 和 Why:
- "我需要一个透明代理,拦截网关流量通过上游 CONNECT 代理转发"
- "DNS 要支持 DoH,因为传统 UDP 有污染风险"
- "隐身检测要区分 HTTP 版本,因为 HTTP/2 的语义不同"
AI 负责 How:
- 生成 tokio 异步服务器骨架
- 实现 SOCKS5 握手状态机
- 编写 DNS 报文解析器
- 处理平台差异的条件编译
编译器负责 Correctness:
- 类型系统验证接口契约
- Borrow checker 证明并发安全
- 穷举 match 消灭遗漏分支
- 生命周期检查防止悬垂引用
这种三角分工之所以高效,关键在于反馈循环是自动化的。AI 生成代码后不需要等人来检查类型是否正确、并发是否安全——编译器秒级给出答案。人只需要在更高的抽象层面参与:需求对不对?架构合不合理?领域逻辑有没有遗漏?
不只是 Rust
这种范式并非 Rust 独有。任何有强类型系统和严格编译期检查的语言都能受益:
- Haskell:更强的类型系统,但生态和 AI 训练数据较少
- OCaml:优秀的类型推断,但社区较小
- TypeScript(strict 模式):类型系统弱于 Rust,但在前端领域实用
- Swift:值类型和可选类型提供了类似的安全保证
Rust 之所以特别适合,是因为它在类型安全的严格程度和实际生态的丰富度之间取得了最好的平衡。tokio、hyper、serde、clap 这些库的质量和文档都是一流的,AI 的训练数据也相当充分。
局限与诚实的反思
这种范式不是万能的:
- 学习曲线仍然存在。人需要理解 Rust 的所有权模型才能有效评审 AI 的代码。如果你完全不懂 Rust,编译器的错误信息对你也是天书。
- 编译器不检查业务逻辑。
is_proxy_request的三条判断规则是否正确覆盖了 HTTP 语义?这需要人的领域知识。编译器只保证代码"类型正确",不保证"逻辑正确"。 - 编译速度是代价。虽然
cargo check很快,但完整的cargo build --release(特别是开启 LTO 时)可能需要几分钟。这是类型安全的税。 - 不是所有项目都需要 Rust。一次性脚本、数据分析、快速原型——Python 仍然是更合适的选择。这种范式最适合需要长期维护、性能敏感、并发密集的系统级项目。
小结
回到开头的问题:用 AI 写代码,选什么语言最高效?
如果目标是"最快生成看起来能跑的代码",Python 赢。
如果目标是"最快生成正确的代码",Rust 赢。
因为在 Rust 的世界里,AI 不是独自工作的。它身边有一个不知疲倦、永不遗漏、毫秒级响应的搭档——编译器。这个搭档不会放过任何类型错误、任何未处理的边界、任何不安全的并发访问。
https_proxy 和 trans_proxy 的开发体验让我确信:Claude Code + Rust 不是在用 AI 替代程序员,而是重新定义了人、AI 和工具之间的分工。人专注于最有价值的判断——定义 What 和 Why;AI 承担最繁重的劳动——实现 How;编译器提供最可靠的保障——验证 Correctness。
这才是 AI 编程的正确打开方式。