Agent 上线前要补齐的运行控制
文章以实际运行的代码片段介绍 LangChain 与 LangGraph 的生产能力,关注循环失控、接口故障和危险工具调用。另提示 create_react_agent 的弃用迁移,以及通过中间件介入模型和工具调用。
AI 深度解读
Agent 能回答天气,不代表它能熬过上线后的第一周。作者把关注点放在循环失控、接口重试、模型故障、危险操作和进程崩溃上,结论很明确:这些问题需要运行时规则,不能只靠提示词约束。
这份材料的价值在于,每项示例都附了作者的运行结果,也暴露了失败尝试。不过,标题虽然写着 10 项,提供的原文在第 9 项代码中途截断,第 10 项完全缺失。下面只解读能够确认的部分。
先管住调用,再谈 Agent 有多聪明
作者使用的环境是 langchain 1.4.9、langchain-core 1.7.6、langgraph 1.2.20、langchain-openai 1.7.4 和 Python 3.13。前 5 项使用真实模型,整组调用花费远低于 1 美分;后面的 LangGraph 运行时实验无需模型。
旧教程里的 create_react_agent 已有弃用提示:从 LangGraph V1.0 开始弃用,计划在 V2.0 移除,入口应迁移到 from langchain.agents import create_agent。但换入口只是起点。
Agent 是模型反复调用工具,直到任务完成的循环。中间件(Middleware)包在循环外,可以检查、修改或终止模型和工具调用。作者用一个永远返回「部署中」的工具,测试以下限制:
middleware=[
ModelCallLimitMiddleware(run_limit=4, exit_behavior="end"),
ToolCallLimitMiddleware(tool_name="check_deploy_status", run_limit=3),
],
要看清两处 run_limit:一个限制模型调用,一个限制指定工具。实验中,工具执行 3 次后被拦截,模型仍想继续,最终由模型调用上限结束运行。
提示词表达意图,中间件执行边界。
exit_behavior="end" 返回结束消息,调用方需要识别它;希望明确失败,可以用 "error"。拿到正常返回值,不能直接认定任务成功。
网络重试也适合放在这一层。作者让 search_runbook 前两次抛出 ConnectionError,配置 ToolRetryMiddleware(max_retries=3, retry_on=(ConnectionError,), initial_delay=0.2, backoff_factor=2.0, jitter=False),第三次成功。实际间隔是 [0.2, 0.41] 秒,模型没有看到前两次错误。
这里的关键是筛选异常和确认幂等性,即重复执行不会重复产生业务结果。检索可以重试,扣款需要幂等键。重试耗尽后,默认 on_failure="continue" 把错误文本交给模型;"error" 则终止运行。
模型回退同样有代价。作者账户上的 gpt-4.1-nano 返回 404,ModelFallbackMiddleware("openai:gpt-4o-mini") 成功接管,返回元数据中的模型是 gpt-4o-mini-2024-07-18。这只能证明该账户当时的现象,不能据此断言模型已全面退役。作者还检查了源码:任何异常都可能触发回退,请求自身的错误也可能被掩盖,因此要记录实际回答的模型。
危险操作要按调用审批,权限还得落到底层
langchain 1.4.6 新增的 interrupt_mode="per_call",让每次工具调用拥有独立的暂停和 ID。它解决的是同一轮模型发出多个操作时,审批结果如何准确对应的问题。
作者让 Agent 并行重启 checkout,并把 search 扩到 40 个副本。人工批准重启,拒绝扩容并说明最多 12 个。结果重启执行,40 副本的扩容没有执行;模型随后提出 12 副本的新调用,再次暂停等待批准。
拒绝一次,不会自动批准模型修改后的方案。
暂停需要检查点保存器(checkpointer)保存状态。示例使用 InMemorySaver,通过 thread_id="incident-42" 关联运行,以中断 ID 组织答案,再用 Command(resume=answers) 恢复;调用指定 version="v2"。扩容工具的 allowed_decisions 被限制为 ["approve", "reject"]。
中间件顺序也有要求:HumanInTheLoopMiddleware 必须排在工具重试或错误处理中间件之前,排在它前面的中间件恢复时会再次执行。
自定义规则可以使用 wrap_tool_call。调用 handler(request) 就执行工具,直接返回 ToolMessage 就跳过。作者据此拦截包含 DROP、DELETE、TRUNCATE、UPDATE 的 SQL,确实阻止了删除工具运行。
但这个例子也展示了两层局限:字符串匹配会误伤 SELECT last_updated FROM orders;工具被拦截后,模型仍向用户提供了删除语句。它保护的是执行入口,并不保证回答内容安全。数据库只读账户才是权限底线,中间件适合作为补充。
保存过状态,不等于崩溃后不会重做
作者对 durability 的实验很有参考价值:在第三步 open_ticket 内调用 os._exit(1),直接杀掉进程,再尝试恢复。
三个模式的结果不同:
exit只在运行退出时保存。强杀后没有状态,恢复抛出EmptyInputError。async后台写入。实验节点只耗时微秒,写入没跟上,只保存了空进度,三个步骤全部重跑。sync在进入下一步前等待保存,保住了fetch_logs和summarise,只重跑open_ticket。
作者最初用 Python 异常代替强杀,三个模式都保留了进度,因为异常仍允许有序结束。异常测试不能替代崩溃测试。
sync 的代价是每一步等待数据库写入。它适合减少带副作用流程的重做,但不能据此推导出「开工单只会执行一次」:如果外部操作成功、状态尚未保存就崩溃,仍可能重复。业务幂等不能省。
另一类故障是节点卡住。LangGraph 1.2 增加节点级 timeout= 和 error_handler=,后者在重试耗尽后可返回 Command,更新状态并指定下一节点。
作者通过 set_node_defaults 设置 timeout=1.0、RetryPolicy(max_attempts=2, initial_interval=0.1),让模拟 CRM 查询两次超时后转到人工处理节点。终端记录总耗时约 3.0 秒,并非严格的 2.1 秒。
这里有个容易踩中的条件:超时只支持异步节点。默认超时也作用于错误处理节点,因此处理函数必须写成 async def;节点内部若用 time.sleep 阻塞,也不能指望按时取消。默认设置还不会自动继承给子图。
缓存和流式输出,也有各自的边界
节点缓存适合相同输入产生相同输出的昂贵步骤。作者模拟了一次耗时 1.5 秒的检索,并为节点配置:
builder.add_node(
"retrieve",
retrieve,
cache_policy=CachePolicy(ttl=300, key_func=lambda s: s["question"].strip().lower()),
)
重点是 key_func:去掉首尾空格、统一小写后,两种写法共用缓存键。配合 builder.compile(cache=InMemoryCache()),首次查询耗时 1.51 秒,第二次显示 0.00 秒,且带有 'cached': True。
这证明了示例命中缓存,没有证明真实 RAG 的性能收益。检索耗时来自 time.sleep(1.5);缓存结果还保留了第一次问题的原始文本,提醒我们:归一化丢掉的信息,必须确实不影响结果。InMemoryCache 重启即失,原文另提到可使用 SqliteCache。
第 9 项讨论 stream(version="v2") 与 GraphOutput.interrupts。可见代码使用 stream_mode=["updates", "custom"],按 part["type"]、part["data"] 读取事件,并用 get_stream_writer() 发送进度。它试图解决旧接口在不同流模式下元组形状变化,以及暂停信息混入状态 __interrupt__ 的问题。
但原文在打印新版中断信息时截断,未提供完整输出,不能补写验证结论。
我的判断
我认同作者把运行边界放在提示词之外。限额、重试、审批、恢复路径,都属于应用必须明确承担的工程责任;内置中间件比散落在每个工具里的重复代码更方便集中检查。
不过,这些是小规模功能实验。作者没有提供吞吐、延迟分布或故障率对照,也没有证明它们整体优于手写循环、普通任务队列或其他工作流方案。「更适合生产」主要是工程经验判断。
其中,强杀恢复、审批拒绝后再次暂停、缓存返回旧文本,都是很有用的实测细节。SQL 字符串过滤和内存保存器则更适合说明机制。框架能减少胶水代码,权限、幂等和持久化设计仍需要团队自己落实。
能拿来做什么
- 为轮询部署状态、反复修复代码的 Agent 同时设置模型和工具调用上限,并让应用区分任务完成、额度耗尽和执行异常。
- 给检索、知识库查询等读取工具集中配置重试,只匹配可恢复异常;给写入操作先设计幂等,再决定是否重试。
- 为重启、扩容等高影响工具接入逐次审批,在前端用中断 ID 对应操作和结果,修改参数后的调用继续独立审批。
- 给长流程补上强杀恢复测试和异步超时测试;接入节点缓存前,检查缓存键是否保留了决定结果的必要信息。