评测集 + 评估器:题库 与 裁判
昨天认识了通用的 Dataset。今天进入评测专属的两个角色:评测集(EvaluationSet)——专门给评测用的题库,和 评估器(Evaluator)——给 AI 的回答打分的"自动裁判"。它们都在 backend/modules/evaluation 下,是 Day 13 实验能跑起来的两大前提。
评测集 和 昨天的 Dataset 是什么关系
type EvaluationSet struct {
ID int64 `json:"id,omitempty"`
SpaceID int64 `json:"space_id,omitempty"`
Name string `json:"name,omitempty"`
Status DatasetStatus `json:"status,omitempty"` // 注意:类型名还叫 DatasetStatus
Spec *DatasetSpec `json:"spec,omitempty"` // 还叫 DatasetSpec
Features *DatasetFeatures `json:"features,omitempty"`
ItemCount int64 `json:"item_count,omitempty"`
ChangeUncommitted bool `json:"change_uncommitted,omitempty"`
EvaluationSetVersion *EvaluationSetVersion `json:"evaluation_set_version,omitempty"`
LatestVersion string `json:"latest_version,omitempty"`
...
}
Dataset 是"通用练习册"模板,今天的 EvaluationSet 就是"专门用来出评测题的那一本"——字段几乎照搬(DatasetStatus/DatasetSpec 这些类型名都直接沿用),但它活在 modules/evaluation 这个独立的领域模块里,有自己的 evaluation_set_app.go、自己的表、自己的版本和条目管理逻辑。可以理解成"评测域"对"数据域"概念的一次复刻和专属化,两者代码是分开维护的,不是同一张表。lodataset)来调用,而不是共享代码。什么是评估器:自动打分的"裁判"
| 你可能听过的说法 | 在 Coze Loop 里对应 |
|---|---|
| LLM-as-a-Judge | Prompt 类型评估器(EvaluatorTypePrompt)——用一个大模型当裁判 |
| 规则打分 / 关键词匹配 | Code 类型评估器(EvaluatorTypeCode)——跑一段代码逻辑 |
| 接第三方评分服务 | CustomRPC 类型评估器——调用你自己的 HTTP/RPC 服务 |
| 让另一个 Agent 来评 | Agent 类型评估器(EvaluatorTypeAgent)——异步,跑得比较久 |
Evaluator 实体拆解:一个类型,四种"身体"
核心实体在 backend/modules/evaluation/domain/entity/evaluator.go:6-27:
type Evaluator struct {
ID int64
SpaceID int64
Name string
EvaluatorType EvaluatorType // Prompt / Code / CustomRPC / Agent
LatestVersion string
Builtin bool // 是否是官方预置评估器
BoxType EvaluatorBoxType // 白盒(可看到实现细节) / 黑盒
SourceType EvaluatorSourceType
PromptEvaluatorVersion *PromptEvaluatorVersion // 四种类型二选一携带对应版本
CodeEvaluatorVersion *CodeEvaluatorVersion
CustomRPCEvaluatorVersion *CustomRPCEvaluatorVersion
AgentEvaluatorVersion *AgentEvaluatorVersion
}
XxxEvaluatorVersion 字段都是指针,同一时刻只有和 EvaluatorType 对应的那个不为空。GetEvaluatorVersionID()(evaluator.go:85-107)这类方法内部用 switch e.EvaluatorType 分发到对应字段——这是 Go 里"轻量级多态"的常见写法(没有接口继承,靠一个 tag 字段 + switch 分支模拟"多种子类型")。以 Prompt 类型评估器的"身体"为例,evaluator_version_prompt.go:17-34:
type PromptEvaluatorVersion struct {
EvaluatorID int64
Version string
InputSchemas []*ArgsSchema // 这个 Prompt 评估器需要哪些输入变量
MessageList []*Message // 评分用的 Prompt 模板本身
ModelConfig *ModelConfig // 用哪个模型来当裁判、温度多少
Tools []*Tool
ReceiveChatHistory *bool
ParseType ParseType // 怎么从模型输出里解析出分数
}
四种评估器类型,怎么选
Prompt(EvaluatorTypePrompt=1)
用大模型当裁判,最灵活,能评估"语义是否正确""是否礼貌"这类主观维度。同步执行,等模型返回。
Code(EvaluatorTypeCode=2)
跑一段确定性代码逻辑(比如正则匹配、JSON 字段比对)。速度快、成本低、结果稳定,适合客观维度。
CustomRPC(EvaluatorTypeCustomRPC=3)
调用你自己部署的评分服务(HTTP/RPC)。适合已有专有评分模型/业务规则的团队。
Agent(EvaluatorTypeAgent=4)
调用一个完整的 Agent 来评估,可能涉及多轮工具调用,耗时更长——所以是异步的。
IsAsync()
evaluator.go:67-69:func (e *Evaluator) IsAsync() bool { return e.EvaluatorType == EvaluatorTypeAgent }——只有 Agent 类型评估器被标记为异步。这意味着实验(Day 13)在编排评分流程时,遇到 Agent 评估器不能"调用完立刻等结果",得走"提交任务 → 后台跑 → 回调/轮询拿结果"的路子,这也是为什么 Day 13 的实验大量依赖 RocketMQ 消息队列。Run vs Debug:两条调用评估器的路
backend/modules/evaluation/application/evaluator_app.go 里有两个容易混淆的入口:
RunEvaluator(第 1087 行)正式运行:先查评估器版本,鉴权(预置评估器免鉴权),调用 evaluatorService.RunEvaluator 落库一条 EvaluatorRecordDebugEvaluator(第 1144 行)调试运行:多一步权益检查 CheckEvaluatorBenefit(用量/额度限制),CustomRPC/Agent 类型还要额外检查"内容可写"权限👶 小白问:预置评估器为什么免鉴权?
👨🏫 老师:Builtin(预置)评估器是官方内置的公共资源,所有空间都能用,不存在"谁有权限用"的问题——鉴权检查的是"这是不是你自己创建的私有评估器",公共资源自然跳过这一步(evaluator_app.go:1095-1106 里 if !evaluatorDO.Builtin 就是这个判断)。
真实路由与数据库表
评估器相关路由横跨 v1/v2/v3——正是"接口经历过多次演进"的痕迹(backend/api/handler/coze/loop/apis/evaluator_service.go):
POST /api/evaluation/v2/evaluator/create # 创建
PUT /api/evaluation/v2/evaluator/update_draft # 更新草稿
POST /api/evaluation/v2/evaluator/:evaluator_id/submit # 提交版本
POST /api/evaluation/v2/evaluator/run # 正式运行 → RunEvaluator
POST /api/evaluation/v2/evaluator/debug # 调试运行 → DebugEvaluator
POST /api/evaluationv3/evaluators/list # v3:新版列表接口
POST /api/evaluation/v1/evaluators_versions/:evaluator_version_id/async_run # 异步运行(Agent 类型走这里)
POST /api/evaluation/v1/evaluators/async_debug # 异步调试
run/debug 分开——接口层面就已经体现了 L04 讲的"同步 vs 异步"的区分。核心表 .../mysql-init/init-sql/evaluator.sql(节选):
CREATE TABLE IF NOT EXISTS `evaluator` (
`id` bigint unsigned NOT NULL,
`space_id` bigint unsigned NOT NULL,
`evaluator_type` int unsigned NOT NULL COMMENT '评估器类型',
`draft_submitted` tinyint(1) DEFAULT '0' COMMENT '草稿是否已提交',
`latest_version` varchar(128) NOT NULL DEFAULT '',
`builtin` int unsigned NOT NULL DEFAULT '2' COMMENT '是否预置,1:是;2:否',
`box_type` int unsigned NOT NULL DEFAULT '1' COMMENT '黑白盒类型',
...
KEY `idx_space_id_evaluator_type` (`space_id`, `evaluator_type`)
) ENGINE = InnoDB;
draft_submitted 字段呼应了 Day 11 学的"未提交改动"概念——评估器同样支持"先编辑草稿、调试满意了再提交成正式版本",实验只能引用已提交的版本号,这样实验结果才可复现(引用的评分逻辑不会因为你后来改草稿而悄悄变化)。今日小结 + 动手 + 预告
🧠 今天你应该能回答
- EvaluationSet 和 Dataset 是同一份代码吗?为什么类型名还叫 DatasetStatus?
- 评估器本质是什么函数?四种类型分别怎么选?
- 哪种类型的评估器是异步的?为什么?
- Run 和 Debug 两个接口为什么要分开,各自多做了什么检查?
- 为什么实验只能引用"已提交"的评估器版本?
✋ 动手 5 分钟(可选)
# 1. 对比 EvaluationSet 和 Dataset 的字段有多相似
diff <(grep -A30 "type EvaluationSet struct" backend/modules/evaluation/domain/entity/evaluation_set.go) \
<(grep -A30 "type Dataset struct" backend/modules/data/domain/dataset/entity/dataset.go)
# 2. 看四种评估器类型的完整枚举定义
grep -n "EvaluatorType" backend/modules/evaluation/domain/entity/evaluator.go | head -20
# 3. 找出所有评估器相关路由,感受 v1/v2/v3 的演进痕迹
grep -n "@router" backend/api/handler/coze/loop/apis/evaluator_service.go
# 4. 看 IsAsync 的真实判断逻辑
grep -n "IsAsync" -A3 backend/modules/evaluation/domain/entity/evaluator.go
experiment_app.go 和背后一串 RocketMQ 消费者,看"实验"这台大机器怎么转起来。