Lab 009:Agent 自动重试几次最合适?成功率与成本曲线
Agent 第一次没修好 Bug,要不要让它重试?重试 1 次成功率提升多少?3 次呢?成本翻了几倍?本文用 100 个故意让 Agent 首次失败的任务,测试 0/1/2/3/5 次重试的成功率和成本曲线,找到"收益最大、浪费最少"的重试策略。
一、实验目标
| 问题 | 核心关注点 |
|---|---|
| 重试几次成功率趋于饱和? | 找到"边际收益接近零"的重试次数 |
| 重试成本是线性增长吗? | 成本增速是否超过成功率增速 |
| 不同类型的 Bug 需要不同的重试次数吗? | 简单 Bug 重试收益 vs 复杂 Bug 重试收益 |
| 重试时应该给 Agent 不同的信息吗? | 相同 Prompt 重试 vs 增加提示后重试 |
实验假设:
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 重试控制器
# 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
# 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% |
累积成功率曲线(带反馈重试):
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 策略对比
成本效率排名(成功率增量 / 成本增量):
1. 带反馈重试(第 1 次):效率 2.44 ★★★
2. 升级重试(第 1 次):效率 2.22 ★★☆
3. 分解重试(第 1 次):效率 1.78 ★★☆
4. 带反馈重试(第 2 次):效率 1.00 ★☆☆
5. 盲重试(任何次数):效率 < 0.5 ☆☆☆盲重试效果最差——同样的 Prompt 同样的随机性,重试只是碰运气。带反馈重试效果最好——告诉 Agent 哪里错了,让它换思路。
五、结论与建议
5.1 推荐重试策略
# 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 最优停止规则
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 可执行的工作方法