自动化毕业设计题目生成系统:从需求建模到高可用部署的实战指南
1. 高校出题的“老毛病”
- 题目雷同:同一届 300 名学生,人工选题常出现“基于深度学习的 XXX”撞车,学生后期查重压力大。
- 人工耗时:教师平均 5 分钟/题,300 题 ≈ 25 工时,且需反复核对学科方向、难度梯度。
- 个性化弱:学生能力画像(考研/就业/竞赛)与题目匹配度低,导致中期换题率高达 18%。
- 历史资产流失:往届优秀题目以 Word 存于个人电脑,无法沉淀为可复用知识库。
一句话总结:重复劳动吃掉教师精力,也吃掉学生选题体验。
2. 技术选型:规则模板、大模型还是混搭?
| 方案 | 优点 | 缺点 | 落地成本 | 适用场景 |
|---|---|---|---|---|
| 纯规则模板 | 可控、可解释、零算力 | 语义单一、需人工维护规则 | 低 | 学科术语固定、预算紧张 |
| 大模型在线生成 | 语义丰富、风格多样 | 贵、慢、不可控(可能跑题) | 高 | 创意写作、开放命题 |
| 混合方案(本文) | 规则保证边界,模型润色多样性;成本可控 | 需做网关编排 | 中 | 高校正式教学,需“稳+省” |
结论:教学场景要“稳”字当头,采用规则引擎生成主干 + 轻量 NLP 随机扰动的混合路线,把 GPU 预算留在真正需要创意的环节。
3. 系统架构:把“变”与“不变”拆干净
整体遵循CQRS + 横向分层思想:
- Config Service(不变):学科、难度、关键词库、模板库,全量放 Git,CI 自动热加载。
- Generator Service(变):无状态容器,只负责把模板渲染成候选题目。
- Deduplication Service(不变):布隆过滤器 + Redis Set,保证幂等提交。
- Cache & Rate-Limit Gateway(变):基于 OpenResty + lua-resty-redis,QPS>500 时降级到“只读缓存”。
如此,教学秘书改模板无需上线;运维扩 Generator 副本即可扛并发,两者互不影响。
4. 核心代码:一条请求的生命周期
下面示例可直接docker-compose up,Python 3.11,依赖仅fastapi==0.110、redis、pydantic。
4.1 项目目录
project_thesis/ ├── app/ │ ├── main.py │ ├── generator.py │ ├── dedup.py │ └── models.py ├── data/ │ └── templates.yml └── tests/4.2 数据模型(models.py)
from pydantic import BaseModel, Field, validator from enum import Enum class LevelEnum(str, Enum): easy = "easy" medium = "medium" hard = "hard" class Topic(BaseModel): tag: str = Field(..., min_length=1, max_length=20, description="学科标签") level: LevelEnum keywords: list[str] = Field(..., min_items=1, max_items=5) class TitleOut(BaseModel): title: str uid: str # 去重唯一标识 level: LevelEnum4.3 规则模板(templates.yml 节选)
templates: - id: cv_ml_1 tag: 计算机视觉 level: medium text: "基于{algo}的{target}识别研究" slots: algo: ["YOLOv8", "ResNet50", "SwinTransformer"] target: ["交通标志", "口罩佩戴", "吸烟行为"]4.4 生成引擎(generator.py)
import random import yaml from pathlib import Path from .models import Topic, TitleOut, LevelEnum import hashlib CFG = yaml.safe_load((Path(__file__).parent.parent / "data" / "templates.yml").read_text()) class RuleGenerator: """无状态、纯内存、线程安全""" def __init__(self): self._pool = [t for t in CFG["templates"]] def pick(self, topic: Topic) -> list[str]: candidates = [ t for t in self._pool if t["tag"] == topic.tag and t["level"] == topic.level.value ] if not candidates: return [] # 随机选 1 个模板,再随机填槽 tpl = random.choice(candidates) slots = {k: random.choice(v) for k, v in tpl["slots"].items()} return [tpl["text"].format(**slots)]4.5 去重逻辑(dedup.py)
import redis, hashlib from .models import Topic r = redis.Redis(host='redis', port=6379, decode_responses=True) def uid(topic: Topic, title: str) -> str: """用 topic+title 生成 64 位哈希,保证同内容同 uid""" raw = f"{topic.tag}:{topic.level}:{title}" return hashlib.blake2b(raw.encode(), digest_size=8).hexdigest() def seen(uid: str) -> bool: return r.sismember("titles", uid) def remember(uid: str): r.sadd("titles", uid)4.6 REST 入口(main.py)
from fastapi import FastAPI, HTTPException from .models import Topic, TitleOut from .generator import RuleGenerator from .dedup import uid, seen, remember app = FastAPI(title="ThesisTitleGen", version="1.0.0") gen = RuleGenerator() @app.post("/generate", response_model=TitleOut) def generate(topic: Topic): titles = gen.pick(topic) if not titles: raise HTTPException(404, "无匹配模板") title = titles[0] _uid = uid(topic, title) if seen(_uid): # 简单重试一次 title = gen.pick(topic)[0] _uid = uid(topic, title) if seen(_uid): raise HTTPException(409, "题目已存在,请稍后再试") remember(_uid) return TitleOut(title=title, uid=_uid, level=topic.level)4.7 单元测试(tests/test_gen.py)
def test_gen_endpoint(client): payload = {"tag": "计算机视觉", "level": "medium", "keywords": ["YOLO"]} r = client.post("/generate", json=payload) assert r.status_code == 200 assert r.json()["title"].startswith("基于")跑pytest即可验证主干逻辑。
5. 并发瓶颈与治理
- 压测结果:4C8G 容器,单副本 250 QPS 时 CPU 70%,RT 均值 38 ms;500 QPS 时 RT 陡增至 220 ms,错误率 5%。
- 根因:Redis
SMEMBER是 O(1),但网络往返 + 框架序列化占大头。 - 缓存策略:
- 本地 LRU 缓存 uid 集合 5 s,命中率 35%,RT 降 40%。
- 对“学科+难度”维度做预生成,定时任务每 30 min 把结果刷入 Redis List,请求直接
LPOP,RT < 10 ms。
- 限流:OpenResty 层按学生学号做令牌桶,峰值 300 QPS/人,防止刷接口占库存。
- 降级:缓存击穿时返回 204,前端引导“手工填写”,保证教学流程不断。
6. 生产环境避坑指南
- 数据隔离:教师模板库与学生生成库分集群,防止实验性刷库污染染生产环境。
- 冷启动延迟:FastAPI 首次加载 YAML 模板 1.3 w 行,首次请求 RT 2 s;采用
preload=True+ Gunicorn--preload解决。 - 学科术语合规性:建立敏感词 Trie 树,生成后 2 ms 内完成过滤;曾误杀“精子质量检测”中的“精子”,通过白名单“精子*检测”解决。
- 模板版本回退:Git Webhook 回调到 Config Service,先灰度 10% 流量,观察 30 min 无 5xx 再全量。
- 幂等键冲突:同一 uid 被并发写入,Redis Set 天然去重,但返回 201 与 409 语义不同;在网关层加
X-Idempotency-Key透传,保证客户端可重试。 - 日志可观测:统一 TraceId = 学号+时间戳,打印到 stdout,Loki 收集;曾靠一条 TraceId 在 30 s 内定位到“模板空槽”导致的
IndexError。
7. 小结与延伸思考
整套系统上线两学期后,出题耗时从 25 工时降到 1.5 工时,学生换题率由 18% 降到 4%,服务器年成本 < 一台入门 MacBook。
如果把“毕业设计”换成“课程设计”或“竞赛命题”,只需:
- 在 Topic 模型新增
scenario字段区分“毕设/课程/竞赛”; - 模板库按场景隔离目录,Config Service 支持多租户前缀;
- 竞赛场景对创意要求更高,可把 RuleGenerator 作为候选队列,再调大模型做 Top-K 重排,实现“稳+创”双轨输出。
下一步,你会如何设计多场景模板市场,让教师像“逛乐高”一样拼装题目?欢迎留言交流。