5664 字
约 18 分钟
2
Stage 到底是什么?如何用业务标签治理复杂请求

Stage 到底是什么?如何用业务标签治理复杂请求

在复杂系统中,我们经常会遇到这样的情况:

同一套技术基础设施,需要处理多种性质完全不同的业务请求。

例如一套 AI 调用系统,可能同时承担:

  • 内容评分;
  • 问题生成;
  • 文档抽取;
  • 图像分析;
  • 报告生成。

它们底层可能都在调用大模型,但业务目的、执行时间、缓存策略、超时配置和故障处理方式都不一样。

如果把所有请求都当成同一种任务处理,系统很快就会出现:

  • 不同业务错误复用结果;
  • 超时参数不合理;
  • 长任务被过早接管;
  • 短任务卡住后迟迟不能恢复;
  • 日志和监控混在一起;
  • 无法针对某类请求单独治理。

这时候就需要引入一个重要概念:

Stage

一句话总结

Stage 是请求的业务类别标签,用来回答“当前这次调用究竟在做什么”。

它通常承担四个职责:

区分业务
+ 隔离请求 Key
+ 选择治理策略
+ 支持监控追踪

Stage 不是线程执行阶段,也不是 RUNNINGFAILED 这样的运行状态。

可以用一句话记住:

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?

这往往是系统从“统一实现”走向“精细化治理”的关键一步。

Stage 到底是什么?如何用业务标签治理复杂请求
http://www.clxhxhhr.top/posts/130/
作者
clxstart
发布于
2026-07-17
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。
文章目录
目录