AI多Agent协作系统实战(十六):当AI Agent学会了“无限重试“——RETEST循环嵌套的血泪修复史

发布时间:2026/8/8 11:11:08
AI多Agent协作系统实战(十六):当AI Agent学会了“无限重试“——RETEST循环嵌套的血泪修复史 系列第16篇 | 从15层任务ID嵌套到最大重试3次中间踩了哪些坑引言凌晨00:48我收到一条飞书通知❌ 复核不通过: TEST-DEV-IOT-1.1-DATABASE 已重派: RETEST-TEST-DEV-IOT-1.1-DATABASE30秒后又来一条❌ 复核不通过: RETEST-TEST-DEV-IOT-1.1-DATABASE 已重派: RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE再30秒❌ 复核不通过: RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE 已重派: RETEST-RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE我盯着屏幕看着任务ID像俄罗斯套娃一样一层层叠加。15分钟后统筹报告变成了这样| 9 | RETEST-RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE | | 10 | RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE | | 11 | RETEST-TEST-DEV-IOT-1.1-DATABASE | | 12 | TEST-DEV-IOT-1.1-DATABASE |4个任务其实是同一个任务的4个分身。而且还在继续生…问题1RETEST无限嵌套现象从ws_server.log可以清晰看到嵌套过程00:48:50 [WS] 复核失败review.py已自动重派RETEST: TEST-DEV-IOT-1.1-DATABASE 00:49:38 [WS] 复核失败review.py已自动重派RETEST: RETEST-TEST-DEV-IOT-1.1-DATABASE 00:50:21 [WS] 复核失败review.py已自动重派RETEST: RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE 00:51:04 [WS] 复核失败review.py已自动重派RETEST: RETEST-RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE每44秒一轮任务ID不断变长。最夸张的时候一个任务的ID变成了RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE15层嵌套52个字符的前缀。而且这个怪物还在继续生长。原因问题出在review.py的dispatch_retest()函数# 原始代码无限制defdispatch_retest(task_id,reason):retest_idfRETEST-{task_id}# 每次都加前缀# ... 派发任务这个函数的逻辑是复核失败生成新任务IDRETEST- 原ID派发新任务看起来没问题但问题是——没有上限。当TEST-DEV-IOT-1.1-DATABASE复核失败变成RETEST-TEST-DEV-IOT-1.1-DATABASE。当RETEST-TEST-DEV-IOT-1.1-DATABASE又失败变成RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE。当RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE还失败…无限循环。修复defdispatch_retest(task_id,reason):自动重派测试任务最多重试3次防止无限循环# 检查重试次数计算task_id中有多少层RETEST前缀retry_counttask_id.count(RETEST-)-(1iftask_id.startswith(RETEST-)else0)iftask_id.startswith(RETEST-):# RETEST-RETEST-xxx 的retry_count 前缀中RETEST的个数partstask_id.split(-TEST-)iflen(parts)1:retry_countparts[0].count(RETEST)ifretry_count3:returnf⚠️ 已达最大重试次数(3次)跳过重派:{task_id}retest_idfRETEST-{task_id}# ... 继续派发关键改动添加重试计数从task_id中解析已重试次数设置上限最多3次超限返回警告不再继续派发问题2计数逻辑的坑第一次尝试简单countretry_counttask_id.count(RETEST-)看起来对但有个边界情况RETEST-TEST-DEV-xxx→ count 1 ✓RETEST-RETEST-TEST-DEV-xxx→ count 2 ✓TEST-DEV-xxx→ count 0 ✓那RETEST-DEV-xxx呢count 1但其实应该也是1次重试。好像没问题等等再想想。如果原始任务ID本身就包含RETEST呢比如FIX-RETEST-0714-xxx。这时count会误判。第二次尝试拆分解析partstask_id.split(-TEST-)iflen(parts)1:retry_countparts[0].count(RETEST)这个逻辑更精确以-TEST-为分隔符拆分第一部分是前缀如RETEST-RETEST统计前缀中RETEST的个数但还有个问题如果原始任务ID是TEST-DEV-xxx拆分后parts [TEST, DEV-xxx]parts[0] TESTcount(‘RETEST’) 0。正确。如果是RETEST-TEST-DEV-xxx拆分后parts [RETEST, DEV-xxx]parts[0] RETESTcount 1。正确。如果是RETEST-RETEST-TEST-DEV-xxx拆分后parts [RETEST-RETEST, DEV-xxx]parts[0] RETEST-RETESTcount 2。正确。最终方案retry_counttask_id.count(RETEST-)-(1iftask_id.startswith(RETEST-)else0)iftask_id.startswith(RETEST-):partstask_id.split(-TEST-)iflen(parts)1:retry_countparts[0].count(RETEST)两套逻辑双重验证确保计数准确。问题3dev_id提取的连锁反应RETEST前缀叠加不只影响计数还影响了review.py中提取原始任务ID的逻辑。原来的问题review.py需要从task_id中提取dev_id来匹配MD文件# 从RETEST-TEST-DEV-IOT-1.1-DATABASE提取DEV-IOT-1.1-DATABASEdev_idtask_id.replace(RETEST-,)当只有1层前缀时这行代码能工作。但当有3层前缀时task_idRETEST-RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASEdev_idtask_id.replace(RETEST-,)# → TEST-DEV-IOT-1.1-DATABASE还是不对因为还有TEST-前缀。而且如果原始任务就是TEST-DEV-xxx去掉RETEST-后还剩TEST-DEV-xxx需要再去掉TEST-。修复方案defextract_dev_id(task_id):从嵌套的RETEST前缀中提取原始dev_id# 去掉所有RETEST-前缀dev_idtask_idwhiledev_id.startswith(RETEST-):dev_iddev_id[8:]# len(RETEST-) 8# 如果还有TEST-前缀也去掉ifdev_id.startswith(TEST-):dev_iddev_id[5:]# len(TEST-) 5returndev_id用循环确保无论嵌套多少层都能正确提取。问题4DB查询的模糊匹配任务ID变形后数据库查询也出了问题。原来的问题SELECT*FROMtasksWHEREtask_idTEST-DEV-IOT-1.1-DATABASE当任务被重派为RETEST-TEST-DEV-IOT-1.1-DATABASE后这条SQL查不到原任务了。修复方案SELECT*FROMtasksWHEREtask_idLIKE%DEV-IOT-1.1-DATABASE用LIKE模糊匹配只要ID包含原始任务名就能查到。但这也带来新问题如果同时存在DEV-IOT-1.1-DATABASE和DEV-IOT-1.1-DATABASE-MOBILE模糊匹配会返回多条记录。最终方案SELECT*FROMtasksWHEREtask_idLIKE%DEV-IOT-1.1-DATABASEANDtask_idNOTLIKE%-%-DEV-IOT-1.1-DATABASEORDERBYcreated_atDESCLIMIT1排除带前缀的记录只取最新的。问题5统筹报告的混乱嵌套的任务ID让统筹报告变得不可读| 9 | RETEST-RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE | RETESTRETESTRETESTTESTDE | | 10 | RETEST-RETEST-TEST-DEV-IOT-1.1-DATABASE | RETESTRETESTTESTDEVIOT1. | | 11 | RETEST-TEST-DEV-IOT-1.1-DATABASE | RETESTTESTDEVIOT1.1DATAB | | 12 | TEST-DEV-IOT-1.1-DATABASE | IOT1.1DATABASE |第2列的完善内容被截断到10个字符根本看不出是什么任务。修复方案在生成报告时提取原始任务名defshorten_task_id(task_id):提取可读的任务名# 去掉所有RETEST-前缀nametask_idwhilename.startswith(RETEST-):namename[8:]# 去掉TEST-/FIX-前缀forprefixin[TEST-,FIX-]:ifname.startswith(prefix):namename[len(prefix):]returnname[:15]# 截断到15字符经验总结1. 自动化机制必须有上限任何自动重试、自动重派的逻辑都必须有最大次数限制。否则一个失败的任务会无限繁殖最终拖垮整个系统。2. 任务ID设计要考虑可追溯性RETEST前缀叠加的设计虽然能保留历史但会让ID变得不可读。更好的方案是在DB中单独记录retry_count字段而不是修改任务ID本身。3. 字符串操作要防御性编程从task_id中提取信息时不能假设ID的格式是固定的。要用循环、正则等稳健的方式处理各种边界情况。4. 日志是最好的调试工具这次问题能快速定位全靠ws_server.log中清晰的记录。如果日志不详细光看统筹报告的混乱输出很难快速找到根因。系列第16篇 | 2026-07-15基于真实事件还原所有代码和日志均来自实际系统欢迎加入QQ频道共同交流。

相关新闻