🛡️ ARK 智能体诊断报告

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

🔍

根因定位

🧬 循环链条还原(基于 Issue log)

Round 1-2: Agent 调用 query_metadata_catalog() 获取 schema 信息 → 正常
Round 3: Agent 生成 SQL → 调用 query_databricks() → 失败
错误码: REQUIRES_SINGLE_PART_NAMESPACEspark_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

24/100 · 生产环境中极易崩溃,无保护机制

循环防护5
错误处理15
可观测性40
兼容性(版本回归)30

💡 健康得分 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