ExecutionPlan 怎么知道最后是哪个供应商答的?
降级链最怕的是悄悄降级没人知道:用户拿到的回答其实来自本地小模型,监控上却一片绿。
用两个入口:要知道「最终由谁答复」,在被降级的 effect 里读 ExecutionPlan.CurrentMetadata;要统计每次失败、做告警,在 Effect.withExecutionPlan 的 onEvent 里观察。
ExecutionPlan 是 Effect 里把「先试 A,重试,不行换 B」写成一个值的机制,写法见 Effect 怎么把多供应商 fallback 写成降级链。它是 unstable 特性,下面的内容基于 effect 4.0.5。这两个入口一个在计划外,一个在计划内,看到的东西不一样,所以要分开讲。
onEvent 能看到什么?
onEvent 在计划外观察。每次尝试发出 AttemptStart,随后有且仅有一个终止事件:AttemptSuccess 或 AttemptFailure。
事件上有三个计数:attempt 是跨步骤的累计次数(从 1 开始),stepAttempt 是本步内次数,stepIndex 是步骤下标(从 0 开始)。AttemptFailure 带完整 cause,缺陷和中断也看得到。
三步降级链的事件轨迹是这样的(前两家分别以 503、429 失败,第三家成功):
AttemptStart #1 step=0
AttemptFailure #1 step=0 503
AttemptStart #2 step=0
AttemptFailure #2 step=0 503
AttemptStart #3 step=1
AttemptFailure #3 step=1 429
AttemptStart #4 step=2
AttemptSuccess #4 step=2它能看到每一次失败,包括被降级吞掉、调用方永远看不到的那些,所以适合做失败率统计和告警。
onEvent 的 handler 有哪些限制?
文档写明了几条性质:
- handler 被内联等待,事件严格有序,所以要廉价。
- handler 不能失败,观察不会改变结果;它的 requirements 会加到最终 effect 上。
- 终止事件像 finalizer,被中断时也会发出。
另外,下游提前停止拉取流(如计划外的 Stream.take)时,被截断的尝试报告 AttemptSuccess,因为是消费者停了,统计失败率时别当成异常。
CurrentMetadata 在哪里能读到什么?
ExecutionPlan.CurrentMetadata 在计划内观察。它是一个 Context.Reference(见 Context.Reference),在被降级的 effect 里读到 { attempt, stepIndex }:
const ask = (prompt: string) =>
Effect.gen(function*() {
const llm = yield* Llm
const { attempt, stepIndex } = yield* ExecutionPlan.CurrentMetadata
yield* Effect.logDebug(`attempt=${attempt} step=${stepIndex} provider=${llm.provider}`)
return yield* llm.complete(prompt)
})这里 Llm 是自定义服务,provider 是当前 Layer 里的供应商名。在计划之外读取,会回落到默认值 { attempt: 0, stepIndex: 0 },我试过。
两个入口该用哪个?
我的分工:要把「最终由谁答复」写进响应或 span,用 CurrentMetadata,它在业务代码里能和当前 Layer 的 provider 名放在一起;要统计失败率、告警,用 onEvent,它能看到每次失败。整体可观测性见 可观测性。
版本是 effect 4.0.5。事件轨迹来自我跑过的模拟供应商示例,CurrentMetadata在计划外回落到默认值这一点我也自己确认过。handler 的内联等待、不能失败、中断时也发出终止事件,以及Stream.take截断时报告AttemptSuccess,这几条是文档写明的性质,我没有逐条另外验证。