feat(harness): 评测闭环 —— 评测结果落库 + 分级 + 告警 + 可查(测温计→恒温器)

此前 eval 只打日志、不闭环。现在:
- 分级:evalLevel 据综合分+忠实度 → ok(≥0.75) / warn(≥0.5 或忠实<0.6) / poor(<0.5);poor 出 slog.Warn 告警。
- 落库:dispatcher 评完经 NATS(SubjectEval) 广播 EvalEvent → 网关订阅写 PG(新表 sundynix_eval,
  按 task_id upsert)。沿用任务状态回写那套(dispatcher 无 DB,经 bus→gateway 落库)。
- 可查:GET /api/v1/tasks/:id/eval 返回 overall/rule/llm/faithful/level/flags/reason/sources。
- 契约 EvalEvent + EvalOK/Warn/Poor;bus PublishEval/SubscribeEval;dispatcher EvalSink(NewOrchestrator 第9参)。

验证:三模块 build+vet+test 全绿;live RAG 任务评测落库,端点返回 overall~1.0 / level=ok / faithful=1 / sources=1。
剩:桌面端质量面板、低分自动重试(P3)。project_analysis 勾掉该项。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-25 15:26:19 +08:00
parent 446b784fc6
commit 10f08ffb14
12 changed files with 173 additions and 10 deletions
@@ -46,6 +46,11 @@ type ApprovalWaiter interface {
WaitApproval(ctx context.Context, taskID string, timeout time.Duration) (*contract.ApprovalDecision, error)
}
// EvalSink 回写自动化评测结果供网关落库(由 NATS bus 实现;可为 nil → 仅打日志不落库)。
type EvalSink interface {
PublishEval(ev *contract.EvalEvent) error
}
// errRejected 是审批节点拒绝(或超时)时图执行返回的哨兵错误:它是合法终态而非故障,
// Handle 据此判 rejected 并优雅收尾(不计熔断失败)。
var errRejected = errors.New("approval rejected")
@@ -82,16 +87,17 @@ type Orchestrator struct {
exec ExecSink
status StatusSink // 任务生命周期状态回写(可为 nil)
approval ApprovalWaiter // HITL 审批等待(可为 nil → 审批节点自动放行)
evalSink EvalSink // 评测结果回写落库(可为 nil → 仅打日志)
turnMu sync.Mutex // 保护 turns(攒批计数,多任务 goroutine 共享)
turns map[string]int // sessionID → 累计轮次,用于每 N 轮触发 consolidate
}
// NewOrchestrator 持有依赖;图按任务的 DSL 在 Handle 内动态编译。
// exec 为执行可视化事件出口(可为 nil,则不发轨迹事件);eval 为自动化评测(可为 nil);
// status 为任务生命周期状态回写出口(可为 nil);approval 为 HITL 审批等待(可为 nil)。
func NewOrchestrator(pool LLM, breaker *harness.CircuitBreaker, eval *harness.Evaluator, sink TokenSink, tools ToolCaller, exec ExecSink, status StatusSink, approval ApprovalWaiter) (*Orchestrator, error) {
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec, status: status, approval: approval}, nil
// exec 为执行可视化事件出口(可为 nil);eval 为自动化评测(可为 nil);status 为状态回写(可为 nil);
// approval 为 HITL 审批等待(可为 nil);evalSink 为评测结果落库出口(可为 nil)。
func NewOrchestrator(pool LLM, breaker *harness.CircuitBreaker, eval *harness.Evaluator, sink TokenSink, tools ToolCaller, exec ExecSink, status StatusSink, approval ApprovalWaiter, evalSink EvalSink) (*Orchestrator, error) {
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec, status: status, approval: approval, evalSink: evalSink}, nil
}
// setStatus 回写一次任务状态流转(status 为 nil 时静默跳过)。
@@ -203,8 +209,33 @@ func (o *Orchestrator) evaluate(t *contract.Task, input, output string, sources
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
r := o.eval.Score(ctx, input, output, sources)
log.Printf("[eval] task %s 综合 %.2f(规则 %.2f / LLM %.2f / 忠实 %.2f,来源 %dflags=%v %s",
t.ID, r.Overall, r.Rule, r.LLM, r.Faithful, len(sources), r.Flags, r.Reason)
level := evalLevel(r)
log.Printf("[eval] task %s 综合 %.2f(规则 %.2f / LLM %.2f / 忠实 %.2f,来源 %dlevel=%s flags=%v %s",
t.ID, r.Overall, r.Rule, r.LLM, r.Faithful, len(sources), level, r.Flags, r.Reason)
if level == contract.EvalPoor { // 低质量告警(可观测 + 后续可挂报警)
slog.Warn("eval poor quality", "task_id", t.ID, "overall", r.Overall, "faithful", r.Faithful, "flags", r.Flags)
}
// 闭环:评测结果落库(供 UI 查询 / 质量趋势 / 门控)。
if o.evalSink != nil {
if err := o.evalSink.PublishEval(&contract.EvalEvent{
TaskID: t.ID, Overall: r.Overall, Rule: r.Rule, LLM: r.LLM, Faithful: r.Faithful,
Level: level, Flags: r.Flags, Reason: r.Reason, Sources: len(sources), TS: time.Now().UnixMilli(),
}); err != nil {
log.Printf("[eval] 回写评测结果失败 task=%s: %v", t.ID, err)
}
}
}
// evalLevel 据综合分 + 忠实度把评测分级(闭环门控/告警用)。
func evalLevel(r harness.Result) string {
switch {
case r.Overall < 0.5:
return contract.EvalPoor
case r.Overall < 0.75 || (r.Faithful > 0 && r.Faithful < 0.6):
return contract.EvalWarn
default:
return contract.EvalOK
}
}
// fetchMemory 经 MCP memory_get 工具召回用户常驻画像。