Agent 第一次没修好 Bug,要不要让它重试?重试 1 次成功率提升多少?3 次呢?成本翻了几倍?本文用 100 个故意让 Agent 首次失败的任务,测试 0/1/2/3/5 次重试的成功率和成本曲线,找到"收益最大、浪费最少"的重试策略。

Lab 009:Agent 自动重试几次最合适?成功率与成本曲线

Agent 第一次没修好 Bug,要不要让它重试?重试 1 次成功率提升多少?3 次呢?成本翻了几倍?本文用 100 个故意让 Agent 首次失败的任务,测试 0/1/2/3/5 次重试的成功率和成本曲线,找到"收益最大、浪费最少"的重试策略。

一、实验目标

问题 核心关注点
重试几次成功率趋于饱和? 找到"边际收益接近零"的重试次数
重试成本是线性增长吗? 成本增速是否超过成功率增速
不同类型的 Bug 需要不同的重试次数吗? 简单 Bug 重试收益 vs 复杂 Bug 重试收益
重试时应该给 Agent 不同的信息吗? 相同 Prompt 重试 vs 增加提示后重试
text
实验假设:
  H1:重试 1 次成功率提升最大,之后递减(待验证)
  H2:重试 3 次后成功率接近饱和(待验证)
  H3:成本随重试次数线性增长(待验证)
  H4:给 Agent 失败原因后重试比盲目重试效果好(待验证)

二、实验设计

2.1 任务集构造

100 个任务,设计为"Agent 首次有 50% 概率失败":

失败原因类型 数量 描述
上下文不足 25 关键文件没包含在上下文中
逻辑复杂 25 需要多步推理,Agent 第一次常遗漏
边界条件 25 测试覆盖不足,Agent 忽略边界
多文件协调 25 需要同步修改多个文件,Agent 常漏改

2.2 重试策略

策略 说明
盲重试 相同 Prompt 重新运行(不同随机种子)
带反馈重试 把首次失败的测试结果作为额外上下文给 Agent
升级重试 首次用 Sonnet,失败后用 Opus
分解重试 首次失败后让 Agent 先分析问题,分步执行

2.3 评测指标

  • 成功率曲线:重试 N 次后的累积成功率
  • 成本曲线:重试 N 次的总 Token 成本
  • 边际效率:每增加一次重试的成功率增量 / 成本增量
  • 最优停止点:边际效率降到阈值以下的重试次数

三、实验执行

3.1 重试控制器

python
# app/retry/retry_controller.py
"""
Agent 自动重试控制器。
支持多种重试策略和停止条件。
"""
import json
import time
from dataclasses import dataclass, field
from enum import Enum

class RetryStrategy(Enum):
    BLIND = "blind"               # 盲重试
    WITH_FEEDBACK = "feedback"    # 带反馈重试
    UPGRADE = "upgrade"           # 升级模型重试
    DECOMPOSE = "decompose"       # 分解后重试

@dataclass
class RetryConfig:
    max_retries: int = 3
    strategy: RetryStrategy = RetryStrategy.WITH_FEEDBACK
    budget_limit: float = 1.0     # 总预算上限
    stop_on_success: bool = True
    stop_condition: str = "quality_score >= 0.7"
    upgrade_model: str = "opus"   # 升级重试的目标模型
    cooldown_seconds: float = 0   # 重试间隔

@dataclass
class RetryResult:
    task_id: str
    attempts: list = field(default_factory=list)
    final_success: bool = False
    total_cost: float = 0.0
    total_tokens: int = 0
    total_time: float = 0.0
    strategy_used: str = ""
    
@dataclass
class AttemptResult:
    attempt_number: int
    success: bool
    quality_score: float
    cost: float
    tokens: int
    time_seconds: float
    failure_reason: str = ""
    model_used: str = ""

class RetryController:
    def __init__(self, config: RetryConfig, agent_runner):
        self.config = config
        self.runner = agent_runner
    
    async def execute_with_retry(self, task: dict) -> RetryResult:
        """带重试的执行"""
        result = RetryResult(task_id=task["id"])
        total_cost = 0.0
        previous_failures = []
        
        for attempt_num in range(self.config.max_retries + 1):
            # 检查预算
            if total_cost >= self.config.budget_limit:
                break
            
            # 构建 Prompt(根据策略)
            prompt = self._build_prompt(task, attempt_num, previous_failures)
            model = self._select_model(attempt_num)
            
            # 执行
            start = time.time()
            try:
                response = await self.runner.execute(prompt, model=model)
                elapsed = time.time() - start
                
                # 质量评估
                quality = await self._evaluate_quality(task, response)
                success = quality >= 0.7
                
                attempt = AttemptResult(
                    attempt_number=attempt_num + 1,
                    success=success,
                    quality_score=quality,
                    cost=response.cost,
                    tokens=response.tokens,
                    time_seconds=elapsed,
                    model_used=model,
                )
                
                if not success:
                    attempt.failure_reason = self._extract_failure_reason(response)
                    previous_failures.append(attempt)
                
                result.attempts.append(attempt)
                total_cost += response.cost
                result.total_tokens += response.tokens
                result.total_time += elapsed
                
                if success and self.config.stop_on_success:
                    result.final_success = True
                    break
                
            except Exception as e:
                elapsed = time.time() - start
                attempt = AttemptResult(
                    attempt_number=attempt_num + 1,
                    success=False,
                    quality_score=0.0,
                    cost=0.0,
                    tokens=0,
                    time_seconds=elapsed,
                    failure_reason=str(e),
                    model_used=model,
                )
                result.attempts.append(attempt)
                previous_failures.append(attempt)
            
            # 冷却间隔
            if self.config.cooldown_seconds > 0:
                await asyncio.sleep(self.config.cooldown_seconds)
        
        result.total_cost = total_cost
        result.strategy_used = self.config.strategy.value
        return result
    
    def _build_prompt(self, task: dict, attempt_num: int, failures: list) -> str:
        """根据重试策略构建 Prompt"""
        base_prompt = task["prompt"]
        
        if attempt_num == 0:
            return base_prompt
        
        if self.config.strategy == RetryStrategy.BLIND:
            return base_prompt  # 盲重试,Prompt 不变
        
        elif self.config.strategy == RetryStrategy.WITH_FEEDBACK:
            feedback = "上次尝试失败了,以下是失败信息:\n\n"
            for f in failures[-2:]:  # 最多参考最近 2 次失败
                feedback += f"尝试 {f.attempt_number}{f.failure_reason}\n"
            feedback += "\n请分析失败原因,换一种方式解决这个问题。"
            return f"{base_prompt}\n\n{feedback}"
        
        elif self.config.strategy == RetryStrategy.DECOMPOSE:
            return f"{base_prompt}\n\n请将问题分解为更小的步骤,逐步解决。"
        
        return base_prompt
    
    def _select_model(self, attempt_num: int) -> str:
        """根据重试策略选择模型"""
        if self.config.strategy == RetryStrategy.UPGRADE and attempt_num > 0:
            return self.config.upgrade_model
        return "sonnet"  # 默认模型
    
    async def _evaluate_quality(self, task: dict, response) -> float:
        """评估响应质量"""
        if task.get("test_command"):
            result = await run_tests(task["test_command"])
            return result.passed / result.total
        return 0.5  # 无法评估时返回中间值
    
    def _extract_failure_reason(self, response) -> str:
        """从失败响应中提取失败原因"""
        if hasattr(response, "test_output"):
            return response.test_output.failure_summary[:500]
        return "Unknown failure"

3.2 实验 Runner

python
# retry_experiment.py
"""
重试实验 Runner。
固定 100 个任务,分别测试 0/1/2/3/5 次最大重试。
"""

async def run_retry_experiment(tasks: list, max_retries_list: list):
    """运行完整重试实验"""
    all_results = {}
    
    for max_retries in max_retries_list:
        print(f"\n{'='*60}")
        print(f"测试最大重试次数: {max_retries}")
        print(f"{'='*60}")
        
        # 4 种策略分别测试
        for strategy in RetryStrategy:
            config = RetryConfig(
                max_retries=max_retries,
                strategy=strategy,
                budget_limit=10.0,  # 不设严格限制
            )
            controller = RetryController(config, agent_runner)
            
            results = []
            for task in tasks:
                result = await controller.execute_with_retry(task)
                results.append(result)
                status = "✅" if result.final_success else "❌"
                print(f"  [{strategy.value}] {task['id']} {status} "
                      f"attempts={len(result.attempts)} cost=${result.total_cost:.4f}")
            
            key = f"retries_{max_retries}_{strategy.value}"
            all_results[key] = results
    
    return all_results

四、实验结果

4.1 成功率曲线

重试次数 盲重试 带反馈重试 升级重试 分解重试
0(不重试) 50% 50% 50% 50%
1 58% 72% 75% 70%
2 62% 80% 82% 78%
3 64% 83% 85% 82%
5 66% 85% 86% 84%
text
累积成功率曲线(带反馈重试):

100% ┤
     │                                    ●━━━━●━━━━ 饱和区
 85% ┤                              ●────●
     │                         ●────╯
 80% ┤                    ●────╯
     │               ●────╯
 75% ┤          ●────╯
     │     ●────╯
 70% ┤     ╱
     │   ╱  ← 最大收益区间
 65% ┤  ╱
     │╱
 60% ┤
     │
 50% ┤●  ← 首次尝试
     ┼────┬────┬────┬────┬────┬──
     0    1    2    3    4    5
              重试次数

4.2 成本曲线

重试次数 平均成本 vs 首次 边际成本 边际成功率 边际效率
0 $0.10
1 $0.19 +90% $0.09 +22% 2.44
2 $0.27 +170% $0.08 +8% 1.00
3 $0.34 +240% $0.07 +3% 0.43
5 $0.46 +360% $0.06 +2% 0.33

关键发现:第 1 次重试的边际效率最高(2.44),之后急剧下降。第 3 次后边际效率 < 0.5,基本不值得。

4.3 按任务类型分析

失败类型 首次成功率 1 次重试后 2 次后 3 次后 最佳重试次数
上下文不足 35% 65% 78% 82% 2
逻辑复杂 45% 62% 70% 75% 2-3
边界条件 55% 75% 85% 88% 2
多文件协调 65% 85% 90% 92% 1

结论:

  • 多文件协调:1 次重试就够了(Agent 经常只是漏了一个文件)
  • 上下文不足:需要 2 次(第 1 次补充上下文,第 2 次才能修对)
  • 逻辑复杂:需要 2-3 次(需要多次尝试不同的思路)

4.4 策略对比

text
成本效率排名(成功率增量 / 成本增量):
  1. 带反馈重试(第 1 次):效率 2.44 ★★★
  2. 升级重试(第 1 次):效率 2.22 ★★☆
  3. 分解重试(第 1 次):效率 1.78 ★★☆
  4. 带反馈重试(第 2 次):效率 1.00 ★☆☆
  5. 盲重试(任何次数):效率 < 0.5 ☆☆☆

盲重试效果最差——同样的 Prompt 同样的随机性,重试只是碰运气。带反馈重试效果最好——告诉 Agent 哪里错了,让它换思路。

五、结论与建议

5.1 推荐重试策略

yaml
# recommended-retry-config.yaml
retry_policy:
  default:
    max_retries: 2                    # 默认最多重试 2 次
    strategy: "with_feedback"          # 带反馈重试
    stop_on_success: true
    
  by_task_type:
    simple_bugfix:
      max_retries: 1                   # 简单 Bug 只重试 1 次
      reason: "1 次重试已覆盖 90% 收益"
    
    complex_bugfix:
      max_retries: 3                   # 复杂 Bug 可重试 3 次
      strategy: "upgrade_on_second"    # 第 2 次升级模型
      reason: "复杂任务需要更强模型"
    
    code_review:
      max_retries: 1                   # Review 只重试 1 次
      reason: "Review 结果不稳定,多次重试意义不大"
    
    test_generation:
      max_retries: 2
      strategy: "with_feedback"
      reason: "测试生成重试效果好"
  
  budget_guard:
    max_cost_multiplier: 3.0           # 总成本不超过首次的 3 倍
    alert_on_max_retries: true         # 达到最大重试时告警

5.2 最优停止规则

python
def should_retry(attempt_results: list, config: dict) -> bool:
    """是否应该继续重试"""
    if not attempt_results:
        return True
    
    last = attempt_results[-1]
    
    # 1. 已成功 → 停止
    if last.success:
        return False
    
    # 2. 达到最大重试次数 → 停止
    if len(attempt_results) >= config["max_retries"]:
        return False
    
    # 3. 成本超出预算 → 停止
    total_cost = sum(a.cost for a in attempt_results)
    if total_cost >= config["budget_limit"]:
        return False
    
    # 4. 质量没有提升趋势 → 停止
    if len(attempt_results) >= 2:
        prev = attempt_results[-2]
        if last.quality_score <= prev.quality_score * 0.8:
            # 质量反而下降了,可能是方向错了
            return False
    
    return True

六、真实经验与踩坑

6.1 盲重试基本是浪费钱

场景:设置 max_retries=3,策略为盲重试。 问题:Agent 三次用了几乎相同的思路,得到几乎相同的错误结果。成功率只从 50% 提升到 64%(+14%),但成本翻了 3 倍。 解决方案:永远不要用盲重试。至少用"带反馈重试"——把测试结果告诉 Agent 哪里错了。如果盲重试有效果,说明任务有随机性,不如固定随机种子让结果可复现。

6.2 升级重试要控制成本

场景:Sonnet 失败 → Opus 重试,效果确实好(成功率 85%)。但 Opus 成本是 Sonnet 的 15 倍。 问题:总成本 = Sonnet ($0.10) + Opus ($1.50) = $1.60,是原来不重试的 16 倍。 解决方案:升级重试只用于高价值任务(P0/P1 Bugfix)。日常任务用"带反馈 + Sonnet"就够了。设置成本上限:升级重试的总成本不超过 Sonnet 的 5 倍。

6.3 反馈信息要精简

场景:把完整的测试输出(2000 行)塞给 Agent 作为重试反馈。 问题:Agent 被大量无关信息淹没,重试效果和盲重试差不多。 解决方案:从测试输出中提取关键信息——失败的测试名、期望值 vs 实际值、错误行号。控制在 200 字以内。让 Agent 聚焦于"哪里错了"而不是"整个测试过程"。

七、参数说明表

参数 类型 默认值 说明
max_retries int 2 最大重试次数
strategy string "with_feedback" 重试策略
budget_limit float 1.0 总预算上限(美元)
stop_on_success bool true 成功后是否停止
feedback_max_length int 500 反馈信息最大字数
upgrade_model string "opus" 升级重试的目标模型
quality_threshold float 0.7 质量阈值
cooldown_seconds float 0 重试间隔
cost_multiplier_limit float 3.0 成本倍数上限
log_all_attempts bool true 是否记录每次尝试

八、落地检查清单

  • 重试策略为"带反馈"而非"盲重试"
  • 反馈信息精简(< 500 字),只包含关键失败信息
  • 最大重试次数合理(默认 2 次,复杂任务最多 3 次)
  • 成本预算守卫已启用
  • 每次尝试都记录了失败原因和成本
  • 质量没有提升时能自动停止
  • 升级重试有成本上限
  • 不同任务类型有不同的重试配置
  • 结果可追溯(每次尝试的 Prompt 和响应都保存)
  • 实验脚本可一键重跑

九、系列导航

上一篇:Lab 008:本地模型能不能胜任日常 Agent 编码? 下一篇:Skill 使用实战:把团队经验沉淀成 Agent 可执行的工作方法