AI 辅助开发实战:高效生成与优化毕业设计题目系统的技术方案
面向中高级开发者,给出可直接落地的 LangChain 实现、Clean Code 示例与生产级避坑清单。
1. 高校毕设选题的三大顽疾
- 重复率高:学院近五年 1200 条历史题目中,语义相似度 >0.85 的占比 38%,导致“换汤不换药”。
- 缺乏个性化:人工开题往往套用固定模板,难以匹配学生技术栈或兴趣标签。
- 领域漂移:导师随手写“基于深度学习的 XX”,既没限定数据集,也没量化指标,后期评审标准模糊。
一句话,选题环节消耗了 30% 的毕业设计周期,却只能靠“导师经验”硬扛。
2. 技术选型:规则、微调还是 Prompt+RAG?
| 方案 | 优点 | 缺点 | 落地成本 | 结论 |
|---|---|---|---|---|
| 纯规则引擎 | 可控、可解释 | 泛化差,新学科需加规则 | 低 | 适合“格式校验”层 |
| LLM 微调(LoRA) | 领域术语准确 | 需标注 2k+ 高质量样本,GPU 训练 6h+ | 高 | ROI 低,除非全校统一部署 |
| Prompt 工程 + RAG | 无需训练,实时引用校内历史题目做向量化去重 | 依赖提示词设计 & 向量库质量 | 中 | 最快出 MVP,后续可插拔微调 |
最终采用“Prompt+RAG”作为生成核心,规则引擎仅负责格式与敏感词兜底。
3. 系统架构与 LangChain 拆解
题目模板注入
将学院模板抽象成“变量槽”:{tech} + {场景} + {数据} + {指标},用 Pydantic 类强制类型校验,防止提示词注入。学科关键词过滤
维护一份 47 个一级学科、528 个二级关键词的 AC 自动机,毫秒级拦截“跑题”请求。历史题目向量去重
采用text-embedding-ada-002对历年题目做 1536 维向量化,写入 Qdrant;生成阶段取 Top-5 最相似题目,若余弦相似度 >0.88 则触发重写。难度分级
利用 LLM 自带的 logit 输出,把“实现难度”“创新度”映射到 1-5 离散标签,再与学院评审表对齐。缓存与异步
相同“学科+关键词”请求 24h 内直接返回 Redis 缓存;异步队列(Celery + Redis Stream)削峰,防止集中选题高峰打爆 Token 预算。
4. 核心代码:Clean Code 示范
以下文件可直接放入src/title_generator/目录,通过poetry install拉起依赖。
# domain.py from pydantic import BaseModel, Field, validator import re class TitleTemplate(BaseModel): tech: str = Field(..., min_length=2, max_length=30) scene: str = Field(..., min_length=3) data: str = Field(..., min_length=3) metric: str = Field(..., min_length=3) @validator('tech') def no_sensitive(cls, v): if re.search(r'\b(?:hack|crack)\b', v, re.I): raise ValueError("Sensitive word detected") return v # rag_store.py from langchain.vectorstores import Qdrant from qdrant_client import QdrantClient class VectorStore: def __init__(self, collection: str = "history_titles"): self.client = QdrantClient(host="localhost", port=6333) self.collection = collection def similarity_search(self, query: str, k: int = 5): return Qdrant( client=self.client, collection_name=self.collection, embeddings=OpenAIEmbeddings() ).similarity_search(query, k=k) # generator.py from langchain.chat_models import ChatOpenAI from langchain.prompts import ChatPromptTemplate from domain import TitleTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "You are a college thesis advisor."), ("user", """ 学科:{discipline} 技术关键词:{tech} 场景:{scene} 数据集:{data} 评估指标:{metric} 要求: 1. 用一句话给出毕设题目,不超过 40 字; 2. 确保与历史题目不重复(相似度<0.88); 3. 给出难度评分 1-5。 历史相似题目:{history} """) ]) class TitleGenerator: def __init__(self, llm: ChatOpenAI, vector_store: VectorStore): self.llm = llm self.vector_store = vector_store self.chain = prompt | llm async def generate(self, tpl: TitleTemplate, discipline: str) -> dict: history = self.vector_store.similarity_search( f"{tpl.tech} {tpl.scene}", k=5 ) output = await self.chain.ainv({ "discipline": discipline, "tech": tpl.tech, "scene": tpl.scene, "data": tpl.data, "metric": tpl.metric, "history": [h.page_content for h in history] }) title, difficulty = self._parse(output.content) return {"title": title, "difficulty": difficulty} def _parse(self, content: str) -> tuple: # 简单正则,可换成 json mode import re t = re.search(r"题目[::]?(.*)", content) d = re.search(r"难度[::]?(\d)", content) return (t.group(1).strip() if t else "", int(d.group(1)) if d else 3)单元测试覆盖_parse与no_sensitive分支,保持 90%+ 行覆盖率。
5. 并发、冷启动与成本控制
冷启动延迟
首次加载提示词模板 + 向量库索引需 1.2s,对选课高峰不可接受。采用进程级 LRU 缓存,把ChatOpenAI与Qdrant实例常驻内存,P99 延迟降至 180ms。Token 花费
单次生成约 550 tokens(含历史题目拼接)。按 0.002$/1k tokens 计算,1000 次选题≈1.1 $;若全校 8000 学生同时点击,日费用 9 $,尚可接受。缓存策略
- Redis Key 设计
title:v1:md5(discipline+tech+scene),TTL=24h - 命中率 62%,日均节省 4.3 $
- Redis Key 设计
限流
采用 Token bucket 算法,单 IP 10req/min,超量返回 429,并提示“已记录你的需求,稍后推送至邮箱”,避免恶意刷接口。
6. 生产环境避坑指南
提示词泄露
不要把系统提示直接返回前端,使用 OpenAI 的system_fingerprint字段做追踪即可。输出不可控
开启response_format={"type": "json_object"}并给出 json schema,降低正则解析失败率。结果审核
引入“双盲审”机制:LLM 生成后先过敏感词 AC 自动机,再送导师端人工勾选“可”“需修改”“拒绝”,拒绝样本回流到向量库负例,用于后续迭代。数据合规
历史题目含学生姓名学号,需在向量化前做脱敏(正则擦除 10 位连续数字)。
7. 迁移与二次开发
把discipline字段换成course即可平移到课程设计;科研选题再加一个funding槽位,用于匹配基金关键词。仓库已上传 GitHub,搜索thesis-ai-generator即可 fork,README 里提供 Docker Compose 一键启动。欢迎提 PR 支持多语言模板、Web 可视化拖拽,甚至接入校内 SSO。
如果你正头疼“选题重复、导师拍脑袋”,不妨把这套轻量级方案跑起来,先让 AI 把脏活累活揽了,再把省下的时间花在真正有价值的实验与验证上。下一步,你准备把它搬到哪个场景?