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

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

老规矩,本文以官网最新文档核对过(Fault tolerancePrebuilt middlewareAgents)。入口继续 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 * as z from "zod";

const searchWeb = tool(
  async ({ q }: { q: string }) => `搜索结果:${q}`,
  {
    name: "search_web",
    description: "搜索网页(外部 API,值得重试)",
    schema: z.object({ q: z.string() }),
  }
);

const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });

const agent = 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";

const primary = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const fallback = new ChatOllama({ model: "llama3.1:8b", temperature: 0 });

const agent = createAgent({
  model: primary,
  tools: [],
  middleware: [
    // 主模型失败后,按顺序尝试备选(可传多个)
    modelFallbackMiddleware(fallback),
  ],
});

注意:

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

四、防死循环:recursionLimit + Call Limit

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

import { GraphRecursionError } from "@langchain/langgraph";

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

但要分清两层闸门:

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

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

import {
  createAgent,
  modelCallLimitMiddleware,
  toolCallLimitMiddleware,
} from "langchain";
import { ChatOllama } from "@langchain/ollama";

const agent = createAgent({
  model: new ChatOllama({ 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 相似度过阈值 向量库;本篇不展开
type CacheEntry = { value: string; expiresAt: number };

const exactCache = new Map<string, CacheEntry>();
const TTL_MS = 5 * 60 * 1000;

function cacheKey(question: string) {
  return question.trim().toLowerCase();
}

function getCached(question: string): string | undefined {
  const hit = exactCache.get(cacheKey(question));
  if (!hit) return undefined;
  if (Date.now() > hit.expiresAt) {
    exactCache.delete(cacheKey(question));
    return undefined;
  }
  return hit.value;
}

function setCached(question: string, value: string) {
  exactCache.set(cacheKey(question), {
    value,
    expiresAt: Date.now() + TTL_MS,
  });
}

// 「现在几点」「今天天气」——实时题别进缓存

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

Circuit Breaker 简述

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

关闭 → 失败累积 → 打开(直接拒)→ 冷却后 → 半开(放一枪试探)→ 成功则关闭
class SimpleBreaker {
  private failures = 0;
  private openUntil = 0;
  constructor(
    private threshold = 5,
    private coolDownMs = 30_000
  ) {}

  get isOpen() {
    return Date.now() < this.openUntil;
  }

  beforeCall() {
    if (this.isOpen) {
      throw new Error("服务暂时不可用,请稍后重试(熔断开启)");
    }
  }

  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;
    }
  }
}

const breaker = new SimpleBreaker();

async function invokeWithBreaker(run: () => Promise<unknown>) {
  breaker.beforeCall();
  try {
    const result = await run();
    breaker.onSuccess();
    return result;
  } catch (e) {
    breaker.onFailure();
    throw e;
  }
}

这是教学级 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 * as z from "zod";

const getWeather = tool(
  async ({ city }: { city: string }) => `${city}:晴,25°C`,
  {
    name: "get_weather",
    description: "查询城市天气",
    schema: z.object({ city: z.string() }),
  }
);

const primary = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const fallback = new ChatOllama({ model: "llama3.1:8b", temperature: 0 });

const agent = 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 }),
  ],
});

export async function safeInvoke(userText: string) {
  try {
    return await agent.invoke(
      { messages: [{ role: "user", content: userText }] },
      { recursionLimit: 25 } // 比 callLimit 留余量
    );
  } catch (err) {
    if (err instanceof GraphRecursionError) {
      return {
        messages: [
          {
            role: "assistant",
            content: "任务步骤过多,请简化问题后再试。",
          },
        ],
      };
    }
    throw err;
  }
}

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

常见坑

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

更多推荐