Appearance
Evaluator SDK
一句话:Evaluator SDK(
noj_evaluator_sdk)跑在 Evaluator 容器里,负责调用用户函数(SolutionRunner)、转发受限 capability,并写出最终结果(result)。
Evaluator SDK 运行在 Evaluator 容器中,用于调用用户解答并输出评测结果。协议线格式见 RPC 与可传递数据。
导入
python
from noj_evaluator_sdk import (
SolutionRunner,
NotFoundError,
RejectedError,
SolutionTimeoutError,
SystemError,
ConnectionError,
result,
)调用用户函数
创建 runner:
python
runner = SolutionRunner()调用用户函数:
python
answer = runner.call("solve", 1, 2)runner.call() 会向 Solution Host 发起一次 RPC 调用。如果调用成功,返回用户函数的返回值。
调用级超时:runner.call() 支持可选 timeout_ms 参数,每次调用可指定独立超时(毫秒)。缺省(None)时由 Judge Worker 回退到题目的 runtime_config.solution.call_timeout_ms:
python
answer = runner.call("solve", 1, 2) # 用题目级默认超时
answer = runner.call("solve", 1, 2, timeout_ms=5000) # 本次调用 5s 超时timeout_ms 必须是正整数或 None
其他值(0 / 负数 / 非整数)会抛出 ValueError。超时后 runner.call() 抛出 SolutionTimeoutError,可捕获后记为失败用例继续评测——不捕获则 evaluator 异常退出、没有 ---RESULT---,最终状态为 error。
调用参数会经过 Neuro OJ codec 编码后通过 RPC 传递。支持的类型和限制见 RPC 与可传递数据。
处理调用错误
runner.call() 会根据错误类型抛出以下异常:
| 异常 | 含义 |
|---|---|
NotFoundError | 目标函数不存在 |
RejectedError | 参数/返回值类型不允许,或帧超过 1 MiB 软上限 |
SolutionTimeoutError | 单次调用超过 call_timeout_ms。若 evaluator 未捕获(evaluate.py 异常退出、无 ---RESULT---),最终状态为 error |
SystemError | host 内部错误、异常执行、IPC 通道异常等不可恢复错误 |
ConnectionError | Solution Host 已关闭 / IPC 通道断开 |
python
try:
answer = runner.call("solve", 1, 2)
except SolutionTimeoutError:
# 超时按失败用例处理
result.wrong_answer(score=0, message="调用超时")
except RejectedError as exc:
# 参数/返回值类型非法
result.wrong_answer(score=0, message=str(exc))
except NotFoundError:
# 函数未实现
result.wrong_answer(score=0, message="函数不存在")参数与返回值类型校验(RejectedError)
runner.call() 在发出 RPC 帧之前会递归校验参数类型:只允许 None / bool / int / float / str / bytes / list / dict(dict 的 key 必须是 str),任何其他类型——包括嵌套在 list / dict 中的自定义对象、set、tuple、函数、生成器、文件句柄等——都会直接抛出 RejectedError,错误消息带路径与类型名:
text
arg[0]: 不支持的类型 MyClass(仅 None/bool/int/float/str/bytes/list/dict)此时 RPC 帧不会发出,Solution 侧完全不知情。帧序列化超过 1 MiB 软上限时同样抛出 RejectedError。
返回值路径对称:Solution 返回不支持类型时,Judge Worker 以 code="Rejected" 的错误帧返回,Evaluator 侧同样收到 RejectedError。
出题人可以用 try/except RejectedError 把这类调用按失败用例处理;不捕获则 evaluate.py 异常退出,该次评测落为 error。
注册 capability(供 Solution 调用)
当题目需要让 solution 使用网络等能力时,用 register_capability 暴露一个精确封装的 handler:
python
from noj_evaluator_sdk import register_capability
def request_llm_completion(prompt: str) -> str:
# evaluator 已联网(runtime_config.evaluator.network.enabled = true)
# ... 调用固定 URL 的外部 API,参数校验由 handler 负责
return completion_text
register_capability("request_llm_completion", request_llm_completion)capability 默认超时:register_capability(name, handler, timeout_ms=None) 可配置 solution 每次调用该 capability 的超时(毫秒)。注册时经 cap_reg 帧上报 Judge,缺省(None)回退题目级 call_timeout_ms:
python
register_capability("request_llm_completion", handler, timeout_ms=10000)- Solution 通过
noj_solution_sdk.call_capability(name, *args)调用;请求经 judge 转发到 evaluator,在 runner 的 reader 线程中同步执行 handler,结果以result帧返回。 - 返回值与
runner.call()相同约束(None / bool / int / float / str / bytes / list / dict);返回值类型非法或帧超限(> 1 MiB)→code="Rejected";handler 异常 →code="Exception"(含清洗后 trace),未注册 →code="NotFound"。 - 不要嵌套双向调用:capability handler 在 reader 线程中同步执行,若 handler 内再调用
runner.call()(回调 solution),双方会互相等待而死锁,只能等评测总超时兜底——不支持这种嵌套。 - 重复注册同名 capability:最近一次生效。
- 安全模型:capability 是 solution 使用网络的唯一入口,不要注册通用 URL 转发(如
fetch_url(url));应封装固定目标的业务函数并做参数校验。详细指南见 如何提供受限网络能力。
输出评测结果
Evaluator 使用 result 模块输出最终结果。score 参数以实际分数计(可为小数),SDK 内部乘以 100 写入结果 JSON。
python
result.accept(score=10, details={"passed": 10}) # 满分,写入 score=1000
result.wrong_answer(score=5, details={"passed": 5}) # 部分分,写入 score=500result 每次评测只能写入一次
accept / wrong_answer 第二次调用会抛 RuntimeError。写入后进程应尽快退出。
新协议下结果 JSON 不再输出 status,只输出 score 与 details;accept / wrong_answer 只是写入分数的便捷方法。评测脚本自身出错时应直接抛出异常或非零退出,由 judge 统一映射为 error;SDK 已移除会写入结果的 runtime_error(),system_error() 现在也是直接抛出 RuntimeError(不再写结果 JSON)。
结果 JSON 中的 score 是 ×100 的整数(与数据库存储一致)。例如满分 10 分时,accept(score=10) 写入 "score": 1000,前端按 (score / 100).toFixed(1) 显示为 10.0 分。
details
details 会作为结构化结果透传给前端。若需要展示测试点明细,推荐使用扁平的 cases 数组。每个测试点必须包含 case_id、status 和布尔标记 hidden(true 为隐藏用例,false 为可见用例);请为每个用例都设置该字段,避免旧脚本被误判为“全部可见”。visibility(visible/hidden)是可选的兼容/人读字段,time_ms、memory_kb、input、expected_output 和 actual_output 按可见性选用。
| 字段 | 必填 | 说明 |
|---|---|---|
case_id | ✅ | 用例标识 |
status | ✅ | 用例级状态(Accepted / WrongAnswer 等,仅参考) |
hidden | ✅ | 布尔:true 隐藏 / false 可见;每个用例都要写 |
visibility | visible / hidden,兼容与人读用 | |
time_ms / memory_kb | 耗时 / 内存,可见与隐藏用例都可给;两者都会被 core 结果白名单收录并落库(memory_kb 自 2026-09-24 起收录,此前会被静默丢弃) | |
input / expected_output / actual_output | 仅可见用例可给;隐藏用例不得出现 |
隐藏用例不能带输入/期望/实际输出
隐藏测试点可以展示状态、耗时和内存,但 MUST NOT 在 details 中写入 input / expected_output / actual_output——否则会经结果投影泄露给做题人(竞赛场景尤其致命)。
常见结构:
python
details = {
"cases": [
{
"case_id": "v001",
"status": "Accepted",
"hidden": False,
"visibility": "visible",
"time_ms": 12,
"expected_output": "3",
"actual_output": "3",
},
{
"case_id": "h001",
"status": "WrongAnswer",
"hidden": True,
"visibility": "hidden",
"time_ms": 15,
},
],
}投影与兼容
提交结果投影会按 hidden 标记在竞赛场景剥离隐藏用例;判定“用例已标记”的条件是含 hidden 或 visibility 字段。若 cases 中任意用例两者都缺,视为旧脚本,整份用例详情 fail-safe 不返回。历史的 visible.cases/hidden.cases 以及 id/expected/actual 字段仍可被提交结果页兼容,但新评测器应使用上述标准字段并带 hidden。
调用 LLM(LLM 题)
启用 LLM 的题目由 Judge Worker 向 Evaluator 容器注入一组环境变量(Solution 容器始终不注入):
| 环境变量 | 含义 |
|---|---|
NOJ_LLM_GATEWAY_URL | noj-llm-gateway 基址 |
NOJ_LLM_TOKEN | 短期 eval_token |
NOJ_LLM_PROVIDER_ID | Provider ID(由 gateway 校验) |
NOJ_LLM_ALLOWED_MODELS | 允许的模型名列表(逗号分隔) |
NOJ_SUBMISSION_ID | 提交 UUID(供题目侧做确定性随机) |
NOJ_REJUDGE_SEQ | 重测序号,缺省为 0 |
题目侧可以直接用 SDK 的 llm.complete() 调用 gateway,无需自己拼 HTTP:
python
from noj_evaluator_sdk import llm
resp = llm.complete(model="qwen-plus", messages=[{"role": "user", "content": "..."}])
text = resp["choices"][0]["message"]["content"]模型缺省取 NOJ_LLM_ALLOWED_MODELS 的第一个;接入细节与预算配置见 出 LLM 调用题。
关闭 runner
runner.close() 可主动关闭 runner(通常不需要,进程结束自动清理)。当前 SDK 不提供 runner.restart()。