知识库Agent生产工程生产工程Agent 系统设计与工程权衡Agent 系统设计与工程权衡
04 · 生产工程生产工程
Roadmap 04进阶Markdown86 min

Agent 系统设计与工程权衡Agent 系统设计与工程权衡

以状态机、至少一次队列、幂等与 Outbox 构建可恢复 Agent,并完成模型路由、弹性、容量和 SLO 设计。以状态机、至少一次队列、幂等与 Outbox 构建可恢复 Agent,并完成模型路由、弹性、容量和 SLO 设计。

#ArchitectureArchitecture#Distributed SystemsDistributed Systems#LLMOpsLLMOps#SRESRE更新于 2026-08-16

专题导读

生产 Agent 不是“在 Controller 里循环调用大模型”。它是一个外部依赖多、延迟长、结果非确定、可能产生业务副作用且按 token 计费的分布式系统。设计重点不在某个框架 API,而在同步/异步边界、状态持久化、消息语义、幂等、副作用一致性、模型路由、故障隔离和可观测性。

面试回答应从用户契约开始:哪些请求必须在交互时限内给结果,哪些创建异步 run;run 如何跨 JVM 恢复;队列重复投递时怎样避免重复退款;数据库状态和消息如何通过 transactional outbox 衔接;模型或工具故障时怎样熔断、降级而不扩大权限;最后用到达率、服务时间、token 配额和错误预算完成容量与 SLO 设计。

知识地图

API/鉴权 → 同步快路径或创建 Run → 状态机 → Outbox → 队列 → Worker → 模型路由/RAG → 工具网关 → 领域服务 → 终态事件/回调

旁路能力:幂等存储、检查点、缓存、审批、隔离舱、熔断器、死信队列、Trace/Metric/Log、预算与限流

设计轴 必须回答的问题 常见失败
交互契约 同步多久、异步如何查询/通知 长连接断开即丢状态
消息与状态 投递语义、重试、终态 重复副作用、僵尸 run
一致性 本地事务与跨服务副作用 DB 成功但消息丢失
模型与工具 路由、配额、超时、兼容性 fallback 质量/权限漂移
弹性 隔离、熔断、降级、恢复 重试风暴、级联故障
容量与 SLO 峰值、长尾、积压、错误预算 只按平均值估算

面试题

Q1:典型 Agent 平台应包含哪些核心组件?

核心回答: 至少包括 API 网关、身份与策略层、run/step 状态机、队列与 worker、模型路由、检索服务、工具网关、领域服务、状态/幂等/outbox 存储、审批服务以及可观测与评估平台。模型是依赖,不是系统边界。

入口负责认证、租户限流、请求幂等和创建 run;编排器只推进显式状态,不把关键状态藏在 prompt 或 JVM 内存;模型路由按任务策略选择兼容模型;工具网关做 schema、授权、超时和审计;领域服务再次验证业务规则。长任务通过队列解耦,终态可查询、回调或推送。

数据面与控制面应分开:数据面执行 run;控制面管理模型、prompt、工具、路由、配额和发布版本。所有配置进入版本向量,便于回放和回滚。外部平台用 adapter 接口封装,避免供应商 SDK 泄漏到编排状态机。

面试追问: 编排器能否直接访问数据库并执行退款?不建议。退款规则与授权属于领域服务,编排器只持有窄工具契约。

常见错误回答: “API → LLM → 数据库”三层足够;它忽略状态、权限、重试和副作用。

Q2:同步和异步的分界如何确定?

核心回答: 依据用户交互时限、P99 依赖延迟、步骤数量、副作用、可恢复性和客户端断连风险决定,而不是固定“超过 30 秒”。同步路径只承载在超时预算内可稳定完成且不需要长期恢复的工作;其余创建持久化 run,返回 202 Accepted + runId

短问答可同步或 SSE 流式返回,但 run 状态仍不应只依赖连接。涉及多工具、人工审批、批量处理或不可控第三方的任务应异步。API 提供幂等创建、状态查询、取消和结果获取;通知只是一种优化,客户端始终可用 runId 补查。

同步预算应逐层分配:例如用户目标 8 秒,入口与排队 0.5 秒、检索 1 秒、模型 5 秒、工具 1 秒、余量 0.5 秒。若某步骤剩余预算不足,不应继续盲目调用。异步也要有完成 SLO 和最大生存时间,不能成为无限等待的借口。

面试追问: WebSocket 能否解决长任务问题?它改善双向交互,不提供持久状态、重试或断线恢复语义。

常见错误回答: “所有 Agent 都异步”或“流式输出就是同步成功”;要依据产品契约分层。

Q3:如何设计可恢复的 Run 状态机?

核心回答: 把状态和转换持久化,转换带版本号并通过 compare-and-set 保证单一有效推进;每一步记录输入摘要、尝试次数、结果引用和下一可执行状态。worker 崩溃后从最后已提交检查点恢复,而不是重新播放全部 prompt。

典型状态为 PENDING → RUNNING → WAITING_APPROVAL/RETRY_WAIT → SUCCEEDED/FAILED/CANCELLED,终态不可逆。step 可有 READY/RUNNING/SUCCEEDED/FAILED。领取任务使用 lease,lease 过期可被其他 worker 接管;原 worker 恢复后因版本或 fencing token 过期不能再提交。

检查点只在外部副作用安全落地后推进。若模型结果很大,状态表存对象引用和摘要,不存无限正文。状态转换与 outbox 事件同本地事务提交,保证“状态可见”与“待发布事件”一致。

面试追问: worker 执行超时,能否立刻把任务交给另一个 worker?可能造成并发执行;要依赖幂等、lease/fencing 和下游约束。

常见错误回答: “把完整对话存在 Redis 就能恢复”;对话不是显式状态机,也未定义副作用提交点。

Q4:队列的至少一次投递语义意味着什么?

核心回答: 至少一次表示系统倾向于不丢消息,但同一消息可能被重复交付。消费者处理成功后在 ack 前崩溃、ack 丢失、可见性超时或 rebalance 都会导致重投,因此业务必须设计幂等;队列本身不会自动给跨数据库和外部 API 的 exactly-once。

消费者流程通常是:解析消息;检查幂等记录;领取执行权;调用步骤;持久化结果与状态;确认消息。确认过早可能丢任务,确认过晚会增加重复。重试区分瞬时和永久错误,采用指数退避、抖动、最大次数;永久错误进入 DLQ 并携带错误分类和原消息引用。

Kafka 等平台的事务/幂等生产能力有明确边界,尤其当副作用发生在外部数据库或 SaaS 时,仍需业务幂等和协调。参考 Kafka Design:Delivery Semantics 时,应说明具体生产者、broker、消费者和外部系统的边界,而不是泛化为“开启 exactly-once 即可”。

面试追问: at-most-once 是否无需幂等?它可能丢消息,且客户端重试仍可能重复创建任务;请求入口同样需要幂等。

常见错误回答: “消息有唯一 ID,所以不会重复”;唯一 ID 只有在消费者持久化并原子检查时才有用。

Q5:Agent 工具调用如何实现幂等?

核心回答: 幂等键应代表同一个业务意图,常由 tenantId + runId + stepId + operation + canonicalParameterDigest 组成;服务端原子创建执行记录,重复请求返回已完成结果或明确的进行中状态。键、参数摘要、状态和结果必须持久化。

仅在调用前 SELECT 再执行会有竞态。可用唯一约束插入 IN_PROGRESS 记录,由一个执行者获胜;完成后写 SUCCEEDED + resultRef。若进程在外部调用成功、结果落库前崩溃,仍存在“不知道是否成功”的窗口,必须查询外部平台的幂等键/业务状态,或进入人工对账,不能盲目重试。

幂等有生命周期:保留期短于上游最大重试时间会再次执行;参数变化不能复用旧结果。读操作天然更接近幂等,但仍要考虑计费、速率和审计副作用。退款、发信等外部平台最好原生支持 idempotency key。

面试追问: runId 单独做幂等键有什么问题?一个 run 可有多个操作或重规划;会误把不同步骤当重复,也无法发现相同步骤参数变化。

常见错误回答: “用 Redis SETNX 就保证 exactly-once”;锁过期、结果持久化和外部副作用窗口仍未解决。

Q6:Transactional Outbox 解决什么问题?不能解决什么?

核心回答: Outbox 解决“同一个本地数据库事务中的业务状态变更与待发布消息记录一致”问题:事务同时写业务表和 outbox,relay 再把 outbox 发布到 broker。它避免业务提交后进程崩溃导致消息永久丢失。

Relay 发布成功但标记已发布前崩溃会重复发送,所以消费者仍要幂等。多个 relay 需要锁、分片或 SKIP LOCKED 类机制;outbox 要有唯一事件 ID、聚合版本、重试和归档。按聚合有序时,需要分区键和版本检查。

Outbox 不能把模型调用、HTTP 工具、消息 broker 和数据库变成一个原子事务,也不能自动撤销外部副作用。跨服务流程依赖幂等、状态机、补偿、对账和人工处置。对于不可逆操作,应先在本地记录意图,再调用支持幂等查询的领域接口。

面试追问: CDC 与轮询 relay 如何选?CDC 延迟低且减少轮询,但运维复杂、依赖日志格式;轮询简单但要处理扫描、锁和延迟。

常见错误回答: “用了 Outbox 就是 exactly-once”。

Q7:模型路由器应依据哪些维度?如何避免路由失控?

核心回答: 路由维度包括任务能力、质量门槛、结构化输出/工具兼容、上下文长度、数据驻留、隐私、延迟、价格、配额和可用性。规则必须版本化、可解释、可回放,并受到每请求/用户/租户预算约束。

先做硬约束过滤:区域、数据分类、工具能力和上下文;再在合格候选中按质量、延迟和成本选。简单分类或抽取可走小模型,高风险推理走经过验证的模型。动态路由可学习,但必须保留稳定基线、探索上限和离线/线上门禁,避免反馈偏差。

Fallback 不是换个模型名:新模型可能不支持相同 schema、工具调用方式、语言质量或安全策略,也可能违反数据驻留。每条回退路径独立评测,记录原因,并限制多级回退造成的延迟与成本放大。

面试追问: 如何衡量路由收益?比较相同任务切片的任务成功率、P95、每成功任务成本和安全失败,不只看 token 单价。

常见错误回答: “优先最便宜,失败再用最强模型”;重试可能让总成本和延迟更高。

Q8:如何设计超时、重试、熔断和隔离舱?

核心回答: 超时限制单次资源占用,重试处理瞬时故障,熔断快速拒绝持续失败依赖,隔离舱限制一个依赖耗尽全部线程/连接/配额。四者协同且受端到端 deadline 约束。

每次调用使用剩余 deadline,连接/首字节/总调用可分别超时。只重试幂等或有幂等键的操作,对 429、部分 5xx 和网络瞬断采用指数退避加随机抖动,尊重 Retry-After,设置最大尝试与重试预算。熔断器按依赖与操作分桶,open 后走明确降级,half-open 只放少量探针。

Java worker 要分离模型、检索和工具线程池/并发信号量;Java 21 虚拟线程降低阻塞线程成本,但不会增加下游连接池、模型配额或数据库吞吐,仍需限并发。重试应在一个层次统一协调,避免 SDK、网关和 worker 三层各重试三次形成放大。

面试追问: 熔断和限流有什么区别?限流控制请求速率/并发;熔断根据依赖失败状态临时拒绝调用。

常见错误回答: “超时后无限重试直到成功”;会制造重试风暴并耗尽预算。

Q9:Agent 可以怎样降级?如何确保降级不破坏安全?

核心回答: 预定义按任务类型的降级矩阵:返回排队状态、使用已验证小模型、只提供检索证据、关闭写工具、返回缓存的公开结果、转人工或明确失败。降级必须保持授权、schema、审批和数据驻留要求,不得为了可用性扩大权限。

模型不可用时,面向查询可退化为搜索结果与来源;写任务可保存草稿但暂停提交;检索不可用时不能让模型凭空生成高风险答案;审批不可用时高风险操作 fail closed。缓存降级要标注时效并检查权限版本。

所有降级路径要有独立 SLI 和演练。用户应知道结果是延迟、部分完成还是质量降低,不能静默返回看似完整的幻觉。恢复时逐步关闭降级,避免积压瞬间压垮依赖。

面试追问: 安全策略服务故障时能否使用最近缓存策略?只在明确设计的低风险、短 TTL 场景;高风险默认拒绝。

常见错误回答: “主模型失败就自动换任意供应商”;可能违反驻留与兼容约束。

Q10:如何规划 Agent 系统容量?

核心回答: 按任务类型分阶段估算到达率、服务时间、并发、扇出、重试放大、token 吞吐、队列积压和外部配额。Little 定律 L = λW 可估平均在途数,但不能替代 P95/P99、突发和重尾建模。

例:峰值 50 run/s,每 run 平均 3 次模型调用,每次平均 2.5 秒,则仅模型阶段平均在途调用约 50 × 3 × 2.5 = 375;若 P95 为 8 秒、重试率 10%,峰值并发会更高。每 run 平均 6,000 token,则需求约 300,000 token/s,还要与供应商每分钟配额比较。工具扇出和审批等待应独立计算,等待审批的 run 占存储但不应长期占 worker 线程。

队列容量以可承受突发和恢复时间估算。若到达 60 run/s、稳定处理 50 run/s,10 分钟会积压 6,000 run;故障恢复后若仍只处理 50/s,永远清不掉,必须有恢复冗余或入口限流。容量计划还包括数据库 IOPS、outbox 扫描、trace 量、对象存储和成本上限。

面试追问: 虚拟线程是否让并发容量无限?不。瓶颈转移到内存、连接池、CPU、下游配额和费用。

常见错误回答: “机器 CPU 还有余量,所以可继续扩”;Agent 常受外部配额和等待时间约束。

Q11:如何为 Agent 制定 SLI、SLO 与错误预算?

核心回答: 从用户旅程定义合格事件:在时限内、满足任务验收、没有违反策略并到达正确终态。交互和异步任务分别制定 SLO;组件指标用于诊断,不能代替端到端目标。

示例:28 天窗口内 99% 的交互查询在 8 秒内返回合格结果;99.5% 的异步 run 在 10 分钟内到达终态;高风险副作用必须具备有效授权与审批。安全事故通常是发布阻断条件,不应用普通可用性错误预算抵消。

按 Google SRE 的 Implementing SLOs方法,错误预算驱动发布节奏;多窗口 burn-rate 告警发现快速或持续消耗。还要观察队列年龄、run 成功率、P95/P99、模型/工具错误、重试放大、预算终止、幂等命中和人工接管。

面试追问: 第三方模型 SLA 低于产品 SLO 怎么办?通过多区域/多供应商、缓存、降级或调整产品目标弥合;不能数学上承诺无法支撑的 SLO。

常见错误回答: “所有组件都 99.9%,整体也 99.9%”;串联系统通常更低,且质量失败未被计算。

Q12:缓存如何设计,才不会造成权限泄露和错误复用?

核心回答: 区分确定性工具缓存、检索缓存、prompt/response 缓存和语义缓存;key 至少包含模型、prompt、工具、索引、数据版本、租户/主体或权限版本,以及影响结果的参数。敏感结果默认不跨主体共享。

缓存前先判断可缓存性、数据分类、时效和删除要求。结构化工具结果可依据领域事件主动失效;RAG 结果绑定索引版本;模型响应缓存仅用于可接受旧结果且输入规范化稳定的任务。语义缓存可能把相似但权限或意图不同的请求混淆,应谨慎用于高风险操作。

缓存命中也要执行当前授权,不能把过去有权读取的结果直接交给权限已撤销的用户。缓存降级应返回数据年龄。防缓存击穿可使用单飞、随机 TTL 和限流,但锁不能无限等待。

面试追问: 温度为 0 的模型输出就一定适合长期缓存吗?不一定,模型服务、检索数据、系统提示和政策都可能变化。

常见错误回答: “prompt 文本相同就用同一 key”;忽略租户、版本和外部状态。

Q13:系统设计题——设计一个每秒 50 个任务、支持写工具的企业 Agent 平台

核心回答: API 网关完成认证、租户配额和请求幂等;短查询走有 deadline 的同步路径,长任务事务性创建 run 与 outbox 后返回 runId。Relay 投递到按任务/租户分区的队列,worker 用 lease 与状态机推进;模型路由先过滤数据驻留和能力,再按质量/成本选;写工具经过网关授权、参数校验、幂等和审批,最终由领域服务提交。

存储层包括关系数据库中的 run/step/idempotency/outbox,针对大输入输出的对象存储,以及独立审计库。队列采用至少一次语义;消费者去重。不可确定外部副作用通过平台幂等键与查询 API 对账。模型、工具分别隔离并发;所有调用继承 run deadline 和预算。DLQ 只接受永久失败或耗尽重试任务,重放前修复原因并保留原事件 ID。

按平均每 run 3 次模型调用、2.5 秒计算,模型阶段平均并发 375;若供应商限制 240,000 token/s,而需求 300,000 token/s,必须通过限流、压缩上下文、缓存、增加合格供应商容量或降低入口目标,不能只加 worker。假设每 run 产生 15 个 1.5 KiB spans,50 run/s 约产生 1.1 MiB/s 原始 span 数据,另计索引与副本。

SLO 分交互与异步;错误预算快速消耗时停止模型/prompt 扩量。模型故障时查询降级为证据列表,写操作暂停或转人工。运维演练重复消息、DB 提交后 worker 崩溃、供应商 429、审批超时、索引不可用和区域故障。

面试追问: 如何防止大租户饿死小租户?入口令牌桶、按租户队列/权重公平调度、并发上限和预算隔离。

常见错误回答: “扩容消费者即可”;外部 token 配额、数据库和工具容量仍可能是瓶颈。

Q14:事故排查题——队列积压上升且出现重复退款,如何处理?

核心回答: 先停止高风险消费者或切换为只读/人工,阻止新增重复副作用;保护证据并确认退款平台的真实状态。队列积压与重复退款可能共同源于依赖变慢、可见性超时过短、ack 时机、worker 崩溃或幂等记录窗口。

查看队列 oldest age、投递次数、消费耗时、lease/可见性超时、重试率和退款 API 延迟;按 idempotency key 对账业务库、执行记录、平台交易和 outbox。若外部调用成功但本地记录失败,重复投递会再次调用;应查询平台同一幂等键结果,而不是继续退款。

处置积压时不要直接无限扩 worker,否则会压垮退款服务。先限流、延长合理可见性、关闭重复的多层重试、打开熔断,再按下游可承受速率排空。对确认重复的交易走业务补偿和客户处理。修复后增加唯一约束、原子领取、外部幂等键、状态查询与事故回归。

面试追问: 能否删除重复消息解决?消息无法单独证明副作用是否发生;必须以业务状态和外部交易为准。

常见错误回答: “Kafka/队列保证 exactly-once,不可能重复”;跨外部退款 API 不在其事务边界。

Java 21 完整示例:至少一次消费下的幂等步骤执行器

示例仅用 Java 21 标准库,模拟队列重复投递时由 ConcurrentHashMap.compute 原子领取同一幂等键。外部平台通过 ExternalTool 接口抽象。状态容量约为有效幂等键数 O(n);单次查找平均 O(1)。该内存实现只适合单 JVM 教学,生产必须用数据库唯一约束/事务或具备一致性的共享存储,并让外部平台接受同一 idempotency key。

JAVA
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

public final class AtLeastOnceIdempotencyExample {
    record StepMessage(
            String tenantId,
            String runId,
            String stepId,
            String operation,
            String parameterDigest) {
        StepMessage {
            Objects.requireNonNull(tenantId);
            Objects.requireNonNull(runId);
            Objects.requireNonNull(stepId);
            Objects.requireNonNull(operation);
            Objects.requireNonNull(parameterDigest);
        }

        String idempotencyKey() {
            try {
                MessageDigest digest = MessageDigest.getInstance("SHA-256");
                for (String field : new String[] {
                        tenantId, runId, stepId, operation, parameterDigest }) {
                    byte[] bytes = field.getBytes(StandardCharsets.UTF_8);
                    digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(bytes.length).array());
                    digest.update(bytes);
                }
                return Base64.getUrlEncoder().withoutPadding()
                        .encodeToString(digest.digest());
            } catch (NoSuchAlgorithmException impossible) {
                throw new AssertionError("SHA-256 is required by the JDK", impossible);
            }
        }
    }

    enum Status { IN_PROGRESS, SUCCEEDED, FAILED_RETRYABLE, FAILED_PERMANENT }

    record Execution(Status status, String result, Instant updatedAt, int attempts) {}

    interface ExternalTool {
        // 真实外部平台应持久识别 idempotencyKey,并支持查询未知结果。
        String invoke(String idempotencyKey, StepMessage message) throws RetryableToolException;
    }

    static final class RetryableToolException extends Exception {
        RetryableToolException(String message) {
            super(message);
        }
    }

    static final class IdempotentStepExecutor {
        private final Map<String, Execution> executions = new ConcurrentHashMap<>();
        private final ExternalTool tool;
        private final Clock clock;
        private final Duration inProgressLease;

        IdempotentStepExecutor(ExternalTool tool, Clock clock, Duration inProgressLease) {
            this.tool = Objects.requireNonNull(tool);
            this.clock = Objects.requireNonNull(clock);
            this.inProgressLease = Objects.requireNonNull(inProgressLease);
            if (inProgressLease.isZero() || inProgressLease.isNegative()) {
                throw new IllegalArgumentException("inProgressLease must be positive");
            }
        }

        Execution handle(StepMessage message) {
            String key = message.idempotencyKey();
            final boolean[] owner = {false};
            Execution claimed = executions.compute(key, (ignored, current) -> {
                Instant now = clock.instant();
                if (current == null
                        || current.status() == Status.FAILED_RETRYABLE
                        || (current.status() == Status.IN_PROGRESS
                        && current.updatedAt().plus(inProgressLease).isBefore(now))) {
                    owner[0] = true;
                    int attempts = current == null ? 1 : current.attempts() + 1;
                    return new Execution(Status.IN_PROGRESS, null, now, attempts);
                }
                return current;
            });

            if (!owner[0]) {
                // 重复消息返回既有结果;IN_PROGRESS 时调用方应稍后重试/续租。
                return claimed;
            }

            try {
                String result = tool.invoke(key, message);
                return complete(key, claimed, Status.SUCCEEDED, result);
            } catch (RetryableToolException error) {
                return complete(key, claimed, Status.FAILED_RETRYABLE, error.getMessage());
            } catch (RuntimeException error) {
                return complete(key, claimed, Status.FAILED_PERMANENT, error.getMessage());
            }
        }

        private Execution complete(
                String key, Execution claimed, Status status, String result) {
            return executions.compute(key, (ignored, current) -> {
                // attempts 是单调 fencing token;过期 owner 不能覆盖新 attempt。
                if (current.status() != Status.IN_PROGRESS
                        || current.attempts() != claimed.attempts()) {
                    return current;
                }
                return new Execution(status, result, clock.instant(), current.attempts());
            });
        }
    }

    public static void main(String[] args) {
        Map<String, String> externalResults = new ConcurrentHashMap<>();
        ExternalTool refundPlatform = (key, message) -> externalResults.computeIfAbsent(
                key, ignored -> "refund-transaction-9001");

        IdempotentStepExecutor executor = new IdempotentStepExecutor(
                refundPlatform, Clock.systemUTC(), Duration.ofSeconds(30));
        StepMessage message = new StepMessage(
                "tenant-a", "run-42", "step-3", "refund", "sha256:abc123");

        Execution firstDelivery = executor.handle(message);
        Execution duplicateDelivery = executor.handle(message);
        System.out.println(firstDelivery);
        System.out.println(duplicateDelivery);
        System.out.println("external invocation count = " + externalResults.size());
    }
}

这个示例刻意暴露了工程边界:进程可能在外部工具成功后、本地状态写成功前崩溃。生产方案要让外部平台持久支持同一 key 并可查询结果,或通过领域对账进入 UNKNOWN/人工状态;不能仅靠 JVM 内存把它描述成 exactly-once。

速记总结

  • 同步路径受交互 deadline 约束;长任务创建持久 run,连接不是状态容器。
  • run/step 用显式状态机、检查点、lease/fencing 和终态约束恢复。
  • 至少一次队列一定要假设重复;唯一消息 ID 不等于业务幂等。
  • 幂等键绑定业务意图和参数摘要;未知副作用先查状态,不盲目重试。
  • Outbox 保证本地业务状态与待发布记录同事务,不保证跨系统原子性或 exactly-once。
  • 路由先满足驻留、权限、能力和 schema 等硬约束,再优化质量、延迟和成本。
  • 超时、有限重试、熔断、隔离舱和降级共同防止级联故障;虚拟线程不消除下游容量限制。
  • 容量按任务阶段、长尾、扇出、重试、token 与外部配额估算;错误预算驱动发布。

参考资料