AI 纪元 追踪人工智能最新进展,深度解读前沿技术

Agent 连打 7 次同一个工具,我给它装了三道刹车

一个查物流的 Agent 在同一个工具上连打 7 次,参数一字不差,账单 11.7 万 token,九万多来自重复返回。Prompt 里写着重复就停,它没停——因为手里没有预算、没有去重、没有必须交人的出口。把这三样写成 LangGraph 的节点和路由,死循环才会在撞墙之前先撞上线。

0 次阅读 约 3126 字 6 分钟 AI 生成
A small wheeled cart circling a miniature rail, stopped by three physical brake mechanisms: a blank gauge gate, a twin-token matching slot, and a red side-exit
A small wheeled cart circling a miniature rail, stopped by three physical brake mechanisms: a blank gauge gate, a twin-token matching slot, and a red side-exit

一个查物流的 Agent,在 get_shipment_status 上连打了 7 次,参数一字不差。第 8 次它换了个写法——订单号从纯数字改成带 SO- 前缀——又打了 3 次。

那次账单 11.7 万 token。九万多来自重复的工具返回。

Prompt 里写着“信息重复时请停止并说明”。它没照做。不是不听话,是它每一步都在重新猜,而猜没有边界。“要不要继续”这个决定,每次都被交回给它重新推理一遍。

腾讯云开发者社区有篇企业级数据分析多智能体系统的改造记录,把 Agent 上生产时遇到的问题归了三类:工具调用死循环、上下文窗口溢出导致忘掉关键信息、以及完全不知道它为什么做了某个决策。三个症状,一个病根——流程的骨架藏在 Prompt 里。可 Prompt 不是骨架。

把“继续还是停”从模型手里拿走

LangGraph 的做法是把流程画成图。节点是纯函数,边是确定的,只有条件边上的路由函数有权决定下一步去哪。

LangChain 官方文档里,它现在是 MIT 许可的开源库,定位是 low-level 的编排框架,客户名单挂着 Klarna、Uber、J.P. Morgan。low-level 的意思很直白:它不替你思考,只负责在你划好的格子里推进。

先看状态。状态不是对话历史,是账本:

from typing import Annotated, Literal, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    steps: int          # 已走过的节点数
    tool_calls: int     # 已执行的工具调用数
    seen: list[str]     # 工具调用指纹,用于去重
    verdict: str        # ok / retry / stalled / done

steps、tool_calls、seen 这三个字段模型碰不到。它只能在 messages 里说话。

然后是路由,唯一有权决定“继续还是停”的地方:

BUDGET = {"steps": 12, "tool_calls": 6}

def route_after_tool(state: AgentState) -> Literal["plan", "repair", "human_review", "__end__"]:
    if state["verdict"] == "done":
        return END
    if state["verdict"] == "stalled":
        return "human_review"
    if state["steps"] >= BUDGET["steps"] or state["tool_calls"] >= BUDGET["tool_calls"]:
        return "human_review"
    if state["verdict"] == "retry":
        return "repair"
    return "plan"

Literal 把出口穷举完了,图在编译时就拿到合法的下一站清单;路由函数返回一个没声明过的节点名,编译直接报错。human_review 跟别的节点一样,是条正规边,不是异常处理里的 catch。

三道刹车:预算、指纹和一根保险丝

第一道是预算。BUDGET 里的 12 和 6 不是拍脑袋,是从历史 trace 的 P95 反推的:正常工单 4 到 5 次工具调用就收敛,给到 6 已经留了余量。预算落在状态里、由路由函数读。Prompt 里写“最多尝试三次”不算数。

第二道是去重指纹。把 (工具名, 参数) 按键排序后哈希,命中 seen 就不再执行,把上一次的结果直接写回消息,同时把 verdict 置成 stalled,交给 human_review。这一招比任何“不要重复调用”的提示词都管用,因为它不依赖模型配合。

第三道是 recursion_limit。LangGraph 的 config 里有这个参数,默认 25 个 superstep,超了抛 GraphRecursionError。很多人把它当保险丝,它也确实是保险丝——但熔断之后你拿到的是一个异常,不是一个能交付的结果。所以线上那张图里,它该设得比业务预算高一点:我们设 20,业务预算是 12,让它只在你算错的时候响。

graph.invoke(
    {"messages": [{"role": "user", "content": ticket}]},
    config={"configurable": {"thread_id": ticket_id}, "recursion_limit": 20},
)

人接手的时候,状态不能丢

human_review 里用 interrupt() 挂起,配 checkpointer 和 thread_id 把状态落盘,人处理完再用 Command(resume=...) 接着跑。整条线能续上,不用从头再来一遍。

站内那篇《AI Agent 调度试验跑了三个月,调度员还在》里,华南一家合资车企的调度 Agent 把突发断供响应从 45 分钟压到 3 分 20 秒,但金额超过 50 万元、或者首次出现的异常,仍然必须人工确认。阈值写死在路由里,不写在 SOP 文档里。文档会被跳过,路由不会。

verify 节点也一样,不该让模型自评“我觉得答案对了”。验收得用外部标准:SQL 能不能跑通、返回字段符不符合 Schema、金额加总跟订单表对不对得上。站内《Prompt 写得再长,也救不了不会认错的 Agent》讲的就是这个——验收靠外部标准,返工只返局部,连续失败要知道停。

卡在哪一步,trace 里得看得见

LangSmith 的 trace 里能直接看到三样:每个节点的执行耗时、状态变更历史、条件边的判定结果。这三样正好对应排查死循环时要问的三个问题——卡在哪个节点、状态有没有前进、路由为什么走了这一支。

改完之后,我们这组工单分派场景的对比:四步流程成功率从 62% 到 89%,平均耗时 45 秒降到 32 秒,单次 token 从 8500 降到 5100。幅度跟站内《长 Prompt 管不住复杂 AI 工作流,状态机负责兜底》里的测试基本吻合,我们只是在自己的业务上又跑了一遍。

模型一行没换。

状态机不会让你少写代码

它让你多写一堆 TypedDict、路由函数和边界判断。换回来的是:出错时你知道错在第几步,哪一步本该拦住它。

只给一条建议的话:先别急着上多 Agent。把单 Agent 的循环用状态图钉死,预算、去重、人工闸门这三条边写出来,再谈协作。多 Agent 的麻烦常常不在数量上,在于每一个 Agent 都能自己决定什么时候停。

站内《Agent 已经能自己写代码,工作流为什么还不能省?》说的是同一件事:鉴权、状态切换、外部写入和回滚,得是固定的、可审计的。让模型处理局部变化,让图守住边界。

相关阅读