当社区里很多人还在用 Python 或 Node.js 构建 AI Agent 时,我选择了一条不同的路:用 Rust 来重写。这并非为了炫技,而是我深信,AI Agent 要迈向规模化与高可靠性的未来,必须有更坚实、更高效的基础设施作为支撑。
为何选择 Rust 重写 Hermes Agent?
hermes-agent 是由 Nous Research 开源的一款自进化 AI 代理框架,在 GitHub 上已经积累了近 20.7k 的星标。它有几个特性让我印象深刻:


这些功能听起来很强大,但它的实现有一个根本性问题——整个项目基于 Python 构建,总计约 5 万行代码。仅 run_agent.py 这个文件就快有 8500 行,维护和扩展的复杂度可想而知。

使用 Python 版本时,我遇到了几个明显的痛点:
- 启动缓慢:冷启动需要 3-5 秒。
- 内存占用高:即使空载,内存占用也超过 200MB。
- 异步/同步桥接地狱:处理
async/sync 转换的 _run_async() 函数长达 50 行,只为应对主线程、工作线程和嵌套事件循环的各种边界情况。
- 部署复杂:依赖 Python 版本、pip 包、Node.js 运行环境(用于 MCP)等,环境配置繁琐。
- 类型安全缺失:运行时错误难以在早期发现。
既然这个框架的理念和功能如此吸引人,我下定决心:用 Rust 对其进行一次彻底的重写。
架构设计:模块化与清晰依赖
一个优秀的 Rust 项目,其架构设计是成功的基石。我将整个系统拆分为 13 个独立的 crate(库),并严格遵循了无环依赖图(DAG)的原则:
hermes-rs/
crates/
hermes-core/ # 共享类型:Message、ToolCall、Platform、Error
hermes-config/ # YAML 配置 + .env + SOUL.md 人格加载
hermes-security/ # 注入扫描、环境变量过滤、路径防护
hermes-state/ # SQLite + FTS5 全文搜索
hermes-llm/ # LLM 客户端:OpenAI 兼容 + SSE 流式
hermes-terminal/ # 终端执行后端:Local + Docker
hermes-skills/ # 技能系统:SKILL.md 解析、CRUD
hermes-tools/ # 工具注册表 + 9 个内置工具
hermes-mcp/ # MCP 协议客户端(stdio + JSON-RPC)
hermes-agent/ # 核心 Agent 循环:对话编排、上下文压缩
hermes-gateway/ # 网关 + 5 个平台适配器
hermes-cron/ # 定时任务调度器
hermes-cli/ # 交互式终端 UI
它们的依赖关系清晰明了:
hermes-cli
└─ hermes-agent
├─ hermes-llm ──── hermes-config ──── hermes-core
├─ hermes-tools ── hermes-terminal ── hermes-security
├─ hermes-mcp
└─ hermes-skills
└─ hermes-gateway
└─ hermes-cron
为什么要拆分得如此细致? 因为 Rust 的编译单元是 crate。这样做带来了几个显著优势:
- 增量编译极快:修改一个工具的实现,不需要重新编译整个 Agent。
- 依赖隔离清晰:例如,
hermes-gateway 可以完全不依赖 hermes-terminal。
- 未来可定制编译:如果某个项目不需要 Telegram 支持,可以通过
cargo build --no-default-features 来剔除相关模块。
核心设计:四个关键 Trait 构建系统骨架
Rust 的 trait 系统是整个项目的灵魂。整个系统围绕四个核心 trait 构建,它们定义了各模块间的交互契约。
1. LlmClient —— LLM 调用抽象
#[async_trait]
pub trait LlmClient: Send + Sync {
async fn complete(&self, req: &CompletionRequest)
-> Result<CompletionResponse, LlmError>;
async fn stream(&self, req: &CompletionRequest,
tx: mpsc::Sender<StreamDelta>)
-> Result<CompletionResponse, LlmError>;
}
Python 版本需要用 if/elif 分支来处理不同 API 提供商的差异。而在 Rust 版本中,每种 API 都是一个独立的 struct,但它们都实现同一个 LlmClient trait。编译器会确保你不会遗漏任何一种情况的处理。
#[async_trait]
pub trait ToolHandler: Send + Sync {
async fn execute(&self, args: serde_json::Value,
ctx: &ToolContext) -> Result<String, ToolError>;
}
每个具体的工具(如文件操作、网络请求)都是一个实现了 ToolHandler 的 struct。它们被注册到一个中央的 ToolRegistry(内部使用 RwLock<HashMap>)中。这种设计既支持运行时的动态工具发现(如通过 MCP 协议),也支持编译时的静态工具注册。
3. TerminalBackend —— 执行环境抽象
#[async_trait]
pub trait TerminalBackend: Send + Sync {
async fn execute(&self, cmd: &str, cwd: Option<&str>,
timeout: Option<Duration>) -> Result<ExecResult, TerminalError>;
async fn cleanup(&self) -> Result<(), TerminalError>;
}
无论是本地 Shell 执行,还是在 Docker 容器中执行命令,都通过同一个接口完成。切换执行环境只需要修改一行配置,无需改动业务逻辑代码。
#[async_trait]
pub trait PlatformAdapter: Send + Sync {
fn platform(&self) -> Platform;
async fn connect(&mut self) -> Result<(), GatewayError>;
async fn send(&self, chat_id: &str, content: &str,
reply_to: Option<&str>) -> Result<SendResult, GatewayError>;
fn set_message_handler(&mut self, handler: MessageHandler);
}
Telegram、Discord、Slack……每个消息平台的适配器都是一个独立的文件,它们都实现这个 PlatformAdapter trait。未来要新增一个平台,只需要实现这四个方法即可,与核心逻辑完全解耦。这种基于 Trait 的设计,是构建可扩展、高内聚低耦合系统的关键,也是现代 后端 & 架构 中倡导的核心模式之一。
攻克难题:彻底告别 Async/Sync 地狱
Python 版本中最让我头疼的就是那个约 50 行的 _run_async() 函数。它需要小心翼翼地处理三种复杂的并发场景:主线程的事件循环、工作线程池中的独立事件循环、以及 Gateway 内部的嵌套异步上下文。
在 Rust 版本中,这个问题从根本上被解决了。
整个应用运行在统一的 tokio 运行时上。所有的工具处理器都是 async fn。并行执行多个工具使用 JoinSet,Gateway 与 Agent 核心间的消息传递使用 mpsc channel。没有 sync/async 的显式桥接,没有线程局部存储的繁琐管理,也没有事件循环生命周期的担忧。
// Python: 需要50行的 _run_async() 来桥接同步和异步世界
// Rust: 直接 .await,一切如此简单
let result = registry.dispatch(&tool_name, args, &ctx).await;
安全保障:编译时守卫
Python 版本包含了大量的运行时安全检查,如环境变量泄露防护、Prompt 注入扫描、路径遍历检测等。这些安全措施在 Rust 版本中都得到了保留,并且借助语言特性得到了增强:
// 编译器保证你必须处理 Result,无法忽略错误
let result = self.backend.execute(command, cwd, timeout, None).await?;
// 类型系统防止你混淆不同的字符串标识符
pub struct SessionSource {
pub platform: Platform, // 强类型枚举,而非普通的字符串
pub chat_id: String,
pub user_id: Option<String>,
// ...
}
平滑迁移:零成本配置兼容
这一点让我颇为自豪——Rust 版本能够直接读取和使用 Python 版本完全相同的配置文件,实现了真正的零迁移成本。
# ~/.hermes/config.yaml 或项目本地 .hermes/config.yaml
model:
default: "anthropic/claude-sonnet-4"
base_url: "https://openrouter.ai/api/v1"
terminal:
backend: "local"
timeout: 180
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
HermesConfig 结构体通过 #[serde(default)] 属性确保即使是不完整的配置子集也能被正确加载和填充默认值。
此外,我还实现了项目级本地配置覆盖功能。在任意项目目录下放置一个 .hermes/config.yaml 文件,它的设置会自动覆盖全局配置。这使得不同项目可以方便地使用各自专属的模型和工具配置。
性能对比:数字说明一切
让我们用最直观的数据来看看这次重写带来的变化:
| 指标 |
Python 版本 |
Rust 版本 |
| 代码行数 |
~50,000 |
~5,000 |
| 模块数 |
100+ 文件 |
13 crate / 66 文件 |
| 编译产物 |
需要 Python 运行时 |
单个 ~25MB 静态二进制文件 |
| 类型安全 |
运行时检查(mypy 可选) |
编译时保证 |
| Async 模型 |
asyncio + threading 混合 |
纯 tokio async |
| 错误处理 |
try/except |
Result<T, E> 全链路 |
技术选型思考
在构建这个 Rust 项目时,每一个技术选型都经过了仔细考量:
| 功能 |
选型 |
理由 |
| 异步运行时 |
tokio |
生态最完善,性能经过充分验证 |
| HTTP 客户端 |
reqwest |
对 SSE (Server-Sent Events) 流式传输支持良好 |
| 序列化 |
serde + serde_json + serde_yaml |
Rust 生态序列化的事实标准 |
| 数据库 |
rusqlite (bundled) |
捆绑 SQLite,实现零外部依赖 |
| 错误处理 |
thiserror + anyhow |
库定义错误用 thiserror,应用层用 anyhow |
| CLI 解析 |
clap |
声明式参数解析,功能强大且易用 |
| 日志 |
tracing |
结构化日志,性能优异,与 tokio 集成好 |
| Docker 交互 |
bollard |
直接调用 Docker Engine API,控制力强 |
总结与展望
用 Rust 重写一个 5 万行的 Python 项目,听起来像是一项艰巨的挑战。但在实际推进的过程中,Rust 强大的类型系统和所有权模型,帮助我提前发现了大量潜藏在 Python 版本中的隐患:
- 数据竞争:Python 版本使用
threading.Lock 来保护某些内存或文件操作,但仍有遗漏的路径。Rust 的 Mutex 则在编译阶段就强制你必须通过它来访问受保护的数据。
- 错误传播:Python 版本中存在一些
except: 语句,无意中吞没了错误细节。Rust 的 ? 操作符让错误的传播变得显式且可追溯。
- 空值处理:Python 代码中遍布
if x is not None:。Rust 的 Option<T> 迫使你在编译时就必须处理好每一个可能为“空”的情况。
我坚信,AI Agent 的未来必然需要更强大的基础设施。 当 Agent 需要 7x24 小时不间断运行、处理成千上万的并发会话、并无缝接入数十个消息平台时,Python 的全局解释器锁(GIL)、动态类型的内存开销以及相对松散的运行时检查,都可能成为瓶颈。
Rust 并非解决所有问题的“银弹”,但对于构建高性能、高可靠性的 人工智能 基础设施而言,它无疑是一个极具前景的正确选择。

项目地址:https://github.com/coder-brzhang/hermes-rs
这个项目目前仍在早期阶段,但核心架构和基础功能已经完成。如果你也对使用 Rust 构建下一代 AI Agent 系统感兴趣,欢迎在 云栈社区 或 GitHub 上交流探讨。我们才刚刚启程。