OJ系统集成及借助大模型实现功能扩展(一)

首先非常感谢小米官方的“百万亿 Token 创造者激励计划”给的token-plan,我申请的理由写了要写数据可视化课设,发了一个月价值100刀乐的Max plan,量大管饱。

屏幕截图(507)

前几周完成了 remote_judge 远端判题子系统的完整开发——Docker 沙箱执行、容器池复用、熔断降级、Seccomp 安全加固,以及 Compose 四服务一键部署。判题系统本身已经是一个功能完备的独立服务,但和 OJ 后端的整合一直停留在"各自能跑"的阶段。本周的工作集中在一个核心问题上:把 OJ 后端和 remote_judge 真正跑通一整条链路,并在此基础上借助 MIMO 大模型对系统进行功能扩展。

当前 OJ 后端本质上还是一个框架——用户系统、题目管理、提交入口等基础 CRUD 都有实现占位,但实际功能、判题状态、采集信息和远端判题系统并没有对齐。remote_judge 作为完整独立的判题后端服务,在功能上与 OJ 后端存在耦合:两者都用 RabbitMQ 做异步判题调度,各自维护独立的题库,导致项目合并时出现了不少问题。这篇博客重点记录解决上述耦合、完成系统联调的过程,以及随后用 MIMO 搭建 agent-service 做 AI 功能扩展的整体架构和流程设计。


一、耦合问题分析

合并前两个系统的状态:

flowchart LR subgraph OJ["OJ Backend (Gin :8080)"] direction TB OJ1["Submission Handler"] -->|"Publish → RabbitMQ"| OJ2["MQ Worker"] OJ2 -->|"gRPC → 内置 Judger"| OJ3[("MySQL (独立题库)")] end OJ ~~~|"各自独立<br/>无连接"| RJ subgraph RJ["remote_judge (chi :8080 / gRPC :9090)"] direction TB RJ1["Server"] -->|"Queue → Worker → gRPC"| RJ2["Judger"] RJ2 -->|"Docker Sandbox<br/>黑名单 → 编译 → 运行 → 比对"| RJ3[("MySQL (独立题库)")] end style OJ fill:#e3f2fd,stroke:#2196f3 style RJ fill:#e8f5e9,stroke:#4caf50

核心矛盾:

问题 OJ 后端 remote_judge 冲突点
消息队列 使用 RabbitMQ 异步调度 同样使用 RabbitMQ 两套队列,提交流转混乱
题库 自有 problems 自有 problems 题目数据双写,判题用例不一致
判题状态 简单的 Accepted / WA / CE 4 中间态 + 7 终态 + 丰富字段 状态模型不对齐
判题结果 扁平字段 完整 JudgeResult(traceId / runtimeMs / caseResults[] 等) 信息丢失
提交 ID uint64 自增 int64 自增 类型不一致

如果强行合并,要么两个 Worker 争抢 RabbitMQ 消息,要么题库数据不同步导致判题用例对不上。


二、两种集成思路

针对上述耦合问题,提出了两种解决思路。

思路一:薄代理 + 共享数据库

OJ 后端删掉所有调度代码变成纯粹的代理层,接收提交后直接转发给 remote_judge 的 cmd/server。调度、判题、结果持久化全权交给 remote_judge。同时两系统共享同一套 MySQL,判题用例通过题号直接查询,远端判题结果直接写入。

flowchart LR Frontend["前端 (Vue 3)"] -->|"HTTP"| OJ["OJ Backend (薄代理)<br/>仅转发提交,不调度"] OJ -->|"HTTP"| Server["remote_judge cmd/server<br/>接收提交"] Server --> Pipeline["Queue → Worker → Judger → Docker Sandbox"] Pipeline --> DB[("共享 MySQL<br/>题库 + 判题结果")] OJ --> DB Pipeline -->|"写入判题结果"| DB style OJ fill:#e3f2fd,stroke:#2196f3 style Server fill:#c8e6c9,stroke:#2e7d32 style Pipeline fill:#c8e6c9,stroke:#2e7d32 style DB fill:#f3e5f5,stroke:#9c27b0

优点: 判题用例不再经 gRPC 跨服务传输(大输入场景价值明显);OJ 后端专注业务,remote_judge 专注判题,职责清晰;共享数据库,数据天然一致。

缺点: 改动量大,OJ 后端需要删除调度模块、重构提交流程;数据库耦合降低部署灵活性。

思路二:直连 Judger 模块

remote_judge 内部本身就是解耦的——cmd/server 负责 HTTP API 和调度,cmd/judger 负责纯判题,两者通过 gRPC 通信。恰好 cmd/judger 的 gRPC 接口和 OJ 后端对判题的需求一致。因此 OJ 后端可以绕过 remote_judge 自己的 Server / Queue / Worker,通过 gRPC 直连 cmd/judger,把它当作一个"判题函数"来调用。

flowchart LR Frontend["前端 (Vue 3)"] -->|"HTTP"| OJ["OJ Backend (Gin :8080)"] OJ -->|"Publish"| RMQ[("RabbitMQ")] RMQ -->|"Consume"| Worker["MQ Worker (OJ 侧调度)<br/>读 MySQL 题目 + 测试点"] Worker -->|"gRPC JudgeRequest<br/>(code, language, testCases...)"| Judger["remote_judge cmd/judger<br/>判题引擎"] Judger -->|"Docker CLI"| Sandbox["Docker Sandbox<br/>C++17 / Go / Python3"] Judger -->|"JudgeResponse<br/>{status, runtimeMs, caseResults...}"| Worker Worker --> DB[("MySQL (OJ 侧题库)")] style OJ fill:#e3f2fd,stroke:#2196f3 style Worker fill:#e3f2fd,stroke:#2196f3 style Judger fill:#c8e6c9,stroke:#2e7d32 style Sandbox fill:#c8e6c9,stroke:#2e7d32 style DB fill:#f3e5f5,stroke:#9c27b0 style RMQ fill:#fff3e0,stroke:#ff9800

优点: 几乎零代码改动,改个配置指向 remote_judge 的 gRPC 地址即可;只需启动 cmd/judger,不需要 remote_judge 完整四服务;思路一能力完整保留在 cmd/server 中,可随时切换。

缺点: gRPC 消息体需包含完整判题用例;OJ 后端仍须维护自己的调度逻辑(Worker + RabbitMQ)。


三、选择与实施

优先选择了思路二。

权衡因素:

  1. 开发效率:思路二几乎零代码改动,思路一需要重构 OJ 后端的提交和调度模块
  2. 风险控制:先从改动最小的方案入手,快速验证链路可行性,后续再考虑架构升级
  3. 可逆性:思路一的完整能力保留在 cmd/server 中,随时可以切换过去

3.1 配置修改

编辑 config.yaml,把判题 gRPC 地址从内置 Judger 指向 remote_judge:

judger:
  grpc_addr: "127.0.0.1:9090"    # remote_judge cmd/judger 的 gRPC 地址
  remote: true                     # 启用远程判题模式

OJ 后端已有的 internal/judger/ 模块实现了 gRPC 客户端,使用自定义 JSON Codec(无需 protoc 编译)。Worker 从 RabbitMQ 消费判题任务后,构建 JudgeRequest 通过 gRPC 发给 remote_judge 的 Judger,获取 JudgeResponse 后写入 MySQL。

3.2 判题状态对齐

remote_judge 的 JudgeResult 远丰富于 OJ 后端原有的扁平状态。扩展了 submissions 表和相关模型以完整接收:

新增字段 来源 含义
trace_id JudgeResponse.traceId 全链路追踪 ID
runtime_ms JudgeResponse.runtimeMs 运行耗时 (ms)
memory_kb JudgeResponse.memoryKb 内存峰值 (KB)
compile_output JudgeResponse.compileOutput 编译输出
error_message JudgeResponse.errorMessage 错误信息
signal JudgeResponse.signal 终止信号
case_results JudgeResponse.caseResults JSON 字段,逐测试点详情

同时新增 case_results 子表存储每个测试点的判定结果,方便前端查询单个测试点的输入 / 期望输出 / 实际输出。

3.3 gRPC ID 类型适配

remote_judge 的 proto 使用 int64 类型 ID,OJ 后端使用 uint64。由于用的是 JSON Codec 而非二进制 protobuf,Go 的 encoding/json 对两者的 JSON 表示一致(都是数字),实际测试中未发现兼容性问题。

3.4 联调验证

启动顺序:

# 1. 基础设施
cd AIOJ-main/backend
docker compose -f docker/docker-compose.yml up -d mysql rabbitmq

# 2. remote_judge 判题服务
cd remote_judge
REMOTE_JUDGE_GRPC_ADDR=127.0.0.1:9090 go run ./cmd/judger

# 3. OJ 后端
cd AIOJ-main/backend
go run ./cmd/server -config config.yaml

# 4. 前端
cd AIOJ-main/frontend
npm run dev

联调过程并非一帆风顺。最初 OJ 后端的 gRPC 客户端使用旧版 JudgeRequest(缺少 run_mode 字段),导致判题结果一直显示 System Error。排查发现是 remote_judge 新加字段在 OJ 侧未设默认值,Judger 解析时认为请求不合法。补上默认值后解决。

调试中的判题沙箱运行截图:

判题沙箱运行截图1

判题沙箱运行截图2

代码沙箱对各种情况(AC、WA、CE、RE、TLE)都能给出正确的判别,逐测试点判定结果显示在 case_results 中,包含每个点的运行时间、内存用量和判定状态。


四、借助 MIMO 进行功能扩展

联调跑通后,系统已具备完整判题能力。但一个现代 OJ 平台仅有判题是不够的——用户代码失败了需要有人告诉他错在哪,通过了题目需要途径沉淀解题思路。这正是大模型的用武之地。

接下来一周多时间,以 mimo-v2.5-pro 为驱动,通过 vibe-coding 方式搭建了 agent-service 的完整架构,并为 OJ 系统扩展了 5 个核心 AI 能力。

4.1 架构定位

agent-service 定位为"AI 中间层"——不直接暴露给前端,所有请求经 OJ 后端转发。OJ 后端负责数据整理和上下文组装,agent-service 负责 AI 推理。

flowchart LR Frontend["前端 (Vue 3)"] -->|"HTTP (JWT)"| OJ["AIOJ Backend<br/>/api/ai/* :8080"] OJ -->|"HTTP (内部代理)<br/>组装上下文后转发"| Agent["agent-service<br/>/api/agent/* :8090"] OJ -->|"gRPC"| Judger["remote_judge<br/>cmd/judger"] Agent -->|"优先"| MIMO["MIMO API<br/>(主模型)"] Agent -->|"降级"| Ollama["Ollama<br/>(降级 + Embedding)"] style OJ fill:#e3f2fd,stroke:#2196f3 style Agent fill:#e8f5e9,stroke:#4caf50 style Judger fill:#fff3e0,stroke:#ff9800 style MIMO fill:#fce4ec,stroke:#e91e63 style Ollama fill:#f3e5f5,stroke:#9c27b0

核心设计原则:

  1. 用户不可感知:agent-service 不暴露端口给前端,所有 AI 请求须经 OJ 后端 JWT 鉴权和限流
  2. 职责分离:OJ 后端负责数据整理(查数据库、组装用户画像 + 提交历史 + 题目信息)、上下文组装、结果持久化;agent-service 只做 AI 推理
  3. 统一标签字典:OJ 后端维护 90+ 算法标签作为唯一字典,agent-service 启动时拉取缓存,AI Prompt 中注入标签列表,确保 AI 输出的算法标签与题库一致
  4. RAG 增强:agent-service 启动时索引 OI-Wiki 文档,请求时检索相关知识块注入 System Prompt,让 AI 回答有据可依

4.2 双 Provider 降级

flowchart TB Start(["API 请求"]) --> Check{AI_PROVIDER?} Check -->|"openai (默认)"| MIMO_First["调用 MIMO API<br/>mimo-v2.5-pro"] Check -->|"ollama"| Ollama_First["调用 Ollama<br/>qwen2.5-coder:7b"] MIMO_First --> MIMO_OK{成功?} MIMO_OK -->|"是"| MIMO_Result["返回结果"] MIMO_OK -->|"否"| MIMO_Fallback["降级: 调用 Ollama"] MIMO_Fallback --> MIMO_FB_OK{成功?} MIMO_FB_OK -->|"是"| MIMO_FB_Result["返回结果"] MIMO_FB_OK -->|"否"| MIMO_Error["'AI 服务暂时不可用'"] Ollama_First --> Ollama_OK{成功?} Ollama_OK -->|"是"| Ollama_Result["返回结果"] Ollama_OK -->|"否"| Ollama_Fallback["降级: 调用 MIMO API"] Ollama_Fallback --> Ollama_FB_OK{成功?} Ollama_FB_OK -->|"是"| Ollama_FB_Result["返回结果"] Ollama_FB_OK -->|"否"| Ollama_Error["'AI 服务暂时不可用'"] style MIMO_Result fill:#c8e6c9,stroke:#4caf50 style Ollama_Result fill:#c8e6c9,stroke:#4caf50 style MIMO_FB_Result fill:#fff9c4,stroke:#fbc02d style Ollama_FB_Result fill:#fff9c4,stroke:#fbc02d style MIMO_Error fill:#ffcdd2,stroke:#f44336 style Ollama_Error fill:#ffcdd2,stroke:#f44336

配置项 AI_PROVIDER 控制优先级,生产环境 openai(MIMO 为主),本地开发 ollama。Chat 和 Embedding 各自独立降级,互不阻塞。Embedding 使用独立模型 nomic-embed-text:latest,不占用对话模型资源。

4.3 RAG 检索增强

RAG 是 agent-service 核心差异化能力——让 AI 引用 OI-Wiki 知识而不是凭空编造。

flowchart TD subgraph Startup["启动阶段"] Load["加载 oiwiki_docs/*.md<br/>(52 篇)"] --> Split["langchaingo 分割<br/>chunkSize: 1000<br/>chunkOverlap: 200"] Split --> Embed["Embedding 向量化"] Embed -->|"成功"| VecIdx["向量索引"] Embed -->|"失败"| KWIdx["关键词索引 (降级)"] end subgraph Request["请求阶段"] Query["用户提问"] --> EmbQ["Query → Embedding"] EmbQ -->|"成功"| CosSearch["余弦相似度 Top-3"] EmbQ -->|"失败"| KWSearch["关键词匹配 Top-3"] CosSearch --> Build["构建 RAG 上下文"] KWSearch --> Build Build --> Prompt["注入 System Prompt"] Prompt --> LLM["LLM 推理"] end style VecIdx fill:#c8e6c9,stroke:#4caf50 style KWIdx fill:#fff9c4,stroke:#fbc02d style CosSearch fill:#c8e6c9,stroke:#4caf50 style KWSearch fill:#fff9c4,stroke:#fbc02d

设计核心理念是"向量优先 + 关键词降级"——宁可降低检索精度也不报错,保证 AI 功能始终可用。修复后 RAG 系统成功索引 600 个文档块。(RAG 的详细实现和迭代过程见(二)。)

4.4 五个 AI 功能

基于上述架构,设计并实现了 5 个核心 AI 功能:

功能 触发场景 核心设计
代码诊断 判题失败后点击"获取诊断" OJ 后端组装题目 + 代码 + 判题结果 + 最近 3 次提交历史;agent-service 注入 RAG 知识 + 标签字典后调用 LLM,返回结构化诊断(summary / issues[] / suggestions[])
解题辅助 做题时获取帮助 三级帮助:hint(提示不给代码)、explain(思路不给代码)、full(完整代码 + 判题验证状态机,最多 3 轮自修正)
AI 对话 侧边栏自由对话 自动注入题目上下文 + 编辑器代码;持久化到 conversations 表;多轮记忆;RAG 自动检索注入
题解生成 通过题目后点击"AI 生成题解" 提交历史按规则筛选(最近 3 天内最多 5 条、含最近一次 AC);前端自动填充到题解编辑器
知识图谱 点击"整理我的知识图谱" OJ 后端统计用户做题数据;agent-service 生成 nodes[] + edges[] + suggestions[];前端 ECharts 层级布局渲染

其中 Solve full 级别的判题验证状态机是设计最复杂的功能——AI 生成的代码不一定正确,需要实际跑一遍来验证:

flowchart TD S1["① 发送题目信息给 agent-service<br/>请求生成代码"] --> S2["② agent-service 返回代码"] S2 --> S3["③ OJ Backend 调用 remote_judge<br/>判题 (不保存记录)"] S3 --> S4{"④ 判题结果 == AC?"} S4 -->|"是"| S_OK["返回代码给前端"] S4 -->|"否"| S5["⑤ 将判题结果 + 错误信息<br/>发回 agent-service<br/>agent-service 修改代码"] S5 --> S6{"⑥ 重试次数 < 3?"} S6 -->|"是"| S3 S6 -->|"否"| S_FAIL["返回: 抱歉,我也无法通过此题"] style S_OK fill:#c8e6c9,stroke:#4caf50 style S_FAIL fill:#ffcdd2,stroke:#f44336

调用链路:agent-service → OJ Backend /api/problems/:id/run → remote_judge gRPC → Docker Sandbox。同时判题等待从硬编码 time.Sleep(2s) 优化为轮询(500ms × 10 次,终态提前返回)。

(五个 AI 功能的 Prompt 设计、数据流时序图、Handler 实现细节见(二)。)


五、MIMO 优化成果

借助 mimo-v2.5-pro 的 vibe-coding 能力,一周多时间完成了大量系统优化。MIMO 优化后的系统:

优化后截图1

优化后截图2

优化后截图3

优化后截图4

优化后截图5

Agent-Service 侧

改进项 说明
RAG 系统完整实现 种子数据 + 爬虫 + Embedding 修复,600 个文档块已索引
AI JSON 结构化输出 所有端点 Prompt 要求 JSON 返回,解析失败降级 rawMarkdown
双 Provider 回退修复 ollama / openai 模式各自正确降级
AI 沙箱调用集成 Solve full 自动调 remote_judge 判题验证
请求体大小限制 Gin 中间件 1MB,防 OOM
双重 Body 读取修复 CodeDiagnosis / Solve 改为 io.ReadAll 一次读取
Judge 验证轮询 硬编码 sleep → 500ms × 10 次轮询
Embedding 模型独立 OpenAI Provider 用 text-embedding-3-small
.env 解析增强 处理引号、注释、空值
优雅关闭 SIGINT / SIGTERM 触发 Shutdown

OJ Backend 侧

改进项 说明
Worker 判题重试 指数退避(500ms / 1s / 2s,最多 3 次)
AI 端点速率限制 复用 PerUserRateLimit,10/min
JWT 角色实时校验 新增 RequireAdminDB 查库验证
Rating 历史曲线 rating_history 表 + ECharts 折线图
题目列表分页修复 过滤条件下 total 计数修正
提交 ID 多实例安全 数据库序列表原子递增
N+1 查询消除 推荐查询改单次 WHERE IN
Mastery 计算优化 全表扫描 → GROUP BY + COUNT
AI 失败 code 修正 失败返回 code:-1
Rating 默认值统一 三处统一为 1200

Frontend 侧

改进项 说明
知识图谱层级化 按难度分层 + 跨类别关联 + 颜色/大小反映掌握度
CodeEditor 功能补充 全屏 + ESC 退出 + wordWrap 切换
代码块复制按钮 MarkdownRenderer 自动为 pre 添加
多页面错误处理 StudyPlan / Stats / AdminAuditLogs 加 catch
Mock 清理 VITE_USE_MOCK 环境变量控制
死代码清理 MySolutions / SolutionDetail / mock.js 移除
标签动态获取 ProblemList 改为 API 获取

最终统计

  • P0 项:6/6 完成
  • P1 项:7/7 完成
  • P2 项:7/7 完成
  • P3 项:4/4 完成
  • 数据库缺陷:10/10 完成
  • 总计:34/34 核心改进项完成(100%)

六、总结

本次工作完成了两件核心事情:

第一,OJ 系统集成。 分析了 OJ 后端与 remote_judge 之间的耦合问题(两套队列、各自题库、状态不对齐、ID 类型不一致),提出了薄代理 + 共享数据库(思路一)和直连 Judger 模块(思路二)两种解决思路。优先选择了改动量最小、风险最低的思路二——gRPC 直连 cmd/judger,同时保留思路一作为后续架构升级的备用方案。联调过程中解决了 gRPC 接口适配(run_mode 字段缺失)、判题状态对齐(7 个新字段)、ID 类型兼容等问题,最终跑通了"前端提交 → OJ Backend → RabbitMQ → Worker → gRPC → remote_judge → Docker Sandbox → 结果回写 → 前端展示"的完整链路。

第二,借助大模型进行功能扩展。 以 mimo-v2.5-pro 为驱动,搭建了 agent-service 的完整架构——双 Provider 降级策略、RAG 检索增强(600 个 OI-Wiki 文档块)、统一标签字典(90+ 标签)、5 个核心 AI 功能(代码诊断、解题辅助含判题验证状态机、AI 对话含多轮记忆和 RAG 注入、题解生成、知识图谱生成)。同时在代码审查中修复了 34 项缺陷,覆盖后端、前端、agent-service、数据库四个维度。

(二)将深入展开 agent-service 的架构设计思路、关键模块实现细节,以及用 MIMO 做 vibe-coding 的迭代经验。

博客中涉及的完整代码见 fused 融合仓库:AIOJ-main/remote_judge/agent-service/

posted @ 2026-06-09 23:30  宋佳奇  阅读(6)  评论(0)    收藏  举报