Stage 到底是什么?如何用业务标签治理复杂请求
在复杂系统中,我们经常会遇到这样的情况:
同一套技术基础设施,需要处理多种性质完全不同的业务请求。
例如一套 AI 调用系统,可能同时承担:
- 内容评分;
- 问题生成;
- 文档抽取;
- 图像分析;
- 报告生成。
它们底层可能都在调用大模型,但业务目的、执行时间、缓存策略、超时配置和故障处理方式都不一样。
如果把所有请求都当成同一种任务处理,系统很快就会出现:
- 不同业务错误复用结果;
- 超时参数不合理;
- 长任务被过早接管;
- 短任务卡住后迟迟不能恢复;
- 日志和监控混在一起;
- 无法针对某类请求单独治理。
这时候就需要引入一个重要概念:
Stage
一句话总结
Stage 是请求的业务类别标签,用来回答“当前这次调用究竟在做什么”。
它通常承担四个职责:
区分业务
+ 隔离请求 Key
+ 选择治理策略
+ 支持监控追踪
Stage 不是线程执行阶段,也不是 RUNNING、FAILED 这样的运行状态。
可以用一句话记住:
Stage 表示“在做什么”,Status 表示“现在怎么样”,Step 表示“做到哪一步”。
一、先理解 Stage 是什么
假设系统中存在四类 AI 请求:
interview-evaluation
interview-followup
interview-extraction
interview-demeanor
它们分别表示:
| Stage | 业务含义 |
|---|---|
interview-evaluation |
对面试回答进行评分 |
interview-followup |
生成后续追问 |
interview-extraction |
从简历中抽取问题 |
interview-demeanor |
分析面试者神态 |
这些字符串不是普通名称,而是在告诉系统:
当前这一次 AI 调用属于哪一种业务。
例如:
{
"stage": "interview-evaluation"
}
表示这是一次评分任务。
而:
{
"stage": "interview-extraction"
}
表示这是一次简历抽题任务。
虽然它们底层都调用 AI,但业务语义完全不同。
二、Stage、Status 和 Step 有什么区别
这是最容易混淆的地方。
Stage:任务是什么
例如:
interview-evaluation
表示这是一个面试评分任务。
Status:任务现在是什么状态
例如:
PENDING
RUNNING
SUCCEEDED
FAILED
TIMEOUT
表示任务当前是否正在执行、已经成功或已经失败。
Step:任务内部执行到哪里
例如:
LOAD_CONTEXT
BUILD_PROMPT
CALL_AI
PARSE_RESULT
SAVE_RESULT
表示任务当前执行到具体哪一步。
三者可以同时存在:
{
"stage": "interview-evaluation",
"status": "RUNNING",
"currentStep": "CALL_AI"
}
这段状态的完整含义是:
当前正在执行一个面试评分任务,并且已经进入调用 AI 的步骤。
因此,可以用下面这组问题区分:
Stage:这是什么任务?
Status:任务现在怎么样?
Step:任务执行到哪里?
三、为什么需要 Stage
Stage 最核心的价值,不是让字段看起来更规范,而是让系统具备按业务治理的能力。
1. 隔离不同业务的请求 Key
Single-flight 要判断两个请求是不是同一个任务,就必须为请求生成唯一 Key。
假设同一个用户会话 ID 是:
sessionA
这个会话可能同时触发:
- 面试评分;
- 追问生成;
- 简历抽题;
- 神态分析。
如果 Key 只使用:
sessionA
系统就无法区分这些任务。
可能出现:
评分请求:
key = sessionA
简历抽题:
key = sessionA
Single-flight 看到第二个请求后,可能错误地认为:
已经有相同任务正在执行,直接等待已有结果。
最后,简历抽题请求可能拿到评分结果。
这就是典型的业务语义冲突。
加入 Stage 后:
interview-evaluation|sessionA
interview-extraction|sessionA
两个 Key 自然属于不同命名空间:
interview-evaluation|sessionA
≠
interview-extraction|sessionA
即使业务 ID 相同,只要 Stage 不同,它们就不会互相复用。
因此,Stage 是请求 Key 的第一层业务隔离。
四、一个更完整的请求 Key 应该怎么设计
在实际系统中,Key 通常不应该只有 Stage 和业务 ID。
可以采用如下结构:
stage | tenantId | businessId | requestVersion | requestHash
例如:
interview-evaluation
| tenant-1001
| session-A
| v2
| 7f3a91
拼接后可以表示为:
interview-evaluation:tenant-1001:session-A:v2:7f3a91
各字段分别承担不同职责:
| 字段 | 作用 |
|---|---|
stage |
区分业务类别 |
tenantId |
防止不同租户错误复用 |
businessId |
标识具体业务对象 |
requestVersion |
隔离不同业务规则或 Prompt 版本 |
requestHash |
判断输入内容是否完全相同 |
例如,同一个会话重新回答了问题:
第一次答案:requestHash = abc
第二次答案:requestHash = xyz
即使 Stage 和 Session 相同,只要输入不同,也不应该共享结果。
五、Stage 的第二个作用:选择不同治理策略
不同业务任务通常不能使用同一套参数。
例如,评分任务可能具备以下特点:
高频
执行较快
结果较小
短时间重复请求较多
简历抽题可能具备:
输入长
执行时间长
结果较大
失败成本高
神态分析可能具备:
计算量大
数据实时性强
结果不适合长期缓存
如果所有业务都使用一套统一配置:
runningTtl = 30s
waitTimeout = 35s
resultTtl = 60s
就可能出现问题。
例如简历抽题正常需要 60 秒,但运行租约只有 30 秒:
任务仍在正常执行
→ 租约先过期
→ 其他节点接管
→ 同一个任务被重复执行
因此,可以根据 Stage 加载不同策略:
single-flight:
stages:
interview-evaluation:
running-ttl: 30s
heartbeat-interval: 5s
wait-timeout: 35s
result-ttl: 60s
local-cache-enabled: true
takeover-enabled: true
interview-followup:
running-ttl: 45s
heartbeat-interval: 5s
wait-timeout: 50s
result-ttl: 2m
local-cache-enabled: true
takeover-enabled: true
interview-extraction:
running-ttl: 120s
heartbeat-interval: 10s
wait-timeout: 130s
result-ttl: 10m
local-cache-enabled: true
takeover-enabled: true
interview-demeanor:
running-ttl: 180s
heartbeat-interval: 15s
wait-timeout: 190s
result-ttl: 30s
local-cache-enabled: false
takeover-enabled: false
此时 Stage 就相当于一个策略路由键:
stage
↓
找到对应的策略
↓
决定这次任务如何执行和治理
六、Stage 通常可以决定哪些参数
Stage 可以参与决定:
- 运行租约时间;
- 心跳间隔;
- 最大等待时间;
- 结果保存时间;
- 是否开启本地缓存;
- 是否允许短期结果回放;
- 是否允许故障接管;
- 最大并发数;
- 限流阈值;
- 失败重试次数;
- 是否压缩结果;
- 是否允许本地降级;
- 业务无进展超时时间;
- 日志等级;
- 告警规则。
可以把它理解为:
同一套执行引擎,根据不同 Stage 切换不同驾驶模式。
七、为什么不能所有请求共用一套策略
假设系统中有三种任务:
| 任务 | 正常耗时 | 结果大小 | 重复概率 |
|---|---|---|---|
| 内容评分 | 5 秒 | 小 | 高 |
| 长报告生成 | 90 秒 | 大 | 中 |
| 图片分析 | 30 秒 | 很大 | 低 |
如果全部配置 20 秒超时:
内容评分:比较合适
长报告:必然误超时
图片分析:可能频繁误接管
如果全部配置 180 秒超时:
长报告:正常
内容评分:卡死后要等很久才能恢复
图片分析:异常发现过慢
所以统一配置看似简单,实际上会迫使所有业务向最慢任务妥协。
Stage 的意义就在于:
不同业务
使用不同的治理参数
八、Stage 的第三个作用:监控和排查问题
假设系统只统计:
singleflight_timeout_total = 100
我们只能知道:
系统发生了 100 次 Single-flight 超时。
但不知道是哪类任务。
加上 Stage 后:
stage=interview-evaluation
timeout=8
stage=interview-followup
timeout=4
stage=interview-extraction
timeout=82
stage=interview-demeanor
timeout=6
问题马上变得清晰:
简历抽题链路的超时明显异常。
我们还可以按 Stage 统计:
执行次数
平均执行时间
P95/P99 延迟
Single-flight 命中率
Owner 数量
Follower 数量
接管次数
等待超时次数
失败率
结果回放次数
本地降级次数
例如:
interview-evaluation:
平均耗时 7 秒
Single-flight 命中率 42%
接管次数 1
interview-extraction:
平均耗时 68 秒
Single-flight 命中率 12%
接管次数 35
这可能意味着:
- 抽题任务的租约太短;
- 抽题调用经常卡住;
- 下游 AI 接口不稳定;
- 接管阈值配置不合理;
- 该 Stage 需要单独限流或扩容。
如果没有 Stage,所有 AI 调用混在一起,后续几乎无法做细粒度治理。
九、Stage 的第四个作用:日志追踪
日志中可以加入 Stage:
stage=interview-extraction
requestKey=resume-1001
ownerId=node-a
ownerToken=18
status=RUNNING
currentStep=CALL_AI
出现故障时,可以直接查询:
所有 interview-extraction 的超时日志
或者:
interview-evaluation 最近的接管记录
一条完整日志可能是:
[WARN]
stage=interview-extraction
requestKey=resume-1001
ownerId=node-a
ownerToken=18
currentStep=CALL_AI
action=STOP_RENEW
reason=NO_PROGRESS_TIMEOUT
这比下面这种日志有用得多:
task timeout, key=7f92a1
后者只能看到一个 Key,无法快速判断它属于哪种业务。
十、Stage 不只是 AI 系统里的概念
Stage 本质上是一种通用业务分类标签。
在支付系统中,可以定义:
payment
refund
withdraw
settlement
在订单系统中,可以定义:
order-create
order-cancel
order-close
order-refund
在文件处理系统中,可以定义:
file-upload
file-convert
file-compress
file-ocr
在内容平台中,可以定义:
article-summary
article-audit
article-translation
article-tagging
在图片系统中,可以定义:
image-resize
image-compress
image-ocr
image-generation
这些任务可能共用同一套任务执行框架,但分别需要不同的:
- Key;
- 超时;
- 缓存;
- 限流;
- 重试;
- 告警;
- 线程池;
- 结果保存策略。
因此,Stage 是一种非常通用的架构思想:
使用稳定的业务类别,把统一技术能力拆成可独立治理的请求空间。
十一、Stage 和内部步骤不能混为一谈
Stage 不应该细化到每个代码步骤。
例如下面这些内容:
evaluation-load-user
evaluation-load-question
evaluation-build-prompt
evaluation-call-ai
evaluation-save-result
更适合定义为:
currentStep
而不是 Stage。
正确结构应该是:
{
"stage": "interview-evaluation",
"currentStep": "BUILD_PROMPT",
"status": "RUNNING"
}
其中:
Stage
表示这是面试评分任务
CurrentStep
表示任务正在构建 Prompt
Status
表示任务当前仍在运行
如果把每一个步骤都定义成 Stage,会产生几个问题:
- Stage 数量爆炸;
- 配置难以维护;
- 监控标签基数过高;
- Key 规则不稳定;
- 业务分类和执行流程混乱;
- 后续修改步骤时影响缓存和统计。
十二、如何判断一个概念应该是 Stage 还是 Step
可以问两个问题。
问题一:它是否代表一个独立业务目的?
例如:
生成追问
这是一个独立业务目的,适合做 Stage。
而:
查询用户信息
只是任务内部的一个步骤,适合做 Step。
问题二:它是否需要独立治理策略?
例如“简历抽题”需要:
- 更长超时;
- 更长结果 TTL;
- 更低并发;
- 不同告警。
那么它适合成为独立 Stage。
如果两个动作始终使用同一套策略,只是执行流程中的不同步骤,通常不需要拆成两个 Stage。
十三、Stage 应该如何命名
一个好的 Stage 名称应该满足以下特点。
1. 能看出业务含义
推荐:
interview-evaluation
resume-extraction
report-generation
不推荐:
task-a
method-1
service-call-2
2. 尽量稳定
Stage 一旦参与:
- Redis Key;
- 缓存 Key;
- 指标标签;
- 日志查询;
- 配置路径;
频繁改名会造成:
- 新旧结果无法共享;
- 历史监控断层;
- 配置失效;
- Redis 残留数据;
- 排查困难。
3. 数量要可控
不要为每个接口、每种参数都新建 Stage。
Stage 应该表达稳定的业务大类,参数差异由请求 Key 或策略配置处理。
4. 使用统一格式
可以统一采用:
领域-动作
例如:
interview-evaluation
interview-followup
resume-extraction
report-generation
这样比单独使用:
evaluation
generation
extraction
更加清晰,也不容易跨领域冲突。
十四、什么时候需要给 Stage 加版本
如果业务语义发生重大变化,旧结果不应继续复用,就需要加入版本。
例如评分 Prompt 从 V1 升级到 V2:
interview-evaluation:v1
interview-evaluation:v2
不过通常更推荐 Stage 保持稳定,把版本单独放到 Key:
interview-evaluation
| prompt-v2
| sessionA
| requestHash
原因是:
- Stage 仍然代表同一业务;
- 监控数据可以继续聚合;
- 不同版本又不会错误复用结果。
最终 Key 可以是:
interview-evaluation:prompt-v2:sessionA:7f29a1
十五、一个请求如何完整使用 Stage
假设系统收到一个评分请求:
stage = interview-evaluation
sessionId = sessionA
tenantId = tenant100
promptVersion = v2
answerHash = abc123
第一步,生成请求 Key:
interview-evaluation
:tenant100
:sessionA
:v2
:abc123
第二步,根据 Stage 加载策略:
runningTtl = 30s
heartbeatInterval = 5s
waitTimeout = 35s
resultTtl = 60s
localCacheEnabled = true
第三步,进入 Single-flight:
是否已有相同 Key 的任务?
如果有:
成为 Follower
→ 等待已有任务
→ 复用结果
如果没有:
成为 Owner
→ 执行 AI 评分
第四步,记录指标:
singleflight_owner_total{
stage="interview-evaluation"
}
第五步,输出日志:
stage=interview-evaluation
requestKey=...
owner=node-a
status=RUNNING
因此,Stage 会贯穿整条链路:
请求分类
→ Key 生成
→ 策略加载
→ 执行协调
→ 日志记录
→ 指标统计
→ 告警治理
十六、一个通用的代码结构
可以先定义 Stage:
public enum RequestStage {
INTERVIEW_EVALUATION("interview-evaluation"),
INTERVIEW_FOLLOWUP("interview-followup"),
INTERVIEW_EXTRACTION("interview-extraction"),
INTERVIEW_DEMEANOR("interview-demeanor");
private final String code;
RequestStage(String code) {
this.code = code;
}
public String code() {
return code;
}
}
然后定义策略:
public record StagePolicy(
Duration runningTtl,
Duration heartbeatInterval,
Duration waitTimeout,
Duration resultTtl,
boolean localCacheEnabled,
boolean takeoverEnabled
) {
}
通过 Stage 获取配置:
public StagePolicy getPolicy(RequestStage stage) {
StagePolicy policy = policies.get(stage);
if (policy == null) {
throw new IllegalArgumentException(
"No policy configured for stage: " + stage
);
}
return policy;
}
构建 Key:
public String buildKey(
RequestStage stage,
String tenantId,
String businessId,
String version,
String requestHash) {
return String.join(
":",
stage.code(),
tenantId,
businessId,
version,
requestHash
);
}
完整执行入口可以抽象成:
public <T> T execute(
RequestStage stage,
String businessId,
String requestHash,
Supplier<T> supplier) {
StagePolicy policy = policyRegistry.getPolicy(stage);
String requestKey = keyBuilder.build(
stage,
currentTenantId(),
businessId,
currentRequestVersion(),
requestHash
);
return singleFlight.execute(
stage,
requestKey,
policy,
supplier
);
}
这段结构最重要的不是代码,而是把职责分开:
Stage
负责表达业务类别
KeyBuilder
负责生成请求身份
PolicyRegistry
负责选择治理策略
SingleFlight
负责执行协调
十七、遇到类似业务时怎么判断要不要引入 Stage
以后再遇到类似需求,可以使用下面的判断清单。
第一问:同一套基础设施是否处理多种业务任务?
例如同一个:
- AI 调用框架;
- 文件处理框架;
- 任务调度器;
- 消息消费者;
- 缓存组件;
- 限流组件。
如果只处理一种任务,Stage 可能没有必要。
如果处理多种业务,就可以继续判断。
第二问:不同任务是否可能使用相同业务 ID?
例如同一个:
sessionId
orderId
userId
documentId
会触发不同业务操作。
如果会,就需要 Stage 隔离 Key 空间。
第三问:不同任务是否需要不同策略?
例如超时、缓存、并发、重试存在明显差异。
如果存在,就适合按 Stage 配置。
第四问:是否需要分别观察和告警?
例如需要知道:
到底是评分慢
还是报告生成慢
那就应该把 Stage 作为日志和指标标签。
只要以上问题中有两个以上回答“是”,引入 Stage 通常就很有价值。
十八、常见错误
错误一:Key 不带 Stage
key = sessionId
风险:
不同业务错误共享 Flight
正确方式:
key = stage + sessionId + requestHash
错误二:把 Status 当成 Stage
错误:
stage = RUNNING
RUNNING 只是状态,不能表示任务在做什么。
正确:
stage = interview-evaluation
status = RUNNING
错误三:把每个代码步骤都设计成 Stage
错误:
load-data
build-prompt
call-ai
save-result
它们是 Step,不是业务类别。
错误四:Stage 只是加进 Key,没有参与策略
如果 Stage 只用于字符串拼接,而所有业务仍然使用同一套参数,就只发挥了隔离作用,没有发挥治理价值。
错误五:Stage 数量无限增长
如果 Stage 根据:
- 用户 ID;
- 请求 ID;
- 模型参数;
- 临时实验名;
动态生成,会导致指标标签基数爆炸,并使配置无法维护。
Stage 应该是数量有限、稳定、可枚举的业务分类。
错误六:Stage 缺少默认处理
如果收到未知 Stage,不能静默使用一套随意配置。
更安全的方式是:
未知 Stage
→ 拒绝执行
或者:
使用明确的保守默认策略
+ 输出告警
十九、建议保留的实现清单
以后真正实现时,可以按下面顺序执行。
1. 盘点业务类型
列出当前基础设施处理的所有独立业务目的。
评分
追问
抽取
分析
报告生成
2. 定义有限的 Stage 枚举
保证:
- 业务语义清晰;
- 数量有限;
- 命名稳定;
- 可以被代码校验。
3. 设计 Key 结构
至少考虑:
Stage
租户
业务 ID
版本
输入哈希
4. 为每个 Stage 配置策略
重点配置:
运行 TTL
等待超时
心跳间隔
结果 TTL
本地缓存
最大并发
是否允许接管
5. 把 Stage 写入日志
保证排障时能看出是哪类任务。
6. 把 Stage 作为监控标签
但 Stage 必须是有限枚举,不能使用高基数字段。
7. 未知 Stage 快速暴露
不要让配置缺失悄悄被忽略。
8. 定期根据指标调整策略
利用:
平均值
P95
P99
超时率
接管率
Single-flight 命中率
持续优化每个 Stage 的参数。
二十、面试中怎么讲
可以这样回答:
在统一的任务执行框架中,我会使用 Stage 表示请求的业务类别。它不是运行状态,也不是任务内部步骤,而是回答当前请求究竟在做什么。Stage 首先参与 Single-flight Key 构建,用来隔离不同业务的请求空间,避免同一个业务 ID 下的评分、抽取或生成任务错误共享结果。其次,系统根据 Stage 加载独立的租约、超时、缓存、接管和限流策略,因为不同任务的执行时间和风险不同。最后,Stage 还会作为日志与监控标签,让我们能够分别观察每类任务的命中率、延迟、失败和接管情况。简单来说,Stage 同时承担业务分类、Key 隔离、策略路由和可观测性治理四个职责。
最终记忆口诀
以后遇到类似问题,可以记住四个字:
分、隔、配、观
分:业务分类
这次请求究竟在做什么?
隔:请求隔离
不同业务是否可能错误共享同一个 Key?
配:策略配置
不同业务是否需要不同超时、缓存和限流?
观:可观测性
出现故障时,能否知道是哪类任务出了问题?
只要一个字段同时帮助系统完成:
业务分类
请求隔离
策略选择
监控追踪
它就非常适合被设计成 Stage。
总结
Stage 不是一个为了让代码看起来高级而增加的字段。
它解决的是统一技术框架中的业务混杂问题。
没有 Stage 时:
所有请求共用一套 Key
所有业务共用一套参数
所有指标混在一起
所有日志难以区分
有了 Stage 以后:
不同业务拥有独立 Key 空间
不同业务使用独立治理策略
不同业务可以分别监控告警
同一套基础设施仍然可以复用
最终可以把 Stage 理解为:
统一执行框架中的业务路由标签。
它告诉系统四件事:
这是什么请求
它能和谁共享结果
它应该使用哪套策略
它应该如何被监控
下一次遇到“同一套技术组件要处理多种性质不同的任务”时,就应该想到:
是否应该先按业务语义划分 Stage?
这往往是系统从“统一实现”走向“精细化治理”的关键一步。