用 Rust 写了一个能自己「上班」的 AI 编码引擎——Zode 核心架构深度解析Rust

市面上 Code Agent 很多,但能让 AI 自己定时干活、能组队协作、能操控浏览器和桌面的,Zode 是第一个。这篇文章聊聊它背后的 Rust 实现。


上个月我把 Zode 开源了,一个用 Rust 写的终端 AI 编码引擎。跟朋友聊的时候,被问到最多的问题是:"Claude Code、Cursor、Copilot 那么多,为什么还要自己写一个?"

答案藏在 Zode 三个"别的 Agent 做不到"的能力里:

  1. 会自己上班 ------ 定时触发、循环监控,人不在线活照跑
  2. 能组队 ------ 多个 AI Agent 流水线协作,设计→实现→审查全自动
  3. 真会"点" ------ 操控 Chrome、操控桌面应用、生成 UI 设计稿

这三个能力背后是三个核心子系统:调度引擎Agent 团队浏览器桥接。这篇文章把它们的技术实现掰开来看。


一、整体架构

Zode 是一个 Cargo workspace,三层结构:

scss 复制代码
zode (CLI 入口)
  └── zode-tui (ratatui 终端 UI)
        └── zode-core (引擎核心)
              └── vendor/agent (AI Agent 运行时)

数据流:用户输入 → Engine 构建 QueryLoop → 工具链(Sandbox → Gate → Search)→ LLM 调用 → 事件流 → TUI 渲染。

关键设计原则:工具装饰链。每个工具注册时经过多层包装:

  • Sandbox 层:限制文件/网络访问
  • PermissionGate 层:拦截敏感操作,弹窗请求用户审批
  • ToolSearch 层:注册到工具搜索索引

Rust 的 trait 系统让这个装饰链实现得非常干净------每一步都是一个 trait object,零额外开销。


二、调度引擎:让 AI 学会「到点上班」

这是 Zode 最独特的功能------其他 Agent 都是你打开它才开始干活,Zode 能自己定时触发。

2.1 使用姿势

bash 复制代码
# 每天早上 9 点拉 PR CI 状态
/schedule every "Mon 09:00" "检查昨天所有 PR 的 CI,失败的总结原因发 Slack"

# 每小时监控生产日志
/schedule every 1h "检查生产日志有没有新 ERROR,有就发给我"

# 每 5 分钟监控 PR CI,失败了自动修
/loop 5m "监控这个 PR,CI 失败就自动修复"

2.2 跨进程去重:毫秒级 CAS

最棘手的问题:如果用户开了两个 Zode 进程,同一个定时任务不能执行两次。

解决方案是 Epoch Millisecond CAS(比较并交换)

rust 复制代码
// 核心逻辑(简化版)
fn try_mark_fired(&self, job: &ScheduleJob, expected_ms: u64) -> Result<(), StoreError> {
    // 持有 schedules.lock(fs4 排他锁)
    let store = self.locked_store.read();
    
    // 原子 CAS:只有 last_fired_ms 仍为旧值时才能写入新值
    if job.last_fired_ms == expected_ms {
        job.last_fired_ms = fire_ms;
        self.write_atomic();
        Ok(())
    } else {
        Err(StoreError::FiredByOther)
    }
}

关键技巧:所有时间计算都在 Epoch Millisecond 域进行(不是本地时间),确保夏令时切换时跨进程计算结果一致。

rust 复制代码
// DST-safe 的 interval slot 计算
pub fn latest_interval_epoch_slot(anchor_epoch_ms: u64, interval_ms: u64, now_epoch_ms: u64) -> u64 {
    if now_epoch_ms <= anchor_epoch_ms { return anchor_epoch_ms; }
    // 在 epoch 域做整除,不受本地时区影响
    anchor_epoch_ms + ((now_epoch_ms - anchor_epoch_ms) / interval_ms) * interval_ms
}

2.3 死进程守护:FS4 锁 + 孤儿恢复

每个 schedule job 在启动时获取一个独立的 fs4 文件锁:

rust 复制代码
pub struct ScheduleAttemptLease {
    lock_file: std::fs::File,
    token: String,       // 唯一执行 token
    job_lock_path: PathBuf,
}

impl Drop for ScheduleAttemptLease {
    fn drop(&mut self) {
        // 进程崩溃时 OS 自动释放锁,无需心跳
        fs4::FileExt::unlock(&self.lock_file).ok();
    }
}

启动时的「孤儿恢复」逻辑:

rust 复制代码
fn recover_orphan_attempts(store: &ScheduleStore) -> Vec<ScheduleJob> {
    store.jobs.iter()
        .filter_map(|job| {
            let lease = job.attempt_lease();
            match lease.try_lock() {
                Ok(_) => {
                    // 锁空闲 + token 匹配 = 上一个进程崩溃了
                    // 但不确定远程副作用是否已发生 → 停用任务,等待人工审查
                    job.disable("orphan_attempt_recovered");
                    Some(job)
                }
                Err(_) => None, // 锁被持有 = 另一个进程正在执行
            }
        })
        .collect()
}

设计哲学:宁愿漏掉一次定时触发,也绝不重复执行一个可能已产生副作用的操作。安全性优先于可用性。


三、Agent 团队:AI 的流水线协作

3.1 Team 架构

每个 tab 有一个 TeamManagerArc<TeamManager>),管理当前团队的 roster:

scss 复制代码
TeamManager
  ├── Board (共享看板)
  │     ├── board.json (CAS 修订计数器 + HMAC-SHA256)
  │     └── Claims (文件路径租约,TTL 自动过期)
  ├── InternalSession (进程内 Agent)
  │     └── 共享 PermissionGate/Hooks/FileCache
  └── ExternalSession (子进程 CLI)
        └── 支持 --resume 恢复上下文

3.2 三阶段 send 协议

team_send 到执行的过程是一个严格的三阶段状态机:

rust 复制代码
impl TeamManager {
    pub async fn send(&self, to: &str, message: &str, claims: &[PathBuf]) -> Result<TeamSendResult> {
        // 阶段 1:Busy-check + Generation 递增
        let guard = self.reset_busy(to).await?;
        let gen = guard.generation();
        
        // 阶段 2:原子 Claim(board lock 内完成)
        self.board.claim_paths(to, claims, TTL_DURATION).await?;
        
        // 阶段 3:Dispatch + 后台 TTL 续期
        let handle = self.backend.send(message).await;
        tokio::spawn(renew_claims_loop(board, to, gen));
        
        // SendCleanup RAII guard:无论成功/取消/panic,都会释放 claims
        let _cleanup = SendCleanup::new(self.board.clone(), to, gen);
        handle.await
    }
}

「代际守卫」------防止过期回调污染状态:

rust 复制代码
fn reset_idle(&self, name: &str, completed_generation: u64) {
    let mut member = self.members.get_mut(name)?;
    // 只有当 send 完成时的 generation 与当前一致时才重置
    // 否则说明已经有一个新的 send 开始了
    if member.generation == completed_generation {
        member.status = MemberStatus::Idle;
    }
}

这个单调递增的 generation 计数器保证了:即使一个旧的 send 回调在网络延迟后返回,也不会覆盖新 send 的状态。简单但可靠。

3.3 三种协作模式

模式 描述 适用场景
流水线 设计→实现→审查→迭代 有明确前置依赖
辩论 多模型独立作答→互审→Leader 裁决 需要多角度思考
蜂群 分片文件→并行开工→Board 汇总 独立任务

四、工具装饰链:Rust 的优雅抽象

4.1 装饰器模式实践

工具注册的核心流程:

rust 复制代码
fn build_tools(&self) -> Vec<Arc<dyn Tool>> {
    let mut tools: Vec<Arc<dyn Tool>> = vec![
        Arc::new(ReadTool), Arc::new(WriteTool), Arc::new(BashTool), /* ... */
    ];
    
    // 第一层:Sandbox
    tools = tools.into_iter()
        .map(|t| self.maybe_wrap_sandbox(t))
        .collect();
    
    // 第二层:Permission Gate(Mutating 工具)
    tools = tools.into_iter()
        .map(|t| self.maybe_wrap_gate(t))
        .collect();
    
    // 第三层:ToolSearch(注册到最后)
    tools.push(Arc::new(ToolSearch::new(tools.clone())));
    
    tools
}

4.2 GateView:展示与执行分离

PermissionGatedTool 最精妙的设计是 GateView trait:

rust 复制代码
pub trait GateView: Send + Sync {
    fn view(&self, input: &serde_json::Value) -> serde_json::Value;
}

对于浏览器工具,BrowserGateView 的实现:

rust 复制代码
impl GateView for BrowserGateView {
    fn view(&self, input: &Value) -> Value {
        let mut enriched = input.clone();
        // 注入运行时上下文------这些信息模型不应"伪造"
        enriched["_target"] = json!(self.target);       // "managed" | "bridge"
        enriched["_page_url"] = json!(self.current_url); // 当前页面 URL
        enriched
    }
}

审批卡片上显示:目标是 managed Chrome 还是用户的 bridge Chrome;当前页面 URL------这些都是 agent 无法伪造的,由 session 状态注入。而 tool 内部收到的仍是原始 input,不受污染。

4.3 并发安全:双重检查锁

同一个 tool 在多个并发调用中应该只提示一次:

rust 复制代码
impl Tool for PermissionGatedTool {
    async fn call(&self, input: Value) -> ToolResult {
        // 第一次检查(快速路径,无锁)
        if self.always_allow.load(Ordering::Acquire) {
            return self.inner.call(input).await;
        }
        
        // 第二次检查(持有锁,防止并发重复提示)
        let _lock = self.approval_lock.lock().await;
        if self.always_allow.load(Ordering::Acquire) {
            return self.inner.call(input).await;
        }
        
        // 弹出审批
        let approved = self.gate.request_approval(&self.view(input)).await?;
        if approved.is_always() {
            self.always_allow.store(true, Ordering::Release);
        }
        self.inner.call(input).await
    }
}

AtomicBool + tokio::sync::Mutex 的组合:绝大多数情况走快速路径(一次原子读),只在第一次审批时短暂持锁。


五、写在最后

Zode 目前还在快速迭代中,很多功能(浏览器操控、桌面自动化、Agent 团队)都在每天变得更加稳定。这个项目的核心哲学是:

AI 不应该是你手里的工具,而应该是你的工程团队。

如果你对 Rust、AI Agent、终端应用感兴趣,欢迎来玩:

如果你也在做类似的 AI Agent 系统,或者对调度引擎、多 Agent 协作有想法,欢迎在评论区交流 👇


标签:Rust、AI Agent、开源、系统设计、异步编程

相关推荐
未秃头的程序猿8 小时前
给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了
java·后端·ai编程
an317428 小时前
6MB 组织树大文件性能优化全流程
前端·javascript·vue.js
Darren2458 小时前
MySQL索引执行计划不走索引下推
后端
程序员清风8 小时前
OpenAI官方发布最新提示词技巧!
java·后端·面试
2zcode9 小时前
项目文档:基于MATLAB的肺结节自动分割与评估系统的设计与实现
人工智能·计算机视觉·matlab
老余说AI9 小时前
AI 时代的劳动力重构:Forward Deployed Engineer (FDE) 的崛起、本质与破局之路
人工智能·ai
明志数科9 小时前
10万小时UMI预训练验证Scaling Law:机器人策略模型的数据范式变革
人工智能·科技·机器人
码事漫谈9 小时前
人机协同的三重范式:HITL、HOTL与HOOTL
后端
武子康9 小时前
Inkling 975B 说明“开放权重“与“普通开发者本地运行“已经分离,内容重点应是部署容量和运行时边界
前端·人工智能·后端
百度Geek说9 小时前
图灵平台:万亿级轨迹数据的秒级检索实战
人工智能