接上一篇。工具注册中心让 Agent 能操控 AOI 设备了。下一个问题:每次工具调用背后都有 LLM 调用,而 LLM API 的可靠性跟工业相机差了两个数量级。
工业设备有个基本假设:你发指令,它执行,响应时间可预测。AOI 相机的采集延迟是毫秒级确定的,运动控制卡的响应周期是 0.16 秒------不快,但稳定。
LLM API 不是这样的。它可能会:
- 在 50ms 内返回(运气好)
- 在 30 秒后超时(网络抖动)
- 返回 429 告诉你"等一下"(隔壁产线也在用同一个 API key)
- 返回 200 但内容是乱码(模型幻觉,极少但存在)
- 完全不响应(服务端挂了)
一个标准的 Agent 循环里有多次 LLM 调用:分析缺陷图片要调用一次,决定用哪个工具要调用一次,改写 XML 参数前要调用一次确认。任何一次失败都可能打断整个闭环。
在产线场景下,"重试"不是一个可以随便做的决定------如果 Agent 正在等待 AOI 检测结果来判决当前批次是否放行,你重试 3 次各等 30 秒,整条线就多等了 90 秒。在 0.16 秒节拍的世界里,这是不能接受的。
一、不同错误,不同命运
容错的第一件事不是"怎么重试",是判断要不要重试:
| 错误 | 你的反应 |
|---|---|
| 429 限流 | 等 API 告诉你等多久,精确等到那个时间再试 |
| 500/502/503 | 指数退避重试,同时切到备用模型 |
| 超时 | 递增超时重试(第一次 30s,第二次 45s) |
| 401/403 | 别重试。 API Key 有问题,重试一万次也没用 |
| Context 超限 | 截断历史消息后重试,而不是增加超时 |
分类器的核心逻辑不是魔法------关键词匹配 + HTTP 状态码:
python
class LLMErrorClassifier:
STATUS_MAP = {429: RATE_LIMIT, 502: SERVER_ERROR, 401: AUTH, ...}
KEYWORD_RULES = [
(["限流", "rate limit", "too many requests"], RATE_LIMIT),
(["超时", "timeout", "ConnectTimeout"], TIMEOUT),
(["认证", "invalid_api_key"], AUTH),
...
]
但有一个细节值得说:从 429 响应中提取 Retry-After header。API 明确告诉你等 15 秒,你就精确等 15 秒------而不是用通用退避公式算一个 2 → 4 → 8 秒的序列。精确等待在产线场景下意味着恢复时间可控。
二、重试不是"等一下再试",是递增超时 + 抖动
固定等待 2 秒重试 3 次的问题:
- 如果超时是因为模型慢(大 prompt + function calling),你每次给同样的 timeout,每次都超时
- 如果多个 Agent 同时被限流后同时重试,它们会再次同时被限流(惊群效应)
所以重试策略要带递增超时 和随机抖动:
python
for attempt in range(1, max_attempts + 1):
# 每次重试给更长的超时
current_timeout = min(base_timeout * (1.5 ** (attempt - 1)), max_timeout)
try:
response = client.create(model=model, messages=messages, timeout=current_timeout)
return response
except Exception as exc:
err_type, retry_after = classify(exc)
if not err_type.retryable:
break # 认证/参数错误不重试
delay = retry_after or (base_delay * (2.0 ** (attempt - 1)))
delay = min(delay, max_delay)
delay *= (0.5 + random.random() * 0.5) # 抖动
time.sleep(delay)
jitter 不是算法优化,是分布式系统的生存策略------没有它,定时器会让所有重试在同一毫秒触发。
三、四级降级:从主模型到优雅降级
重试 3 次全失败后,传统做法是抛异常。但在 Agent 场景下,用户(操作员)还在等检测结果------你不能返回一个 Python traceback。
降级链是四层递进:
arduino
DeepSeek-Chat(主模型)
↓ 连续失败
DeepSeek-Reasoner(备用模型,推理更强但更慢)
↓ 也失败
LLM 响应缓存(近期成功调用的语义匹配)
↓ 未命中
"检测系统暂时异常,请稍后重试或切换人工复判模式"
缓存降级在这一层特别有用。 AOI 产线的 Agent 对话有很强的重复性------"分析这张图的缺陷类型"、"检查当前 canny 阈值是否需要调整"、"输出这批的 NG 统计"。这些 prompt 的语义高度相似,LRU 缓存(100 条 / TTL 5 分钟)的命中率在实际使用中比想象的高得多。API 全挂时,缓存仍然可以返回上一次相似分析的结果结构。
缓存 key 的生成方式直接但不粗暴:
python
key = hashlib.md5(json.dumps(messages, sort_keys=True) + "|" + model).hexdigest()
在 Agent 循环中,消息格式高度结构化(system prompt + user message + tool result),hash 冲突不是实际风险。
四、统一的结果封装:让 Agent 知道发生了什么
每一次 LLM 调用,无论经历了什么路径(直连成功 / 重试后成功 / 切备用模型成功 / 缓存命中 / 优雅降级),都返回同一个结构:
python
@dataclass
class LLMResult:
content: str # 生成的文本
model: str # 实际用的模型(可能是备用的)
latency_ms: float # 总耗时(含重试等待)
success: bool
attempts: int # 经历几次调用才拿到结果
is_fallback: bool # 是不是降级结果
fallback_level: int # 0=主模型, 1=备用, 2=缓存, 3=兜底
recovery_hint: str # 给 Agent 的恢复建议:"限流中,建议降低调用频率"
Agent 的上层逻辑不需要关心重试了几次------只看 success 和 content。但 recovery_hint 会告诉它为什么 失败以及怎么恢复,这对自愈型 Agent 至关重要。
比如 recovery_hint 说"当前模型触发限流",Agent 可以决定:暂时跳过非关键的工具调用,只保留 AOI 检测和 XML 读取这两个核心工具。
五、怎么接入 LangGraph:透明替换
LLMGuard 的 API 是原始的 OpenAI 消息格式([{"role": "user", "content": "..."}])。为了让它在 LangGraph 的 create_react_agent 中无缝替换 ChatOpenAI,需要一个轻量的 LangChain wrapper:
python
class GuardedChatModel(BaseChatModel):
guard: LLMGuard
def _generate(self, messages, **kwargs):
# LangChain messages → OpenAI dicts
# LangChain tools → OpenAI function calling format
result = self.guard.chat(messages=openai_messages, tools=tools, ...)
# OpenAI response → LangChain AIMessage (含 tool_calls)
...
对 Agent 循环完全透明------它只知道自己在调一个 ChatModel,不知道下面重试了 2 次还切了一次备用模型。
六、跟互联网 Agent 场景的区别
互联网场景(客服、搜索、文档问答)下,LLM 调用是"越多越快越好"。产线场景下,需求不一样:
- 不是越快越好,是可预测。 你知道一次调用最坏要多久(四级降级的总超时是可计算的),才能决定 Agent 能不能在节拍内完成决策。
- 不是越多越好,是够用就行。 缓存命中率高不是因为技术好,是因为产线场景的 prompt 确实比 C 端对话规律得多。
- 失败不只是 UX 问题。 互联网 Agent 挂了用户换个姿势重问。产线 Agent 在检测 → 调参闭环中挂了,需要人工介入------容错层的最坏情况设计比最好情况优化重要得多。
LLMGuard 本身只有 ~1000 行,核心逻辑不复杂。真正的工程难点不在代码量,而在想清楚每次失败后系统应该处于什么状态------这需要你真的在产线上跑过,才知道哪些错误类型最常见、降级到什么程度还能接受。
下一篇可能会写最核心的模块:AOI XML 配置改写------也就是 Agent 怎么读懂检测参数、判断调整方向、安全地写入上位机配置。这是整个项目里跟"通用 Agent"最无关、跟我的日常工作关系最紧密的部分。