
1. 为什么在 ChatGPT App 中,错误是常态而不是事故在上一讲中,我们讨论了如何把任务拆成步骤并在 ChatGPT App 中构建多步工作流。现在在这套方案上加入现实世界:错误、超时,以及用户中断。在传统 Web 中,逻辑常常围绕“幸福路径”,错误被视作罕见且“事故级”的东西——红色的 500 页面之类。而在 ChatGPT App 中情形不同:你在由 LLM、外部 API、MCP、widget 以及可能随时关闭标签页的用户组成的分布式系统里工作。错误与中断是日常。有几个特点会让事情更复杂:首先,LLM 具有非确定性。即使同样的 prompt,它也可能做出略有不同的决策:调用不同的工具、改变参数,甚至觉得最好“再确认一下”。其次,网络与基础设施约束。来自 ChatGPT 的 tool‑call 有超时(通常几十秒),你的 Next.js/Vercel 后端也有。如果外部 API 变慢,一切都可能半途而废。第三,还有 UX 因素:用户分心、关闭聊天,一天后才回来,而你不可能一直在数据库里持有一笔长事务。由此得到本讲的核心观点:容错工作流 = 我们预设任何一步都可能失败,并明确说明那时该做什么。错误不仅是给用户看的消息,还是给模型的信号,它可以据此调整策略、建议回滚、尝试其他工具,或体面地结束场景。2. 工作流中的错误版图:都有哪些要想正确处理故障,首先要学会区分它们。在基于 ChatGPT Apps 的 LLM 应用里,典型会遇到几类错误。技术错误。 这就是分布式系统的老三样:网络超时、来自你方或外部 API 的 5xx、MCP 服务器崩溃、工具 handler 里的 bug。比如,在 GiftGenius 中,你的 MCP 工具 search_products 访问目录服务,而对方返回 503 Service Unavailable。这就是自动重试(retry)的候选。逻辑(模型)错误。 包括模型拒绝(判定请求违反策略)、幻觉,或工具响应里损坏的 JSON。模型可能为 tool‑call 生成了不正确的参数,你的 JSON 校验未能通过。这通常是输入数据错误,而非基础设施问题。业务错误。 与业务语义相关:商品售罄、用户预算对所选筛选条件而言过小、优惠码无效、预订已过期。在 GiftGenius 中,这类似“在 500 个候选中没有一个符合既定约束”。此处重试很少有用:要么调整参数,要么向用户解释约束不现实。UX 中断。 用户主动中断流程:关闭 ChatGPT,在 widget 里点“返回”,取消操作,把上一步的回答改了。这也应被视作正常流程,而非错误。重要的是要学会在这种情况下恢复与回滚状态,我们稍后会讨论。逻辑与技术错误的交界处有个特殊棘手案例——代理的无限循环:模型接收错误,想着“嗯,我再试一次”,再次报错,如此往复直到上下文或预算耗尽。防护此类行为是错误设计的重要部分。3. 基本策略:retry、fail‑fast、rollback、让用户参与任何错误都可以看作一个分叉点:我们要么重试这一步,要么回滚,要么让用户参与。重要的是,这些策略可以组合。对于技术性和暂时性故障(网络抖动、API 返回 503),合理做法是有限重试并带回退(backoff)。 对于逻辑与业务错误(“校验器不接受预算”“商品已售罄”)重试无意义,应快速失败(fail‑fast)并请用户修改输入或参数。对于已经对外部世界产生了改变的操作(创建订单、预订),需要回滚(rollback)——或者在 UI/上下文中“回退一步”,或者执行真正的补偿性动作(取消订单、退款)。还有一些场景天然需要用户参与:比如支付系统因“发卡行拒绝”而失败,无法由你自动修复。模型应当清晰解释发生了什么,并给出选项:换卡、降低金额,或放弃购买。为了让工作流可靠,强烈建议为每个步骤明确写出:可能出现哪些错误类型,以及你在每种情况下的处理——自动重试、回滚、请求用户输入,还是仅记录日志并结束分支。4. 重试与回退(backoff):何时以及如何做先从开发者最自然的反应开始:“那我们再试一次吧。”想法没错,但细节决定成败。哪些错误值得重试一条来自集成实践的好经验:网络错误和 5xx 可带间隔重试,而 4xx 大概率不该重试。也就是说,如果你收到了 503、504 或干脆没等到外部 API 响应,那么加上一点延迟后重试是有意义的;但如果服务器返回 400 Bad Request 或 422 Unprocessable Entity,更可能是数据问题,用同样参数重试不会改变结果。TypeScript 版 callWithRetry 小工具我们来为 MCP 或后端层写个小工具,方便在工具里复用:type RetryOptions = { maxRetries: number; baseDelayMs: number; }; async function callWithRetryT( fn: () = PromiseT, { maxRetries, baseDelayMs }: RetryOptions ): PromiseT { let attempt = 0; // 我们不需要无限循环 while (true) { try { return await fn(); } catch (err: any) { attempt++; const status = err?.status ?? err?.response?.status; // 对 4xx 不进行重试 const isClientError = typeof status === "number" status = 400 status 500;