SkillLite 这个项目最早压根不是 Rust 写的。它最初的形态是一套 Python 实现的 Agent 技能编排框架,目标是把工具调用、API 封装、Prompt 模版统一成可复用的 Skill,让上层 Agent 通过函数调用(function calling)动态加载和执行技能。当时选 Python 的理由和大多数团队一模一样:生态成熟、上手快,LangChain/LangGraph 那套思路能直接抄作业,更重要的是——团队里没有任何一个人写过 Rust。
转折发生在线上接入的会话量逐步上来之后。Agent 运行时不像普通 Web 服务那样"请求来、响应走"就结束了,它要维护会话上下文、串联多轮工具调用、处理 LLM 返回的流式输出,还要在任务中途响应外部取消请求。Python 版本在这些场景下暴露的瓶颈越来越明显:冷启动太慢、内存占用高、GIL 把并发上限卡死。于是我们做了一个在当时的团队里争议极大的决定:用 Rust 重写核心运行时,最终完成了约五万行代码的替换。
这篇文章不是要证明"Rust 比 Python 好"这种口水命题,而是想把 SkillLite 从 Python 到 Rust 的整个决策过程、架构设计、性能数据和踩坑经历完整记录下来。如果你正在做 AI Agent 框架选型,或者团队正在犹豫要不要把一部分 Python 服务换成 Rust,这篇文章应该能给你一些参考。
1. 为什么要动这刀:AI Agent 运行时比其他后端服务更吃性能
1.1 Agent 运行时到底和普通 Web 服务有什么不一样
很多人觉得 Agent 服务也是"接 HTTP 请求、调 LLM、返回结果",性能压力全在模型推理上,本地代码优化没什么必要。这个想法在 demo 阶段是对的,但一旦接入真实业务,情况就完全不同了。
普通 Web 服务是无状态的,请求处理完就释放资源,最多用 Redis 存一下 session。Agent 运行时必须把会话状态、工具调用栈、中间产物、用户偏好这些数据一直挂在内存里。一个复杂的多轮 Agent 任务可能要做 5 到 10 次工具调用,中间还穿插 LLM 的多次往返。每做一次函数调用,就要做参数解析、权限校验、技能分发、结果收集、上下文拼接,这些全是计算密集的小操作。
Python 版本的 SkillLite 把这些操作串在一个 asyncio 事件循环里。单线程跑起来问题不大,但一旦 Session 数量上来,每个 Session 都是独立任务,事件循环里同时挂着几百个协程,Python 的解释器开销、对象创建开销、GIL 切换开销就全部变成了延迟。最难受的是,asyncio 的取消机制对 Agent 任务支持非常差,一个工具调用正在执行时,外部要取消整个任务链,经常出现取消信号丢失或者协程泄漏。
1.2 SkillLite Python 版本的量化瓶颈
我们的业务主要是给企业内部的客服、数据分析助手提供 Agent 能力,高峰期并发会话在 200 到 300 个左右。按道理这个量级不算夸张,但实测数据很打脸:
| 指标 | Python 版表现 | 业务容忍度 |
|---|---|---|
| 服务冷启动(新容器拉起) | 800ms 到 1.5s | 小于 200ms |
| 单次函数调用 P95 延迟 | 12ms 到 30ms | 小于 5ms |
| 300 并发会话时内存占用 | 2.1GB | 小于 1GB |
| 频繁创建/销毁 Session 时的 OOM 频率 | 每周 2 到 3 次 | 零容忍 |
最让人头疼的是内存。Python 对象模型在大量小对象频繁创建销毁时,内存碎片和 GC 停顿都会被放大。Agent 任务里到处都是字典、列表、字符串切片,Python 解释器为了维持灵活的动态类型,每个对象都带一坨类型信息和引用计数。300 个会话跑一个小时,内存占用能从 800MB 涨到 2GB,然后崩溃。
冷启动问题也很致命。我们当时想把 Agent 能力部署到边缘节点,做到"近端推理、就近响应",但 Python 版本的启动时间直接把这条路堵死了。容器每次扩容都要拉模型配置、加载技能注册表、初始化连接池,800ms 起步的延迟根本满足不了毫秒级弹性的要求。
真正让我们下决心重写的,是 Agent 的一次线上故障。某个凌晨的流量高峰,一个会话任务链里有 3 个技能并行执行,asyncio 事件循环里出现了一个死锁,所有协程全部挂起。服务看起来还活着,健康检查也过了,但所有请求都在等一个永远等不到的信号。三小时后值班同学才发现问题,手动重启才恢复。这种问题在 Python 里几乎无法根治,只能通过超时机制缓解。我们那时候才意识到,Agent 运行时需要的不是"写起来方便",而是"行为和性能都可预测"。
2. 选型不是"Rust 好"这么简单:Go、C++ 和 Rust 三方对比
2.1 为什么第一个排除了 Go
SkillLite 要重写时,Go 是最先被讨论的候选。社区里很多人觉得 Go 和 Rust 都是"高并发后端语言",选哪个差别不大。但我当时在技术评审会上直接说:Go 解决不了我们的核心问题。
Go 的优点确实明确:goroutine 并发模型简单,部署也方便,静态编译出单一二进制。但 Go 最大的短板是自动垃圾回收。GC 在低延迟场景下表现不稳定,虽然 Go 的 GC 一直在优化,但 STW(Stop The World)停顿还是时不时发生。Agent 运行时里有大量小对象频繁创建,GC 压力会非常大。我们做过一个简单的压力测试:模拟 1000 个并发协程反复创建和销毁 map、slice,Go 的 P99 延迟会出现周期性尖刺,尖刺时间和 GC 周期高度相关。
另外,Go 的类型系统对状态建模的表达力有限。SkillLite 要处理"技能定义""技能执行结果""Agent 状态快照"这些复杂数据模型,Go 的 interface 加 struct 组合虽然能用,但表达起来很啰嗦,大量使用空接口后类型安全就形同虚设了。当时团队里有人说"用 Go 只是把 Python 的动态类型问题换了个形式继续存在",这句话我觉得很准确。
C++ 当然也被列进了候选。性能绝对够,但 C++ 的内存安全是硬伤。Agent 运行时跑在公网环境,要解析各种外部输入、加载第三方技能,内存漏洞一旦被利用,后果比性能问题严重得多。我们没有一支专职做 C++ 的团队可以支撑长期维护,这个选项在讨论第一天就被否决了。
2.2 Rust 在 Agent 场景下的三个决定性优势
Rust 最终胜出,不是因为性能数字好看,而是因为它在"可预测性"上做到了我们想要的极致。
第一,无 GC 带来的延迟可预测性。Rust 没有垃圾回收器,所有内存都在编译期决定好生命周期。这意味着运行时不会出现"不知道什么时候触发 GC、GC 要停多久"的不确定性。Agent 对话场景里,用户等一个回复已经有几百毫秒的模型延迟了,本地的工具调用延迟如果还能稳定控制在微秒到毫秒级别,那整个交互链路的延迟就完全可控。这点对后面的边缘部署计划尤其重要。
第二,所有权模型对并发安全的保证。Agent 运行时最大的并发风险是共享状态,Python 时代我们花了很多精力做锁、做同步,结果还是出了前面的死锁问题。Rust 在写代码的阶段就把数据竞争问题暴露出来——var 能不能跨线程传、共享变量有没有用 Arc 包起来、可变引用有没有同时存在多个,全部由编译器把关。我们把 Python 版的死锁问题整理成用例,用 Rust 重写之后这类问题几乎不可能发生。
第三,无运行时、静态二进制部署。Rust 编译出来就是一个静态链接的可执行文件,直接扔到目标机器上就能跑,不需要 Python 运行时、不需要装依赖、不需要虚拟环境。对边缘设备、嵌入式场景(我们后来确实接了一个具身智能方面的需求),这种部署方式简直是降维打击。
2.3 团队能接受 Rust 的前提条件
Rust 的学习曲线是真实存在的,这点不能装看不见。我们团队当时的背景是:大部分人写 Python,一个人写过两年 C++,还有一个人把《Rust 权威指南》翻了大半本。在决定用 Rust 之前,我先花了两周做了一个技术验证:用 Rust 写一个最小的 Agent skill 注册和执行引擎,核心功能包括热加载技能、函数调用参数校验、结果回传。两周后,这个 demo 跑通了,代码量只有 Python 版的 40%,单次函数调用延迟降到了 Python 版的 1/10。
这个 demo 在团队里起到了关键的说服作用。大家亲眼看到,Rust 版本不仅性能好,代码结构也因为类型系统的约束变得更清晰了。当然,代价是前两个月开发效率确实低于 Python,我们通过拆分任务、结对编程、每次 code review 专门扫 unsafe 代码的方式把学习成本控制在了可接受范围内。如果你要复制这条路,团队里最好至少有一个人愿意承担"Rust 导师"的角色,否则项目很可能在第一个月就卡在借用检查器上。
3. SkillLite 在 Rust 下的架构重构:从动态分发到编译期约束
3.1 整体设计思路:不再是"解释器",而是"编译器"
Python 版 SkillLite 的本质是一个小型的动态解释器:技能库存放在配置中心,运行时通过字符串匹配找到对应的处理器,再用 eval 或者 getattr 动态执行。灵活是真灵活,但每加一个新技能,都要经过一次"字符串查找 -> 动态加载 -> 参数校验 -> 执行"的链,性能损耗大,而且一旦技能名拼错,要跑到运行时才报错。
Rust 版彻底改变了这个模式。我们把"技能"从运行时的动态配置变成了编译期的静态注册,用宏(macro)和 trait 对象组合的方式重新设计了技能加载机制。
一个技能的定义从 Python 版的这样:
@skill.register("translate_text") def translate_text(text: str, target_lang: str) -> str: # 调用翻译 API return api_call(text, target_lang)变成了 Rust 版的这样:
#[derive(Serialize, Deserialize)] struct TranslateParams { text: String, target_lang: String, } #[skill(name = "translate_text", description = "Translate text to target language")] async fn translate_text(params: TranslateParams) -> Result<String, SkillError> { let client = TranslationClient::new(); client.translate(¶ms.text, ¶ms.target_lang).await } register_skill!(translate_text);这个变化看起来只是语言不同,实质上是把"技能"从一个运行时字符串变成了一个编译期实体。参数结构、校验逻辑、执行函数在编译时就被类型系统约束住了,不可能出现 Python 版那种"传一个非法参数,运行到一半才报错"的情况。
3.2 五万行代码的模块划分
SkillLite 的最终代码量大约五万行,分布在下面几个核心模块里:
- skill-core:技能定义、trait 实现、参数校验框架,大约 8000 行
- skill-registry:技能注册表、静态注册宏、技能发现机制,大约 5000 行
- agent-runtime:Agent 会话管理、任务调度、事件循环、取消处理,大约 15000 行
- function-calling:LLM 结构化输出的解析、参数映射、函数调用管线,大约 12000 行
- persistence:会话状态快照、技能执行历史记录,大约 5000 行
- edge-runtime:边缘部署适配、资源限制、嵌入式平台支持,大约 5000 行
这个拆分思路是"让每个模块只做一件事,模块之间用 trait 定义边界"。skill-registry 不关心技能内部实现是什么,只要实现了 Skill trait 就能被注册;agent-runtime 不关心具体技能逻辑,它只负责调度和执行;function-calling 模块负责把 LLM 输出的 JSON 或者结构化指令解析成类型安全的调用参数。
这样拆的好处是测试特别好写。每个模块都是独立的 crate,可以单独跑单元测试和集成测试。五万行代码看着很多,但真正核心的 agent-runtime 只有一万五千行,其他部分都是围绕这个核心的配套模块。
3.3 宏编程:Agent 技能声明式配置的关键
Rust 的宏在 SkillLite 里扮演了非常关键的角色,这可能是 Python 版完全替代不了的能力。Python 的装饰器虽然能做到类似的事,但装饰器本质上是运行时包装,它改变不了函数签名,也无法在编译期做静态校验。
我们自定义了两个核心宏:
#[skill(...)]:属性宏,用来标注一个技能函数。它会根据函数签名自动生成参数解析和校验代码,同时把技能的元信息(名称、描述、参数 schema)写入一个编译器可见的静态注册表中。register_skill!:声明宏,把技能函数注册到一个全局的静态分发表中。这个分发表使用OnceLock或者在编译期用phf这类库做静态哈希映射,查找复杂度是 O(1),没有运行时字符串匹配的开销。
用宏还有一个额外好处:技能清单可以生成给大模型用的 tool schema。我们在属性宏里直接声明了#[derive(Serialize)]生成 JSON Schema,每次技能定义变更,工具的 schema 也跟着变,不可能出现 Python 版那样"定义和文档不一致"的问题。
3.4 agent-runtime 的并发设计:tokio 下的结构化并发
Agent 会话的并发模型是整个架构设计里最核心的部分。Python 版用的是 asyncio 单线程事件循环,Rust 版我们选择了 tokio 多线程运行时。
每个 Agent 会话对应一个 tokio task,会话之间的状态完全隔离,共享数据通过Arc<RwLock<State>>保护。一个会话内部可能同时开出多个子任务,比如同时调用翻译技能和搜索技能。这些子任务需要取消机制——用户中断对话时,整条任务链要全部取消。
tokio 提供了tokio::select!宏和任务取消相关的工具,但真正复杂的是"取消安全"设计。一个技能执行到一半被取消时,要确保没有脏数据写进数据库,没有未完成的请求泄漏。我们为此引入了一个类似"结构化并发"的抽象:所有子任务都必须在一个TaskScope内创建,TaskScope 被 drop 时,所有子任务会被自动取消并等待完成。这比 Python 版的手动取消靠谱得多,因为编译器保障了"任务一定能被清理干净"。
4. 性能实测:五万行 Rust 代码换回来了什么
4.1 测试场景设计
性能测试我们没有盲目地测单纯的 mock 接口,而是构造了和真实业务一致的场景:一个 Agent 会话,模型返回结构化函数调用请求,运行时解析参数、加载技能、执行技能、把结果回传给模型。整个链路涵盖了 function calling 的完整流程。压测工具用 Rust 写的,模拟 100 到 1000 个并发会话,每个会话执行 3 到 5 次技能调用。
对比对象是 Python 版 SkillLite 的线上版本。两台机器相同配置,16 核 32GB 内存,各自独立部署。
4.2 数据对比
| 指标 | Python 版 | Rust 版 | 提升幅度 |
|---|---|---|---|
| 冷启动时间(空载启动到可服务) | 850ms | 18ms | 47 倍 |
| 单次技能调用 P50 延迟 | 6ms | 0.4ms | 15 倍 |
| 单次技能调用 P95 延迟 | 28ms | 1.1ms | 25 倍 |
| 500 并发会话时内存峰值 | 3.8GB | 480MB | 8 倍 |
| 1000 并发会话时的成功率 | 92% | 99.99% | - |
| 无 GC 场景下延迟抖动标准差 | 8ms | 0.2ms | 40 倍 |
最让我触动的是 P95 延迟。Python 版的 P95 是 28ms,虽然比 P50 慢了四倍多,但还能接受。Rust 版的 P95 只有 1.1ms,而且抖动非常小。这意味着在 Agent 对话场景里,用户几乎感知不到工具调用的本地耗时,整个交互延迟基本上就是模型推理的时间。
内存上从 3.8GB 降到 480MB,这个意义不只是省成本,而是真正解锁了边缘部署。480MB 的内存占用对一些嵌入式设备还是偏高,但已经可以在树莓派级别的硬件上跑起来了。我们后来在一个资源受限的边缘设备上做了测试,Rust 版跑起来非常流畅,Python 版在那个设备上连启动都成为问题。
4.3 性能是怎么优化出来的
性能不是写完就有的,Rust 版也经历了三轮优化。
第一轮是减少无谓的 clone。刚开始写的时候,很多地方为了省事直接 clone 字符串和结构体,导致内存分配极其频繁。优化思路是梳理数据流,把参数传递改成借用,用Cow处理需要修改的情况。优化后单次调用的内存分配次数从 400 多次降到了 50 多次。
第二轮是避开全局锁。agent-runtime 里的共享状态最开始统一用一个大RwLock包着,并发一高就出现锁竞争。后来拆成无锁的结构:技能的注册表编译期就固定了,用静态只读数据;会话状态按照 session id 做 shard,每个 shard 一个锁,不同会话之间的锁完全隔离。
第三轮是重写热点路径。函数调用的参数校验、JSON 解析我们一开始用 serde_json 的默认行为,后来在关键路径上换成了simd-json,解析耗时又降了一半。还有技能结果的 JSON 序列化,我们避免了多次拷贝,直接写到流式 writer 里,省掉了中间缓冲。
这些都是常规性能优化手段,但前提是 Rust 的类型系统和所有权模型给了你足够的信心做激进优化,不用担心改完一个引用就引入数据竞争。
5. 从 Python 迁移到 Rust 踩过的坑:所有权、异步与宏展开
5.1 借用检查器对 Agent 状态建模的改造
从 Python 到 Rust 最痛苦的不是语法,而是思维模式的转换。Python 里"持有一个对象然后随手修改字段"是理所当然的操作,到了 Rust 里全成了借用检查器的报错。
Agent 会话状态是最典型的例子。Python 版里一个 Session 就是一个字典,技能执行过程中可以往里面塞任意字段:当前步骤、中间结果、用户偏好。Rust 版里我们一开始老老实实定义了AgentState结构体,但很快发现一个问题:不同的技能需要访问状态的不同部分,如果都往结构体上堆字段,要么用大量的Option,要么就得频繁获取和释放锁。
后来我们做了一次妥协:把"强类型的会话状态"和"KV 形式的临时状态"分开。强类型状态用 Rust 结构体保存,负责核心流程的控制;临时状态放一个HashMap<String, Value>,但要通过一个自定义 trait 来访问,保证不会出现跨线程的数据竞争。这个设计既保留了 Rust 的类型安全优势,又避免了一些场景下建模过于僵化的问题。
Rust 的借用检查器让我重新理解了 Agent 状态管理的本质:状态不是一个可以随意篡改的对象,而是一个需要明确定义所有权的资源。谁可以读它、谁可以写它、什么时候释放,这些问题在架构设计阶段就应该想清楚,而不是等运行时报错再回来找 bug。
5.2 tokio 异步生态和 asyncio 的思维差异
Python 的 asyncio 是单线程协程,虽然也支持 await,但本质上所有协程跑在同一个事件循环线程里,变量跨线程的问题基本不存在。Rust 的 tokio 默认是多线程运行时,你的 async fn 可能在任何一个工作线程上执行,这就引出了Send + 'static的约束。
在 Python 里你可以写一个全局共享的连接池,每个协程直接用,完全不用考虑线程安全。到了 Rust 里,连接池比如 redis 连接或者 HTTP 客户端连接,必须是Send的,否则无法跨线程。刚开始我们经常遇到"future is not Send"的编译错误,需要给客户端包一层Arc,或者通过tokio::sync::Mutex保护连接。
还有一个差异在超时和取消。asyncio 的超时直接用asyncio.wait_for就行,但在 tokio 里要特别注意超时后的状态清理。如果tokio::time::timeout触发了,future 被 drop,但 future 内部持有的资源(比如已经发出但还没收到响应的 HTTP 请求)不会自动撤销。技能执行过程中如果被超时中断,我们需要在 drop 逻辑里做补偿操作,比如记录一条失败日志、回滚部分写入的状态。这些坑都是实践后才能体会到的。
5.3 工具链与落地部署的细节
Rust 的工具链整体体验是好的,但有几个实际问题。
第一个是依赖下载速度。国内网络环境拉 crates.io 的依赖很慢,需要配置国内镜像源。我们在.cargo/config.toml里配置了 rsproxy 之类的镜像,显著改善了构建体验。如果部署环境是内网,还需要提前把依赖 vendoring 下来,用cargo vendor生成离线依赖目录。这个对信创和内部系统尤其重要,不能依赖外网拉包。
第二个是交叉编译。我们要把 Agent 运行时部署到 ARM 的边缘设备上,交叉编译成了必备技能。Rust 的交叉编译比 Python 好太多,Python 要在目标设备上装解释器和依赖,Rust 只要一个目标平台的 toolchain。我们用的是cross工具,一条命令就能写出 ARM 版本;后来发现cargo-zigbuild在某些 glibc 版本上也很好用,能解决旧系统自带 glibc 版本过低的问题。
第三个是编译时间。五万行代码的增量编译,如果配置不当能等到天荒地老。我们在.cargo/config.toml里做了两个关键配置:
[profile.release] lto = "thin" codegen-units = 16 opt-level = 3 [build] rustc-wrapper = "sccache"lto = "thin"在保持大部分链接时优化效果的同时显著加快链接速度;codegen-units = 16让编译器并行处理更多代码单元;sccache 作为编译缓存,让不同分支的编译共享缓存,节省了大量时间。我见过很多团队因为这三点没配置,编译一次要十几分钟,就觉得 Rust 开发效率太低而放弃,其实大部分是配置问题。
宏展开调试是另一个坑。我们用宏自动生成技能注册代码,但宏报错的时候错误信息非常天书化,经常是"expected type, found{"这种完全定位不到问题在哪儿的提示。解决方案是装一个cargo expand工具,把宏展开后的代码打印出来看。这个方法救了我无数次。写宏的时候也要克制,尽量让宏只做机械的代码生成,复杂的逻辑放在普通函数里,否则调试成本会直线上升。
5.4 一个典型的线上问题的排查过程
这里分享一个我们当时排查了三天的线上问题,非常经典。上线 Rust 版 SkillLite 之后,某天突然反馈说部分会话的上下文丢失了,技能执行结果没有正确回填到对话历史里。这个问题在 Python 版从来没出现过,因为 Python 版的上下文是一个全局字典,技能执行完直接往里面塞;Rust 版我们做了更严格的模块划分,上下文由 agent-runtime 统一管理,技能只能通过返回结果回传数据。
排查过程一开始认为数据格式有问题,反复检查了序列化和反序列化代码,没有发现异常。后来怀疑是锁竞争导致状态被覆盖,给共享状态加了专门的测试用例,也复现不出来。
最后是在一个很偶然的时机抓到了线索:负责状态回填的模块里,我们用了tokio::spawn开启了一个后台任务来异步写上下文。后台任务的执行顺序不保证,当两个技能同时完成时,两个后台任务同时写上下文,后写的数据覆盖了先写的。因为用户提问的措辞,这个覆盖往往看不出来,但偶尔会丢掉关键上下文。
修复方式是取消了后台任务,改为在技能完成后同步地、按顺序地写上下文。性能影响微乎其微,但彻底解决了问题。这个坑给我的教训是:Rust 的优势是明确性,但如果自己用异步任务把执行顺序搞乱,反而会引入 Python 时代不存在的隐式错误。Rust 不是银弹,它依然需要你正确地设计并发模型。
6. 复盘结论:Rust 并不是所有 AI Agent 项目的答案
6.1 适合用 Rust 的 Agent 场景
经过了这次从 Python 到 Rust 的完整迁移,我梳理了一下到底什么样的 Agent 项目适合用 Rust 做。
第一类是延迟敏感的交互式 Agent。比如语音助手、实时对话机器人,用户的体感延迟是整个系统成功与否的核心指标。这类场景下,Rust 的延迟可预测性优势非常明显。语音交互场景里,本地工具调用的延迟哪怕只是 5ms 和 1ms 的区别,在整条链路上积累起来都会变得显著。
第二类是边端部署的 Agent。具身智能设备、嵌入式机器人、需要在地端实时处理数据的场景,对内存占用和运行效率极其敏感。Rust 的无 GC、静态编译、极小运行时这几个特质,简直是边端部署的完美匹配。我们后来接的具身智能相关需求,就是用 SkillLite 的 edge-runtime 跑的。
第三类是长时间运行的服务端 Agent 运行时。一个 Agent 服务可能需要在线上跑几个月不回滚,内存泄漏、并发崩溃这类问题在大规模部署时会无限放大。Rust 的所有权模型和类型系统在编译期就排除了一大批这类 bug,让服务具备更强的长期稳定性。
6.2 不适合用 Rust 的场景
反过来,有些场景我觉得大可不必折腾 Rust。
如果你还在快速验证产品 idea,Agent 的核心逻辑每天都在变,今天换成 LangChain,明天换成自研框架,那乖乖用 Python 就好。Rust 的开发效率在前期确实比不上 Python,类型系统的约束会让快速迭代时感觉束手束脚。
如果业务非常依赖 Python 生态的库,比如 pandas、numpy、scikit-learn、以及大量 AI 开源仓库都是 Python 优先,强行用 Rust 会非常痛苦。SkillLite 之所以能切,是因为我们核心逻辑不依赖第三方 Python 库,只有 HTTP 调用和 JSON 处理,在 Rust 生态里都有高质量替代品。
另外,如果团队完全没有 Rust 背景,也没有人愿意花时间去啃 Rust 的所有权和异步模型,那我还是劝你三思。我们不缺任何一个能用 echo 写出 hello world 的 Rust 开发者,缺的是能从编译错误里快速定位设计问题的工程师。团队的学习意愿和培训投入必须作为选型的重要考量。
6.3 想走这条路的话,我给你几个实在的建议
第一,不要一次性重写。SkillLite 虽然是整体重写,但我们是分模块进行的:先把最核心的 function-calling 模块用 Rust 重写,通过 FFI 或者独立服务的方式接入 Python 系统,验证性能提升之后再逐步替换其他模块。直接一把梭的大重写风险极高,很容易半年过去还跑不起来。
第二,认真设计你的状态模型。Rust 最大的开发成本是状态和并发的设计。如果你沿用 Python 时代的"全局字典 + 随手改"的做法,死磕一天都过不了编译。反过来,如果你愿意花时间设计清晰的 state 结构、明确的共享边界,Rust 的类型系统会帮你省掉未来几个月调 bug 的成本。
第三,让团队先做一个可独立验证的技术 demo。就像我们当初做的那个 skill 注册引擎,范围要小、结论要明确:性能提升多少、代码可维护性如何、团队整体的接受度怎么样。用数据说话,比开会争论语言优劣有用一百倍。
最后说说 Rust 生态近一年给我带来的感受。Rust 社区在 AI Agent 方向的关注度越来越高,有人问 LangGraph 为什么没有官方 Rust 版本,也有人组了 agent rust 方向的 discussion。目前 Rust 的 Agent 生态相比 Python 还有差距,但 AI Agent 这个领域本身还很年轻,核心的价值——性能、安全、可部署性——恰好是 Rust 的传统强项。如果和我们的实践互相印证,我觉得未来一两年 Rust 在 Agent 基础设施层的位置会越来越稳。
SkillLite 迁移成 Rust 之后,团队内部也形成了一个共识:语言选型不是追新也不是守旧,而是把项目要达到的目标、团队能付出的成本、生态能提供的支撑放到一张表里做综合权衡。如果本文的复盘能帮你在做类似决策时少走一点弯路,那就值得了。