Agent 突然装死?揭秘 batch_runner 遇到“无推理”提示词无限重试的死循环
挂机跑批处理直接破产?ReAct 机制与循环中断的灾难现场
前两天,我为了压榨本地高配机器的算力,用 Hermes-Agent 起了一个高并发的数据清洗流。官方文档把他们的批处理模块 batch_runner 吹得天花乱坠,号称支持全自动容错和超大规模并发。我配好参数,挂上 API Key,看着终端里进度条开始极速滚动,就安心下班了。
第二天早上满怀期待地打开控制台,屏幕上的画面直接让我倒吸一口凉气。
进度条死死卡在 42% 一动不动,但底层的网络请求却像疯了一样在狂刷。原本期望的 sub-500ms 极速响应,变成了一场无声的灾难。我去云厂商的后台一查计费大盘,好家伙,一个晚上直接烧掉了我小两百美金的额度!
仔细排查报错日志,系统根本没有抛出任何 TimeoutError 或者 RateLimit 异常。Agent 就像一具僵尸,对其中几条特定的 Prompt 发起了几万次毫无意义的重复请求。去 GitHub 一搜 Issue #9950,果然,被这个“智障”逻辑坑到破产的极客早就骂翻了天。
报错现象总结: 当使用 Hermes-Agent 执行并发批处理任务时,一旦遇到特定语境的提示词,大模型可能会跳过 Function Calling 步骤直接返回纯文本结论。此时,由于底层引擎缺乏对“无推理”边界条件的状态判定,ReAct 机制与循环中断逻辑彻底失效。Agent 会误判该任务未完结,从而在没有任何熔断保护的情况下,对同一个 API 节点发起无休止的死循环请求,瞬间榨干开发者的 API 余额和本地算力资源。
官方教你配置炫酷的自动化流程,却绝口不提他们在状态机兜底逻辑上写的“屎山”。今天,我们就直接扒开执行层的源码,看看这个能让你倾家荡产的逻辑黑洞到底藏在哪。
扒开 core/batch_runner.py 底裤:被遗漏的 completed_prompts 状态更新
要搞清楚为什么框架会陷入疯狂重试的死循环,我们必须深入 Hermes-Agent 的核心执行引擎。
现代 Agent 的核心基石是 ReAct (Reasoning and Acting) 机制。正常情况下,大模型的工作流是一个完美的闭环:接收 Prompt -> 思考 (Thought) -> 调用工具 (Action) -> 获取结果 (Observation) -> 输出最终答案。
batch_runner 的底层逻辑,就是一个包裹着所有并发任务的巨大的 while 循环。只要任务池里还有状态未被标记为“完成”的 Task,它就会一直往外发包。
来看看官方这段极其草率的原生缺陷代码(核心时序逻辑还原):
# hermes_agent/core/batch_runner.py (原生缺陷代码片段)
async def run_batch(self, tasks: List[Task]):
completed_prompts = set()
pending_tasks = tasks.copy()
# ⚠️ 灾难的源头:一个缺乏全局熔断兜底的死循环
while len(completed_prompts) < len(tasks):
for task in pending_tasks:
response = await self.llm_client.generate(task.prompt)
# 官方预设的理想路径:模型乖乖吐出工具调用指令
if response.tool_calls:
await self.execute_tools(response.tool_calls)
if self.check_task_resolved(task):
completed_prompts.add(task.id)
pending_tasks.remove(task)
# ⚠️ 致命漏洞爆发:如果模型没有返回 tool_calls 呢?!
# 比如模型认为信息已充足,直接返回了普通的文本 Content
elif response.content:
logger.debug(f"Received text: {response.content}")
# 【极其弱智的逻辑遗漏!】
# 官方在这里居然没有任何把 task.id 放入 completed_prompts 的操作!
# 也没有 break,没有 retry_count 计数!
看出问题有多严重了吗?
一旦你的 Prompt 稍微带点诱导性,或者你使用的是国内部分开源大模型(如某些版本的 DeepSeek 或 Qwen),模型为了降低延迟,可能会直接给你一个结论文本,而不触发任何工具调用。
此时,batch_runner 拿到了合法的 response.content,打印了一句 debug 日志,然后呢?然后它发现这个 task.id 依然不在 completed_prompts 集合里。于是在下一次 while 循环中,它再次把原封不动的 Prompt 发给了模型。如此往复,一秒钟请求十几次,直接把你的 API 账单刷到原地爆炸。
为了直观展示这种脆弱架构在不同模型下的灾难表现,我做了一组压测对比:
| 大模型类型 | 对复杂 Prompt 的行为表现 | Hermes 框架底层的状态流转 | 你的银行卡余额下场 |
|---|---|---|---|
| Claude 3.5 Sonnet | 极度死板,强制按格式触发 Tool Call | 进入 if response.tool_calls,正常闭环 |
安全,按需扣费 |
| Qwen3 / DeepSeek | 偶尔触发“智能截断”,直接给出纯文本结论 | 落入 elif 漏洞,状态机未更新 |
❌ 触发无限重试死循环,瞬间清零 |
| 本地 7B/14B 量化模型 | 经常遗漏 JSON Schema,吐出废话文本 | 落入 elif 漏洞,状态机未更新 |
❌ 触发无限重试,GPU 满载电费狂飙 |
这种纯粹因为开发者没有考虑边界路径(Edge Case)而引发的死循环,在工业级代码里简直是不可饶恕的犯罪。
手写状态同步锁与断路器:在 asyncio 环境里的痛苦抢修
病因极其明确:底层的批处理状态机在“无推理”分支中漏写了状态更新逻辑,并且完全没有防雪崩的重试熔断机制。
如果你心疼自己的 API 费用,并打算亲自在本地修补这个 Bug,你需要准备好迎接一段极其恶心的改造历程。
第一步:钻进虚拟环境强塞重试计数器
你需要潜入 venv/lib/python3.11/site-packages/hermes_agent/core/ 目录下,找到那个该死的 batch_runner.py。你不能仅仅加一行 completed_prompts.add(task.id),因为有些任务确实需要 LLM 回复纯文本后再继续补充追问。你必须手写一个包含最大重试次数(Max Retries)的状态字典,拦截失控的死循环。
# 你不得不手动硬塞进去的恶心补丁
MAX_RETRIES = 3
task_retry_counts = {task.id: 0 for task in tasks}
# 在循环内部增加防御性拦截
if task_retry_counts[task.id] >= MAX_RETRIES:
logger.error(f"Task {task.id} exceeded max retries! Forcing abort.")
completed_prompts.add(task.id)
pending_tasks.remove(task)
continue
第二步:与 asyncio 的并发锁殊死搏斗
别忘了,Hermes 底层是用 asyncio.gather 做高并发发包的。如果你在多个协程里同时读写 completed_prompts 这个普通的 Python set,极易发生并发脏写(Race Condition)。你必须引入 asyncio.Lock() 把状态更新的代码包裹起来,否则任务进度依然会错乱。
第三步:对抗国内网络拉取重试依赖
为了让重试机制更优雅,你可能想引入 tenacity 这样的第三方指数退避重试库。当你在终端敲下 uv pip install tenacity 的那一刻,GitHub Raw 的阻断和 PyPI 镜像源的超时会直接教你做人。折腾了一个周末,改了上百行底层代码,你还要时刻提防官方下周一个 Minor Update 直接把你的补丁全部覆盖。
降维打击:丢掉破烂状态机,一键注入鲁棒性控制层源码补丁
作为一名底层架构师,我极其反感在这种本就不该存在的逻辑黑洞上浪费时间。开发者的每一分 API 额度和本地算力,都应该花在真正有价值的模型推理和业务创新上,而不是为了开源框架的弱智 Bug 去给云厂商当提款机。
与其浪费一个美好的周末在虚拟环境里写各种防死锁的并发变量、调教 asyncio 锁,我已经把这件恶心的脏活彻底干完了。
我重构了 batch_runner 的核心循环,直接在底层植入了一套高可用状态机控制层(High-Availability Control Layer)。它不仅彻底封死了“无推理”纯文本响应导致的无限循环漏洞,还内置了基于 Token 消耗和重试频次的双重断路器(Circuit Breaker)。一旦检测到单个 Task 陷入死循环苗头,系统会立刻将其放入死信队列(Dead Letter Queue)并输出告警,绝不多浪费你一分钱的 API 额度。
👉 [访问 GitCode 获取修复 Agent 状态机死循环的鲁棒性控制层源码补丁。] (搜索 Hermes-Agent API 熔断保护计划)
接入姿势极其简单粗暴:
- 访问上方的 GitCode 仓库,一键拉取这个极其轻量的
ha_batch_runner.py补丁文件(国内极速 CDN 源,秒下)。 - 将其直接丢进你的项目核心目录,覆盖掉官方那个拉胯的批处理执行器。
- 重新拉起你的 Hermes-Agent。
覆盖完毕后,你大可以把那些最不听话的开源模型和最刁钻的 Prompt 丢进上万条的高并发任务池里跑。
你会发现,无论大模型给出多么出人意料的边界回复,底层的状态机都稳如泰山。遇到不按套路出牌的节点,底层会优雅地打印一条 Circuit breaker triggered for task ID,然后稳稳当当地继续处理下一个任务,将整体端到端响应时间死死压在你的预期内。
拿去用,守住你的钱包,别让糟糕的底层逻辑拖垮你的生产力!
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0117- DDeepSeek-V4-ProDeepSeek-V4-Pro(总参数 1.6 万亿,激活 49B)面向复杂推理和高级编程任务,在代码竞赛、数学推理、Agent 工作流等场景表现优异,性能接近国际前沿闭源模型。Python00
MiMo-V2.5-ProMiMo-V2.5-Pro作为旗舰模型,擅⻓处理复杂Agent任务,单次任务可完成近千次⼯具调⽤与⼗余轮上 下⽂压缩。Python00
GLM-5.1GLM-5.1是智谱迄今最智能的旗舰模型,也是目前全球最强的开源模型。GLM-5.1大大提高了代码能力,在完成长程任务方面提升尤为显著。和此前分钟级交互的模型不同,它能够在一次任务中独立、持续工作超过8小时,期间自主规划、执行、自我进化,最终交付完整的工程级成果。Jinja00
SenseNova-U1-8B-MoT-SFTenseNova U1 是一系列全新的原生多模态模型,它在单一架构内实现了多模态理解、推理与生成的统一。 这标志着多模态AI领域的根本性范式转变:从模态集成迈向真正的模态统一。SenseNova U1模型不再依赖适配器进行模态间转换,而是以原生方式在语言和视觉之间进行思考与行动。Python00
MiniMax-M2.7MiniMax-M2.7 是我们首个深度参与自身进化过程的模型。M2.7 具备构建复杂智能体应用框架的能力,能够借助智能体团队、复杂技能以及动态工具搜索,完成高度精细的生产力任务。Python00