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:
@@ -3,6 +3,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
@@ -64,6 +65,19 @@ func main() {
|
||||
log.Printf("[gateway] subscribe task status: %v", err)
|
||||
}
|
||||
|
||||
// 评测闭环:订阅 dispatcher 回写的自动化评测结果,落 PG 供 UI 查询 / 质量趋势 / 门控。
|
||||
if _, err := bus.SubscribeEval(func(ev *contract.EvalEvent) {
|
||||
flags, _ := json.Marshal(ev.Flags)
|
||||
if err := db.SaveEval(context.Background(), &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,
|
||||
}); err != nil {
|
||||
log.Printf("[gateway] 落库评测 %s 失败: %v", ev.TaskID, err)
|
||||
}
|
||||
}); err != nil {
|
||||
log.Printf("[gateway] subscribe eval: %v", err)
|
||||
}
|
||||
|
||||
r := router.New(db, cache, bus, blobStore)
|
||||
addr := envOr("GATEWAY_ADDR", ":8080")
|
||||
log.Printf("[gateway] listening on %s", addr)
|
||||
|
||||
@@ -89,6 +89,21 @@ func (h *Handler) TaskStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"task_id": id, "status": status, "detail": detail})
|
||||
}
|
||||
|
||||
// TaskEval: GET /api/v1/tasks/:id/eval —— 取一次任务的自动化评测结果(综合/质量/忠实度/分级/flags)。
|
||||
func (h *Handler) TaskEval(c *gin.Context) {
|
||||
e := h.db.GetEval(c.Request.Context(), c.Param("id"))
|
||||
if e == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "尚无评测结果(任务未完成或评测进行中)"})
|
||||
return
|
||||
}
|
||||
var flags []string
|
||||
_ = json.Unmarshal([]byte(e.Flags), &flags)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"task_id": e.TaskID, "overall": e.Overall, "rule": e.Rule, "llm": e.LLM,
|
||||
"faithful": e.Faithful, "level": e.Level, "flags": flags, "reason": e.Reason, "sources": e.Sources,
|
||||
})
|
||||
}
|
||||
|
||||
// ApproveTask: POST /api/v1/tasks/:id/approve {approved, node?, note?} —— 人工审批决定(HITL)。
|
||||
// 把决定经 NATS 发给 dispatcher,解除审批节点的阻塞(批准放行 / 拒绝中止)。
|
||||
func (h *Handler) ApproveTask(c *gin.Context) {
|
||||
|
||||
@@ -69,6 +69,11 @@ 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)
|
||||
}
|
||||
|
||||
// ServeConfig 让网关作为配置控制面,响应某 kind 的配置请求。
|
||||
func (b *Bus) ServeConfig(kind string, provide func() *contract.ModelConfig) (func() error, error) {
|
||||
return b.inner.ServeConfig(kind, provide)
|
||||
|
||||
@@ -53,6 +53,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
p.POST("/tasks", h.SubmitTask) // 解析 DSL 并 Publish 到 NATS(带已验证 uid)
|
||||
p.GET("/tasks/:id", h.TaskStatus) // 任务生命周期状态(UI 轮询 submitted/running/done/failed/timeout/waiting/rejected)
|
||||
p.POST("/tasks/:id/approve", h.ApproveTask) // HITL 人工审批决定(批准/拒绝)
|
||||
p.GET("/tasks/:id/eval", h.TaskEval) // 自动化评测结果(综合/质量/忠实度/分级)
|
||||
p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||
p.GET("/memory", h.ListMemory) // 列出当前用户偏好(记忆面板)
|
||||
p.DELETE("/memory", h.DeleteMemory) // 软删一条偏好(?key=)
|
||||
|
||||
@@ -22,3 +22,19 @@ type Task struct {
|
||||
Status string `gorm:"size:32"` // submitted / running / done / failed / timeout
|
||||
Detail string `gorm:"type:text"` // 失败/超时原因等(状态机回写)
|
||||
}
|
||||
|
||||
// Eval 是一次任务的自动化评测结果(dispatcher 评完经 NATS 回写,每任务一条,按 task_id upsert)。
|
||||
type Eval struct {
|
||||
BaseModel
|
||||
TaskID string `gorm:"uniqueIndex;size:64"`
|
||||
Overall float64 // 综合分 [0,1]
|
||||
Rule float64 // 规则分
|
||||
LLM float64 // LLM 质量分
|
||||
Faithful float64 // RAG 忠实度分(0=无来源未评)
|
||||
Level string `gorm:"size:16"` // ok / warn / poor
|
||||
Flags string `gorm:"type:text"` // 命中问题(JSON 数组字符串)
|
||||
Reason string `gorm:"type:text"` // 评语
|
||||
Sources int // 检索来源数
|
||||
}
|
||||
|
||||
func (Eval) TableName() string { return "sundynix_eval" }
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
@@ -65,7 +66,7 @@ func OpenPostgres(dsn string) *Postgres {
|
||||
migrateLegacyIntIDs(db)
|
||||
migrateDocLinkToID(db)
|
||||
|
||||
if err := db.AutoMigrate(&User{}, &Task{}, &LLMModel{}, &KB{}, &Doc{}, &Agent{}, &DocLink{}, &Pricing{}); err != nil {
|
||||
if err := db.AutoMigrate(&User{}, &Task{}, &Eval{}, &LLMModel{}, &KB{}, &Doc{}, &Agent{}, &DocLink{}, &Pricing{}); err != nil {
|
||||
log.Printf("[store] postgres AutoMigrate 失败,降级运行: %v", err)
|
||||
return &Postgres{}
|
||||
}
|
||||
@@ -144,6 +145,31 @@ func (p *Postgres) GetTaskStatus(ctx context.Context, id string) (status, detail
|
||||
return t.Status, t.Detail
|
||||
}
|
||||
|
||||
// SaveEval 落库一条评测结果(按 task_id upsert:重评覆盖)。
|
||||
func (p *Postgres) SaveEval(ctx context.Context, e *Eval) error {
|
||||
if p.db == nil {
|
||||
return nil
|
||||
}
|
||||
return p.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "task_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"overall", "rule", "llm", "faithful", "level", "flags", "reason", "sources", "updated_at",
|
||||
}),
|
||||
}).Create(e).Error
|
||||
}
|
||||
|
||||
// GetEval 取一条任务的评测结果(不存在返回 nil)。
|
||||
func (p *Postgres) GetEval(ctx context.Context, taskID string) *Eval {
|
||||
if p.db == nil {
|
||||
return nil
|
||||
}
|
||||
var e Eval
|
||||
if err := p.db.WithContext(ctx).Where("task_id = ?", taskID).First(&e).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return &e
|
||||
}
|
||||
|
||||
// CountTasks 返回已提交任务数(降级模式返回 0)。
|
||||
func (p *Postgres) CountTasks(ctx context.Context) (int64, error) {
|
||||
if p.db == nil {
|
||||
|
||||
Reference in New Issue
Block a user