Skip to main content

lellm_agent/tools/
retry.rs

1//! 工具重试策略 — 错误分类、退避、提示注入。
2
3use std::time::Duration;
4
5use super::ToolCallResult;
6
7/// 工具错误类型分类
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ToolErrorKind {
10    Timeout,
11    PermissionDenied,
12    NotFound,
13    NetworkError,
14    ParseError,
15    Unknown,
16}
17
18impl ToolErrorKind {
19    pub fn is_retriable(self) -> bool {
20        matches!(self, Self::Timeout | Self::NetworkError | Self::Unknown)
21    }
22
23    pub fn max_attempts(self) -> u32 {
24        match self {
25            Self::Timeout => 5,
26            Self::NetworkError => 3,
27            Self::Unknown => 3,
28            _ => 0,
29        }
30    }
31
32    pub fn backoff_ms(self, attempt: u32) -> u64 {
33        match self {
34            Self::Timeout => (2_u64).saturating_pow(attempt + 1) * 1000,
35            Self::NetworkError | Self::Unknown => 3000,
36            _ => 0,
37        }
38    }
39
40    pub fn hint(self) -> &'static str {
41        match self {
42            Self::Timeout => "该操作超时,请检查参数或尝试更轻量的替代工具",
43            Self::PermissionDenied => "权限不足,请确认当前角色是否允许此操作",
44            Self::NotFound => "资源未找到,请检查参数拼写",
45            Self::NetworkError => "网络异常,请重试或考虑降级方案",
46            Self::ParseError => "输出格式不匹配,请严格遵循 JSON Schema",
47            Self::Unknown => "操作失败,请分析错误信息并调整策略",
48        }
49    }
50}
51
52/// 退避策略
53#[derive(Debug, Clone)]
54pub enum BackoffStrategy {
55    Fixed(Duration),
56    Exponential { base: Duration, max: Duration },
57}
58
59/// 重试策略
60pub struct RetryPolicy;
61
62impl RetryPolicy {
63    pub async fn execute_with_retry<F, Fut>(kind: ToolErrorKind, f: F) -> ToolCallResult
64    where
65        F: Fn() -> Fut,
66        Fut: std::future::Future<Output = ToolCallResult>,
67    {
68        let max = kind.max_attempts();
69        if max == 0 {
70            return f().await;
71        }
72
73        for attempt in 0..max {
74            let result = f().await;
75            match result {
76                ToolCallResult::Ok(_) => return result,
77                ToolCallResult::Err(msg) if attempt == max - 1 => {
78                    return ToolCallResult::Err(format!(
79                        "{} (retried {} times, hint: {})",
80                        msg,
81                        max,
82                        kind.hint()
83                    ));
84                }
85                ToolCallResult::Err(_) => {
86                    let delay = kind.backoff_ms(attempt);
87                    if delay > 0 {
88                        tokio::time::sleep(Duration::from_millis(delay)).await;
89                    }
90                }
91            }
92        }
93        f().await
94    }
95}