❝当所有人都在用Python堆砌Agent时,我决定用Rust给自己找点“麻烦”。
周末的午后,我盯着屏幕上的编译输出,长长地舒了一口气。
过去两年,AI Agent 无疑是技术圈最火热的关键词。LangChain、AutoGPT、CrewAI 等框架层出不穷,它们绝大多数基于 Python——这是理所当然的选择,Python 拥有最成熟的 AI 生态,迭代速度极快。
但作为一个常年被线上内存问题折磨的底层开发者,我一直在想:有没有可能用 Rust 打造一个生产环境可用的 AI Agent?
它不仅要有 Python 版 Agent 的灵活,还要有 Rust 独有的性能和安全基因。
于是,mini-agent 诞生了。今天,我想和你分享这次“找麻烦”的旅程中,我学到的那些事。
为什么是Rust?不只是为了“快”
在 AI Agent 的赛道上,Python 凭借生态优势一家独大。但当我们想要构建一个每天稳定运行、资源可控、易于部署的个人助手时,Python 的 GIL、高内存占用和复杂的依赖管理就成了痛点。
Rust 的优势逐渐浮出水面:
- 确定性行为:没有 GC 停顿,运行时表现可预测,这对于需要实时响应的 Agent 至关重要。
- 类型安全:复杂的 Agent 逻辑在编译期就能发现大部分问题,而不是等到凌晨三点告警。
- 单二进制部署:
cargo build 之后就是一个可执行文件,告别 yml 环境和依赖冲突。
架构三要素:Provider、Tool 和 Agent
我设计的 mini-agent 核心思想非常简洁,只有三个部分:一个 Provider,一个 Tool,和一个 Agent。
// 实现这个 trait 来接入新的 LLM 后端#[async_trait]pubtraitLlmProvider: Send + Sync {fnprovider_name(&self) -> &str;asyncfncomplete( &self, messages: &[Message], tools: &[&dyn Tool], model: &str, ) -> Result<Completion, AgentError>;}// 实现这个 trait 来给 Agent 赋予新的能力#[async_trait]pubtraitTool: Send + Sync + 'static {fnname(&self) -> &'staticstr;fndescription(&self) -> &'staticstr;fnparameters_schema(&self) -> Value;asyncfnexecute(&self, args: Value) -> Result<String, AgentError>;}
Agent 驱动一个经典的 ReAct 风格循环 —— 规划、行动、观察,直到模型返回最终答案或达到最大步骤限制。
我踩过的最深的坑:JSON响应解析
如果问我这次开发最难的部分是什么,毫无疑问是 JSON 响应解析。
“每个 LLM 提供商返回的 JSON 结构都略有不同。” 这句话说起来轻巧,做起来真要命。
OpenAI 和 OpenRouter 还算兼容,我可以共享辅助函数。但 Anthropic 完全不同——它不返回 tool_calls,而是在 content 数组里返回 tool_use 块:
{"content": [ { "type": "text", "text": "让我计算一下。" }, { "type": "tool_use", "id": "abc", "name": "add_numbers", "input": { "a": 4, "b": 7 } } ]}
这意味着我必须为 Anthropic 编写一个单独的解析器,然后转换成相同的内部 Completion 结构。
也正是这个时候,Rust 真正帮到了我。
Rust 的类型系统强迫我必须显式处理每一种情况,而不是让事情悄无声息地失败。在开发过程中我发现了一个严重 bug:Anthropic provider 竟然一直在静默丢弃系统提示,因为我硬编码了 let system_prompt: Option = None;,从未真正从消息历史中提取它。代码编译完全通过,功能从未生效。这种“静默失败”在 Python 里可能潜伏很久,但在 Rust 里,更好的错误处理机制让我不得不直面问题。
错误处理:从字符串到结构化
最初的错误处理代码长这样:
#[error("Provider error: {0}")]ProviderError(String),#[error("Invalid response from LLM: {0}")]InvalidResponse(String),
看起来没问题?问题大了——这对调用者毫无用处。你无法对字符串内容做模式匹配,无法在此基础上构建重试逻辑。
于是我彻底重构:
#[derive(Error, Debug)]pubenumAgentError {#[error("Network error: {0}")] Network(#[from] reqwest::Error),#[error("Provider '{provider}' error{}: {message}", .status.map(|s| format!(" (HTTP {s})")).unwrap_or_default())] Provider { provider: String, message: String, status: Option<u16>, },#[error("Tool '{tool}' failed: {reason}")] ToolExecution { tool: String, reason: String, },#[error("Agent reached the maximum of {0} steps without a final answer")] MaxSteps(usize),// ...}
现在调用者可以做真正有用的事情:
impl AgentError {pubfnis_retryable(&self) -> bool { matches!(self, Self::Network(_) | Self::Provider { status: Some(500..=599), .. } ) }pubfnis_client_error(&self) -> bool { matches!(self, Self::Provider { status: Some(s), .. } if *s >= 400 && *s < 500) }}
这是 Rust 让我惊喜的一点。借用检查器和类型系统起初让人觉得受限,但它们推动你走向更好的设计。我最终得到的错误处理比用其他任何语言写的都要干净,不是因为我有先见之明,而是因为 Rust 让偷懒的方法变得足够痛苦,逼着我用正确的方式做事。
安全是一等公民
在构建 Agent 时,安全不是附加功能,而是基石。Agent 很强大,但没有边界的权力就是 liability。
ferrum-bot 项目的做法值得借鉴:文件操作限制在工作区内、内置危险命令检测(如 rm -rf /)、网络请求阻止访问内网 IP。通过这些设计,确保 Agent 是一个有用的工具,而不是系统威胁。
应用场景:不仅是大而全
在构建框架的过程中,我发现并非所有场景都需要一个全功能的 Agent。“薄 Agent”的概念逐渐进入我的视野——用 LLM 增强一个非常具体和狭窄的功能,比如客服工单路由。
这种场景下,一个小型微调模型(如 Llama 3.2 1B)加上 Rust 的轻量服务,就能用极低成本实现高吞吐的分类服务。结合 llama.cpp 的 Rust 绑定和 Axum 框架,整个服务的二进制文件可能只有几十 MB,启动时间毫秒级,内存占用可控。
未来已来:Rust在AI生态的潜力
写到这里,我想起 Ebbforge 项目展示的那种可能性——用 Rust 实现群体智能引擎,在普通硬件上运行 1000 万个 Agent,通过 TD-RL 和生物启发的记忆衰减机制实现自愈。虽然这和传统的 LLM Agent 有所不同,但它展示了 Rust 在 AI 领域的极限潜力。
还有 ADK-Rust 这样的项目,正在构建生产就绪的 Rust Agent 框架,支持 Ollama 本地模型、mistral.rs 高性能推理、MCP 协议集成。生态正在以惊人的速度成长。
写在最后
从开始对 Rust 一无所知,到如今能熟练地用 async-trait 设计抽象的 Provider 层,这次“找麻烦”的旅程教会我的不止是技术。
借用一位开发者的话:“你不需要一个 Agent 框架。你需要的是一个简单的 while 循环:观察、决策、行动、反思。 ”
而 Rust,恰好是写这个 while 循环的最佳语言之一。
如果你也想试试,这里有我准备的示例代码:
letmut agent = Agent::new(Box::new(provider), model);agent.add_tool(AddNumbersTool);let result = agent.run("42 + 58 等于多少?").await?;println!("{}", result);
你的第一个 Rust Agent,可能就在这个周末诞生。