当前位置: 首页 > news >正文

Nodejs也能写Agent - 23.LangGraph篇 - 可靠性与容错

上一篇我们管住了上下文窗口:该 trim 的 trim,该摘要的摘要,RAG 也不再一股脑乱塞。窗口干净了,Agent 就稳了吗?

并没有。生产里 LLM 会超时、会 429、Ollama 会挂、ReAct 会空转烧光额度——喂对了内容,照样可能直接崩给你看。本篇管「挂了怎么办」——重试、Fallback、防死循环,让异常变成优雅降级,而不是用户脸上的 stack trace。

老规矩,本文以官网最新文档核对过(Fault tolerance、Prebuilt middleware、Agents)。入口继续createAgent——别再抄createReactAgent。重试 / Fallback / 调用上限,优先挂官方中间件,别先上手写一整套invokeWithRetry

一、生产里 Agent 怎么死

先认清「会死在哪」,再谈怎么救。

场景典型原因表现
超时模型负载高、网络慢、prompt 过长挂起后 timeout
429 限流API 配额用尽、并发过高Rate limit exceeded
模型不可用Ollama 没起、模型没 pull、服务宕机ECONNREFUSED、model not found
上下文超限messages + RAG 超窗400 / context length exceeded
失控循环模型一直吐tool_calls烧 token、拖超时,最后 recursive 爆

官网把错误按「谁来修」分得更清楚——不同错误不该用同一招:

错误类型谁来修策略官方手段
瞬时故障(网络、限流)系统自动指数退避重试modelRetryMiddleware/toolRetryMiddleware
LLM 可恢复(工具失败、解析翻车)模型错误进ToolMessage,让模型改主意工具返回错误串(JS 尚无ToolErrorMiddleware
用户可修复(缺信息、指令不清)暂停等人interrupt/ HITL
供应商宕机系统自动换备选模型modelFallbackMiddleware
失控循环系统自动封顶调用次数modelCallLimitMiddleware/toolCallLimitMiddleware+recursionLimit
未知异常开发者往上抛,别瞎 catch无中间件硬吞

工具失败时把错误信息回给模型,它往往能自己换招。官网 JS 侧ToolErrorMiddleware尚未提供——继续走「错误内容进 ToolMessage」即可,别等一个还不存在的 API。

瞬时

供应商挂

工具可恢复

失控循环

未知

错误发生

错误类型?

Retry 退避

Fallback 模型

错误进 ToolMessage

CallLimit / recursionLimit

向上抛出

二、瞬时故障:重试

网络抖一下、偶发 429,立刻失败只会逼用户狂点刷新。策略上:

策略说明
固定间隔每次等一样久;简单,但 429 时可能越重试越堵
指数退避第 n 次等initialDelayMs * backoffFactor^n;给服务喘息时间
maxRetries一般 2~3 次封顶,别无限重试

Chat 模型自身也可能有maxRetries,但挂在 Agent 上时,官网主路径是中间件——模型调用和工具调用各管各的:

import{createAgent,modelRetryMiddleware,toolRetryMiddleware,}from"langchain";import{ChatOllama}from"@langchain/ollama";import{tool}from"@langchain/core/tools";import*aszfrom"zod";constsearchWeb=tool(async({q}:{q:string})=>`搜索结果:${q}`,{name:"search_web",description:"搜索网页(外部 API,值得重试)",schema:z.object({q:z.string()}),});constllm=newChatOllama({model:"qwen2.5:7b",temperature:0});constagent=createAgent({model:llm,tools:[searchWeb],systemPrompt:"需要外部信息时调用 search_web。",middleware:[// 模型:超时 / 限流 / 5xx 类瞬时错误modelRetryMiddleware({maxRetries:3,backoffFactor:2.0,initialDelayMs:1000,}),// 工具:只重试会抖的外部调用;本地 read 别无脑重试toolRetryMiddleware({maxRetries:2,tools:["search_web"],backoffFactor:2.0,initialDelayMs:500,}),],});

要点:

  • 默认就是指数退避backoffFactor: 0才退回固定间隔。
  • toolRetryMiddlewaretools: [...]收窄范围——官网原话:文件系统read失败多半重试也没用,网页搜索超时才值得再试。
  • onFailure: "continue"时,重试用尽可返回带错误说明的AIMessage,让 Agent 有机会收尾,而不是整段炸穿。

手写for+sleep也能懂原理;生产别重复造轮子,中间件已经把退避、jitter、失败策略打包好了。

三、主模型挂了:Fallback

重试用尽、或者主模型整机不可用时,换备选继续服务:

主模型 qwen2.5:7b 失败 → 备选 llama3.1:8b(或其它已 pull 的模型)
import{createAgent,modelFallbackMiddleware}from"langchain";import{ChatOllama}from"@langchain/ollama";constprimary=newChatOllama({model:"qwen2.5:7b",temperature:0});constfallback=newChatOllama({model:"llama3.1:8b",temperature:0});constagent=createAgent({model:primary,tools:[],middleware:[// 主模型失败后,按顺序尝试备选(可传多个)modelFallbackMiddleware(fallback),],});

注意:

  • 备选能力可能不同——回答风格、工具遵从度都会变。日志或 UI 最好标一句「已切换备选模型」,别假装什么都没发生。
  • Fallback 解决的是「模型/供应商挂了」;解决不了「问题本身无解」。盯 fallback 触发率,太高说明主路径在持续抽风。

四、防死循环:recursionLimit+ Call Limit

ReAct 环里,模型若一直返回tool_calls而不给最终答案,就会空转:

import{GraphRecursionError}from"@langchain/langgraph";try{awaitagent.invoke({messages:[{role:"user",content:"帮我做一件超复杂的事"}]},{recursionLimit:15}// 生产可配环境变量,默认别太大);}catch(err){if(errinstanceofGraphRecursionError){// 别把 stack 甩给用户return"任务步骤过多,请简化问题后再试。";}throwerr;}

但要分清两层闸门:

闸门数的是什么超限表现
recursionLimitLangGraphsuper-step(调度滴答),不是「模型调用次数」GraphRecursionError
modelCallLimitMiddleware/toolCallLimitMiddleware业务语义上的模型/工具调用次数exitBehavior: "end"优雅收束

挂了beforeModel/afterModel这类会编译成独立节点的中间件,会多占 super-stepwrapModelCall包在原节点里,一般不另计。所以:recursionLimit要留余量,真正「最多调几次模型」交给 Call Limit 更直观。

import{createAgent,modelCallLimitMiddleware,toolCallLimitMiddleware,}from"langchain";import{ChatOllama}from"@langchain/ollama";constagent=createAgent({model:newChatOllama({model:"qwen2.5:7b",temperature:0}),tools:[/* ... */],middleware:[modelCallLimitMiddleware({runLimit:15,// 单次 invoke 内最多 15 次模型调用exitBehavior:"end",// 到顶优雅结束,而不是甩异常}),toolCallLimitMiddleware({runLimit:30,// 单次 invoke 内工具调用封顶}),],});

runLimit:一次用户请求内计数,下轮重置。threadLimit:整条会话累计,需要 Checkpointer。两道闸一起上:Call Limit 管业务预算,recursionLimit防图调度层面真失控。

五、缓存与熔断(轻量)

中间件解决「这次调用怎么扛」;缓存和熔断解决「别把下游打爆 / 别重复烧钱」。

精确缓存 vs 语义缓存

类型命中条件MVP 思路
精确缓存问题字符串完全一致(可 hash)内存Map+ TTL
语义缓存embedding 相似度过阈值向量库;本篇不展开
typeCacheEntry={value:string;expiresAt:number};constexactCache=newMap<string,CacheEntry>();constTTL_MS=5*60*1000;functioncacheKey(question:string){returnquestion.trim().toLowerCase();}functiongetCached(question:string):string|undefined{consthit=exactCache.get(cacheKey(question));if(!hit)returnundefined;if(Date.now()>hit.expiresAt){exactCache.delete(cacheKey(question));returnundefined;}returnhit.value;}functionsetCached(question:string,value:string){exactCache.set(cacheKey(question),{value,expiresAt:Date.now()+TTL_MS,});}// 「现在几点」「今天天气」——实时题别进缓存

注意:不同用户 / 不同thread_id是否共享缓存要想清楚;带隐私或个性化的回答,默认不要全局共享。

Circuit Breaker 简述

连续失败时,与其每次都去撞已经挂掉的 Ollama,不如短暂拒绝

关闭 → 失败累积 → 打开(直接拒)→ 冷却后 → 半开(放一枪试探)→ 成功则关闭
classSimpleBreaker{privatefailures=0;privateopenUntil=0;constructor(privatethreshold=5,privatecoolDownMs=30_000){}getisOpen(){returnDate.now()<this.openUntil;}beforeCall(){if(this.isOpen){thrownewError("服务暂时不可用,请稍后重试(熔断开启)");}}onSuccess(){this.failures=0;this.openUntil=0;}onFailure(){this.failures+=1;if(this.failures>=this.threshold){this.openUntil=Date.now()+this.coolDownMs;this.failures=0;}}}constbreaker=newSimpleBreaker();asyncfunctioninvokeWithBreaker(run:()=>Promise<unknown>){breaker.beforeCall();try{constresult=awaitrun();breaker.onSuccess();returnresult;}catch(e){breaker.onFailure();throwe;}}

这是教学级 MVP。完整半开态、按依赖隔离,可后续对标 resilience4j 一类库;本篇目标是建立心智,不是重造运维平台。

六、组合:生产最小可靠骨架

把前面几招叠在一颗createAgent上:

import{createAgent,modelRetryMiddleware,toolRetryMiddleware,modelFallbackMiddleware,modelCallLimitMiddleware,toolCallLimitMiddleware,}from"langchain";import{ChatOllama}from"@langchain/ollama";import{GraphRecursionError}from"@langchain/langgraph";import{tool}from"@langchain/core/tools";import*aszfrom"zod";constgetWeather=tool(async({city}:{city:string})=>`${city}:晴,25°C`,{name:"get_weather",description:"查询城市天气",schema:z.object({city:z.string()}),});constprimary=newChatOllama({model:"qwen2.5:7b",temperature:0});constfallback=newChatOllama({model:"llama3.1:8b",temperature:0});constagent=createAgent({model:primary,tools:[getWeather],systemPrompt:"需要天气时调用 get_weather。",middleware:[modelRetryMiddleware({maxRetries:2,backoffFactor:2.0,initialDelayMs:1000,}),toolRetryMiddleware({maxRetries:2,tools:["get_weather"],}),modelFallbackMiddleware(fallback),modelCallLimitMiddleware({runLimit:15,exitBehavior:"end"}),toolCallLimitMiddleware({runLimit:30}),],});exportasyncfunctionsafeInvoke(userText:string){try{returnawaitagent.invoke({messages:[{role:"user",content:userText}]},{recursionLimit:25}// 比 callLimit 留余量);}catch(err){if(errinstanceofGraphRecursionError){return{messages:[{role:"assistant",content:"任务步骤过多,请简化问题后再试。",},],};}throwerr;}}

retry / fallback / 触顶时你才能在 LangSmith 里看见「到底换过几次、卡在哪」。可靠性没有可观测,就是盲修。

常见坑

  1. 无重试直接失败:偶发抖动逼用户狂点;至少模型侧maxRetries: 2
  2. 重试无退避:429 时立刻重试只会更堵;用指数退避。
  3. 工具无脑全量重试:本地/确定性失败不值得重试;tools: [...]收窄。
  4. 只有recursionLimit、没有 Call Limit:super-step ≠ 业务调用次数;两道闸一起上更稳。
  5. Fallback 不告知:备选质量不同,日志/UI 应标注。
  6. 缓存误用:天气、时间、用户私有回答不该进全局精确缓存。
  7. 熔断阈值乱调:太敏感则误杀;太钝则雪崩。
  8. 瞎 catch 未知异常:官网建议:处理不了的往上抛,方便调试。
http://www.jsqmd.com/news/1241898/

相关文章:

  • 题目元数据的结构化设计:JSON Schema 在算法题库管理中的应用
  • 2026毕业论文模板小程序大横评:学范文等五家实力比拼,避坑省钱看这篇就够了
  • 湿疹管理的三个黄金法则:保湿、避刺激与科学用药
  • 2026 年 7 月宝格丽中国官方授权售后网点完整名录|官方搬迁、新店启用统一公示公告 - 亨得利客户服务中心
  • Cesium被收购后的GIS技术变革与开发者指南
  • 终于找到免费的本地Agent了!量大管饱,真干活~
  • 上虞颜值与口感兼顾的 5 家咖啡融合馆
  • 【万字文档+源码】基于SpringBoot+Vue个人健康管理系统-可用于毕设-课程设计-练手学习-学习资料分享
  • 算力服务商客观对比:GPU型号_计费_可用性_区域
  • 小样本学习(Few-shot Learning)从入门到进阶
  • Adobe Acrobat Pro完整安装指南:从下载到功能测试全流程
  • 内网IM数据防泄露架构:加密、水印、审计三位一体
  • 企业营业执照怎么注销?存在异常处罚可以直接办理注销吗? - 信息快递
  • OpenClaw 2.7.9 微信接入全方案|本地 / 云端 / 命令行三种落地流程
  • Transformer与Yan架构:AI模型效率与泛化的技术对比
  • 异地停业困扰!营业执照可以异地注销吗?跨省个体户可以线上异地注销吗? - 信息快递
  • Cortex-M4中断唤醒与Thumb-2指令集实战解析
  • 低成本搭建空间计算开发环境:替代Vision Pro的开源方案
  • Python与Hashcat实战:从NTLM哈希破解看Windows密码安全审计
  • Mac部署Qwen 3.6大模型全攻略
  • Unity3D火场逃生模拟游戏开发:真实感与游戏性的平衡设计
  • 服装品牌连锁店适合云仓托管模式吗:从库存黑洞到全渠道一盘货的突围之路
  • 021、RepVGG重参数化骨干:训练时多分支与推理时单路结构的YOLOv8实现
  • 羽球搭子 HarmonyOS 实战(19):云端 Repository 的本地优先封装
  • N2HET高级定时器指令集解析:MOV64、PCNT、PWCNT实战指南
  • PLM系统哪家好?2026年国产PLM系统深度选型指南
  • 霍邱黄金回收避坑指南|30 年本土老牌门店,全县免费上门无隐形扣费 - 福顺金黄金回收
  • 多步智能搜索:从静态检索到因果效用的技术演进与实践
  • 半监督学习在食物分类中的应用与实践
  • iOS开发必备:第三方库分类与选型实战指南