1. 为什么我要用 Vibe Coding 搭一个 C++17 在线判题系统
在线判题系统(Online Judge,简称 OJ)是每个学编程的人都绕不开的东西:你提交一段 C++17 代码,后台编译、跑测试用例、比对输出、返回 AC/WA/TLE。听起来像是一个"大厂内部系统",但真动手写,核心逻辑其实没那么玄乎——难的是环境、编译参数、沙箱隔离、上下文管理这些琐碎活儿。
我这次想聊的不是"如何从零手写一个 OJ",而是用 Vibe Coding 的方式,在 OpenCode 这个终端 AI 编程工具里,把 C++17 判题系统的最小可用版本跑起来。所谓 Vibe Coding,就是你用自然语言描述意图,AI 帮你补全代码、改配置、查报错,你负责把关方向。它特别适合 OJ 这种"逻辑清晰但细节多"的项目。
这篇文章面向三类人:一是刚接触 AI CLI 工具、想找个真实项目练手的开发者;二是写过 C++ 但没搭过判题服务的同学;三是已经在用 OpenCode,但不知道怎么管理上下文、怎么让 AI 记住项目约定的朋友。读完之后,你应该能拿到一份可复制的settings.json骨架、一段 OpenCode 配置片段,并且亲手启动判题服务、提交一份样例代码看到结果。
整个流程我会拆成六块:先讲清楚问题和场景,再准备 TaoToken 作为模型接入层,然后给可复制的配置,接着验证请求,再排常见错误,最后按不同需求分流到对应的入口。全程命令和参数都能直接抄。
2. 前置准备:Node.js、npm 与 TaoToken 接入
2.1 Node.js 和 npm 到底在干嘛
OpenCode 是用 Node.js 写的 CLI 工具,所以你得先有 Node.js 运行时。打个比方:Node.js 就像 Python 解释器,npm 就像 pip。你装完 Node.js,node和npm两个命令就都有了。
Linux(以 Ubuntu 24.04 为例)下,系统自带的 apt 源版本往往偏低,建议引入 NodeSource 源装 v24:
curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash - sudo apt install -y nodejs node -v npm -vWindows 11 更简单,去 Node.js 官网下载 LTS 安装包,一路 Next,然后在 PowerShell 里node -v验证即可。装完确认版本号在 v20 以上,否则 OpenCode 可能跑不起来。
2.2 安装 OpenCode
OpenCode 通过 npm 全局安装,Windows 和 Linux 命令一致:
sudo npm i -g opencode-ai opencode --version装完之后,先别急着写代码。OpenCode 的工作目录就是你运行opencode命令时所在的目录,它默认只能操作这个目录及其子目录。所以正确姿势是:先建项目目录,cd进去,再启动。
mkdir cpp17-judge && cd cpp17-judge opencode2.3 为什么模型接入层选 TaoToken
OpenCode 本身不带模型,你得给它接一个模型提供方。TaoToken 在这里扮演的是"统一模型接入层"的角色:你拿到一个 API Key,就能在 OpenCode 里配置模型,不用为每个模型单独折腾一套鉴权。
具体操作:登录 TaoToken 控制台,在 API Keys 页面创建一个 Key,复制保存。这个 Key 后面会写进 OpenCode 的配置里。控制台地址是 https://taotoken.net/console ,API 端点是 https://taotoken.net/api ,注意 API 地址后面不加任何多余路径。
提示:API Key 只显示一次,创建后立刻保存到本地密码管理器或环境变量里,别直接提交到 Git。
如果你还没决定用哪个模型,可以先在模型对话页面试一下手感,确认响应速度和代码能力符合预期,再写进配置。模型对话入口:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat
3. 可复制配置:settings.json 骨架与 OpenCode 片段
3.1 项目目录结构
先把判题系统的骨架搭出来。最小可用版本我建议这样分:
cpp17-judge/ ├── settings.json # 判题服务配置 ├── AGENTS.md # 给 OpenCode 的项目约定(持久上下文) ├── src/ │ ├── server.js # 判题 HTTP 服务 │ └── judge.js # 编译 + 运行 + 比对逻辑 ├── submissions/ # 用户提交的代码 ├── testcases/ # 测试用例 │ └── a-plus-b/ │ ├── 1.in │ ├── 1.out │ ├── 2.in │ └── 2.out └── package.json3.2 settings.json 骨架
这份配置定义了判题服务的端口、编译参数、超时和内存限制。C++17 的关键是-std=c++17,别写成-std=c++1z那种老写法。
{ "server": { "port": 3000, "host": "127.0.0.1" }, "judge": { "compiler": "g++", "compileFlags": ["-std=c++17", "-O2", "-Wall", "-static"], "timeLimitMs": 2000, "memoryLimitMb": 256, "outputLimitKb": 1024, "workDir": "./submissions", "testcaseDir": "./testcases" }, "model": { "provider": "taotoken", "baseUrl": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "modelName": "your-model-name" } }几个参数值得单独说:-static让编译产物静态链接,避免运行环境缺库;timeLimitMs设 2000 是给 C++ 留足余量,实际判题可以按题目调整;memoryLimitMb在 Linux 下配合ulimit或 cgroup 生效,Windows 下需要另想办法。
3.3 OpenCode 配置片段
OpenCode 的模型配置可以写在项目根目录的配置文件里,也可以通过命令面板交互式接入。推荐前者,方便版本管理。把 API Key 放进环境变量,配置文件里只引用变量名:
export TAOTOKEN_API_KEY="sk-你的key"然后在 OpenCode 里通过ctrl+a进入 model provider 选择界面,选 TaoToken,粘贴 Key,模型就接入了。如果你想让配置持久化,可以在 OpenCode 的配置目录里写:
{ "provider": { "taotoken": { "type": "openai-compatible", "baseURL": "https://taotoken.net/api", "apiKey": "{env:TAOTOKEN_API_KEY}" } }, "model": "taotoken/your-model-name" }{env:TAOTOKEN_API_KEY}这种写法让 OpenCode 从环境变量读取,避免明文写 Key。接入文档里有更完整的字段说明,遇到字段不识别可以去查:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
3.4 AGENTS.md:让 AI 记住项目约定
这是 Vibe Coding 里最容易被忽略、但最影响体验的一环。OpenCode 会把AGENTS.md的内容作为系统提示词的一部分,相当于给 AI 一份"项目说明书"。我建议至少写清楚这几条:
# 项目约定 - 语言标准:C++17,编译命令固定为 g++ -std=c++17 -O2 -Wall -static - 判题结果枚举:AC / WA / TLE / MLE / RE / CE - 服务框架:Node.js 原生 http 模块,不引入 Express - 所有用户提交代码写入 submissions/,文件名用 submission_<id>.cpp - 测试用例目录结构:testcases/<problem-id>/<case>.in 和 .out - 不要修改 settings.json 里的 compileFlags,除非我明确要求有了这份约定,你后面让 AI 改代码时,它不会突然给你换成 C++20,也不会自作主张引入一堆依赖。这就是"上下文管理"的第一层:用文件固化长期约定。
4. 启动判题服务并提交样例代码
4.1 用 OpenCode 生成判题核心逻辑
进入项目目录,启动 OpenCode:
cd cpp17-judge opencode在 OpenCode 里,用Tab键切到 Build Agent(能改代码的那个),然后输入你的意图。比如:
请实现 src/judge.js,导出一个 judge(submissionPath, testcaseDir) 函数。 要求:用 child_process 调用 g++ 编译,编译失败返回 CE 并带上 stderr; 编译成功后逐个跑测试用例,用 spawn 执行可执行文件,超时 2000ms 返回 TLE; 比对 stdout 和 .out 文件,忽略行尾空格,不一致返回 WA。OpenCode 会生成代码,你审一遍逻辑,重点看超时处理和输出比对。如果它用了exec而不是spawn,让它改——exec不方便做超时 kill。
4.2 启动服务
src/server.js用 Node.js 原生http模块起一个 POST 接口,接收代码和题目 ID:
const http = require('http'); const fs = require('fs'); const path = require('path'); const { judge } = require('./judge'); const settings = require('../settings.json'); const server = http.createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/submit') { res.writeHead(404); return res.end('not found'); } let body = ''; req.on('data', chunk => body += chunk); req.on('end', async () => { const { problemId, code } = JSON.parse(body); const submissionPath = path.join( settings.judge.workDir, `submission_${Date.now()}.cpp` ); fs.writeFileSync(submissionPath, code); const result = await judge(submissionPath, path.join(settings.judge.testcaseDir, problemId)); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); }); }); server.listen(settings.server.port, settings.server.host, () => { console.log(`judge server on http://${settings.server.host}:${settings.server.port}`); });启动:
node src/server.js看到judge server on http://127.0.0.1:3000就说明服务起来了。
4.3 准备测试用例
以经典的 A+B 为例,testcases/a-plus-b/1.in:
1 21.out:
3再补一组边界用例,2.in写-1000000 1000000,2.out写0。
4.4 提交样例代码验证
另开一个终端,用 curl 提交:
curl -X POST http://127.0.0.1:3000/submit \ -H "Content-Type: application/json" \ -d '{ "problemId": "a-plus-b", "code": "#include <iostream>\nint main(){long long a,b;std::cin>>a>>b;std::cout<<a+b<<std::endl;return 0;}" }'预期返回:
{"status":"AC","passed":2,"total":2,"timeMs":12}如果返回CE,看返回体里的stderr字段,通常是头文件或语法问题。如果返回WA,检查输出比对逻辑是不是把换行也算进去了。
5. 本篇常见错误排查
5.1 编译报错:g++ 找不到或版本太低
现象:提交后立刻返回 CE,stderr 里写g++: command not found或error: unrecognized command-line option '-std=c++17'。
原因:系统没装 g++,或者版本低于 5.0(C++17 需要 g++ 5 以上,完整支持建议 g++ 9+)。
解决:
g++ --version sudo apt install -y g++ build-essentialUbuntu 24.04 默认源里的 g++ 版本足够新,一般不会踩这个坑。
5.2 超时误判:TLE 但本地跑得飞快
现象:本地手动跑可执行文件 10ms 出结果,判题服务却返回 TLE。
原因:spawn的超时计时从进程创建开始算,如果编译产物是动态链接、首次加载慢,或者测试用例输入很大,都可能超。另外,如果你在judge.js里用了setTimeout但没kill子进程,超时后进程还在跑,会污染后续用例。
解决:确认超时后调用child.kill('SIGKILL'),并且把timeLimitMs按题目难度调整,别一刀切 1000ms。
5.3 上下文爆炸:AI 开始答非所问
现象:聊了十几轮之后,OpenCode 生成的代码开始偏离项目约定,比如突然用 C++20 的std::format,或者把settings.json的字段名改了。
原因:上下文窗口被历史对话塞满,LLM 注意力分散。这是 Vibe Coding 最典型的坑。
解决:三个动作配合使用。第一,把关键约定写进AGENTS.md,让它作为系统提示词常驻;第二,用/compact压缩当前会话,把历史总结成摘要;第三,针对新功能点用/new开新会话,别在一个 session 里从判题逻辑聊到前端页面。
5.4 模型接入失败:401 或 model not found
现象:OpenCode 里发消息报鉴权错误,或者提示模型不存在。
原因:API Key 没设进环境变量,或者baseURL写错(比如多加了/v1),或者模型名和 TaoToken 控制台里的不一致。
解决:先echo $TAOTOKEN_API_KEY确认变量有值;再检查配置里的baseURL是不是https://taotoken.net/api,不要带多余路径;最后核对模型名。如果还不行,去 API Keys 页面重新生成一个 Key 试试:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys
5.5 工作目录搞错:OpenCode 改不到文件
现象:让 OpenCode 改src/judge.js,它说找不到文件,或者改到了别的地方。
原因:启动opencode时不在项目根目录。OpenCode 只能操作工作目录内部的内容。
解决:退出,cd到项目根目录,重新opencode。养成习惯:先pwd确认位置,再启动。
6. 按需求分流:接下来该往哪走
跑通最小判题流程之后,你的下一步取决于目标。
如果你是想长期用 AI 辅助写代码、搭 Agent 工作流,那 Coding Plan 更适合你,它针对持续编码场景做了上下文和额度的优化:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan
如果你只是想先验证某个模型写 C++ 判题逻辑靠不靠谱,直接在模型对话里贴代码片段试就行,不用配 OpenCode:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat
如果你在接入过程中卡在鉴权或字段配置上,先看接入文档,大部分报错都有对应说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
如果你用的是 Claude Code 那套工作流,想对比一下配置方式,可以看这个入口:https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=claude_code
最后说个我自己的习惯:每次开新功能点之前,先花两分钟更新AGENTS.md,把这次要遵守的约定写进去,再/new开一个干净会话。这样 AI 不会带着上一轮的包袱干活,判题系统的编译参数和结果枚举也不会莫名其妙被改掉。上下文管理这件事,工具给了你/compact和/new,但真正决定效果的,是你有没有把项目约定固化下来。