LLM 调用的四层错误分类与流式超时检测
2026/8/30 23:18:16 网站建设 项目流程

TL;DR(30 秒速览)

  • Agent 调用 LLM API 报错了——API Key 过期?上下文超长?网络超时?限流?不同 Provider 的错误格式完全不同
  • 四层错误分类:结构化异常 → HTTP 状态码 → Provider 特定异常 → 关键词兜底
  • 三种错误类型:Context Overflow(压缩后重试)、Transient(指数退避重试)、Non-Transient(立即失败)
  • 双流停滞检测:TTFT 240s(首 token 超时)+ Inter-chunk 120s(chunk 间隔超时)
  • 异常链遍历MAX_CAUSE_DEPTH = 16防止循环引用,每层检查所有四种策略
  • 核心代码:LlmErrorClassifier(237 行)+AgentLoopRunner流式超时(335 行)

前情提要:上一篇我们讲了 全异步 MCP 集成——双传输协议、Reactor → Coroutine 桥接、按用户懒加载。今天讲 LLM 调用出错时怎么办。


核心矛盾

Agent 调用 LLM API 报错了。

OpenAI 说"context_length_exceeded",Anthropic 说"prompt is too long",DashScope 说"maximum context length is"——意思一样,格式完全不同。怎么处理?

Provider错误格式
OpenAI400 - {"error":{"code":"context_length_exceeded"}}
Anthropic400 - {"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long"}}
DashScope400 - {"code":"InvalidParameter","message":"maximum context length is 128000"}

EasyAI 的方案:四层检测策略 + 统一分类


四层错误分类

Layer 1: 结构化异常(NonTransientAiException / TransientAiException) ↓ 未命中 Layer 2: HTTP 状态码(400 = 上下文溢出, 429 = 限流, 5xx = 服务不可用) ↓ 未命中 Layer 3: Provider 特定异常(OpenAI ApiError, Anthropic ApiException) ↓ 未命中 Layer 4: 关键词兜底("context_length", "rate_limit", "timeout" 等)

上下文溢出检测

// LlmErrorClassifier.isContextOverflow()funisContextOverflow(e:Throwable):Boolean{varcurrent:Throwable?=ewhile(current!=null){// Layer 1: Spring AI 结构化异常if(currentisNonTransientAiException){if(isContextOverflowMessage(current.message))returntrue}// Layer 2: HTTP 400 状态码valmessage=current.message?.lowercase()?:""if((message.startsWith("400")||message.contains("400 -"))&&isContextOverflowMessage(message))returntrue// Layer 3: Provider 特定异常if(isProviderContextOverflowException(current))returntruecurrent=current.cause}// Layer 4: 关键词兜底(只检查最外层异常)returnisContextOverflowMessage(e.message?.lowercase()?:"")}

关键词匹配

privatefunisContextOverflowMessage(message:String):Boolean{returnmessage.contains("context_length")||// OpenAImessage.contains("prompt is too long")||// Anthropicmessage.contains("maximum context length")||// DashScopemessage.contains("context length exceeded")||message.contains("too many tokens")}

关键:不能把 API Key 错误误判为上下文溢出——否则无限重试。

超时检测

funisTimeout(e:Throwable):Boolean{varcurrent:Throwable?=ewhile(current!=null){if(currentisTransientAiException)returntrue// Layer 1if(currentisSocketTimeoutException||// Layer 2currentisTimeoutException||currentisResourceAccessException)returntrue// Layer 3-4: 消息关键词valmessage=current.message?.lowercase()?:""if(message.contains("timeout")||message.contains("timed out"))returntruecurrent=current.cause}returnfalse}

异常链遍历

// 异常可能嵌套多层:RuntimeException → IOException → SocketTimeoutException// MAX_CAUSE_DEPTH = 16 防止循环引用privateconstvalMAX_CAUSE_DEPTH=16

每层都检查所有四种策略,确保深层嵌套的异常也能被正确分类。


双流停滞检测

问题

LLM 流式响应可能"卡住":HTTP 连接还活着,但不再发送新 chunk。传统的 HTTP 超时不检测这种情况。

双超时机制

// AgentLoopRunnercompanionobject{/** TTFT: 从发送请求到收到第一个 chunk */privateconstvalFIRST_CHUNK_TIMEOUT_SECONDS=240L/** Inter-chunk: 两个 chunk 之间的最大间隔 */privateconstvalSTREAM_STALL_TIMEOUT_SECONDS=120L}
超时含义
TTFT240s从发送请求到收到第一个 chunk(LLM 需要时间思考)
Inter-chunk120s两个 chunk 之间的最大间隔(流式响应中途停滞)

实现

// AgentLoopRunner 中的流式消费循环varreceivedContentChunk=falsewhile(true){// TTFT 用更长的超时(LLM 需要处理 Prompt)valbaseTimeout=if(!receivedContentChunk&&chunkCount==0){FIRST_CHUNK_TIMEOUT_SECONDS.seconds// 240s}else{STREAM_STALL_TIMEOUT_SECONDS.seconds// 120s}valresult=withTimeoutOrNull(baseTimeout){channel.receiveCatching()}if(result==null){// 超时!valphase=if(chunkCount==0)"first token (TTFT)"else"subsequent chunk"throwTimeoutException("LLM stream stalled: no$phasereceived within${timeoutSec}s")}valchunk=result.getOrNull()?:break// 跳过空 chunk(SSE keepalive),不重置计时器if(chunk.results.isEmpty()||chunk.results.all{it.output.text.isNullOrEmpty()}){continue// 空 chunk 不表示 LLM 在工作}receivedContentChunk=true// 处理 chunk...}

独立于 HTTP 层

不在 Netty/OkHttp 层设置超时(那是连接级超时),而是在应用层检测:每个 chunk 到达时重置计时器。


Agent 循环中的错误处理

上下文溢出自愈

AgentLoop.runInnerLoop() → callLLMWithOverflowHandling() → 调用 LLM → 捕获异常 → LlmErrorClassifier.isContextOverflow(e)? → true: 触发 ContextCompactionOrchestrator 压缩 → 压缩完成后透明重试(同一轮) → false: 正常异常处理

重试策略

// AgentLoopRunner 中的重试逻辑if(retryCount<context.maxRetries&&isTimeoutException(e)){retryCount++valbackoffMs=retryCount*1000L// 线性退避logger.warn("LLM call timed out, retrying ({}/{}) after {}ms",retryCount,context.maxRetries,backoffMs)push(RetryEvent(messageId,retryCount,context.maxRetries,backoffMs,...))delay(backoffMs.milliseconds)// 重置累加器fullText.clear()fullThinking.clear()}else{throwe// Non-Transient 或重试次数用尽}
错误类型处理策略重试
Context Overflow压缩上下文 → 重试最多 1 次
Transient(超时/5xx/限流)指数退避重试最多 N 次
Non-Transient(API Key 无效)立即失败不重试

熔断器集成

// 连接错误、流停滞、5xx → 报告给端点熔断器if(LlmErrorClassifier.isEndpointOutage(e)){breaker?.recordFailure()}// 限流和客户端错误不计入熔断

跨 Provider 兼容性

Provider已验证的错误格式
OpenAI400 - {"error":{"code":"context_length_exceeded"}}
Anthropic400 - {"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long"}}
DashScope400 - {"code":"InvalidParameter","message":"maximum context length is 128000"}

四层检测确保即使 Provider 改变了错误格式,关键词兜底也能覆盖。新增 Provider 时只需在 Layer 3 添加 Provider 特定检测。


监控与日志

// 每次错误分类都记录日志logger.warn("LLM error classified as {}: {}",type,message)// 上下文溢出自愈时推送 SSE 事件// 前端可以看到"上下文超长,正在自动压缩"// 流停滞超时时记录logger.error("LLM stream stalled: no {} received within {}s (provider={}, lastChunk={})",phase,timeoutSec,providerName,lastChunkSummary)

踩坑记录

原因解法
Anthropicmessage_delta不含input_tokensusage 报告不完整UsageAwareTokenEstimator合理性窗口检测
429 误判“processed 429 items” 中的数字\b429\b全词匹配
Spring AITransientAiException不含 5xx某些版本遗漏Layer 2 HTTP 状态码检测作为补充
异常链循环引用e.cause形成环MAX_CAUSE_DEPTH = 16
空 chunk 重置计时器SSE keepalive 被误认为 LLM 在工作只在实际内容 chunk 时重置

总结

维度简单 try-catchEasyAI 四层分类
错误识别只看 message四层策略逐层检测
跨 Provider每个 Provider 单独处理统一分类接口
上下文溢出报错终止自动压缩 → 重试
流停滞HTTP 超时(连接级)应用层双超时检测
重试策略固定间隔分类驱动(压缩/退避/终止)

EasyAI 的LlmErrorClassifier用 237 行 Kotlin 代码实现了完整的错误分类——四层检测、三种类型、双流停滞检测、跨 Provider 兼容。

核心思想:好的错误处理不是 try-catch 打印日志,而是分类驱动——不同类型的错误有不同的自愈策略。


下一篇:让 LLM 稳定输出 JSON,我们踩了四道坎

Agent 的最终输出要以 JSON 交给下游系统——格式错一个字符全链路就断。EasyAI 的方案:宽容提取 → Schema 校验重试 → 原生结构化输出 → 大 JSON 分块提交,四级纵深防御。


开源地址:https://github.com/haibingzhao/easyai

欢迎 Star、Issue 和 PR。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询