feat(eval): 评测结果回写升 JetStream 持久 —— 消灭最后一处 core NATS 回写 (P1)

完成度审计 P1 + 记忆 nats-durability 的既定规矩「计费/需落库的回写一律
JetStream+幂等,别fire-and-forget」。此前 eval 是全仓最后一处 core NATS pub-sub
回写:网关离线/慢消费者时评测结果直接丢——而质量趋势/门控都依赖它。

照抄已升级的 status/usage 范式(同为 dispatcher→gateway→PG 回写):
- contract 加 StreamEval/ConsumerEval;bus 加 EnsureEvalStream + ConsumeEval
  (durable consumer,AckExplicit,落库失败 Nak 重投自愈),PublishEval 改
  js.Publish 同步等 ack。删 core NATS 的 SubscribeEval。
- gateway/dispatcher 两个 wrapper 在 connect 时 EnsureEvalStream;gateway main
  的评测订阅从 SubscribeEval(fire-and-forget)换 ConsumeEval(handler 返 error→
  Nak),接入优雅停机 drain。
- 幂等前提已满足:SaveEval 按 task_id upsert,at-least-once 重投只覆盖不重复。

验证:e2e 去重测试(TestGatewayQueueDedup)升级到 ConsumeEval,50 条两副本合计
处理50次零重复;live 真端到端:提交任务→跑完→评测经新 eval 流落 PG(level=ok)
一次成功;两服务启动 eval 流 ensure 无报错。go build/vet/test 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-18 11:52:40 +08:00
parent 3d15cdb493
commit 940330cdb7
6 changed files with 81 additions and 31 deletions
+11 -8
View File
@@ -100,17 +100,17 @@ func main() {
log.Printf("[gateway] consume task status: %v", serr)
}
// 评测闭环:订阅 dispatcher 回写的自动化评测结果,落 PG 供 UI 查询 / 质量趋势 / 门控。
if _, err := bus.SubscribeEval(func(ev *contract.EvalEvent) {
// 评测闭环:持久消费 dispatcher 回写的自动化评测结果,落 PG 供 UI 查询 / 质量趋势 / 门控。
// JetStream at-least-once + SaveEval 按 task_id upsert 幂等;落库失败返 error → Nak 重投自愈。
evalDrain, everr := bus.ConsumeEval(context.Background(), func(ctx context.Context, ev *contract.EvalEvent) error {
flags, _ := json.Marshal(ev.Flags)
if err := db.SaveEval(context.Background(), &store.Eval{
return db.SaveEval(ctx, &store.Eval{
TaskID: ev.TaskID, Overall: ev.Overall, Rule: ev.Rule, LLM: ev.LLM, Faithful: ev.Faithful,
Level: ev.Level, Flags: string(flags), Reason: ev.Reason, Sources: ev.Sources, Corrected: ev.Corrected,
}); err != nil {
log.Printf("[gateway] 落库评测 %s 失败: %v", ev.TaskID, err)
}
}); err != nil {
log.Printf("[gateway] subscribe eval: %v", err)
})
})
if everr != nil {
log.Printf("[gateway] consume eval: %v", everr)
}
// 成本护栏:持久消费 dispatcher 回写的任务 token 用量(计费事实源,JetStream at-least-once + 幂等)。
@@ -182,6 +182,9 @@ func main() {
if usageDrain != nil {
usageDrain(context.Background())
}
if evalDrain != nil {
evalDrain(context.Background())
}
log.Println("[gateway] 已优雅停机")
}
+7 -4
View File
@@ -33,7 +33,10 @@ func MustConnect(url string) *Bus {
if err := inner.EnsureUsageStream(context.Background()); err != nil {
log.Fatalf("[nats] ensure usage stream: %v", err)
}
log.Printf("[nats] connected %s, task + ingest + status + usage streams ready", url)
if err := inner.EnsureEvalStream(context.Background()); err != nil {
log.Fatalf("[nats] ensure eval stream: %v", err)
}
log.Printf("[nats] connected %s, task + ingest + status + usage + eval streams ready", url)
return &Bus{inner: inner}
}
@@ -78,9 +81,9 @@ func (b *Bus) PublishApproval(dec *contract.ApprovalDecision) error {
return b.inner.PublishApproval(dec)
}
// SubscribeEval 订阅 dispatcher 回写的自动化评测结果(落 PG)。
func (b *Bus) SubscribeEval(onEvent func(*contract.EvalEvent)) (func() error, error) {
return b.inner.SubscribeEval(onEvent)
// ConsumeEval 持久消费 dispatcher 回写的自动化评测结果(落 PGat-least-once + 幂等)。
func (b *Bus) ConsumeEval(ctx context.Context, h func(context.Context, *contract.EvalEvent) error) (func(context.Context), error) {
return b.inner.ConsumeEval(ctx, h)
}
// ConsumeUsage 持久消费 dispatcher 回写的任务 token 用量(计费,at-least-once + 幂等)。