概述

今天对作业批改系统进行了一次全面改造,解决了三大问题:高并发下的 AI 批改瓶颈、数据库查询性能、以及 OpenClaw OneBot 插件的消息分发故障。本文记录完整的排障过程和解决方案。


一、异步批改:从同步阻塞到 Redis Stream

问题

原系统 HomeworkController.submitHomework() 的流程是:

1
学生提交 → Controller 线程阻塞等待 OpenClaw AI 批改(30-300s) → 写DB → 返回

30 个学生同时提交 → 30 个 Tomcat 线程全阻塞 → 新请求被拒绝 → 数据库连接池(HikariCP max 20)耗尽 → 雪崩。

方案

参考 [InterviewGuide] 的 Redis Stream 异步架构:

1
2
3
提交 → 入库(PENDING) → 入队(Redis Stream) → 秒回"已收到"

单线程 Consumer → 调 OpenClaw → 写回结果 → ACK

实现

新增 5 个文件:

文件 作用
config/RedisConfig.java RedissonClient Bean
common/async/AsyncTaskConstants.java Stream Key、重试次数常量
common/async/GradingStreamProducer.java 生产者:入队 / 重试入队
common/async/GradingStreamConsumer.java 消费者:单线程消费 → 调AI → 写DB → ACK
Submission.java statuserrorMessage 字段

关键代码变更:

1
2
3
4
5
6
7
8
9
// HomeworkController.submitHomework() — 原来同步阻塞 300 秒
String response = openClawService.chat(message, "homework_xxx");
// ... 解析、保存 ...

// 改为异步入队 + 秒回
submission.setStatus("PENDING");
submissionMapper.insert(submission);
gradingStreamProducer.sendTask(submission.getId());
return ResponseEntity.ok(Map.of("code", 200, "message", "已收到,正在排队中"));

前端 PageOne.vue 同样改造:提交后轮询 GET /api/homework/result/{id},展示 PENDING → PROCESSING → COMPLETED 状态。

坑点redisson-spring-boot-starter 依赖 spring-boot-starter-data-redis(项目未引入),改为纯 redisson 即可。


二、数据库查询性能优化

问题

Dashboard 仪表盘等页面存在大量 SELECT * 查询,每次都拉取了 raw_response 字段(每条 10-50KB 的 AI 返回 JSON)。

假设班级 50 人,每人 3 份作业(150 条),getMetrics() 一次就传输约 7MB 无用数据。

修复清单

方法 问题 修复
getMetrics() SELECT * 只为算平均分 SELECT total_score 只拉一个 int
getFrequentErrors() SELECT * 只为解析错误 SELECT raw_response 只拉一个字段
getStudentOverview() N+1:每人逐条查 SELECT * GROUP BY user_id 批量聚合 1 次
getScoreDistribution() 每份提交都计入,一人 3 次算 3 个数据点 改为按学生平均分统计
TaskController.getTasks() 每个作业查一次 SELECT * FROM submission SELECT task_id, COUNT, AVG ... GROUP BY task_id 批量 1 次
ChatHistoryMapper.selectByUserId() 无 LIMIT,重度用户 5000 条 → 5MB LIMIT 200
SubmissionMapper 学生进度(2个) s.* 含无用 raw_response 只选需要的 18 列

前端 PageFive.vueloadTasks() 从串行 for...await 改为 Promise.allSettled 并行请求。

新增 5 个专项查询方法:selectScoresByClassIdselectRawResponsesByClassId(×2)、selectStudentStatsByClassIdselectTaskStatsByClassId

效果:

指标 之前 之后
getMetrics() 数据传输 ~7MB ~2KB
getFrequentErrors() SQL 拉全表 只拉 raw_response
getStudentOverview() SQL 次数 51 次 2 次

三、C 语言批改 Skill 规范化

问题

OpenClaw 的 homework-grader skill 原本面向 Java 通用编程,知识点评分每次名称不一致,导致 Dashboard 的热力图和高频错题每次刷新都不一样。

修复

SKILL.md 重写~/.openclaw/workspace/skills/homework-grader/SKILL.md):

  1. 知识点封闭化 — 按谭浩强《C语言程序设计》第五版第 1-10 章目录列出,AI 必须从列表中选择
  2. 分级扣分体系
级别 severity 单次扣分 示例
致命 critical 10-20 无法编译、核心逻辑错误
主要 major 5-10 输出错误、内存泄漏、数组越界
轻微 minor 1-3 命名不规范、缺少注释、缩进问题
  1. C 语言专项 errors.type:语法错误、逻辑错误、内存错误、指针错误、输入输出、规范问题、安全问题
  2. 一致性约束:同份作业多次批改 totalScore 偏差 ≤ ±5 分,同类 minor 问题最多扣 3 次
  3. 20+ 个具体 C 场景扣分标准= 错写 ==(-5)、malloc 无 free(-5)、scanf 缺 &(-5) 等

后端归一化DashboardServiceImpl.java):

新增 KP_CANONICAL 映射表,100+ 别名统一归一,例如:

1
2
3
4
5
KP_CANONICAL.put("指针", "指针变量");
KP_CANONICAL.put("指针基础", "指针变量");
KP_CANONICAL.put("for循环", "for循环");
KP_CANONICAL.put("循环", "for循环");
// ...

getKnowledgeMastery() 在统计前先做名称归一化,确保不同批改返回的同义词合并。


四、OpenClaw OneBot 插件排障

问题

QQ 群 @机器人 后,WebChat 控制台的 session 里能看到 agent 回复,但 QQ 群收不到消息。

日志关键线索:

1
2
3
dispatchReplyWithBufferedBlockDispatcher returned successfully.
dispatch finally block: receivedFinal=false, hasBuffered=false,
bufferLen=0, chunks=0

排查过程与根因

# 尝试 结果
1 ctxPayload 缺少 SessionKey 加上后仍不行
2 disableBlockStreaming: true 仍然不行
3 main agent 能回复,jarvis 不能 锁定 jarvis 专属问题
4 日志发现 agentDir 导致 embedded-run 去掉 agentDir
5 去掉 workspace 测试 仍不行
6 日志发现每次 jarvis 处理时插件都重载 根因!
1
2
3
22:45:00 dispatching message for session agent:jarvis:onebot:group:875860223
22:45:01 [@kirigaya/openclaw-onebot] v1.1.1 加载中... ← 重载!
22:45:01 [@kirigaya/openclaw-onebot] 加载完成

OneBot 插件在 jarvis 消息处理时被 plugins.allow 为空触发的自动扫描重载了。旧的 dispatcher 回调被销毁,agent 的流式回复到达时没有接收者。

最终修复

  1. openclaw.json"plugins": { "allow": ["openclaw-onebot"] } — 阻止自动扫描
  2. process-inbound.jsSessionKey: sessionId — dispatcher 路由修正
  3. jarvis 配置 去掉 agentDir,保留 workspace — 避免 embedded-run 隔离

五、经验总结

  1. AI 调用必须异步化 — 同步等待几十到几百秒是并发杀手,Redis Stream 是最轻量的解耦方案
  2. 永远不要 SELECT * — 大字段(raw_response)是数据库性能的头号杀手
  3. 批量聚合优于 N+1GROUP BY 一次查询替代循环逐条查
  4. AI 输出需要后端归一化 — 自由文本的知识点评分必须映射到规范名称
  5. 插件重载是隐蔽炸弹plugins.allow 显式声明可以避免很多诡异问题
  6. 分而治之排障 — main agent 能工作 jarvis 不能 → 锁定配置差异 → 逐项排除

相关代码

  • 后端项目:F:\firedemo\demo
  • 前端项目:F:\firedemo\vue-project
  • OpenClaw Skill:~\.openclaw\workspace\skills\homework-grader\SKILL.md
  • OneBot 插件:C:\Users\LKL\.openclaw\extensions\openclaw-onebot
  • Gateway 配置:C:\Users\LKL\.openclaw\openclaw.json