Appearance
双容器评测模型(Judge Model)
在 Neuro OJ 中,代码评测基于双容器协同沙箱展开。每次代码提交均在后台并行拉起两个高度受控的 Docker 容器:Evaluator 容器(运行出题人的评分程序)与 Solution 容器(运行选手的解答代码),二者通过评测宿主进程(noj-judge)进行双向 RPC 消息路由。
与传统 OJ 的本质差异
传统在线评测(如 ACM/ICPC、OI 题目)通常将测试用例作为纯文本灌入用户程序的标准输入(stdin),再捕获标准输出(stdout)进行行级比对(Diff)。而在 AI 算法、深度学习与智能体评测场景下,这种形式存在严重瓶颈:
| 评测维度 | 传统 OJ 文本流评测 | Neuro OJ 双容器 RPC 评测 |
|---|---|---|
| 交互契约 | 标准输入输出(stdin $\to$ stdout 文本比对) | 原生 Python 函数调用(强类型入参与返回值) |
| 测试数据管理 | 一次性文本文件重定向,容易被选手一次性预读取 | 由出题人 evaluate.py 动态加载,隐藏用例物理隔离于选手容器之外 |
| 调试输出 | print() 与答案混杂,易导致格式错误(PE) | 选手 print() 写入协议通道后由底层自动剥离,不污染评测输出 |
| 交互式评测 | 需借助特殊交互器(Interactive Judge)管道 | 天然原生交互:出题人可连续多次调用选手函数,支持多轮对话与智能体对抗 |
双容器角色分工与输出通道
1. Evaluator 容器(裁判方)
Evaluator 容器由出题人提供的 evaluate.py 主导。它独占访问纯净题目包内的所有私密测试数据,拥有完全的评分控制权:
- 核心判定权力:决定调用选手的哪个函数、传递何种边界参数、如何比对返回值的数学/逻辑正确性、计算最终得分以及构造反馈给做题人的详细
details.cases; - 输出通道划分:
stdout:同时承载 NDJSON 协议帧、评测输出文本以及作为评测终结标志的---RESULT---行;stderr:仅作为评测执行日志使用,绝对不承载任何 RPC 帧。
2. Solution 容器(做题方)
Solution 容器负责隔离执行选手提交的解答:
- 入口注入规范:评测引擎将选手代码以硬编码文件名
main.py注入沙箱工作目录/workspace; - Solution Host 协议守护:容器拉起
noj_solution_sdk.host守护进程,以--entry /workspace/main.py动态加载用户模块(模块名固定为user_solution),自动提取顶层导出的公共函数并进入事件监听循环; - 常驻内存特性:在单次评测生命周期内,Solution Host 是单例常驻的。同一选手的模块实例在多次
runner.call()之间共享全局变量与内存状态(系统不提供重启重置功能)。
调用异常与提交终态映射
在 RPC 交互过程中,选手的代码缺陷可能引发不同层次的错误。Evaluator SDK 会将底层协议错误映射为对应的 Python 异常:
| 异常情况 | 底层协议 code | Evaluator 捕获异常 | 典型排错原因 |
|---|---|---|---|
| 函数未定义 | NotFound | NotFoundError | 选手没有定义题面要求的顶层函数(如拼写错误) |
| 执行崩溃抛错 | Exception | SystemError | 选手代码内部抛出未捕获异常(包含清洗后的 Traceback) |
| 非法返回类型 | Rejected | RejectedError | 选手返回了不可序列化的对象(如函数引用、生成器)或帧体积超过 1 MiB |
关键准则:调用失败 ≠ 提交失败
上述异常是选手代码在执行单次测试点时产生的错误,绝不等于本次提交失败!
- 平台采用二元终态设计:提交终态只有
finished(正常评测完成)与error(评测系统异常); - 优秀的出题人应当在
evaluate.py中使用try...except稳健捕获这些异常,将其记录为当前测试点的失败(如WrongAnswer/RuntimeError),并最终给出finished+ 0 分; - 只有当
evaluate.py自身发生未捕获的严重崩溃、沙箱整体超时或容器无法启动时,终态才会归为error。
两层超时机制与终态仲裁
系统在架构上设计了严格的两层超时时间锁,防止选手恶意利用死循环或出题人评分逻辑冗长导致评测节点挂起:
| 超时层级 | 监控主体 | 默认控制权 | 终态表现与做题人可操作性 |
|---|---|---|---|
| 评测全局总超时 | noj-judge 评测宿主 | 题目 runtime_config.time_limit_ms | error。系统直接熔断容器,选手无法仅凭优化单个函数规避 |
| 单函数调用超时 | SolutionRunner 内部计时 | 题目 call_timeout_ms或单次参数 timeout_ms | 若出题人正常捕获,体现为 finished + 0 分;若出题人未捕获导致评测脚本终止,落为 error |
| 容器冷启动超时 | noj-judge 容器生命周期 | 系统级宿主配置 | error。评测环境未按期就绪 |
单次函数调用全链路时序
以下展示一次标准的 runner.call("solve", a, b) 端到端执行链路: