版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
第 1 章 为什么你的兜底一次都没被触发过
给一条链挂上兜底分支,上线后一个月,兜底那行日志一次都没出现过。有人因此觉得「兜底白写了」,也有人反过来发现:真出事那天,兜底照样没触发。这两种情况指向的是同一件事——你没弄清兜底到底在什么条件下才被调用。
1.1 重试、降级、人工:三层防线不是一件事
先把三个词定清楚,后面才不会互相串。
- 重试(英文 retry):同一个步骤原样再执行一次。
- 降级(也叫兜底,英文 fallback):「重试也救不回来」之后,换一条备选路径。
- 人工:连兜底给出的结果也不可信时,才轮到人。
三层的分工和触发条件完全不同:
| 层 | 解决什么问题 | 什么时候起作用 | 本文对应的实测位置 |
|---|---|---|---|
| 重试 | 同一动作的偶发失败(网络抖动、限流) | 上一次抛异常,且异常类型在重试名单里 | 第 2 章:不传参默认调 3 次 |
| 降级(兜底) | 主路径整体不可用 | 重试次数耗尽后仍然抛异常 | 第 3 章:只有抛异常才轮到它 |
| 人工 | 兜底给出的结果也不可信 | 兜底命中后仍需要人复核 | 第 7 章:让兜底「响」一次 |
把这三层混成一句「我加了容错」,就是你后来判断不了兜底为什么不触发的根源。
1.2 兜底没触发,只可能是三种情况
- 主链真的没失败。这是绝大多数情况。第 3 章的实测会给出一个很干脆的数字:主链成功时,兜底调用次数是0。
- 失败了,但异常不在名单里。你写
exceptions_to_handle=(ValueError,),而实际抛出来的是别的类型,异常会直接冒泡上去,兜底一次都不会执行。 - 你以为失败了,程序认为成功。主链「正常返回」
None、空串、空字典,在程序眼里是一次成功,兜底不会接住。
第 3 种最贵。接口返回 200、内容是空的,这种假成功既没有异常、也不会有重试,你只会在下游某一步发现结果不对,然后回头怀疑模型。
1.3 本文的实测口径
下面所有行为结论都来自本机实测,不是转述别人的经验:
- 环境:Python 3.13.14 + langchain-core 1.6.6,实测日期 2026-10-15;
- 脚本:
work/2026-10-15_重试降级实测.py,配套日志work/2026-10-15_重试降级实测.log; - 口径:只依赖 langchain-core,不联网、不调用任何模型接口,用一个计数器记录每一次真实调用,所以结论可复现,不依赖你的密钥和额度;
- 交叉核验:方法语义另与官方 reference 对照(reference.langchain.com,本机可直连,截至 2026-10-15),参数默认值与本机实测一致。
下文凡写「实测」,指的就是上面这份脚本与日志里的原样输出;本文按这些结论给出的判据与写法属于归纳,会在附表 A 里单独标出。
第 2 章 重试这一层:不传参就是 3 次
2.1 「重试 3 次」到底是几次调用
先把这个最容易算错的地方钉死。下面这段是最小复现,它只做一件事:数一数.with_retry()不传任何参数时,函数体真的被执行了几次。
"""数一数 .with_retry() 不传参时,真实调用了几次。"""fromlangchain_core.runnablesimportRunnableLambda counter={"n":0}defflaky(x):counter["n"]+=1raiseRuntimeError(f"第{counter['n']}次调用失败")chain=RunnableLambda(flaky).with_retry()# 不传任何参数try:chain.invoke("q")exceptExceptionase:print("抛出:",type(e).__name__)print("真实调用次数 =",counter["n"])本机实测输出是抛出: RuntimeError与真实调用次数 = 3。
结论只有一句:stop_after_attempt=3是「尝试次数上限」,不是「额外重试 3 次」。你如果按 1 + 3 = 4 次去算超时预算,就少算了一次调用。
2.2 四个入参,先看哪个
with_retry的签名不长,但有四个入参会直接改变你的线上行为。
| 参数 | 默认值(官方 reference,截至 2026-10-15) | 本机实测 | 你该做什么 |
|---|---|---|---|
stop_after_attempt | 3 | 不传参真实调用 3 次;写 1 则只调 1 次 | 显式写出来,别吃默认值 |
wait_exponential_jitter | True | 3 次尝试总耗时 3.55 s | 结合超时预算决定留不留 |
retry_if_exception_type | (Exception,) | 未单独实测 | 默认会把编程错误一起重试,见第 6 章 |
exponential_jitter_params | None | 未单独实测 | 要收紧退避时改这里(initial / max / exp_base / jitter) |
第二行值得多说一句:等待时间是真实存在的开销,不是装饰。默认情况下它几乎一定大于你单次调用的耗时。
2.3 退避是真在等:3.55 秒 对 0.001 秒
把上面那个开关关掉,再量一次,差异就非常直观。
"""量一遍退避耗时:默认 对 关掉指数退避。"""importtimefromlangchain_core.runnablesimportRunnableLambdadefflaky(x):raiseRuntimeError("失败")forkwargsin[{"stop_after_attempt":3},{"stop_after_attempt":3,"wait_exponential_jitter":False}]:chain=RunnableLambda(flaky).with_retry(**kwargs)t0=time.time()try:chain.invoke("q")exceptException:passprint(kwargs.get("wait_exponential_jitter",True),"%.3fs"%(time.time()-t0))本机实测:3 次尝试总耗时 = 3.55s(含指数退避 + 抖动);而关掉指数退避之后是总耗时 = 0.001s。
指数退避的意思是:第一次失败后等一小会儿再试,第二次失败等得更久,等待时间按倍数增长;抖动(jitter)是在这个等待时间上加一点随机量,避免所有请求在同一时刻一起重试。这两件事共同构成了那 3.55 秒。
判据可以直接写成一条算式:一次失败调用的最坏耗时 ≈ 单次超时 × 尝试次数 + 退避总时长。如果你的接口本身在 95 分位就要几百毫秒,默认参数下的一次失败会挂住 3 秒以上,这笔账要先放进你上游的超时预算里再决定参数。
还有一个方向相反的坑:重试会把你的请求量成倍放大到上游。一次失败变成 3 次调用,如果上游已经因为过载而变慢,重试只会让它更慢——这就是俗称的重试风暴。两条纪律:只对幂等操作开重试(幂等指同一个请求重复执行多次,结果和只执行一次相同,比如查询;而「下单」「扣款」这类改动状态的操作不是);以及在重试之外,给上游留一条限流或备用通道,而不是让 3 次尝试都砸向同一个地址。
第 3 章 降级这一层:只有抛异常才轮到它
3.1 顺序:先耗尽重试,再按顺序试兜底,落第一个
"""重试耗尽后才轮到兜底;多个兜底落第一个;主链成功时兜底 0 次。"""fromlangchain_core.runnablesimportRunnableLambda counter={"n":0}defflaky(x):counter["n"]+=1raiseRuntimeError("失败")chain=RunnableLambda(flaky).with_retry(stop_after_attempt=2).with_fallbacks([RunnableLambda(lambdax:{"from":"fallback","got":x}),RunnableLambda(lambdax:{"from":"fallback2","got":x}),])print("重试耗尽 ->",chain.invoke("q"),"| 主链调用次数 =",counter["n"])c2={"n":0}defshould_not_run(x):c2["n"]+=1raiseAssertionError("兜底不应该被执行")chain2=RunnableLambda(lambdax:{"from":"main","got":x}).with_fallbacks([RunnableLambda(should_not_run)])print("主链成功 ->",chain2.invoke("q"),"| 兜底调用次数 =",c2["n"])本机实测的两行输出是:重试耗尽 -> {'from': 'fallback', 'got': 'q'} | 主链调用次数 = 2,以及主链成功 -> {'from': 'main', 'got': 'q'} | 兜底调用次数 = 0。
两句话读完:重试 2 次都失败后才轮到兜底,两个兜底里落的是第一个(第一个成功就不再试第二个,第二个兜底只有第一个也抛异常时才轮得到);主链成功时,兜底的调用次数是 0。所以「兜底没触发」有一半以上的概率是它本来就不该触发。官方 reference 对触发条件的表述也是这个意思:新的 Runnable 会先试原始的 Runnable,失败后按顺序尝试每一个兜底(截至 2026-10-15)。
3.2 十组实测行为总表
下面这张表是全文的主干。左列是你写的写法,中列是本机实测结果,右列是据此该做的事。建议把它存成你自己的排障清单。
| 场景 | 实测结果 | 该怎么做 |
|---|---|---|
.with_retry()不传参 | 抛 RuntimeError,真实调用3次 | 按「3 次调用」算预算,别按「1 + 3」 |
stop_after_attempt=1 | 真实调用1次 | 不想重试就显式写 1,别靠默认值 |
| 重试 2 次 + 两个兜底 | 落第一个兜底,主链调用 2 次 | 兜底按「从好到差」排序,越可信的越靠前 |
| 主链成功 | 兜底调用0次 | 别用兜底日志判断链路是否健康 |
主链返回None/ 空串 / 空字典 | 原样返回None、''、{},兜底不触发 | 空结果要自己判,见第 4 章 |
| 整条序列挂兜底 | 兜底收到原始输入{'order_id': 'A-1'} | 兜底里不要假设输入已被处理过 |
| 只给失败的那一步挂兜底 | 后续步骤继续跑,收到兜底的返回值 | 看后续步骤能不能吃兜底输出,见第 5 章 |
exceptions_to_handle=(ValueError,)遇到RuntimeError | 异常直接冒泡,兜底不触发 | 名单要么写全,要么先记清会抛什么 |
exception_key+ 非字典输入 | 抛 ValueError,要求输入是字典 | 用exception_key就全链传字典 |
| 默认退避(3 次尝试) | 总耗时3.55 s;关掉退避0.001 s | 先量一遍再定额度 |
最后一行是唯一一条「不是关于触发、而是关于代价」的结论,但它决定了前面九条在你系统里是否可接受。
第 4 章 最容易踩的一条:把「空结果」当成失败
4.1 程序眼里的失败只有一种
在 Runnable 序列里,失败的定义是「抛出了异常」,没有第二种。返回值是什么,框架不评价。这就解释了第 3 章那条看着奇怪的输出:主链返回None、空串、空字典时,兜底调用次数都是 0——因为从框架的角度看,它们都是正常返回。
4.2 「假成功」为什么不会触发兜底
"""空结果不是失败:要么自己判空,要么把它转成异常。"""fromlangchain_core.runnablesimportRunnableLambda fb=RunnableLambda(lambdax:{"from":"fallback","got":x})# 主链「正常返回」空串:兜底不会接住print("空串 ->",repr(RunnableLambda(lambdax:"").with_fallbacks([fb]).invoke("q")))# 判据:把空结果显式转成异常,兜底才有机会被调用EMPTY=(None,"",{},[])defguard(fn):def_run(x):out=fn(x)ifoutinEMPTY:raiseValueError("空结果")returnoutreturnRunnableLambda(_run)print("加护栏 ->",guard(lambdax:"").with_fallbacks([fb]).invoke("q"))实测行为:三种空值原样返回None、''、{},兜底一律不触发;加上上面这层护栏(把空结果显式转成异常的小包装)之后,同一个返回值才会落到兜底上(护栏写法的输出为本机复跑核验,环境同上)。
这一段就是「别只兜异常,还要兜空结果」的完整含义:你要先把「空」定义成失败,框架才可能把它当失败处理。
线上常见的空结果有三种来源,判空时最好都覆盖到:接口层——请求成功、返回体是空的(HTTP 200,但内容为空);模型层——模型正常返回,但文本内容没有或只有空白;解析层——结构化输出解析器返回了空列表或空字典,而不是抛错。这三种在日志里都长得像一次成功调用,只有你自己加的那层判断能拦住它们。
4.3 判空放在哪一层
| 判空位置 | 能挡住什么 | 代价与风险 |
|---|---|---|
| 主链出口(判空后转异常) | 假成功,兜底能接到 | 要改主链,或加一层包装 |
| 兜底入口 | 只能挡兜底自己返回空 | 挡不住主链的假成功,兜底根本没被调用 |
| 最外层统一包装 | 不改主链,集中定义「空」 | 「空」的定义必须写死,见下 |
判据是「先把定义写下来,再决定放哪」:空字典算成功还是失败?0算不算空?空列表算不算空?这三问答不出来,判空放哪一层都会出错。答得出来,就把这段判断放在最外层统一包装里。
第 5 章 兜底放整链还是放单步
5.1 整链兜底:收到的是原始输入
5.2 单步兜底:后续步骤会继续跑
这两条是同一段代码的两个变体,一起看差异最清楚。
"""整链挂兜底 对 只给失败那一步挂兜底。"""fromlangchain_core.runnablesimportRunnableLambdadefboom(x):raiseRuntimeError("中间步骤炸了")defstep2(x):return{"stage":"step2_ran","input_was":x}fb=RunnableLambda(lambdax:{"from":"fallback","got":x})# A. 整条序列挂兜底whole=(RunnableLambda(boom)|RunnableLambda(step2)).with_fallbacks([fb])print("整链 ->",whole.invoke({"order_id":"A-1"}))# B. 只给失败的那一步挂兜底step1=RunnableLambda(boom).with_fallbacks([RunnableLambda(lambdax:{"from":"step1_fallback","got":x})])print("单步 ->",(step1|RunnableLambda(step2)).invoke({"order_id":"A-1"}))本机实测的两行输出是整链 -> {'from': 'fallback', 'got': {'order_id': 'A-1'}},以及单步 -> {'stage': 'step2_ran', 'input_was': {'from': 'step1_fallback', 'got': {'order_id': 'A-1'}}}。
第一行里兜底拿到的是{'order_id': 'A-1'},也就是原始输入,不是中间步骤处理过的结果。第二行里,后面的step2照常执行,它拿到的是兜底的返回值。这就是「兜底放错位置」最常见的两种翻车方式:兜底代码若直接假设「输入已经被前面清洗过」,整链场景下会立刻报错;而单步场景下你要额外保证兜底返回的形状能被后面的步骤吃下去。
5.3 该放整链还是单步
| 维度 | 放整条链 | 只放失败的那一步 |
|---|---|---|
| 兜底收到的输入 | 原始输入 | 该步骤的输入(原始输入或上一步输出) |
| 后面的步骤 | 全部跳过 | 继续执行,拿到兜底返回值 |
| 适用情形 | 后面步骤吃不下兜底输出;或需要整段重来保证一致 | 后面步骤能吃兜底输出;只补其中一段 |
| 典型例子 | 多步生成里的关键步骤失败,且不完整就不发出 | 检索失败就给一段空上下文,继续往下走 |
| 主要风险 | 兜底里假设输入已处理过,直接报错 | 兜底输出形状不对,后面步骤静默出错 |
| 日志能看出什么 | 只知道整段降级了 | 能看出是哪一步降级的 |
5.4 一句话判据
后续步骤能不能吃兜底输出——能吃,就把兜底挂在失败的那一步上;不能吃,或者兜底输出与主链中间结果的形状对不上,就挂在整条链上,让后面的步骤整体跳过。另加一条硬要求:兜底输出的形状必须与它替代的那一步完全一致,否则你只是把报错换成了更难查的错值。
大模型学习路线图:第 5 章讲的是兜底该挂在哪一层,如果你希望把「从环境搭建到能自己动手做 Agent」的路径系统过一遍,这份路线图按阶段标了每一步该学什么、哪些可以先跳过。放在资料包里,扫码即可获取:
第 6 章 兜底没触发,还有两个原因:异常名单与输入形状
6.1 exceptions_to_handle:名单之外一律冒泡
"""兜底不触发的两个原因:异常类型不在名单里、输入不是字典。"""fromlangchain_core.runnablesimportRunnableLambdadefraise_runtime(x):raiseRuntimeError("运行时错误")# 名单里只有 ValueError,实际抛的是 RuntimeErrorchain=RunnableLambda(raise_runtime).with_fallbacks([RunnableLambda(lambdax:{"from":"fallback"})],exceptions_to_handle=(ValueError,),)try:chain.invoke("q")exceptExceptionase:print("名单外 ->",type(e).__name__)# exception_key 要求输入是字典chain2=RunnableLambda(raise_runtime).with_fallbacks([RunnableLambda(lambdax:{"saw":x})],exception_key="err")try:chain2.invoke("q")# 传进去的是字符串exceptExceptionase:print("非字典输入 ->",type(e).__name__)# 传字典时,兜底能拿到异常对象defcatch(x):return{"fallback_saw_type":type(x.get("err")).__name__}chain3=RunnableLambda(raise_runtime).with_fallbacks([RunnableLambda(catch)],exception_key="err")print("字典输入 ->",chain3.invoke({"q":"q"}))本机实测的三行输出是名单外 -> RuntimeError、非字典输入 -> ValueError、字典输入 -> {'fallback_saw_type': 'RuntimeError'}。
三行读出来的结论:exceptions_to_handle只兜它名单里的类型,名单外的一律直接冒泡;exception_key想要把异常对象交给兜底,前提是输入本身是字典,否则连兜底都还没轮到就先报错了(本机实测到的提示原文片段是input must be a dictionary);输入是字典时,兜底里能从该键上取到异常对象本身,而不再是「出错了」这三个字。
官方 reference 对这两条有明确表述:exceptions_to_handle默认(Exception,),exception_key若指定字符串,被处理的异常会作为输入的一部分在指定键下传给兜底,「基础 Runnable 及其兜底必须接受字典作为输入」(截至 2026-10-15)。
6.2 例外名单怎么定
默认(Exception,)意味着什么都会重试、什么都会被兜底接住——包括你代码里的拼写错误。这是把 bug 藏起来的最快方式。名单要显式写,并且逐个想清楚:网络超时该进(换个路由或再试一次确实可能成功)、限流该进(退避后重试,或切到备用通道)、数据解析失败视情况进(换个模型可能成功,但每一次都要留记录),而代码错误(键名拼错、类型不对)和配置问题(鉴权失败)都不该进——前者被兜住只会让 bug 藏得更深,后者应该先把配置修对。
6.3 名单与字典,一次定死
判据很简单:名单里只放「换一条路确实可能成功」的异常,其余一律让它冒泡;一旦用了exception_key,就把整条链的输入统一成字典,别让某一步偷偷改成字符串。
第 7 章 上线前量一遍:次数、退避、空结果
7.1 先量退避,再定次数
第 2 章那两个数字值得单独拎出来:默认退避下 3 次尝试总耗时3.55 秒,关掉之后0.001 秒。它们之间差三个数量级,而你的下游只给了你一个超时上限。顺序应该是:先在你的机器上按第 2 章那段计时代码量一次,再回头决定stop_after_attempt和抖动参数,不要先写死次数再祈祷不超时。
7.2 上线前的打勾清单
| 检查项 | 打勾条件 | 没打勾时怎么办 |
|---|---|---|
| 重试次数 | 显式写过stop_after_attempt,且知道它等于几次调用 | 补上参数,重跑计数器确认 |
| 退避预算 | 在本机量过一次真实总耗时 | 跑第 2 章那段计时 |
| 例外名单 | 逐个列出预期异常,不用默认(Exception,) | 按 6.2 的清单列一遍 |
| 空结果 | 主链出口有判空,或明确写了「空也算成功」 | 加第 4 章的护栏包装 |
| 兜底位置 | 能说清后续步骤吃不吃兜底输出 | 按 5.4 的判据定 |
| 兜底输出形状 | 与它替代的那一步完全一致 | 在兜底出口加一条断言 |
| 兜底留痕 | 触发时写日志或计数器,并带来源标记 | 见 7.3 |
7.3 最后一条:让兜底「响」一次
第 3 章那张表里最容易被忽略的是「主链成功时兜底调用 0 次」。它有个直接后果:如果你不给兜底留痕,「没触发」和「触发了但同样返回了一个空结果」在你的日志里长得一模一样。
两个具体做法:兜底命中时写一条带来源标记的日志(例如返回值里多带一个来源字段,日志里打出来);同时给兜底挂一个计数器,上线后第一周每天看一眼它是 0 还是非 0。0 不一定是好事,非 0 也不一定是坏事——但你必须知道。
这个计数器怎么读,其实有两种信号:长期为 0,说明你的重试和例外名单可能把问题都消化在上一层了,这时该去看重试次数(如果重试次数也在涨,说明主路径本身不稳,只是被重试掩盖了);某天突然升高,说明上游变了(超时变多、限流变严、接口改字段),这时候兜底给出的结果还准不准,需要你人工抽看几条再决定要不要让它继续兜。
《LangChain + LangGraph + MCP 智能体开发实战》视频课:第 7 章讲的是上线前的检查与留痕,如果你准备把这些容错写法真正接到一条能跑的链上,这套课从私有化部署一路讲到 Agent 全流程,可以当作动手时的参考。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| 事实 | 出处(文档名 + 发布方 + 链接) | 本文位置 |
|---|---|---|
| 环境版本:langchain-core 1.6.6、Python 3.13.14,实测日期 2026-10-15 | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 1.3 |
.with_retry()不传参真实调用 3 次后抛异常;stop_after_attempt=1只调 1 次 | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 2.1、2.2 |
默认退避下 3 次尝试总耗时 3.55 s;wait_exponential_jitter=False时 0.001 s | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 2.3、7.1 |
| 重试耗尽后才轮到兜底;两个兜底时落到第一个(主链真实调用 2 次) | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 3.1 |
| 主链成功时兜底调用次数为 0 | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 3.1、7.3 |
主链正常返回None、空串、空字典均不算失败,兜底不触发 | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 3.2、4.1、4.2 |
整条序列挂兜底时,兜底收到原始输入{'order_id': 'A-1'}而非中间结果 | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 5.1、5.2 |
| 只给失败的那一步挂兜底时,后续步骤继续执行并收到兜底返回值 | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 5.2、5.3 |
exceptions_to_handle=(ValueError,)遇到RuntimeError时异常直接冒泡 | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 6.1 |
exception_key要求输入为字典,非字典输入抛 ValueError;字典输入时兜底可拿到异常对象(实测为 RuntimeError) | 本机实测(work/2026-10-15_重试降级实测.py);日志 work/2026-10-15_重试降级实测.log | 6.1 |
with_retry签名与默认值:stop_after_attempt=3、wait_exponential_jitter=True、retry_if_exception_type=(Exception,)、exponential_jitter_params=None | 《with_retry》,LangChain Reference(reference.langchain.com,本机可直连,页面标注方法版本 v1.6.10),https://reference.langchain.com/python/langchain-core/runnables/base/Runnable/with_retry | 2.2 |
with_fallbacks签名与默认值:exceptions_to_handle=(Exception,)、exception_key=None;原始 Runnable 失败后按顺序尝试每个兜底;exception_key使用时基础 Runnable 与兜底都必须接受字典输入 | 《with_fallbacks》,LangChain Reference(reference.langchain.com,本机可直连),https://reference.langchain.com/python/langchain-core/runnables/base/Runnable/with_fallbacks | 3.1、6.1 |
| 「护栏」包装(把空结果转成异常)与两处取舍表(判空位置、整链 对 单步)为本文归纳,非框架内置能力 | 本机复跑核验(同一 langchain-core 1.6.6 / Python 3.13.14 环境);其输出与本文归纳的口径 | 4.2、4.3、5.3、5.4 |
附表 B:术语速查表
| 术语 | 一句话解释 | 在本文哪里用到 |
|---|---|---|
| 重试(retry) | 同一个步骤原样再执行一次 | 第 1、2 章 |
| 降级 / 兜底(fallback) | 重试救不回来之后,改走备选路径 | 第 1、3、5 章 |
| 异常(exception) | 程序主动中断并向上抛出的错误信号,是框架唯一认可的失败形式 | 第 3、4、6 章 |
| 假成功 | 程序正常返回、但内容为空或不可用的结果 | 第 1、4 章 |
| 指数退避 | 失败后等待时间按倍数递增再重试 | 第 2、7 章 |
| 抖动(jitter) | 在等待时间上加一点随机量,避免请求同时重试 | 第 2 章 |
| Runnable | LangChain 里可被编排执行的最小单元,链和兜底都是 Runnable | 第 3、5 章 |
exceptions_to_handle | 兜底的例外名单,只兜名单内的类型 | 第 6 章 |
exception_key | 把异常对象塞进输入的某个键传给兜底(要求输入是字典) | 第 6 章 |
| 护栏 | 把「空结果」显式转成异常的一层包装 | 第 4 章 |
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。