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,然后稳稳当当地继续处理下一个任务,将整体端到端响应时间死死压在你的预期内。
拿去用,守住你的钱包,别让糟糕的底层逻辑拖垮你的生产力!
GLM-5.1GLM-5.1是智谱迄今最智能的旗舰模型,也是目前全球最强的开源模型。GLM-5.1大大提高了代码能力,在完成长程任务方面提升尤为显著。和此前分钟级交互的模型不同,它能够在一次任务中独立、持续工作超过8小时,期间自主规划、执行、自我进化,最终交付完整的工程级成果。Jinja00
MiniMax-M2.7MiniMax-M2.7 是我们首个深度参与自身进化过程的模型。M2.7 具备构建复杂智能体应用框架的能力,能够借助智能体团队、复杂技能以及动态工具搜索,完成高度精细的生产力任务。Python00- QQwen3.5-397B-A17BQwen3.5 实现了重大飞跃,整合了多模态学习、架构效率、强化学习规模以及全球可访问性等方面的突破性进展,旨在为开发者和企业赋予前所未有的能力与效率。Jinja00
HY-Embodied-0.5这是一套专为现实世界具身智能打造的基础模型。该系列模型采用创新的混合Transformer(Mixture-of-Transformers, MoT) 架构,通过潜在令牌实现模态特异性计算,显著提升了细粒度感知能力。Jinja00
LongCat-AudioDiT-1BLongCat-AudioDiT 是一款基于扩散模型的文本转语音(TTS)模型,代表了当前该领域的最高水平(SOTA),它直接在波形潜空间中进行操作。00
ERNIE-ImageERNIE-Image 是由百度 ERNIE-Image 团队开发的开源文本到图像生成模型。它基于单流扩散 Transformer(DiT)构建,并配备了轻量级的提示增强器,可将用户的简短输入扩展为更丰富的结构化描述。凭借仅 80 亿的 DiT 参数,它在开源文本到图像模型中达到了最先进的性能。该模型的设计不仅追求强大的视觉质量,还注重实际生成场景中的可控性,在这些场景中,准确的内容呈现与美观同等重要。特别是,ERNIE-Image 在复杂指令遵循、文本渲染和结构化图像生成方面表现出色,使其非常适合商业海报、漫画、多格布局以及其他需要兼具视觉质量和精确控制的内容创作任务。它还支持广泛的视觉风格,包括写实摄影、设计导向图像以及更多风格化的美学输出。Jinja00