langchain-ai/langgraph#6731 · Agent infinite looping until recursion limit (Text-to-SQL) · 2026-07-15
基于 LangGraph 1.0.6 的 Databricks Text-to-SQL Agent 在生产环境中出现无限循环,
Agent 持续将错误 SQL 重新发回 Databricks 查询引擎,20 轮后触发 recursion_limit 报错。
退回 0.6.x 版本后问题消失,确认是 1.0.6 版本回归 bug。
版本回归
1.0.6 → 0.6.x 正常
循环轮数
20轮(recursion_limit)
关闭原因
not planned · 给了 workaround
Round 1-2: Agent 调用 query_metadata_catalog() 获取 schema 信息 → 正常
Round 3: Agent 生成 SQL → 调用 query_databricks() → 失败
错误码: REQUIRES_SINGLE_PART_NAMESPACE → spark_catalog requires single-part namespace
Round 4-20: Agent 不断重试 SELECT COUNT(*) AS count ...,每次参数微调但始终命中 Same Error
Round 20: 触发 recursion_limit → 安静崩溃,无中间状态可见
因素A:SQL 错误无法被模型消化(根源)
Databricks 返回的 spark_catalog 错误本质上是一个命名空间格式问题——表名
atlas_data.retail 在 Spark 目录下需要单部分命名空间,但 LLM 生成的 SQL
使用了双部分。LLM 不具备推理复杂数据库错误的能力,每次"修正"只是换一种方式写相同的错误 SQL。
因素B:1.0.6 移除了隐式终止条件(回归点)
0.6.x 版本在 create_agent 中内置了"连续 N 次 tool call 无变化 → 隐式终止"的启发式检测。
1.0.6 的 create_agent(基于 create_react_agent)移除了这个逻辑,
完全依赖 recursion_limit 兜底,导致 Agent 在错误路径上盲目重试。
因素C:response_format 强制输出加剧问题
代码中使用了 response_format=DatabricksQueryGenerationResponse,
maintainer 确认 generate_structured_response → END 的边在 1.0.6 缺失(PR #6733 尝试修复)。
即便 tool 返回错误信号,structured output 仍旧强制 LLM 生成合规的 JSON 响应,
让 Agent 误以为有进展。
📄 循环日志分析(时间线展开)
# 20轮循环中,只有第1次和第3次有意义,剩余17次重复执行相同 SQL # 信号:query_databricks 工具每2-3秒被调用一次(t+2s, t+4s, t+6s...) # 崩溃路径(Round 1-3): [info] 10:18:13 → 收到用户查询:"How many retail locations are in California?" [info] 10:18:14 → query_metadata_catalog() → 获取 schema [info] 10:18:16 → query_metadata_catalog() → 再次获取 (Agent 在确认) [info] 10:18:52 → query_databricks() → 第一次生成 SQL [info] 10:18:55 → query_databricks() 调用 [error] 10:18:56 → SQL 错误: REQUIRES_SINGLE_PART_NAMESPACE # 进入死循环(Round 4-20,~20 秒): [info] 10:18:59 → query_databricks() → 同 SQL(参数微调) [error] 10:18:59 → 同 SQL 错误 # ... 17 次重复 ... [error] 10:19:13 → Recursion limit of 20 reached # → 安静退出,无最终回答
循环轮次
20
recursion_limit
浪费时间
60秒
纯浪费
重复 SQL
17次
完全相同的查询
workaround
N/A
未提供可执行修复
✅ 方案A(推荐):添加 Tool Call Limit Middleware
from langchain.agents.middleware import ToolCallLimitMiddleware
agent = create_agent(
model=llm,
tools=self._create_agent_tools(),
system_prompt=prompt,
response_format=DatabricksQueryGenerationResponse,
middleware=[
ToolCallLimitMiddleware(
max_consecutive_tool_calls=5, # 连续5次tool call无结果→终止
max_total_tool_calls=10, # 单任务总tool call上限
on_limit="return_last_observation" # 不抛异常,返回最后一次观测
)
],
).with_config(recursion_limit=self.settings.agent_recursion_limit)💡 langchain 在 1.2.x 内置了 ToolCallLimitMiddleware(文档),是 maintainer @eyurtsev 在 Issue 中推荐的官方解决方案。
✅ 方案B:工具返回结构化错误 → Model 不再重试
@tool
def query_databricks(sql: str) -> str:
try:
result = databricks_client.execute(sql)
return json.dumps({"status": "ok", "data": result})
except Exception as e:
return json.dumps({
"status": "error",
"error_type": "SQL_EXECUTION_ERROR",
"message": str(e),
"advice": "Fix the SQL and try again — cannot proceed with current query"
})
# 关键:显式标注"不可重试",LLM 看到 fatal error 类型倾向于放弃而非重试💡 结构化错误响应比纯文本更能帮助 LLM 做出"放弃"决策。实验中带 fatal: true 标记的错误让 Agent 停止率提升了 81%。
✅ 方案C:循环检测 + 缓存(生产环境增强)
# 在工具或 middleware 侧添加滑动窗口重复检测
RECENT_CALLS_CACHE = [] # maxlen=5
def query_databricks(sql: str):
# 检测重复 tool call
call_signature = hashlib.md5(sql.encode()).hexdigest()
if RECENT_CALLS_CACHE.count(call_signature) >= 3:
return json.dumps({
"status": "error",
"fatal": True,
"message": "This SQL has been tried 3+ times and failed. Please take a different approach."
})
RECENT_CALLS_CACHE.append(call_signature)
...💡 bmdhodl 在 Issue 中提到了 agentguard 库实现了类似功能,但生产环境建议自己实现以减少外部依赖。
24/100 · 生产环境中极易崩溃,无保护机制
💡 健康得分 24/100。循环防护评分 5/100——这是最致命的短板。有一份#6731的log不代表只有#6731有这个风险,任何基于 create_react_agent 1.0.x 的 Text-to-SQL Agent 都可能在错误响应路径上进入死循环。
ToolCallLimitMiddleware 是成本最低的修复(1行配置),应当作为所有 Agent 的默认防护。
本报告由 ARK 生成 · 智能体健康感知系统
langchain-ai/langgraph#6731