LLM 错误处理模块
提供统一的错误类型、分类和处理机制。
错误类型:
厂商业务码(智谱 1309、OpenAI insufficient_quota@429 …)的细分在 provider 边界做
(im.ttalk.agent.provider.common.business-codes),core 只认状态码能下的结论(402)。
使用示例:
(require '[im.ttalk.agent.model.error :as errors])
;; 创建错误 (errors/error :timeout-error "请求超时")
;; 检查是否可重试(直接关键字访问) (:retryable? err) ; => true
;; 安全执行 (errors/with-error-handling #(api-call))
LLM 错误处理模块 提供统一的错误类型、分类和处理机制。 错误类型: - :network-error 网络连接错误(可重试) - :timeout-error 请求超时(可重试) - :rate-limit-error 速率限制(可重试) - :auth-error 认证失败(不可重试) - :validation-error 参数验证失败(不可重试) - :parse-error 响应解析失败(不可重试) - :provider-error Provider 特定错误 - :quota-exhausted-error 欠费 / 额度用尽 / 402(不可重试;**不是限流**,可切备用模型) - :context-overflow-error 请求超出上下文窗口(不可重试、不切备用,留给压缩恢复) - :empty-completion-error HTTP 200 但无有效内容(可重试,但只走 ChatModel 的独立预算) 厂商业务码(智谱 1309、OpenAI insufficient_quota@429 …)的细分在 provider 边界做 (`im.ttalk.agent.provider.common.business-codes`),core 只认状态码能下的结论(402)。 使用示例: (require '[im.ttalk.agent.model.error :as errors]) ;; 创建错误 (errors/error :timeout-error "请求超时") ;; 检查是否可重试(直接关键字访问) (:retryable? err) ; => true ;; 安全执行 (errors/with-error-handling #(api-call))
(auth-error? err)检查是否为认证错误(401/403 或 :auth-error 类型)
检查是否为认证错误(401/403 或 :auth-error 类型)
(classify-exception e)把工具执行抛出的异常分类为故障类别,决定屏障处的路由:
subagent/delegate 抛的)→ 屏障处暂停,resume 时续跑那个子 agent
而不是重跑这次调用。单列一类是因为措辞会到用户眼前
(RUN_FINISHED.outcome.interrupts[].reason),借 :environment 会写成
「环境类错误」——而这压根不是错误判定顺序:
把工具执行抛出的异常分类为故障类别,决定屏障处的路由:
- :semantic (缺省)模型造成、只有模型能修 → 序列化为结果回给模型
- :transient 重试同一调用有意义(超时/限流/网络抖动)→ 工具级自动重试
(仅当工具声明 :retry;幂等前提)
- :environment 模型修不了、重试无用(认证失效/配额/磁盘满)→ 屏障处暂停等人
- :subagent-suspended **不是失败**:委派出去的子 agent 停下来等人答复了
(`subagent/delegate` 抛的)→ 屏障处暂停,resume 时**续跑那个子 agent**
而不是重跑这次调用。单列一类是因为措辞会到用户眼前
(`RUN_FINISHED.outcome.interrupts[].reason`),借 `:environment` 会写成
「环境类错误」——而这压根不是错误
判定顺序:
1. ex-data 显式 :error-class(工具作者标注,最高优先级)
2. canonical error(D5 词汇)::retryable? true → :transient;
:auth-error → :environment;其余 → :semantic
3. 常见网络异常(SocketTimeoutException/ConnectException/HttpTimeoutException)
→ :transient
4. 其余 → :semantic(contain-throwable t)catch-Throwable 三元组(fatal 检查 / nil-message 回退 / 分类)的统一入口。
致命的(OOM 等)原样上抛(fatal-throwable? 判据);非致命的返回
{:message m :class c}——调用方据此组装自己的错误形状(terminal map /
success-false map / invoke-one 的 :value+:error)。
DRY 4 处手抄:tool/invoke、chat-client/invoke-tool 两个 terminal、
react/invoke-one 此前各自重复 fatal-check + nil-message + classify-exception
三步,三元组漂移即出 bug。
catch-Throwable 三元组(fatal 检查 / nil-message 回退 / 分类)的统一入口。
致命的(OOM 等)原样上抛(`fatal-throwable?` 判据);非致命的返回
`{:message m :class c}`——调用方据此组装自己的错误形状(terminal map /
success-false map / invoke-one 的 :value+:error)。
**DRY 4 处手抄**:`tool/invoke`、`chat-client/invoke-tool` 两个 terminal、
`react/invoke-one` 此前各自重复 fatal-check + nil-message + classify-exception
三步,三元组漂移即出 bug。(context-overflow? err)请求超出上下文窗口(:context-overflow-error)。不重试、不切备用,交压缩恢复。
请求超出上下文窗口(:context-overflow-error)。不重试、不切备用,交压缩恢复。
(empty-completion? err)空补全(:empty-completion-error)。
空补全(:empty-completion-error)。
(error type message)(error type message opts)创建错误 map
参数:
返回: 错误 map {:type :xxx :message "..." :retryable? bool}
示例: (error :timeout-error "请求超时" {:status 504})
创建错误 map
参数:
- type: 错误类型(关键字)
- message: 错误消息
- opts: 可选参数 map
- :cause 原因异常
- :status HTTP 状态码
- :provider Provider 名称
- :context 上下文信息
- :code 厂商业务码 / 细分码(如 "insufficient_quota"、:model-mismatch),可选
返回:
错误 map {:type :xxx :message "..." :retryable? bool}
示例:
(error :timeout-error "请求超时" {:status 504})(error? x)检查是否为错误 map
参数:
返回: boolean
检查是否为错误 map 参数: - x: 任意值 返回: boolean
(exception->error e)(exception->error e context)将异常转换为错误 map
参数:
返回: 错误 map
示例: (exception->error (IOException. "连接失败"))
将异常转换为错误 map 参数: - e: 异常对象 - context: 上下文信息(可选) 返回: 错误 map 示例: (exception->error (IOException. "连接失败"))
(fatal-throwable? t)该 Throwable 是否不该被收敛为工具错误,必须原样上抛。
背景:工具执行的各层此前一律 catch Exception,于是 Error 全部逃逸——
一个工具的深递归 StackOverflowError 会打死整个 agent 循环,而分层错误路由
(:semantic/:transient/:environment)的全部意义就是「一个工具坏了不牵连别人」。
但也不能无差别 catch Throwable:吞掉 OOM 只会掩盖真因,且收敛动作本身还要
分配内存,多半当场再炸。故要一条判据。
致命(放行):VirtualMachineError 中除 StackOverflowError 之外的那些
(OutOfMemoryError / InternalError / UnknownError——JVM 自身已坏,
继续跑工具没有意义)、ThreadDeath。
不致命(收敛):其余一切,尤其是
StackOverflowError——工具自己的递归 bug,栈一退就恢复,正是最该被
收敛成「这一个工具失败」的那类。这是我们与 Scala NonFatal 的有意分歧:
它把整个 VirtualMachineError 划为致命,但那是通用库的保守取舍;这里的
Throwable 来自用户工具函数体,栈溢出是它最常见的自伤方式。AssertionError(工具里的 assert)、LinkageError / NoClassDefFoundError
(该工具缺可选依赖)——都只说明这个工具不可用,不该牵连整轮。收敛后经 classify-exception 归类(缺省 :semantic:工具 bug 重试无意义)。
该 Throwable 是否**不该被收敛为工具错误**,必须原样上抛。 背景:工具执行的各层此前一律 `catch Exception`,于是 `Error` 全部逃逸—— 一个工具的深递归 `StackOverflowError` 会打死整个 agent 循环,而分层错误路由 (:semantic/:transient/:environment)的全部意义就是「一个工具坏了不牵连别人」。 但也不能无差别 `catch Throwable`:吞掉 OOM 只会掩盖真因,且收敛动作本身还要 分配内存,多半当场再炸。故要一条判据。 **致命(放行)**:`VirtualMachineError` 中除 StackOverflowError 之外的那些 (`OutOfMemoryError` / `InternalError` / `UnknownError`——JVM 自身已坏, 继续跑工具没有意义)、`ThreadDeath`。 **不致命(收敛)**:其余一切,尤其是 - `StackOverflowError`——**工具自己的递归 bug**,栈一退就恢复,正是最该被 收敛成「这一个工具失败」的那类。这是我们与 Scala `NonFatal` 的**有意分歧**: 它把整个 `VirtualMachineError` 划为致命,但那是通用库的保守取舍;这里的 Throwable 来自**用户工具函数体**,栈溢出是它最常见的自伤方式。 - `AssertionError`(工具里的 `assert`)、`LinkageError` / `NoClassDefFoundError` (该工具缺可选依赖)——都只说明**这个工具**不可用,不该牵连整轮。 收敛后经 `classify-exception` 归类(缺省 :semantic:工具 bug 重试无意义)。
(format-error err)格式化错误为可读字符串
参数:
返回: 格式化的字符串
示例: (format-error {:type :timeout-error :message "请求超时" :status 504}) ; => "[TIMEOUT-ERROR] 请求超时 (HTTP 504)"
格式化错误为可读字符串
参数:
- err: 错误 map
返回:
格式化的字符串
示例:
(format-error {:type :timeout-error :message "请求超时" :status 504})
; => "[TIMEOUT-ERROR] 请求超时 (HTTP 504)"(http-error? err)检查是否为 HTTP 相关错误(网络/超时/有状态码)
检查是否为 HTTP 相关错误(网络/超时/有状态码)
(http-response->error response provider)将 HTTP 错误响应转换为错误 map
参数:
返回: 错误 map
示例: (http-response->error {:status 401 :body "Unauthorized"} :openai)
将 HTTP 错误响应转换为错误 map
参数:
- response: HTTP 响应 {:status n :body ...}
- provider: Provider 名称
返回:
错误 map
示例:
(http-response->error {:status 401 :body "Unauthorized"} :openai)(quota-exhausted? err)欠费 / 额度用尽(:quota-exhausted-error)。不可重试,但可切备用模型。
欠费 / 额度用尽(:quota-exhausted-error)。不可重试,但可切备用模型。
(rate-limit-error? err)检查是否为速率限制错误(:rate-limit-error 类型,或 429 且不是欠费)。
OpenAI 兼容端点欠费也回 429(insufficient_quota)——那不是限流, provider 边界已把它细分为 :quota-exhausted-error,这里不能再认回去。
检查是否为速率限制错误(:rate-limit-error 类型,或 429 且**不是欠费**)。 OpenAI 兼容端点欠费也回 429(insufficient_quota)——那不是限流, provider 边界已把它细分为 :quota-exhausted-error,这里不能再认回去。
(throw! err)将错误转换为异常并抛出
参数:
抛出: ExceptionInfo
将错误转换为异常并抛出 参数: - err: 错误 map 抛出: ExceptionInfo
(with-error-handling f)(with-error-handling f context)执行函数并捕获异常转换为错误
参数:
返回: [:ok result] 或 [:error error-map]
示例: (with-error-handling #(api-call) {:operation "call-llm"})
执行函数并捕获异常转换为错误
参数:
- f: 要执行的函数
- context: 上下文信息(可选)
返回:
[:ok result] 或 [:error error-map]
示例:
(with-error-handling #(api-call) {:operation "call-llm"})(with-ex-data t extra)返回一个追加了 extra 键的等价异常(重试的 :attempts、主备切换的 :fallback 报告用)。
ex-info 的 data 不可变,只能换一个实例:消息、cause、栈逐字保留,data = (merge 原 data extra)
——调用方拿到的仍是同一个 canonical error(:type/:status/:retryable? 不丢),只是多了几个键。
非 ExceptionInfo(IOException 等)先 exception->error 归一,原异常作为 cause——
保证 D5「ex-data 即 canonical error」不破。
返回一个**追加了 extra 键**的等价异常(重试的 :attempts、主备切换的 :fallback 报告用)。 ex-info 的 data 不可变,只能换一个实例:消息、cause、栈逐字保留,data = (merge 原 data extra) ——调用方拿到的仍是同一个 canonical error(:type/:status/:retryable? 不丢),只是多了几个键。 非 ExceptionInfo(IOException 等)先 `exception->error` 归一,原异常作为 cause—— 保证 D5「ex-data 即 canonical error」不破。
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |