如果要问今年计算机专业课设里哪个方向最热,答案几乎没有悬念:接入大模型的智能问答。
原因很现实:老师在评审时对"增删改查管理系统"已经审美疲劳了,而一个能基于课程资料准确回答问题的助手,演示起来效果完全不同——你问它"快速排序最坏情况为什么退化到 O(n²)",它能引用你上传的课件第 42 页给出推导,这个画面足够让老师记住你。
但难点也很明显:大部分人的实现只是调了个聊天接口,没有知识库,回答全靠模型瞎编,一问具体知识点就露馅。真正的做法是 RAG(检索增强生成)。
这篇把整条链路讲清楚:文档解析 → 切片 → 向量化 → 检索 → 拼装上下文 → 流式输出 → 引用溯源。
一、技术选型
为什么用 pgvector 而不是 Milvus:课设环境装 Milvus 是灾难,Docker 拉镜像 2G,内存吃满。pgvector 只需
CREATE EXTENSION vector,和你已有的 PostgreSQL 是同一个库,部署成本为零。这个取舍写进文档,是很好的"工程权衡"论据。
二、整体链路
上传课件 → Tika 解析纯文本 → 按语义切片(800字/重叠100) → 调嵌入模型 → 存 vector(1536) 到 pgvector ↓ 用户提问 → 向量化 → 相似度检索 Top-K → 重排 → 拼成 Prompt → 调大模型(SSE 流式) → 返回答案 + 引用来源这条链路每一步都有讲究,下面挑最关键的三个讲。
三、核心难点 1:切片策略决定回答质量
RAG 效果好不好,70% 取决于切片,而不是模型。三种切法对比:
public List<Document> split(MarkdownDocument doc) { TokenTextSplitter splitter = new TokenTextSplitter( 800, // 单块目标长度(token) 100, // 块间重叠,防止答案刚好跨在切口上 5, // 最小块长度,太碎的直接丢弃 10000, true // 保留段落边界 ); List<Document> chunks = splitter.apply(List.of(doc)); // 关键:给每块补上元数据,后面引用溯源全靠它 for (int i = 0; i < chunks.size(); i++) { chunks.get(i).getMetadata().put("source", doc.getMetadata().get("filename")); chunks.get(i).getMetadata().put("page", doc.getMetadata().get("page")); chunks.get(i).getMetadata().put("chunkIndex", i); } return chunks; }元数据是引用溯源的基础。没有source和page,模型就算答对了你也说不出"这句话来自课件第 42 页",而这个引用恰恰是演示时最有说服力的地方。
四、核心难点 2:检索质量,Top-K 不够就加"重排"
只做向量相似度检索有个通病:召回的块"看起来相关"但对回答没用。解决办法是两步检索:
public String answer(String question, String kbId) { // 第一步:向量检索,多召回一些(Top 20) SearchRequest req = SearchRequest.query(question) .withTopK(20) .withSimilarityThreshold(0.5) .withFilterExpression("kbId == '" + kbId + "'"); // 限定知识库,避免串库 List<Document> candidates = vectorStore.similaritySearch(req); // 第二步:重排,压缩到 Top 5 再喂给模型 List<Document> top = rerank(question, candidates, 5); String context = top.stream() .map(d -> "【来源:" + d.getMetadata().get("source") + " 第" + d.getMetadata().get("page") + "页】\n" + d.getText()) .collect(Collectors.joining("\n\n---\n\n")); return chatClient.prompt() .system(SYSTEM_PROMPT) .user(u -> u.text(""" 只根据下面的资料回答问题,资料中没有的内容请直接说"资料中未涉及",不要编造。 回答末尾用 [1][2] 标注引用序号。 {context} 问题:{question} """).param("context", context).param("question", question)) .call() .content(); }withFilterExpression这一步很多人会漏:如果你的系统里同时有"数据结构""操作系统"两个知识库,不加过滤就会串库,问数据结构却召回操作系统的内容。
系统提示词里那句"资料中没有就说未涉及"是整个方案的灵魂——它把"胡说八道"变成了"诚实拒答",这是 RAG 相比纯聊天最本质的改进,文档里一定要重点写。
五、核心难点 3:SSE 流式输出
大模型生成一段 500 字的回答要 10 秒,如果等全部生成完再返回,用户会以为卡死了。必须用 SSE 边生成边推:
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> stream(@RequestParam String q, @RequestParam String kbId) { return chatClient.prompt() .user(buildPrompt(q, kbId)) .stream() .content() .map(chunk -> ServerSentEvent.builder(chunk).build()) .concatWith(Flux.just(ServerSentEvent.builder("[DONE]").build())); }前端用fetch+ReadableStream逐块追加:
const res = await fetch(`/api/chat/stream?q=${encodeURIComponent(q)}&kbId=${kbId}`, { headers: { Authorization: `Bearer ${token}` } }) const reader = res.body.getReader() const decoder = new TextDecoder() let answer = '' while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) if (chunk.includes('[DONE]')) break answer += chunk.replace(/^data:\s*/, '') messages[messages.length - 1].content = answer // 响应式更新 }一个小细节:SSE 的数据格式是data: xxx\n\n,前端要剥掉前缀;空行是消息分隔符,别直接JSON.parse整块。
六、页面效果
对话页的核心是"答案 + 引用",左边是会话列表,右边是消息流,答案下方显示引用来源文档和页码:
引用条这一块我特意做了"来源:课件第 42 页"的形式——它直接回答了老师最可能的质疑:"你怎么证明答案不是编的?"
知识库管理页负责文档的上传、切片状态和检索命中率统计:
检索命中率这个指标值得做:记录每次问答用户是否点了"有用/没用",命中率 = 有用次数 / 总次数。虽然简单,但它让系统有了"可度量"的属性,文档里的"系统评估"章节就有数据可写了——这一章绝大多数人都是空着的。
七、四周排期(这个题目比前几个多一周)
如果只有两周:砍掉文档解析,改成手动粘贴文本入库;砍掉重排,Top-K 直接取 5。核心链路仍然是完整的 RAG,不影响答辩。
八、三个容易翻车的点
- 成本失控:嵌入模型按量计费,一个 200 页的课件全量向量化不便宜。开发阶段先把课件截取前 20 页测试,跑通再全量。
- 超时:大模型接口默认超时可能只有 10 秒,长回答直接断流。把
RestClient的读超时调到 60 秒。 - 敏感信息:别把学校内部资料、学生信息传到第三方接口。这一点在文档的"合规性说明"里写一句,是很成熟的考虑。
九、这个题目为什么容易拿高分
- 选题新:大模型 + RAG 是当下最热的工程方向,老师也会好奇
- 有真实效果:演示时能答对具体知识点,比"我实现了增删改查"强太多
- 技术栈有层次:文档解析、向量检索、流式通信、提示工程,每一层都能写一节
- 可讲清楚取舍:为什么用 pgvector 不用 Milvus、为什么切片 800 字、为什么要重排——这些都是"你思考过"的证据
最后
这个题目代码量其实不大(后端核心不到 15 个类),但概念密度高,文档写起来最费劲:向量、嵌入、切片、召回、重排、提示词,每个词都要解释清楚又不显得像抄的。
如果时间紧张,建议先把"能跑通的最小链路"搭出来(上传一段文本 → 提问 → 得到带引用的回答),再回头补功能和文档。跑通之后再写文档,思路会顺很多——这一点对所有课设都适用。
五篇选题到这里就写完了:Java 后端、Python 后端、小程序、React 全栈、AI 应用,覆盖了目前主流的五个方向。需要哪一篇的详细实现(比如完整的库表脚本或接口清单),可以留言告诉我。