智能工作流并发上来先守住哪条线
把大模型接进单据、客服或审核流程后,请求数不再是唯一的压力指标。同样数量的请求,输入长度、输出上限、工具调用和供应商配额都可能不同。系统在高峰时失控,常见原因不是某一处限流器写错,而是入口、队列、worker 和外部模型之间没有统一的容量边界。
第一步是画出请求会经过的路径:上传或提交、预处理、模型调用、规则校验、人工复核、落库。每一段要有可接受的等待时间和队列容量。容量不是越大越好;无界内存队列只是把拒绝延后到进程内存耗尽时发生。队列满时应明确选择:立即拒绝、延后处理、仅接收高优先级任务,或退回不依赖模型的流程。
限流要同时看请求和工作量
供应商可能按请求数、输入或输出 token、并发连接以及不同模型分别限制。业务侧也有自己的成本和公平性要求。入口可以根据已知输入和输出上限做保守估算,但字符数不能准确换算 token,尤其是多语言文本、图片和工具上下文。估算是调度信号,不应被当成计费事实。
实际用量返回后,应当把它记录到任务账本中,用于调整后续预算和报警。为了避免某个租户占满全部能力,限额至少分为全局、租户和单任务三层。重要的是拒绝响应要可操作:告诉客户端是稍后重试、进入队列,还是需要缩小输入;不要把所有情况都伪装成普通 500 错误。
type Admission struct { EstimatedUnits int64 Priority string } func decide(admission Admission, available int64, queueFull bool) string { if admission.EstimatedUnits > available { return "queue_or_reject" } if queueFull && admission.Priority != "high" { return "reject" } return "accept" }这个例子没有实现令牌桶,也没有替代供应商返回的限流信息。生产实现还要考虑并发安全、时钟、额度刷新、任务取消和多实例共享状态。把决策和执行拆开,至少能让拒绝原因更容易测试和观察。
异步队列要有状态和幂等性
对于不要求立刻完成的单据处理,提交接口可以先校验基础字段并创建任务,再由 worker 按容量拉取。客户端拿到任务 ID 后查询状态或订阅事件。这样能够吸收短时波动,但前提是任务有清楚的生命周期,例如queued、running、succeeded、failed、cancelled,而不是只留下“正在处理”。
重试同样要沿着状态机设计。网络超时不等于模型没有执行,写入业务系统的操作也可能已经成功。对每次提交使用幂等键,保存外部调用标识;重试前查询已有结果,必要时由人工处理不确定状态。只有确认可安全重试的故障才使用指数退避和随机抖动,权限错误、输入校验错误通常不应重试。
降级要保持业务语义诚实
模型不可用时,很多流程可以退回规则校验、人工队列或稍后处理。但降级结果必须让用户和后续系统知道其依据变了。例如,“已完成智能核验”和“已完成基础字段校验,等待复核”是不同状态,不能用同一个成功标记混过去。对于高风险决策,降级到更保守的结果往往比自动放行更合适。
预处理阶段也值得单独限额。大图片、压缩包或视频解码可能在模型调用前就耗掉大量内存和 CPU;应限制文件类型和大小,把重计算放到受控 worker,并为任务设置整个链路的截止时间。
用压测结果指导阈值
压测应模拟输入长度分布、真实的外部错误比例、重复提交和 worker 慢启动,而不是只发送同一种小请求。观察排队时间、拒绝率、端到端延迟、实际 token 用量、内存和重试次数,并按租户与工作流版本拆分。若某次提示词更新让平均输入变长,单看请求数可能完全看不出容量正在下降。
并发增长前最该守住的是准入边界:系统要知道哪些请求可以立即开始、哪些需要等待、哪些必须被拒绝或转交。队列、限流和降级围绕这条边界协作,才能避免一次上游波动扩散成整条工作流的故障。