错误处理

RAF 采用分层错误处理策略——区分框架级错误、工具级错误和 LLM 传输错误,确保错误在正确的层级被捕获和处理。

错误体系总览

AgentError (框架级)
├── ChatClientError    — LLM 通信失败
├── ToolError          — 工具执行/反序列化失败
├── WorkflowError      — 工作流编排错误
├── ConfigError        — 配置错误(缺少 chat_client 等)
├── AgentNotFound      — Agent 未注册
├── StreamError        — 流式处理错误
├── Serialize          — 序列化/反序列化错误
└── Other(anyhow)      — 未分类错误

ToolResult (工具级)
├── ok: true, data: Some(Value)   — 成功
├── ok: false, error: Some(String) — 预期错误(文件不存在等)

AgentError 枚举

pub enum AgentError {
    ChatClientError(String),    // LLM 客户端错误
    ToolError(String),          // 工具执行或参数反序列化失败
    WorkflowError(String),      // 工作流编排错误
    ConfigError(String),        // 配置错误
    AgentNotFound(String),      // Agent 未注册
    StreamError(String),        // 流式处理错误
    Serialize(String),          // 序列化/反序列化错误
    Other(#[from] anyhow::Error), // 未分类(透明透传)
}

每个变体都通过 `#[error("...")]` 提供人类可读的消息模板。

Result 类型别名

pub type Result<T> = std::result::Result<T, AgentError>;

框架中几乎所有的异步方法都返回 `Result<T>`,统一错误类型。这使得调用方可以用 `?` 轻松传播错误。

// Agent 运行
async fn run(&self, messages: Vec<ChatMessage>, ...)
    -> Result<BoxStream<'static, Result<AgentResponseResult>>>;

// 工具执行(返回框架级错误或工具级错误)
async fn execute(&self, arguments: Value)
    -> Result<ToolResult>;

错误的三个层级

层级 1:框架级错误 (`AgentError`)

当框架基础设施出现问题时抛出,如:

- `ConfigError`:`AgentBuilder::build()` 未设置 `chat_client` - `AgentNotFound`:`AgentRegistry` 找不到指定 ID 的 Agent - `StreamError`:SSE 流解析异常 - `Serialize`:消息序列化失败

// AgentBuilder 中的典型错误
let agent = AgentBuilder::new("agent")
    // 忘记设置 chat_client
    .build()?;
// 返回:Err(AgentError::ConfigError("chat_client is required"))

层级 2:工具级错误 (`ToolResult`)

当工具执行过程中遇到预期错误时,**不应**返回 `Err(AgentError)`,而应返回 `ToolResult::error()`。

这样 LLM 可以看到错误信息并调整行为:

// ❌ 错误做法:遇到文件不存在就 panic 或返回框架错误
if !path.exists() {
    return Err(AgentError::ToolError("file not found".into()));
}

// ✅ 正确做法:返回工具级错误,LLM 可以看到并重试
if !path.exists() {
    return Ok(ToolResult::error("文件不存在: src/missing.rs"));
}

**两种错误的区别**:

| 类型 | 何时使用 | LLM 可见? | 示例 | |------|----------|-----------|------| | `Err(AgentError::ToolError)` | 参数反序列化失败、工具执行崩溃 | 否(框架拦截) | `serde_json::from_value()` 失败 | | `Ok(ToolResult::error(msg))` | 业务级错误(文件不存在、权限不足) | 是(注入消息历史) | 文件不存在、路径越界 |

层级 3:LLM 传输层错误

`AgentError::ChatClientError` 用于包装 API 通信失败,如:

- 网络超时 - API 返回 4xx/5xx 状态码 - SSE 流断开 - API Key 无效

// deepseek_client.rs 中的典型模式
let resp = client.get(&url)
    .header("Authorization", format!("Bearer {}", api_key))
    .send()
    .await
    .map_err(|e| AgentError::ChatClientError(format!(
        "deepseek list_models failed: {}", e
    )))?;

ToolResult 结构体

pub struct ToolResult {
    pub ok: bool,                       // 成功标记
    pub data: Option<serde_json::Value>, // 结构化结果
    pub error: Option<String>,          // 错误描述
}

构造方法

// 成功
ToolResult::success(serde_json::json!({
    "path": "src/main.rs",
    "content": "...",
    "bytes_read": 1024,
}))

// 简单错误
ToolResult::error("文件不存在: src/missing.rs")

// 带结构化数据的错误(如校验失败的字段详情)
ToolResult::error_with_data(
    "参数校验失败",
    serde_json::json!({"invalid_fields": ["path"]}),
)

框架对 ToolResult 的处理

`FunctionInvokingChatClient` 将 `ToolResult` 序列化为 JSON 字符串,作为 `ChatMessage::tool()` 注入 LLM 对话:

// 框架内部处理逻辑(简化版)
let tool_result = tool.execute(arguments).await?;
let content = serde_json::to_string(&tool_result)?;
let message = ChatMessage::tool(content, tool_call_id);
messages.push(message);

错误传播路径

flowchart TB
    subgraph "LLM 传输层"
        HTTP[HTTP 错误] --> |ChatClientError| Agent
    end

    subgraph "工具执行层"
        Parse[参数反序列化失败] --> |ToolError| Agent
        Biz[业务级错误] --> |ToolResult::error| LLM
        Success[执行成功] --> |ToolResult::success| LLM
    end

    subgraph "框架配置层"
        Config[配置缺失] --> |ConfigError| User
        NotFound[Agent 未注册] --> |AgentNotFound| User
    end

    subgraph "用户代码"
        User[调用方]
    end

    Agent[Agent/Builder] --> |Result| User
    LLM --> |注入对话| Agent

错误处理最佳实践

1. 使用 `?` 传播框架错误

async fn handle_request(agent: &dyn IAgent) -> Result<()> {
    let stream = agent.run(messages, Some(session), None).await?;
    //                   ↑ 如果 LLM 调用失败,自动返回错误
    // 处理流...
    Ok(())
}

2. 工具中用 ToolResult 处理业务错误

impl MyTool {
    async fn call(&self, arguments: Value) -> Result<ToolResult> {
        let args: Args = serde_json::from_value(arguments)
            .map_err(|e| AgentError::ToolError(format!("反序列化失败: {}", e)))?;
        //      ↑ 框架级错误:参数格式不对

        match std::fs::read_to_string(&args.path) {
            Ok(content) => Ok(ToolResult::success(json!({"content": content}))),
            Err(e) => Ok(ToolResult::error(format!("读取文件失败: {}", e))),
            //                ↑ 工具级错误:LLM 能看到并重试
        }
    }
}

3. 流式错误的处理

`agent.run()` 返回的流中,每个 item 都是 `Result<AgentResponseResult>`。需要区分流级错误和块级错误:

let mut stream = agent.run(messages, Some(session), None).await?;  // 流级错误
while let Some(item) = stream.next().await {
    match item {
        Ok(chunk) => { /* 正常处理 */ }
        Err(e) => {
            // 块级错误:可能是单个 SSE 事件解析失败
            eprintln!("流块错误: {}", e);
            // 根据业务决定是否继续
        }
    }
}

4. anyhow 透明透传

`AgentError::Other` 使用 `#[from] anyhow::Error` 实现了 `From` trait,任何 `anyhow::Error` 可自动转换。这允许在框架内部和扩展中使用 `anyhow` 而无需手动封装。

下一步

了解错误处理机制后,请阅读 **[Crate 地图](./crate-map.md)**,了解全部 15 个 crate 的职责和依赖关系。