拆解 Warp AI Agent(一):类型即协议——23 种 Action 的编译期安全设计

这是「拆解 Warp AI Agent」系列的第一篇。全系列 5 篇,从 Warp 源码提炼 7 个可复用的 AI Agent 架构模式。本文聚焦最底层的设计决策:为什么用 Rust 类型系统来定义 Agent 的工具协议,而不是 JSON Schema?


一、问题:AI Agent 的工具调用,出错率有多高?

构建 AI Agent 时,工具调用(Tool Call)是最容易出错的一环:

  1. LLM 返回了不存在的工具名 --- 调用端没有对应处理器
  2. 参数类型错误 --- 传了字符串但期望整数
  3. 结果类型不匹配 --- 工具返回了 A 类型但 Agent 期望 B 类型
  4. 取消状态遗漏 --- 用户取消后某个工具忘了处理 cancelled 状态

这些问题在 Python/TypeScript 等动态类型语言中,只能在运行时通过 try-catch 兜底。Warp 的选择不同------让编译器替你检查


二、六层架构全景

先看全貌,再聚焦第一层:

复制代码
┌──────────────────────────────────────────────────────────────┐
│ Layer 6: Agent SDK / Harness    (Oz, Claude, Gemini CLI)     │
├──────────────────────────────────────────────────────────────┤
│ Layer 5: Ambient Agents         (后台任务, 自动执行)          │
├──────────────────────────────────────────────────────────────┤
│ Layer 4: Conversation / Task    (对话状态机, Todo, CodeReview)│
├──────────────────────────────────────────────────────────────┤
│ Layer 3: Action Model           (风险分级, 队列调度)          │
├──────────────────────────────────────────────────────────────┤
│ Layer 2: MCP Protocol           (Transport, Provider, 重连)   │
├──────────────────────────────────────────────────────────────┤
│ Layer 1: Core Primitives  ◄─── 本篇焦点                      │
│   AIAgentActionType (23种) + AIAgentActionResultType          │
│   DiffValidation + Citation + ContentHash                     │
└──────────────────────────────────────────────────────────────┘

第一层是整个 Agent 系统的地基。如果 Action 定义有漏洞,上面五层都建在沙滩上。


三、AIAgentActionType:23 种 Action,编译期穷举

3.1 完整枚举

rust 复制代码
// crates/ai/src/agent/action/mod.rs (827行)
#[derive(Debug, Clone, Eq, PartialEq, EnumDiscriminants)]
pub enum AIAgentActionType {
    // Shell 操作
    RequestCommandOutput {
        command: String,
        is_read_only: Option<bool>,    // LLM 判断是否只读
        is_risky: Option<bool>,        // LLM 判断是否有风险
        wait_until_completion: bool,
        uses_pager: Option<bool>,
        rationale: Option<String>,     // AI 的执行理由
        citations: Vec<AIAgentCitation>,
    },
    WriteToLongRunningShellCommand {
        block_id: BlockId,
        input: bytes::Bytes,
        mode: AIAgentPtyWriteMode,     // Raw / Line / Block
    },

    // 文件操作
    ReadFiles(ReadFilesRequest),
    UploadArtifact(UploadArtifactRequest),
    RequestFileEdits { file_edits: Vec<FileEdit>, title: Option<String> },

    // 搜索操作
    SearchCodebase(SearchCodebaseRequest),
    Grep { queries: Vec<String>, path: String },
    FileGlob { patterns: Vec<String>, path: Option<String> },
    FileGlobV2 { patterns: Vec<String>, search_dir: Option<String> },

    // MCP 操作
    ReadMCPResource { server_id: Option<Uuid>, name: String, uri: Option<String> },
    CallMCPTool { server_id: Option<Uuid>, name: String, input: serde_json::Value },

    // 文档操作
    ReadDocuments(ReadDocumentsRequest),
    EditDocuments(EditDocumentsRequest),
    CreateDocuments(CreateDocumentsRequest),

    // 交互操作
    AskUserQuestion { questions: Vec<AskUserQuestionItem> },

    // 协作操作
    StartAgent {
        version: StartAgentVersion,
        name: String,
        prompt: String,
        execution_mode: StartAgentExecutionMode,
        lifecycle_subscription: Option<Vec<LifecycleEventType>>,
    },
    SendMessageToAgent { addresses: Vec<String>, subject: String, message: String },

    // 其他
    SuggestNewConversation { message_id: String },
    SuggestPrompt(SuggestPromptRequest),
    InitProject,
    OpenCodeReview,
    ReadShellCommandOutput { block_id: BlockId, delay: Option<ShellCommandDelay> },
    UseComputer(UseComputerRequest),
    InsertCodeReviewComments { repo_path: PathBuf, comments: Vec<InsertReviewComment>, base_branch: Option<String> },
    RequestComputerUse(RequestComputerUseRequest),
    ReadSkill(ReadSkillRequest),
    FetchConversation { conversation_id: String },
    TransferShellCommandControlToUser { reason: String },
}

关键设计

  • is_read_onlyis_risky 由 LLM 自己标注------让 AI 自己评估风险等级,而不是由宿主事后判断
  • rationale 字段要求 LLM 给出执行理由,用于 UI 展示和审计
  • citations 追踪每条 Action 的来源,确保可追溯

3.2 为什么不用 JSON Schema?

Claude Code 和大部分 Agent 框架用 JSON Schema 定义工具:

json 复制代码
{
  "name": "read_file",
  "parameters": {
    "type": "object",
    "properties": {
      "path": { "type": "string" }
    }
  }
}

这种方式的问题:

问题 JSON Schema Rust Enum
新增工具忘记加处理器 运行时报错 编译报错
参数类型不匹配 运行时解析 编译期检查
取消状态遗漏 容易漏处理 match 穷举强制覆盖
重构时遗漏调用点 需要全局搜索 编译器自动定位

四、cancelled_result():穷举的力量

Warp 为每种 Action 类型定义了 cancelled_result() 方法:

rust 复制代码
impl AIAgentActionType {
    pub fn cancelled_result(&self) -> AIAgentActionResultType {
        match self {
            Self::RequestCommandOutput { .. } => 
                AIAgentActionResultType::RequestCommandOutput(
                    RequestCommandOutputResult::CancelledBeforeExecution,
                ),
            Self::CallMCPTool { .. } => 
                AIAgentActionResultType::CallMCPTool(CallMCPToolResult::Cancelled),
            Self::ReadFiles(..) => 
                AIAgentActionResultType::ReadFiles(ReadFilesResult::Cancelled),
            Self::UseComputer(..) => 
                AIAgentActionResultType::UseComputer(UseComputerResult::Cancelled),
            // ... 23 种变体,每种都有对应的 Cancelled 结果
            // 新增 Action 时,如果忘记加这一行,编译器会报错!
        }
    }
}

这是整个设计最精妙的部分 :Rust 的 match 要求穷举所有变体。新增一种 Action 类型时,如果你忘记在 cancelled_result() 中添加对应分支,编译直接失败

对比 Python 的实现:

python 复制代码
# Python: 忘记处理新 Action 的 cancelled 状态?
# 没有任何提示,直到运行时才发现
def cancelled_result(self):
    if self.type == "RequestCommandOutput":
        return RequestCommandOutputResult(cancelled=True)
    elif self.type == "ReadFiles":
        return ReadFilesResult(cancelled=True)
    # 忘了 CallMCPTool?没关系,走 else 分支返回 None...
    # 然后在某个不相关的角落崩了

五、三态结果模型:Success / Error / Cancelled

每个 Action 的结果都有三个状态:

rust 复制代码
// crates/ai/src/agent/action_result/mod.rs
pub enum AIAgentActionResultType {
    RequestCommandOutput(RequestCommandOutputResult),
    ReadFiles(ReadFilesResult),
    CallMCPTool(CallMCPToolResult),
    // ...
}

// 每种 Result 内部都有三态:
pub enum RequestCommandOutputResult {
    Success { output: String, exit_code: i32, ... },
    Error { message: String, ... },
    CancelledBeforeExecution,
}
rust 复制代码
impl AIAgentActionResultType {
    pub fn is_successful(&self) -> bool { ... }
    pub fn is_failed(&self) -> bool { ... }
    pub fn is_cancelled(&self) -> bool { ... }
}

为什么 Cancelled 是一等公民? 因为在 Agent 场景中,取消不是异常------是常态。用户随时可能取消一个正在执行的命令、拒绝一个文件编辑、中止一个 MCP 调用。如果把 Cancelled 当作 Error 的子集,会导致:

  • 取消统计和错误统计混淆
  • 取消后的恢复逻辑和错误恢复逻辑耦合
  • 无法区分"我主动停了"和"出了问题"

六、StartAgentExecutionMode:本地与远程的统一抽象

rust 复制代码
pub enum StartAgentExecutionMode {
    Local {
        /// None = Warp 内置 Agent
        /// Some("claude") = 委托给 Claude CLI
        /// Some("gemini") = 委托给 Gemini CLI
        harness_type: Option<String>,
    },
    Remote {
        environment_id: String,
        skill_references: Vec<SkillReference>,
        model_id: String,
        computer_use_enabled: bool,
        worker_host: String,
        harness_type: String,
        title: String,
    },
}

注意 Local { harness_type: Option<String> } 这个设计------同一个 enum 统一了"用 Warp 自己的 Agent"和"委托给 Claude/Gemini CLI"两种执行路径。这让上层代码不需要关心 Agent 跑在本地还是远程、用的是自己的引擎还是第三方 CLI。


七、与业界方案对比

维度 Warp (Rust Enum) Claude Code (JSON Schema) Hermes Agent (TypeScript) OpenAI Function Call
工具定义 编译期枚举 JSON Schema 运行时注册 JSON Schema
类型安全 编译期保证 运行时校验 运行时校验 运行时校验
取消处理 穷举覆盖 按需处理 按需处理 无内置支持
新增工具遗忘 编译报错 运行时 404 运行时 404 运行时 404
风险自评 is_read_only/is_risky 无内置 无内置 无内置
执行理由 rationale 字段 无内置 无内置 无内置

Warp 的核心优势:把 Agent 工具协议的安全检查从运行时左移到了编译期。在 Rust 项目中,这意味着:

如果代码能编译通过,那么工具调用的类型安全就已经保证了。


八、可复用模式:Type-Driven Tool Protocol

提炼为通用模式:

复制代码
┌─────────────────────────────────────────┐
│         Type-Driven Tool Protocol        │
├─────────────────────────────────────────┤
│ 1. 用代数数据类型(ADT)定义所有工具       │
│    - 每个 variant = 一种工具             │
│    - 每个 variant 的字段 = 工具参数       │
│                                          │
│ 2. 用 ADT 定义所有结果                   │
│    - 三态: Success / Error / Cancelled   │
│    - cancelled_result() 穷举覆盖         │
│                                          │
│ 3. 用 match 穷举保证覆盖率              │
│    - 新增工具 → 编译器强制补全所有分支   │
│    - 消除"忘记处理"类 bug                │
│                                          │
│ 4. LLM 自评风险等级                      │
│    - is_read_only + is_risky 字段        │
│    - 让 AI 自己标注,不依赖人工判断       │
└─────────────────────────────────────────┘

在非 Rust 语言中的变体

  • TypeScript : 用 discriminated union + exhaustive switch(never 类型兜底)
  • Python : 用 @dataclass + match (Python 3.10+) + exhaustive check 函数
  • Go: 用 interface + type switch(无编译期穷举保证,需要代码生成辅助)

九、总结

模式 实现 核心价值
Type-Driven Tool Protocol AIAgentActionType 23 变体枚举 编译期保证工具类型安全
Exhaustive Cancellation cancelled_result() match 穷举 新增 Action 不会遗漏取消处理
Three-State Result Success/Error/Cancelled 三态 取消是一等公民,不与错误混淆
LLM Self-Rating is_read_only + is_risky + rationale AI 自己评估风险,无需人工标注

一句话总结 :Warp 用 Rust 的类型系统把"工具协议"从运行时约定变成了编译期约束------能编译就能跑,跑起来就不会在工具类型上出错


系列导航

相关推荐
周末程序猿7 分钟前
图解 120 个大语言模型(LLM)核心概念(1-30)
人工智能
EQUINOX112 分钟前
【论文阅读】| MoCo精读
论文阅读·人工智能·python·深度学习·机器学习
刘一说44 分钟前
AI科技热点日报 | 2026年07月09日
人工智能·科技
LYFlied1 小时前
Anthropic Agent隔离架构解读:比权限弹窗更可靠的是限制爆炸半径
人工智能·架构·软件工程·claude·安全架构·ai agent
hz567891 小时前
电子远程评标系统哪家好?安全、合规、易用平台评测指南
安全·云计算·音视频·实时音视频·信息与通信
MartinYeung51 小时前
[论文学习]大语言模型安全综述:攻击、防御、对齐、度量与护栏的深度解析
学习·安全·语言模型
敲代码的嘎仔1 小时前
实习日志day6--实习日志day6--title命名规范化&businessType纠正&补充缺失的@Log注解&报警与通信模块补充&产出阶段总结文档
java·开发语言·人工智能·git·python·实习·大二
ZZZMMM.zip1 小时前
基于鸿蒙HarmonyOS NEXT开发AI英语口语应用:智能口语练习新体验与鸿蒙Flutter框架跨端实
人工智能·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
程序喵大人1 小时前
【AI专栏】图解Transformer - 第05章:LLM 推理工程
人工智能·深度学习·llm·transformer
甲维斯1 小时前
差距太大了!GPT5.6Sol 正面战 Fable5,啥也不是!
人工智能·ai编程