个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangChain 降级、重试与超时:让服务在模型抽风时还能用](#LangChain 降级、重试与超时:让服务在模型抽风时还能用)
-
- 一、先分清三种故障
- 二、第一层:重试(with_retry)
-
- [2.1 用 with_retry 更灵活](#2.1 用 with_retry 更灵活)
- [2.2 重试的三个坑](#2.2 重试的三个坑)
- 三、第二层:降级(with_fallbacks)
-
- [3.1 换模型](#3.1 换模型)
- [3.2 换供应商(更有价值)](#3.2 换供应商(更有价值))
- [3.3 降级到非模型方案](#3.3 降级到非模型方案)
- [3.4 什么时候会触发降级](#3.4 什么时候会触发降级)
- 四、第三层:超时
-
- [4.1 设在哪里](#4.1 设在哪里)
- [4.2 流式场景的超时](#4.2 流式场景的超时)
- 五、三层的正确组合顺序
- 六、监控:别等用户告诉你挂了
- 七、完整示例
- 八、小结
LangChain 降级、重试与超时:让服务在模型抽风时还能用
模型 API 会限流、会超时、会 5xx。
你的服务不能跟着一起挂。
这篇讲三层防护:重试 → 降级 → 超时 ,
以及它们的组合顺序------顺序错了等于白做。
一、先分清三种故障
| 故障 | 特征 | 处理 |
|---|---|---|
| 瞬时故障 | 429 限流、503、网络抖动 | 重试(很快就好) |
| 持续故障 | 某个模型整体不可用 | 降级(换模型/换服务) |
| 慢 | 不报错但很慢 | 超时(别耗尽资源) |
三种要分别处理,混在一起会互相干扰。
二、第一层:重试(with_retry)
python
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini",
max_retries=2, # 构造参数:自动重试 2 次
timeout=30,
)
这是最省事的方式。LangChain 内置指数退避 ,
只重试可重试错误(限流/5xx/网络),参数错误不会重试(重试也没用)。
2.1 用 with_retry 更灵活
python
chain = (prompt | llm | parser).with_retry(
stop_after_attempt=3,
wait_exponential_jitter=True, # 指数退避 + 抖动
retry_if_exception_type=(TimeoutError, ConnectionError),
)
with_retry 可以套在链的任意一层,而不只是 LLM:
python
# 只给检索这一步加重试
retrieval = retriever.with_retry(stop_after_attempt=3)
chain = {"context": retrieval | format_docs, "question": RunnablePassthrough()} | ...
2.2 重试的三个坑
坑一:重试次数相乘
python
llm = ChatOpenAI(max_retries=3) # 3 次
chain = (prompt | llm).with_retry(stop_after_attempt=3) # 再 3 次
# 最坏情况 3 × 3 = 9 次调用!
只在最外层加一层重试,别叠。
坑二:重试放大成本
一次请求重试 3 次 = 最多 4 倍费用。
如果你的失败率是 20%,平均成本就涨了 20%------这是隐形成本。
坑三:对不可重试的错误重试
参数错误(400)、认证失败(401)重试一万次也没用。
LangChain 已经帮你过滤了,但自己写重试逻辑时要注意:
python
import asyncio
from openai import RateLimitError, APITimeoutError
async def smart_retry(coro_factory, max_attempts=3):
for i in range(max_attempts):
try:
return await coro_factory()
except (RateLimitError, APITimeoutError) as e:
if i == max_attempts - 1:
raise
await asyncio.sleep(2 ** i) # 1s, 2s, 4s
except Exception:
raise # 其他错误直接抛,不重试
三、第二层:降级(with_fallbacks)
重试解决"偶发",降级解决"这个东西彻底不行了"。
3.1 换模型
python
from langchain_openai import ChatOpenAI
primary = ChatOpenAI(model="gpt-4o", timeout=30, max_retries=1)
backup = ChatOpenAI(model="gpt-4o-mini", timeout=30, max_retries=1)
last = ChatOpenAI(model="gpt-3.5-turbo", timeout=30, max_retries=1)
llm = primary.with_fallbacks([backup, last])
chain = prompt | llm | parser
chain.invoke({"q": "..."}) # gpt-4o 挂了 → 自动换 gpt-4o-mini → 再换 3.5
按顺序尝试,第一个成功的就用。
3.2 换供应商(更有价值)
真正的生产级降级是跨供应商 ------
OpenAI 挂了换 Anthropic,都挂了换本地模型:
python
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_community.chat_models import ChatOllama
llm = (
ChatOpenAI(model="gpt-4o-mini", timeout=30)
.with_fallbacks([
ChatAnthropic(model="claude-3-haiku-20240307", timeout=30),
ChatOllama(model="qwen2.5", timeout=60), # 最后兜底本地模型
])
)
本地模型兜底很实用 :它可能效果差点,但至少不会"完全不可用"。
对客服这类场景,"回答质量下降"远比"直接报错"可接受。
3.3 降级到非模型方案
最极端的兜底------不用模型:
python
from langchain_core.runnables import RunnableLambda
def keyword_fallback(inputs):
"""用关键词规则硬匹配一个答案"""
return KEYWORD_ANSWERS.get(inputs["question"], "抱歉,服务暂时不可用,请稍后再试。")
llm_chain = prompt | llm | parser
final = llm_chain.with_fallbacks([RunnableLambda(keyword_fallback)])
3.4 什么时候会触发降级
关键点:with_fallbacks 只在"抛异常"时才切换。
如果模型返回了一个质量很差但没报错 的结果(比如空字符串、
答非所问),它是不会降级的。要处理这种情况,得自己加校验:
python
def validate_and_fallback(chain, fallbacks, inputs):
try:
result = chain.invoke(inputs)
if is_bad(result): # 自己定义"什么是坏结果"
raise ValueError("结果质量不合格")
return result
except Exception:
for fb in fallbacks:
try:
return fb.invoke(inputs)
except Exception:
continue
return "抱歉,服务暂时不可用。"
四、第三层:超时
4.1 设在哪里
两处都要设:
python
# 1. 模型层
llm = ChatOpenAI(model="gpt-4o-mini", timeout=30)
# 2. 应用层(Web 服务)
import asyncio
result = await asyncio.wait_for(chain.ainvoke(inputs), timeout=45)
应用层的超时要略大于模型层 ------
否则可能出现"应用层已经返回超时,模型层还在跑"的悬挂请求。
4.2 流式场景的超时
流式调用不能用简单的 wait_for(因为它是持续产出):
python
async def stream_with_deadline(chain, inputs, first_token_timeout=15, total_timeout=120):
start = asyncio.get_event_loop().time()
first = True
async for chunk in chain.astream(inputs):
now = asyncio.get_event_loop().time()
if first:
if now - start > first_token_timeout:
raise TimeoutError("首字超时")
first = False
if now - start > total_timeout:
raise TimeoutError("总时长超时")
yield chunk
首字超时(TTFT)和总时长要分开设 ------
用户能容忍"慢慢输出",但不能容忍"点了没反应"。
五、三层的正确组合顺序
用户请求
↓
【超时】整体包一层 deadline
↓
【降级】with_fallbacks([备用1, 备用2, 规则兜底])
↓
【重试】每个分支内部 max_retries / with_retry
顺序反了会出问题:
- 如果重试包在降级外面 → 模型 A 失败,重试 A 三次,才降级到 B。
浪费时间,用户等太久; - 正确是:先降级(快切),每个降级分支内部自己重试。
python
# ✅ 正确:降级在外,重试在内
primary = ChatOpenAI(model="gpt-4o", max_retries=1) # 内部重试 1 次
backup = ChatOpenAI(model="gpt-4o-mini", max_retries=1)
llm = primary.with_fallbacks([backup])
chain = (prompt | llm | parser).with_config(recursion_limit=5)
六、监控:别等用户告诉你挂了
python
from langchain_core.callbacks import BaseCallbackHandler
import logging
class FallbackMonitor(BaseCallbackHandler):
def on_chain_error(self, error, *, run_id, **kwargs):
logging.error(f"chain_error run={run_id} type={type(error).__name__}")
def on_llm_error(self, error, *, run_id, **kwargs):
metrics.incr(f"llm.error.{type(error).__name__}")
三个要看的指标:
| 指标 | 告警线 | 含义 |
|---|---|---|
| 错误率 | > 2% | 供应商出问题了 |
| 降级触发率 | > 5% | 主模型不可靠,考虑换 |
| P95 延迟 | 超 SLA | 有慢请求 |
降级触发率特别重要 ------它告诉你"主用方案还行不行"。
如果每天都大量走降级,说明主模型该换了,而不是继续凑合。
七、完整示例
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
prompt = ChatPromptTemplate.from_template("{question}")
def make_llm(model, **kw):
return ChatOpenAI(model=model, timeout=kw.get("timeout", 30),
max_retries=kw.get("max_retries", 1), temperature=0)
primary = make_llm("gpt-4o-mini")
backup = make_llm("gpt-3.5-turbo")
def rule_fallback(inputs):
return "抱歉,AI 服务暂时不可用,已为你转人工客服。"
chain = (
prompt
| primary.with_fallbacks([backup])
| StrOutputParser()
).with_fallbacks([RunnableLambda(rule_fallback)])
async def safe_answer(question: str) -> str:
try:
return await asyncio.wait_for(chain.ainvoke({"question": question}), timeout=45)
except asyncio.TimeoutError:
return "处理超时,请稍后再试。"
八、小结
- 三种故障分开处理:瞬时→重试、持续→降级、慢→超时;
- 重试只在最外层加一层,多层叠加会让调用次数相乘、成本翻倍;
with_fallbacks只在抛异常 时切换,
质量差但不报错的结果需要自己加校验;- 跨供应商降级最有价值,最后可以用本地模型或规则兜底;
- 正确顺序:超时包最外层 → 降级 → 每个分支内部重试;
- 监控降级触发率,持续偏高说明主方案该换而不是继续兜。
下一篇讲批量与并发------batch 的正确用法和速率控制。