上一篇我们管住了上下文窗口:该 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;
}

但要分清两层闸门:

闸门数的是什么超限表现
recursionLimitLangGraph 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 未知异常:官网建议:处理不了的往上抛,方便调试。

更多推荐