grading-backend
给 AI 培训项目后端实现打分。50 学员分成 ~12 组,按 spec-driven 方式实现本地部署的后端服务。本 skill 让 Claude(或 Qoder / Cursor / Claude Code agent)按统一 rubric 给某一组的后端打分,产出可给学员的 markdown 报告 + 可汇总 leaderboard 的 JSON。
前置依赖
- 本地可跑:待评后端能在本机启动(
npm run dev/uvicorn .../go run ./..././gradlew bootRun等),默认http://localhost:8080(按项目 README 调整),有 health check 端点(/health//healthz//ping)或其他就绪信号。 - Spec 文档位置:
<project>/docs/spec.md或<project>/README.md或<project>/spec/*。找不到则先向用户确认 spec 路径再开工,不要凭空评分。 - Node.js 环境:Node ≥ 18,用来跑
npx ajv-cli校验 JSON、npx autocannon做压测。 - curl:所有接口探针和取证命令基于 curl,macOS / Linux 自带即可。
- 输出目录:所有证据和报告写到待评项目根目录下的
.grading/(已由 spec 约定 gitignore)。
约定的工作目录:所有 probe 脚本和 .grading/ 输出都默认在待评项目根目录下执行。即评分时:
- cd 到被评项目(如
cd group-workshop/group-2) - 用绝对路径调用 probe:
bash $WORKSHOP_ROOT/skills/grading-backend/probes/probe-robustness.sh team-2 http://localhost:8080 /api/items .grading/会落到被评项目根下,与该项目共生
共享契约(必读)
评分开始前,必须先读完本文件 ## 评分标尺 / ## 报告模板 / ## 证据硬约束 三段,以及 ../score-schema.json。这些是 frontend / backend 两个 skill 共用的硬约束,不读会导致输出格式不一致、无法汇总 leaderboard:
- 见本文件 ## 评分标尺 — 0–10 标尺含义 + S/A/B/C/D 档位定义。所有维度打分必须对照此标尺。
- 见本文件 ## 报告模板 — 统一 Markdown 报告模板。产出的
report.md必须照此结构填充。 ../score-schema.json— JSON 摘要的 schema。产出的summary.json必须通过此 schema 校验。- 见本文件 ## 证据硬约束 — 打分 ≥7 或 ≤4 必须 ≥2 条证据,5–6 分必须 ≥1 条。违反则打分视为无效。
评分标尺(0–10)
本段是 grading-frontend 与 grading-backend 两个 skill 共享的唯一打分锚点。两侧所有维度得分都必须按本表落点,再用证据说明为什么不是相邻的锚点。
1. 分数锚点
| 分数 | 名称 | 判定标准 |
|---|---|---|
| 10 | 工业级范本 | 可直接作为下一届教学样例;评分人找不到该维度的改进空间;相对已见过的最好学员作业也明显领先 |
| 9 | 优秀 | 核心完全到位,仅 1–2 处非关键瑕疵(例如边缘场景文案、极端断点、冷路径日志) |
| 7 | 良好 | 主流程到位,但存在 ≥3 处可改进;不影响交付但明显看得出赶工痕迹 |
| 5 | 及格 | Happy path 能跑,但明显粗糙;有 1 类系统性漏洞(如缺状态、缺校验)但未致命 |
| 3 | 不及格 | 该项存在尝试痕迹但严重不完整;主要子项缺失或逻辑错误 |
| 0 | 未实现 | 完全缺失,或实现了但跑不起来 |
| N/A | 不适用 | spec 明确未要求,或该维度在本项目栈上无意义;必须写原因,不计入总分 |
允许的分数:0、3、5、7、9、10 是锚点;1/2/4/6/8 仅在证据支持"介于两锚点之间"时使用。不允许出现小数。
2. 打分操作规则
- 先选锚点,再微调:拿到证据后先对齐最接近的锚点(10/9/7/5/3/0),再用证据决定 ±1 微调。禁止"先拍个 8 再补证据"。
- 高低分必须有证据:
score ≥ 7→ 至少 2 条证据score ≤ 4→ 至少 2 条证据5–6→ 至少 1 条证据- 详细规则见本文件
## 证据硬约束段
- N/A 必须写原因:
na_reason字段必填,格式"spec §X.Y 未涉及此项"或"技术栈 Z 无此概念"。N/A 不计入总分。 - 不能同时低分 + 高评:如果评语是"很好,只差一点",分数不能 ≤ 5。
- 锚点冲突时就低不就高:若一个维度既符合 9 的正面描述也命中 5 的负面描述,取 5(警戒作用)。
3. 总分计算
公式(N/A 不计入分母):
total = Σ(score_i × weight_i) / Σ(weight_i for dim_i where score_i != null) × 100 / 10
等价形式:先对非 N/A 维度算加权平均(0–10),再把有效权重归一化到 100,最后乘 10 映射到 0–100。
例子:前端 10 维总权重 100,若 微文案 (权重 6) 被判 N/A:
- 有效权重总和 = 94
- 其余 9 维加权和 = Σ(score_i × weight_i)
total = (Σ / 94) × 10
4. 总分档位
| 档位 | 分数区间 | 含义 |
|---|---|---|
| S | ≥ 90 | 可作为本届教学样例 |
| A | 80–89 | 合格交付,小范围打磨即可上线 |
| B | 70–79 | 主流程 OK,系统性问题需要返工 |
| C | 60–69 | 能 demo,不能交付;多处结构性缺陷 |
| D | < 60 | 未达培训目标,需重做关键模块 |
档位只用于班级 leaderboard 横向对比,学员反馈以维度得分 + 证据 + 改进清单为主。
报告模板
两个 grading skill 产出的 .grading/reports/<team>-<side>.md 必须严格按本模板。字段缺一不可;无信息时填 —,不要省略行。模板本体用四个反引号包裹的 Markdown 代码块呈现,复制时去掉最外层反引号。
使用说明
- 占位符形如
{队伍名},落地时替换为实际值 - 维度详评块按
grading-frontend10 维 /grading-backend9 维完整展开,不能合并或省略 - 任一维度证据不足时,score 填
null,na_reason填EVIDENCE_MISSING: ...,并在末尾"评分风险"段汇总 - 报告的同级必须存在对应 JSON:
<team>-<side>.json,schema 见../score-schema.json
模板
# 项目评分报告 — {队伍名}({前端 | 后端})
- 评分人:{Claude@model-id / 助教姓名}
- 日期:{YYYY-MM-DD}
- Spec 版本:{git sha 或 docs/spec.md 路径}
- 被评项目 commit:{sha}
- 评分耗时:{xx 分钟}
## 总分:{score} / 100 — 等级 {S | A | B | C | D}
## 维度汇总
| # | 维度 | 得分 | 权重 | 加权 | 一句话 |
|---:|---|---:|---:|---:|---|
| 1 | {维度名} | {0–10 或 N/A} | {w} | {score×w/10} | {一句话概述} |
| 2 | ... | ... | ... | ... | ... |
> 有效权重总和:{Σw_non-NA};N/A 维度:{列表或 "无"}
## 逐维度详评
### 1. {维度名} — {score}/10(权重 {w})
**证据**
- `path/to/file.ext:L12-L40` — {这条证据说明了什么}
- 截图:`.grading/shots/{team}-{view}.png`
- 命令:`curl -s localhost:8080/api/x` → `.grading/probes/{team}-x.log`
**要到 10 差什么**
1. {具体、可操作的改进建议}
2. {...}
3. {...}
---
### 2. {维度名} — {score}/10(权重 {w})
(同样模板,完整列出所有维度)
---
## 亮点
- {值得表扬的 2–4 条,要带证据}
## 最该优先修的 3 件事
1. **{问题}** — 影响:{哪个维度掉了多少分};改法:{一两句话说清}
2. ...
3. ...
## 评分风险(可选)
仅当出现以下情况时填写,否则删除整节:
- 证据不足被判 `EVIDENCE_MISSING` 的维度列表
- 评分人对某维度存疑但无法进一步取证的原因
- 项目未跑起来 / spec 缺失等影响评分可信度的事实
校验清单(写完报告后自检)
- 总分 = Σ(加权列) × 10 / Σ(有效权重),数值对得上
- 每个维度都有至少一条引用(证据 / 截图 / 日志 / spec 条目)
- 所有 N/A 都有
na_reason - "最该优先修的 3 件事" 每条都回指了具体维度
- 对应 JSON 已写入
.grading/reports/<team>-<side>.json
证据硬约束
本段是打分是否有效的判定规则。grading-frontend 和 grading-backend 两个 skill 在写入每个维度分数前必须先检查本表;证据不达标的打分视为无效。
设计目的:防止 agent 在缺乏实际观察的情况下拍脑袋打分。宁可多一个 EVIDENCE_MISSING,不要编一个好看的分数。
1. 证据条数规则
| 分数段 | 最少证据条数 | 说明 |
|---|---|---|
| 9–10 | 3 | 高分必须可复现;至少覆盖:1 条代码引用 + 1 条运行期证据(截图/probe/log) + 1 条 spec 比对 |
| 7–8 | 2 | 需同时体现"主流程到位"与"非关键瑕疵" |
| 5–6 | 1 | 一条即可,但必须能指出具体缺陷 |
| 3–4 | 2 | 低分不能靠印象;必须列出 ≥2 个具体失败点 |
| 0 | 1 | 至少证明"真的没有":grep 结果、spec 条目 + 代码库缺失、启动失败日志 |
| N/A | 1 | 必须引用 spec 条目或技术栈说明,证明该项"不适用" |
2. 证据合法形式
每条证据必须是以下 4 类之一,不得为自然语言转述:
- 代码引用:
path/to/file.ext:L{start}-L{end}或path/to/file.ext:L{line}- 路径必须相对项目根
- 必须真实存在(评分前 open / grep 确认过)
- 截图:
.grading/shots/<team>-<view>.png- 文件必须存在;后端维度一般不使用
- 可附简短说明:"登录页 desktop 1280×800,有 layout shift"
- 命令日志:
.grading/probes/<team>-<probe>.log或在证据行内直接给出命令与关键输出- 形如:
curl -s -o /dev/null -w "%{http_code}" localhost:8080/api/x -d '{}' → 500 - 压测结果引用 autocannon 输出文件
- 形如:
- Spec 引用:
spec §3.2/docs/spec.md:L120-L145/ issue 链接- 判定 N/A 或 Spec 一致性时必用
3. 违规处理
3.1 证据不足
若按 §1 无法凑够所需证据条数:
score置为nullna_reason写为"EVIDENCE_MISSING: 需要 N 条证据,仅能取得 M 条(已尝试:<动作清单>)"- 在报告末尾
## 评分风险段落列出该维度 - 该维度不计入总分分母(与正常 N/A 一致),但在评分风险段标注"非 spec 排除项"
3.2 证据伪造 / 占位
评分人/agent 若写入以下任一形式的证据,该维度直接作废并降级为 EVIDENCE_MISSING:
- 路径不存在的 file:line(lint:评分后用
test -f批量校验) - 截图文件不存在或大小为 0
- 命令日志内容与结论自相矛盾
- 描述性语言("代码组织得不错")未附具体引用
3.3 高低分无证据
score ≥ 7但证据 < 2 条 → 降为 6,或标EVIDENCE_MISSINGscore ≤ 4但证据 < 2 条 → 升为 5,或标EVIDENCE_MISSING
选择哪个处理取决于评分人对维度的把握;两种方式都必须在"评分风险"中注明。
4. 证据收集建议顺序
先做便宜的、可批量的,再做昂贵的:
- 静态 (cheap):
grep/rg扫关键词、看 package.json / schema / migration - Spec 对齐:逐条把 spec 检查项对到代码文件
- 运行期:启 dev server / 后端服务,跑截图脚本 / probe 脚本
- 交互:走完主流程,看 console / network / 日志
- 测试:跑 test suite,看覆盖率与失败清单
每跑完一步把产物落盘到 .grading/,在报告证据行里引用相对路径即可。
配套资源
按需读取(不是每次都读全部):
./anti-patterns.md— 后端反模式清单。打"健壮性"、"代码分层"、"性能"三个维度时必读,用来识别 controller 里写 SQL、裸try/except: pass吞异常、循环里 query 造成 N+1、写操作非幂等、错误响应结构每接口不同等典型滑坡。./examples/good-report.md和./examples/mediocre-report.md— 两份标定样例报告。打分前读一次做分布校准,避免全班都打 8 分这种分不开档的问题。./probes/probe-robustness.sh— 健壮性探针组合,跑 8 个场景(空 body / 错类型 / 超长字段 / 缺必填 / 越权 / 幂等重放 / 并发冲突 / 未登录)。取证时直接bash ./probes/probe-robustness.sh <team> <base-url> <path>,不要手搓 curl。每个 probe 写.grading/probes/<team>-<probe-name>.log,总表写.grading/probes/<team>-robustness-summary.md。./probes/probe-performance.sh— 基于npx autocannon的 p50/p95/p99 压测脚本,输出.grading/probes/<team>-autocannon.log(原始 autocannon 输出)和.grading/probes/<team>-perf-summary.txt(解析后的 p50/p95/rps + rubric 建议)。
评分流程(5 步)
步骤 1:定位与启动
- 确认 spec 路径,记录
spec_sha(若 spec 在 git 仓库内)。 - 读完 spec,列出:接口清单(method + path + 入参 + 出参 + 状态码) / 数据模型 / 权限角色 / 关键写操作。
- 读
package.json/pyproject.toml/go.mod/pom.xml识别技术栈(Node/Python/Go/Java、ORM、测试框架、日志库),记录project_sha。 - 启动后端服务,确认
http://localhost:<port>/<health>可访问(curl 拿到 200 或 spec 约定的就绪响应)。如启动失败直接记录在报告"启动阻塞"一节,本维度不硬给 0 分,但在"Spec 一致性"里扣分。
步骤 2:收集证据(evidence-first)
静态证据:
- 用 Grep/Glob 扫接口清单、分层目录(controller/service/repo)、schema / 迁移、测试目录、日志代码。
- 为每个维度预留至少 1 处
file:line引用。
动态证据:
# 健壮性:对主要写接口跑一遍 8 个 probe
bash ./probes/probe-robustness.sh <team> http://localhost:8080 /api/items
# 性能:对 GET 主列表接口 & POST 主写接口各压一次
bash ./probes/probe-performance.sh <team> http://localhost:8080 /api/items
# 测试套件(按栈择一)
npm test -- --coverage # Node
pytest --cov # Python
go test -cover ./... # Go
# 手工 curl:对 spec 列出的每个接口至少调一次,记录 method + path + 状态码 + 响应体
curl -i -X GET http://localhost:8080/api/items
curl -i -X POST http://localhost:8080/api/items -H 'Content-Type: application/json' -d '{"name":"demo"}'
所有 probe 日志落到 .grading/probes/<team>-*.log,手工 curl 的关键输出也要 tee 进 .grading/probes/<team>-curl.log。
交互证据:按 spec 的主业务流(注册 → 登录 → 创建 → 查询 → 更新 → 删除,或 spec 指定流程)串一遍,记录每步状态码、响应体片段、异常。
步骤 3:按 9 维度逐条打分
⚠️ 取证不足时的强制行为
- 维度打分 ≥7 或 ≤4 时,证据条数低于 evidence-requirements.md 规定的最低条数 → score 必须置 null,na_reason 写 "EVIDENCE_MISSING: 已尝试 X、Y,未能取得 Z"
- 这与 "spec 未要求" 的 N/A 是两回事:N/A 写 "N/A — spec §X.Y 未规定"
- 所有 EVIDENCE_MISSING 维度必须在报告底部 "## 评分风险" 段汇总列出
对照本文件下方 rubric(以及本文件 ## 评分标尺 段),逐维度 0–10 打分。每一条打分必须附证据(file:line / curl log 行号 / probe log 行号 / 测试输出行号)。spec 未要求的维度写 "N/A — 原因",不计入总分。
步骤 4:产出 report.md + summary.json
按本文件 ## 报告模板 段填 .grading/reports/<team>-backend.md;按 ../score-schema.json 填 .grading/reports/<team>-backend.json。每维度附"要到 10 差什么"2–4 条。
步骤 5:自检(必须通过)
# JSON schema 校验
npx ajv-cli validate -s skills/score-schema.json \
-d .grading/reports/<team>-backend.json
# 证据数量自检(每维度统计 file:line + probe log + curl log 引用总数)
grep -cE "\.(ts|js|py|go|java|sql|log):" .grading/reports/<team>-backend.md
自检不过则回到步骤 2 补证据,不得放过。
9 个评分维度(权重合计 100)
| # | 维度 | 权重 |
|---|---|---|
| 1 | Spec 一致性 | 20 |
| 2 | 健壮性 | 15 |
| 3 | API 设计 | 12 |
| 4 | 测试 | 12 |
| 5 | 数据建模 | 10 |
| 6 | 代码分层 | 10 |
| 7 | 性能 | 8 |
| 8 | 可观测性 | 7 |
| 9 | 文档 | 6 |
1. Spec 一致性(权重 20)
关注点
- spec 列出的每个接口(method + path)是否都实现了。
- 入参字段、出参字段、状态码、错误码是否与 spec 对齐。
- 权限 / 角色分支(如管理员接口)是否完整。
- 既不偷工减料,也不无关超纲。
锚点
- 10:spec 列出的所有接口 100% 实现;入参 / 出参 / 状态码 / 错误码完全对齐;超纲功能明确标注为增强。
- 9:所有接口都实现,仅 1–2 处非关键字段或错误码与 spec 微差(如 response 里多了一个无害字段、错误码用 400 而 spec 要 422),核心路径 100% 对齐。
- 8:主要接口 + 次要接口全部实现,spec 边角有遗漏(如 1 个非主路径查询参数未支持、1 个 soft-delete 恢复接口缺失),业务闭环完整。
- 7:主要接口齐全,但遗漏 1–2 个次要接口或 2–3 处字段细节 / 状态码错位。
- 5:核心 CRUD 在,但多处字段偏离 spec,或 1 个关键接口(如审核、权限切换)未实现。
- 3:大段偏离 spec,只实现了 demo 级接口,真实业务流程缺失。
取证方法
- 把 spec 接口清单与代码里的路由注册逐一对应:
# Node/Express 举例 grep -rnE "app\.(get|post|put|patch|delete)|router\.(get|post|put|patch|delete)" src/ # FastAPI grep -rnE "@(app|router)\.(get|post|put|patch|delete)" . # Go/Gin grep -rnE "\.(GET|POST|PUT|PATCH|DELETE)\(" . - 对 spec 的每个接口手工 curl 一遍,记录状态码与响应体。
- 缺失项记录 spec 章节号 + 期望行为 + 实际行为。
证据要求
- 至少 4 条
file:line引用到路由 / handler 文件。 - 一份 spec vs 实现的对齐表(报告里用 markdown 表格:接口 / spec 要求 / 实际 / 状态)。
- 至少 3 条 curl log 覆盖关键接口。
2. 健壮性(权重 15)
关注点
- 所有异常路径是否被处理(不是
try/except: pass吞错)。 - 输入是否校验(类型、范围、必填、长度)。
- 关键写操作是否幂等(重试不重复创建)。
- 并发冲突是否考虑(乐观锁 / 事务)。
- 未登录 / 越权访问是否正确拒绝。
锚点(基于 probe-robustness.sh 8 个 probe 的通过数)
- 10:≥7 个 probe 通过;有明确错误码体系;关键写操作幂等;并发路径有锁或事务。
- 9:8 个 probe 通过 7 个,仅 1 处边界返回 500 而非结构化错误(如超长字段未截断就抛 DB error);幂等 / 并发 / 错误码体系完整。
- 8:6/8 probe 合理(主操作幂等做了,越权 / 未登录拒绝正确),1–2 处边界未处理但返回的是统一错误结构而非堆栈。
- 7:≥5 个 probe 通过;主路径健壮,边界 / 并发有 1–2 处疏漏。
- 5:≥3 个 probe 通过;happy path 能跑,异常路径多处直接 500。
- 3:<3 个 probe 通过;基本没做异常处理,或报错直接把堆栈抛给前端。
取证方法
- 跑
bash ./probes/probe-robustness.sh <team> <base-url> <path>,记录 8 个 probe 的通过数(每个 probe 有期望状态码)。 - 检查响应码和错误信息格式是否统一:
grep -rnE "try\s*\{|try:" src/ | head -20 grep -rnE "except\s*:|catch\s*\(\s*\)" src/ # 裸 except / catch grep -rnE "pass\s*$|// ignore" src/ - 对同一写接口用
curl打两次相同 payload,看是否产生两条记录(幂等)。
证据要求
- 1 份
<team>-robustness-summary.md(标注通过数X/8)+ 至少 3 份<team>-<probe-name>.log。 - 至少 3 个 probe 的 curl + 响应摘录(报告里贴片段)。
- 至少 2 条
file:line引用校验 / 错误处理代码。
3. API 设计(权重 12)
关注点
- RESTful 路径语义(资源复数名词、层级清晰)或 RPC 风格的一致性。
- 状态码用对(200/201/204/400/401/403/404/409/422/500)。
- 错误响应结构统一(如
{code, message, details}),不是每接口一种形状。 - 分页 / 过滤 / 排序有约定(
?page=&size=或 cursor)。 - 请求 / 响应字段命名一致(camelCase 或 snake_case 二选一不混用)。
锚点
- 10:路径和状态码全部合规;错误结构统一;分页 / 过滤 / 排序标准化;字段命名统一。
- 9:路径 / 状态码 / 错误结构全部合规,分页和命名统一,仅 1 处过滤参数风格与全站不完全一致(如一个接口用
q其他用keyword)。 - 8:RESTful 合规且错误结构统一,分页做了但 1 个接口返回 201 而 spec 约定 200(或反之),字段命名统一。
- 7:整体合规,但 1–2 个状态码错位(如 POST 成功返 200 而不是 201)或 1 处错误结构不一致。
- 5:多数接口能用,但状态码乱用、错误结构每接口一种、分页无约定。
- 3:全部 200 +
{ok: false},无状态码体系,路径随意。
取证方法
- 搜路由注册 + 手工 curl 一遍关键接口看响应结构:
for p in /api/items /api/users /api/auth/login; do curl -s -o - -w "\n[HTTP %{http_code}]\n" http://localhost:8080$p done - 抽取 3–5 条错误响应,对比形状是否一致。
- 分页接口:
curl "http://localhost:8080/api/items?page=1&size=10"看是否识别。
证据要求
- 至少 5 条 curl log 覆盖成功 / 失败 / 分页三类。
- 至少 2 条
file:line引用统一错误处理中间件(或反面证据说明缺失)。 - 一份错误响应结构对比表(报告里 3–5 行即可)。
4. 测试(权重 12)
关注点
- 是否有自动化测试(单元 + 集成)。
- 关键路径(登录、核心写操作、错误分支)是否覆盖。
- 测试是否真跑通,不是 skip 一片。
- 覆盖率数据。
锚点(基于测试覆盖率)
- 10:覆盖率 ≥70%;覆盖关键 happy + error 路径;集成测试真打接口;CI 本地
npm test/pytest/go test一次绿。 - 9:覆盖率 ≥65% 且 happy + error 路径都覆盖,集成测试真打接口,仅 1 个次要错误分支(如幂等冲突)未测。
- 8:覆盖率 ≥55%,主要 happy + 核心 error 分支都有集成测试,次要模块仅单元测试未做集成覆盖。
- 7:覆盖率 ≥50%;主要 happy path 覆盖,error 路径少量。
- 5:覆盖率有但多数只 happy path,或集成测试缺失仅单元测试。
- 3:几乎没有测试(<20% 或只有 1–2 个 sanity test)或大量 skip / TODO。
取证方法
- 按栈跑测试 + 覆盖率:
把输出 tee 到npm test -- --coverage # Node pytest --cov --cov-report=term # Python go test -cover ./... # Go ./gradlew test jacocoTestReport # Java.grading/probes/<team>-test.log。 - 统计测试文件数与被跳过的用例:
grep -rnE "\.skip|@pytest\.mark\.skip|t\.Skip\(|xit\(|xdescribe\(" . - 抽一个 error 路径测试用例读 5–10 行,看是否真的断言了错误码而非只跑过。
证据要求
- 1 份 test 输出 log(含覆盖率数字)。
- 至少 3 条
file:line引用测试文件(good case)或缺失(bad case)。 - 一条 skip 统计:
skip 数 / 总数。
5. 数据建模(权重 10)
关注点
- schema / 模型定义与 spec 对齐(字段、类型、约束、关系)。
- 主键、外键、唯一索引、必要索引齐全。
- 有 migration 脚本 / 版本化,不是手改表。
- 字段命名、类型选择合理(时间用
timestamptz/datetime,金额不用 float)。
锚点
- 10:schema 与 spec 100% 对齐;外键 / 唯一约束 / 索引完整;有 migration 版本链;命名和类型都合理。
- 9:schema 对齐且约束完整,migration 版本链干净,仅缺 1 个次要查询索引(不影响主热路径)。
- 8:字段类型 / 外键 / 主键 / 唯一约束都对,migration 存在,但 1 处复合索引缺失或 1 个字段用
text而 spec 建议varchar(n)。 - 7:schema 对齐,但缺 1–2 个索引或 1 处外键缺失,migration 有但不规整。
- 5:schema 勉强能跑,多处字段类型错位(如用 varchar 存时间),索引全无。
- 3:无 schema 定义 / 无 migration,或字段跟 spec 大幅偏离。
取证方法
- 找 schema 文件:
find . -type f \( -name "*.sql" -o -name "schema.prisma" -o -name "models.py" -o -name "entity*.go" -o -name "*Entity.java" \) | head find . -type d -name "migrations" -o -name "migrate" - 对每个核心表记录:字段 / 类型 / 约束 / 索引,对照 spec。
- 看 migration 目录是否有版本编号、是否 down script 齐全。
证据要求
- 至少 3 条
file:line引用 schema / model / migration 文件。 - 一份核心表字段对照表(表名 / 字段 / 类型 / spec 期望 / 实际)。
- 索引清单(从
CREATE INDEX或 ORM 注解里抽)。
6. 代码分层(权重 10)
关注点
- controller / handler / route 层只做参数解析和响应包装,不写业务。
- service / usecase 层承载业务逻辑,不直接写 SQL。
- repository / dao 层封装数据访问,controller 不绕过它直接拿 ORM。
- 跨层依赖方向单一(controller → service → repo),不反向依赖。
锚点(基于 grep controller 里是否有 SQL/ORM 调用)
- 10:三层清晰;controller 内无 SQL / ORM 调用;service 内无 http 响应组装;循环依赖为 0。
- 9:controller 全部是薄壳(参数解析 + 转发 service),service 不碰 http,仅 1 处 repo 里拼了一点点业务判断应归 service。
- 8:分层整体清晰,controller 基本只做转发,但 1 个 handler 里直接调了一次 ORM(如简单 count 查询绕过 repo),依赖方向仍单向。
- 7:整体分层,但有 1–2 个 controller 顺手调了 ORM 或在 handler 里拼了复杂业务。
- 5:有分层意图但大量泄漏:controller 里经常直接
prisma.xxx/db.query,service 里写res.json(...)。 - 3:一锅端,所有逻辑都在 route handler 里,没有 service / repo 概念。
取证方法
- Grep controller 目录有无直接 SQL / ORM:
命中越多越说明泄漏。# Node/TS grep -rnE "prisma\.|knex\(|db\.query|createQueryBuilder|sequelize\." src/controllers src/routes src/handlers 2>/dev/null # Python grep -rnE "session\.query|db\.execute|cursor\.execute" app/api app/routes 2>/dev/null # Go grep -rnE "db\.Query|db\.Exec|gorm\.|sqlx\." internal/handler internal/controller 2>/dev/null - 抽 2 个 controller 文件读头 30 行,看逻辑是否"薄"。
- 画依赖箭头(口述即可):
controller -> service -> repo是否单向。
证据要求
- 至少 1 组 grep 统计:「controller 目录命中 SQL/ORM 次数 / 总 controller 文件数」。
- 至少 3 条
file:line(好的 service / 坏的泄漏各举例)。 - 一句话描述依赖方向。
7. 性能(权重 8)
关注点
- 关键接口响应时间(p50 / p95 / p99)。
- 无 N+1 查询(循环里发请求 / query)。
- 合理的索引支撑主查询。
- 有必要的缓存 / 批处理(spec 要求时)。
锚点(基于 autocannon p95,10 并发 5 秒,本地)
- 10:主 GET 列表接口 p95 < 100ms;写接口 p95 < 300ms;N+1 零命中。
- 9:主 GET p95 < 150ms,写 p95 < 400ms,N+1 零命中,仅 1 处次要聚合查询未加索引但 p95 仍在可接受区间。
- 8:主 GET p95 < 200ms,写 p95 < 500ms,热路径无 N+1,冷路径(如后台导出)存在 1 处 N+1 但不影响生产体验。
- 7:p95 < 300ms;偶有 1–2 处 N+1 但不在热路径。
- 5:p95 < 800ms;热路径明显 N+1 或缺索引。
- 3:p95 ≥ 800ms 或直接接口超时 / 打挂服务。
取证方法
- 跑
bash ./probes/probe-performance.sh <team> <base-url> <path>,读 p50/p95/p99。 - 检查 N+1:
# 找循环里调 query / repo 的反模式 grep -rnE "for\s|forEach|\.map\(" src/ | grep -E "repo\.|query|findBy" - 对主查询看 EXPLAIN(可选):
EXPLAIN SELECT ... FROM items WHERE ...。
证据要求
- 1 份
<team>-autocannon.log+ 1 份<team>-perf-summary.txt(含 p50/p95/p99)。 - 至少 2 条
file:line体现 N+1 或优化手段(或反面证据)。 - 一条关于索引 / 缓存 / 批处理的结论。
8. 可观测性(权重 7)
关注点
- 有结构化日志(JSON 或至少 key=value),不是满屏
print/console.log。 - 关键操作有 trace:请求 ID / 用户 ID / 业务动作。
- 错误日志带上下文(请求体摘要、堆栈)。
- 有 metrics / health 端点(起码
/health)。
锚点(基于 console.log / print 占日志语句比例)
- 10:统一日志库(pino / zap / loguru / slog / logback);所有接口有请求进入 + 结束日志 + requestId;
print/console.log数 ≤ 2。 - 9:统一日志库 + 全接口 requestId + 结构化日志,
print/console.log≤ 5 且都在非生产路径(如 seed 脚本),/health可用。 - 8:结构化日志覆盖主要接口且有 requestId,但 1–2 个次要接口缺请求结束日志,
print/console.log残留 5–10 处。 - 7:有日志库,但部分关键路径漏打,或偶有
console.log残留(3–10 处)。 - 5:日志库有但只打错误;
print/console.log占比 > 30%。 - 3:全靠
print/console.log,没有 requestId / 结构化,错误靠 try/catch 打一句。
取证方法
- 统计日志风格:
算两个总数相除得到 "jank 比例"。grep -rncE "console\.log|console\.debug|\bprint\(" src/ | sort -t: -k2 -n -r | head grep -rncE "logger\.|log\.(info|warn|error|debug)" src/ | sort -t: -k2 -n -r | head - 查
/health或/metrics:curl -i http://localhost:8080/health。 - 抽一个 error 分支读,看是否打了上下文。
证据要求
- 一组 grep 统计(logger 数 vs print/console.log 数)。
- 至少 2 条
file:line引用日志配置或关键调用。 - 1 条
/health或/metrics的 curl 响应。
9. 文档(权重 6)
关注点
- README 含启动步骤、依赖、环境变量、示例调用,不假设读者已懂。
- 接口文档(OpenAPI / Swagger / Postman collection / README 章节)存在且与实现同步。
- 示例 curl / 请求体可直接复制跑通。
- 关键设计决策(分层、鉴权、数据模型)有一小段说明。
锚点
- 10:README 从零到跑通 5 分钟内完成;OpenAPI 文档完整、可在
/docs打开;有架构图或 ADR。 - 9:README 跑通流畅,OpenAPI 完整且与实现一致,有架构图,仅 ADR / 设计决策说明简略(1–2 条一句话带过)。
- 8:README 启动步骤 + 环境变量 + 示例 curl 齐全,OpenAPI 可打开且主接口字段对齐,无架构图但有一段分层 / 鉴权文字说明。
- 7:README 够用,接口文档有但 1–2 处字段过期;无架构图。
- 5:README 只有 "npm install && npm start",接口文档靠读代码。
- 3:几乎无文档,spec 之外无任何说明。
取证方法
- 按 README 从零跑一遍(或复查步骤 1 的启动体验),记录卡点。
- 找接口文档:
find . -type f \( -name "openapi*.y*ml" -o -name "swagger*.json" -o -name "*.postman_collection.json" \) curl -i http://localhost:8080/docs # Swagger UI curl -i http://localhost:8080/openapi.json # FastAPI / 多数框架 - 抽示例 curl 从 README 粘出来跑一次,记录是否通。
证据要求
- 至少 2 条
README.md:line或docs/*.md:line引用。 - 1 次"照 README 跑"的实测结果(通 / 不通 + 卡点)。
- 接口文档位置 + 1 条示例 curl log。
产出 checklist
评分结束前逐项勾选,任一不通过就回到相应步骤补齐:
-
.grading/reports/<team>-backend.md已按本文件## 报告模板段结构填完,9 个维度全部评完(或明确标 N/A)。 -
.grading/reports/<team>-backend.json已产出,并通过npx ajv-cli validate -s skills/score-schema.json校验。 - 每个维度都有证据引用,且满足本文件
## 证据硬约束段(≥7 / ≤4 分 ≥2 条;5–6 分 ≥1 条)。 -
.grading/probes/含:<team>-robustness-summary.md+ 各 probe<team>-<probe-name>.log、<team>-autocannon.log+<team>-perf-summary.txt、<team>-test.log、<team>-curl.log。 - 报告末尾给出"最该优先修的 3 件事",每条对应到具体维度和 file:line。