简介:基于DeepSeek大模型的MBTI人格测试微信小程序毕业设计源码,面向计算机相关专业需要完成毕设的学生,以及想学习微信小程序与大模型API集成开发的初中级开发者,可作为项目落地时的参考模板。资源共37个文件,压缩包约28KB,以JavaScript逻辑文件为主,辅以JSON数据配置、WXSS样式文件和WXML页面结构,属于轻量但完整的项目骨架。从内容预览可以看到,项目包含欢迎、测试、结果等多个页面模块,工具目录下整理了多份题目数据与MBTI配置数据,全局入口和公共样式文件也单独维护,整体目录层次分明,数据和逻辑分离,方便替换题目、调整MBTI维度及对接DeepSeek接口。代码量不大却覆盖了测试流程、结果展示和配置读取等核心环节,适合快速读懂并在此基础上升级交互体验。当前已有93人学习,可作毕设选题落地或小程序AI应用练习的参考。
1. 基于DeepSeek的MBTI人格测试小程序:不只是调个API那么轻巧
选毕设题目时看到“基于DeepSeek的MBTI人格测试微信小程序”,第一反应是“套个接口罢了”。真把代码吃透之后发现,DeepSeek接入只占三成工作量,剩下七成全砸在MBTI题目状态机、提示词收敛、微信小程序端渲染适配和数据一致性上。这套源码解决的核心问题有三个:让DeepSeek的对话输出稳定收敛到16型人格判定、避免跑题和重复作答、以及在小程序端做到测试过程无感刷新。适合用它做毕设的同学,也适合给社团或企业内做轻量测评工具的同学。下面是拆包和实测的完整记录。
2. DeepSeek接口接入:鉴权、模型参数与请求链路全拆解
2.1 小程序目录分层:一份可以照着迁移的工程结构
解压源码包之后,先看工程根目录,用的是微信小程序原生开发模式,套了一层云函数。页面只保留了三个,核心业务逻辑都收在cloudfunctions目录里,这个设计一开始我是有点意外的——按惯性思维,应该把后台逻辑写在自己的 Node 服务里才对。看完代码才明白,用云函数有几个明显好处:不需要自己买服务器、不用配 Nginx、小程序端直接wx.cloud.callFunction就能触发,而且 API Key 不会出现在前端代码里。
project/ ├── cloudfunctions/ │ ├── mbti-chat/ │ │ ├── index.js # 对话入口,调用 DeepSeek 接口 │ │ ├── config.json # 运行配置与定时触发器 │ │ └── package.json │ └── mbti-result/ │ ├── index.js # 结果判定与报告生成 │ └── package.json ├── miniprogram/ │ ├── app.js │ ├── app.json │ ├── pages/ │ │ ├── index/ # 首页:测试入口与说明 │ │ ├── test/ # 答题页 │ │ └── result/ # 结果页 │ └── utils/ │ ├── api.js # 云函数调用封装 │ └── store.js # 本地缓存封装 └── project.config.json这个分层的意图很清楚:DeepSeek 的调用放在mbti-chat云函数里,端侧只拿得到整理后的回答和下一题,拿不到密钥;结果判定独立成mbti-result,将来想扩展报告导出或者对比分析,不用动答题流程。三个页面之间的流转靠状态字段控制,也就是题目状态机,这个细节放到第 3 章展开。
一次完整请求的链路是:答题页收集用户点选的选项 →store.js先把答案写入本地缓存 →api.js调mbti-chat→ 云函数把历史答案和当前题目上下文拼给 DeepSeek → 模型返回建议和下一题内容 → 云函数统一成 JSON 回传 → 答题页渲染下一题。
实测一轮往返耗时大约 1.5 到 3 秒,这个数字跟题目数量和上下文长度直接相关。这里有个新手极易忽略的点:云函数冷启动。第一次调用比后续调用多出 1 到 2 秒,表现在用户身上就是“转圈半天才出一题”。源码里config.json带了一个定时触发器,每分钟主动唤醒mbti-chat一次,我测试下来首题加载速度能改善很明显。
2.2 鉴权与模型参数:能跑通和能用是两码事
DeepSeek 接口走的是 OpenAI 兼容格式,鉴权方式是 Bearer Token。这套源码里关键的调用参数都写在云函数的配置里,拆开看一下:
{ "permissions": { "openapi": [] }, "triggers": [ { "name": "keepWarm", "type": "timer", "config": "0 */1 * * * * *" } ] }这个config.json里没直接放模型参数,真正的请求体写在index.js里。常见做法是把密钥放到云函数的环境变量中,而不是硬编码进代码,防止代码入库后密钥泄漏。我一般会在开发阶段先放一个临时常量,调试通了再迁到环境变量。
模型调用参数里,最影响测试结果的是temperature和max_tokens。源码里的默认值我建议照着这份表去理解:
| 参数 | 源码默认值 | 作用 | 调整建议 |
|---|---|---|---|
temperature | 0.2 | 控制回答随机性,越低越稳定 | MBTI 测试场景建议 0.1-0.3 |
max_tokens | 1024 | 限制单次回复长度 | 题目较长可放宽到 2048 |
top_p | 0.9 | 核采样概率 | 与 temperature 二选一调 |
timeout | 30s | 单次请求超时 | 云函数默认 3 秒不够用 |
MBTI 测试本质上是确定性任务,用户选什么就记什么,模型只需要理解题目和生成话术,不需要发散。temperature调高之后,同一个选项可能生成完全不同的题目描述,直接影响结果一致性。这点我在第 5 章还会展开,这里先记住一个结论:做测试类应用,temperature不要超过 0.3。
max_tokens是另一个坑点。如果模型需要先输出一段分析再给下一题,1024 个 token 通常够用;但如果用户历史回答很长,模型可能会在生成长文本时被截断,导致返回的 JSON 只有前半部分,端侧解析直接报错。后面避坑章会专门讲这个现象。
2.3 请求封装:把 DeepSeek 调用包成可复用的对话函数
mbti-chat云函数的index.js是整个项目的心脏,它把用户的历史答案、当前题目、系统提示词拼装好,然后调用 DeepSeek 接口。业务上拆得很干净,我做了代码提取,关键部分长这样:
// cloudfunctions/mbti-chat/index.js const cloud = require('wx-server-sdk') const axios = require('axios') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event) => { const { question, history, sessionId } = event const apiKey = process.env.DEEPSEEK_API_KEY // 拼装消息序列:系统提示词固定,历史题目和答案按序插入 const messages = [ { role: 'system', content: buildSystemPrompt() }, ...history.map((item) => ({ role: item.role, content: item.content, })), { role: 'user', content: question }, ] const resp = await axios.post( 'https://api.deepseek.com/chat/completions', { model: 'deepseek-chat', temperature: 0.2, max_tokens: 1024, messages, }, { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, timeout: 30000, } ) return formatResponse(resp.data) }这段代码逻辑上是三层:
第一层是消息序列拼装。system提示词固定描述 MBTI 测试规则,history按时间顺序插入用户之前每道题的答案和模型出的题,最后把当前问题作为user消息追加。注意顺序不能乱,聊天模型对消息顺序极其敏感,乱序直接导致模型“失忆”。
第二层是请求参数。model字段用的deepseek-chat,如果你在平台侧创建了其他模型别名,这里要同步改。temperature固定 0.2 是为了保证输出稳定性,实测在这个值下连续跑同一套题目,结果一致性可以接受。
第三层是错误处理。formatResponse函数负责统一返回值,如果 DeepSeek 返回错误码,会在这一层把错误信息包装成前端能识别的结构。源码里对网络异常和业务错误做了区分,前端拿到不同的 code 可以给出不同提示。
history参数里的内容格式是源码默认的,前端在调用之前已经做了一次半结构化处理:每道题保存了“题目编号 + 用户选项 + 该题维度倾向”,传给云函数时再拼成自然语言描述。这种方式比直接传原始数组更稳,因为模型能直接读到语句化的上下文,而不是面对一堆 id。
3. MBTI测试提示词与题目引擎:把自由对话收敛成16型判定
3.1 直接丢题库给大模型为什么会翻车
很多接触过大模型接口的同学,会想“我直接把 MBTI 题库全部塞给 DeepSeek,让它自己出题自己判,不就行了”。源码里没有用这个方案,原因很现实。
第一,MBTI 题目是标准化量表,每道题有明确的选项和计分方向,大模型生成的题目容易和标准信度打架。第二,MBTI 判定依赖四维度得分,模型如果自由发挥,可能给出“你像是 INTJ,但也可能是 ENTJ”这种模糊结论,对毕设答辩来说不够硬。第三,题库数据有重复测试的需求,同一道题在不同会话里让模型重新生成,题目描述不一致,结果就没法比较。
这套源码的做法是把题目引擎放在前端状态机,DeepSeek 只做两件事:根据当前题目生成人性化的引导语句,以及根据完整答题历史生成最终报告。题目的顺序、选项、维度权重全部由代码控制,模型不参与出题,只参与表达。“表达”这件事可以发散,但“判定”这件事必须收敛。
3.2 系统提示词与 Few-shot 设计:两层保障缺一不可
源码里buildSystemPrompt函数组装了系统提示词。核心结构是三层指令组合:
你是MBTI人格测试助手。 规则: 1. 每次只出一道题,不解释题目背景。 2. 根据用户选项,给出简短回应,再出下一题。 3. 所有回答使用中文。 4. 用户完成全部题目后,根据四维度得分输出16型人格结论。 5. 禁止输出任何超出16型人格之外的分析。这段提示词看起来简单,但实测效果差异很大。我在复现时发现,不加规则 2 的时候,模型会在用户答完一题后自己补一段“这道题说明你在某某维度上的倾向是……”的长篇分析,把测试节奏拖得很慢。加了规则 2 之后,每次回复被约束在一个很紧凑的结构里,基本就是“回应 + 下一题”。
源码里还加了一组 Few-shot 示例,这是第二个保障层。Few-shot 不是给模型题库,而是给模型“怎么出下一题”的对话范例:
用户:选A 助手:明白,这一题你更看重外部世界的直接反馈。下一题: 面对困难时,你更倾向于______。 A. 马上找朋友商量 B. 独自静一静再想办法这个示例教给模型两件事:其一,每一轮是多轮对话的延续,必须引用上一题用户的选项;其二,回应要简短克制,不能每轮都变成“心理分析小作文”。我把 Few-shot 删掉测试过一次,模型在第三四题就开始出现“你已经体现了明显的 Fi 倾向”这种话,测试体验被破坏得很明显。
3.3 题目流转与结果判定:前端状态机和云端计分
题目流程不是简单地把 93 道题一股脑塞给模型。源码在test页面里维护了一个状态机,核心字段是currentIndex、answers和phase。phase有三个值:answering、loading、finished。
状态流转规则是:用户点选选项 → 答案写入answers数组 →currentIndex + 1→ 判断是否还有下一题 → 有则请求云函数生成下一题,没有则跳转到结果页。
每道题对应一个维度方向,源码里有一个映射表,题目和 MBTI 四个维度的对应关系是硬编码的。计分逻辑在mbti-result云函数里,不是让模型直接给结论,而是先由代码统计四个维度的倾向票数,再把汇总结果喂给模型生成描述文本。
| 维度 | 计数规则 | 示例题目方向 |
|---|---|---|
| E/I | 选 E 加一分 | 聚会后你更倾向于继续社交还是回家独处 |
| S/N | 选 S 加一分 | 你更相信直接经验还是直觉判断 |
| T/F | 选 T 加一分 | 做决定时优先逻辑还是优先感受 |
| J/P | 选 J 加一分 | 你更喜欢明确计划还是随机应变 |
这个设计的巧妙之处在于,模型在对话过程中的输出只影响用户体验,不影响最终判定。判定结果完全由代码算出来,模型只负责把判定结果润色成一段像模像样的人格描述。毕设答辩时被问到“AI 在测试里到底承担什么角色”,这个答案可以讲得清清楚楚。
4. 小程序端渲染适配:从 AI 响应到测试页面的完整链路
4.1 WXML 结构与数据绑定:聊天流式页面的骨架
答题页test在视觉上是一个聊天窗口,模型消息和用户选项交替出现。微信小程序的 WXML 不支持复杂表达式,数据绑定要靠 Page 里的data对象驱动。源码里这个页面用了一个messageList数组来存所有消息,渲染层是一个简单的循环:
<!-- pages/test/test.wxml --> <scroll-view scroll-y class="chat-scroll" scroll-into-view="{{lastMsgId}}"> <block wx:for="{{messageList}}" wx:key="id"> <view class="msg-row {{item.type === 'user' ? 'msg-user' : 'msg-ai'}}"> <text>{{item.content}}</text> <view wx:if="{{item.type === 'ai' && item.options}}"> <button wx:for="{{item.options}}" wx:key="value" >// pages/test/test.js 节选 onOptionTap(e) { const value = e.currentTarget.dataset.value const currentQ = this.data.currentQuestion // 1. 立即反馈用户点选,不阻塞 UI this.setData({ messageList: this.data.messageList.concat([ { id: this.genId(), type: 'user', content: currentQ.template.replace('{x}', value) }, ]) }) // 2. 本地记录答案,用于断点续测 this.saveAnswer(currentQ.id, value) // 3. 请求下一题,异步更新 messageList wx.showLoading({ title: '思考中...' }) api.nextQuestion(currentQ.id, value).then((res) => { this.setData({ messageList: this.data.messageList.concat([res.message]), currentQuestion: res.message, }) }).finally(() => wx.hideLoading()) }这个写法有几个细节值得学。第一,用户点选之后先本地追加一条消息,视觉反馈立即出现;第二,请求云函数期间用 loading 遮罩,避免重复点选;第三,saveAnswer每次同步写入本地存储,这是第 4.3 节断点续测的基础。
卡顿的根源通常在concat大数组。如果消息列表有几十轮,每轮都要拼接整个数组,国产中低端机在渲染长列表时会有明显帧率下降。源码给了一个折中:消息超过 20 条时,把最早的消息折叠成一条“历史记录已折叠”,列表长度有上限,滑动性能可控。
4.3 云开发还是自建后端:选型对比与决策逻辑
这套毕设源码用的是云开发,但选型不是没有代价的。把 DeepSeek API 的调用放在云函数里,好处是免鉴权、免运维、免服务器费用;坏处是云函数有超时和并发限制,如果测试人数一多,可能触发并发上限报错。
| 维度 | 云开发 | 自建后端 |
|---|---|---|
| 部署成本 | 低,控制台直接部署 | 中高,需要服务器和运维 |
| 超时限制 | 默认 3 秒,可配置到 60 秒 | 自己控制 |
| 并发上限 | 受配额限制,有免费额度 | 取决于服务器配置 |
| 密钥安全 | 云函数环境变量,较好 | 需要自己做权限管理 |
| 适合场景 | 毕设、小规模演示 | 正式上线、大量并发 |
如果你只是做毕设答辩,云开发足够。如果要把这个小程序推到线上给几百个人同时测,就要换自建后端,因为云函数的冷启动和并发控制会成为瓶颈。我的建议是毕设阶段先照抄云开发方案,等答辩过了想上线,再把mbti-chat迁到自己的 Node 服务里,前端只改api.js里的调用方式即可。
4.4 本地缓存与断点续测:不丢答案的后悔药
测试做到一半退出小程序,再进来能接着做,这是源码里已经实现好的能力,实现载体是store.js。它封装了wx.getStorageSync和wx.setStorageSync,保存的数据结构是:
mbti_test_state = { currentIndex: 12, finished: false, answers: [ { qid: 1, value: 'A', dimension: 'E' }, { qid: 2, value: 'B', dimension: 'N' } ] }页面onLoad时先读缓存,如果finished为true则直接跳结果页;否则恢复currentIndex和answers,再根据最后一题内容触发生成下一题。这个恢复逻辑放在onLoad而不是onShow,是为了避免每次从后台切回来时重复触发。
这里有个人人都会踩的坑:wx.setStorageSync同步写入会在数据量大时短暂阻塞主线程。答题阶段一次写一个答案没问题,但如果你把整个answers数组每次都整体写入,几十轮之后低端机会明显卡顿。源码的做法是每次都写完整对象,因为毕设场景数据量不大;如果你要优化,改成只写增量字段,等退出时再合并完整状态。
5. 常见问题与避坑:为什么有人跑得顺滑,有人一堆报错
5.1 现象:JSON 解析偶发失败,AI 回复被截成半截
复现的时候最容易遇到的报错是“JSON.parse 解析失败”,表现为模型出题出到一半,页面弹出错误提示,卡死在 loading 状态。
原因是max_tokens设置太小时,模型长输出被截断,返回的 JSON 只有前半段。尤其用户历史答案累积较长之后,模型需要生成的“过渡话术 + 下一题 + 选项”总长度变大,截断概率直线上升。解决方法是把max_tokens从 1024 提高到 2048,同时要求模型只返回结构化的{ 回应, 下一题, 选项 }对象,减少多余文字。如果还偶发,可以在云函数里补一层 try-catch,解析失败时自动重试一次。
我一般会在本地先构造一条超长历史答案,连续测试 10 次,看 JSON 解析是否稳定,再决定要不要调max_tokens。
5.2 现象:同一套题目答两次,结果人格类型不一样
测试两次结果不一致,在 AI 测评类应用里是致命的。明面上是模型随机性问题,根因在temperature参数。源码默认 0.2 不是随便写的,我调到 0.8 测过一次,同一份答案跑三次,三次出来三种人格描述,甚至有一次把“你偏向外向”写成“你偏向内向”,直接导致维度结论和报告文本自相矛盾。
解决的思路不是只调低temperature,还要把判定和生成做分离。正如第 3 章所述,最终人格类型由代码计分判定,模型只做文本润色,这样即便模型措辞有波动,标签结论不会变。如果还有偏差,检查是否把历史答案完整传给了模型,缺历史消息时模型只能靠猜。
5.3 现象:答题页滑动长文本时帧率骤降,安卓真机尤为明显
毕设答辩用真机演示时,滑动消息列表卡顿是非常尴尬的现场事故。原因是setData传递了整个messageList大数组,微信的视图层每次都要重新 diff 整棵节点树。安卓低端机在消息超过 20 条后帧率会掉到个位数。
解决之道是限制列表长度和分离更新粒度。消息超过 20 条折叠历史;新消息单独setData而不是重新赋值整个数组。如果要用健壮做法,把消息列表拆成currentView和historyCache两部分,渲染只依赖currentView。源码用的是第一种方案,够用。
5.4 现象:云函数偶发超时,测试卡死在“思考中”
第一次真机预览时,这个坑几乎必踩。云函数默认超时时间是 3 秒,而 DeepSeek 单次请求在历史答案较长时经常超过 3 秒,前端等不到返回值,loading 一直转。
解决方法是去云开发控制台把mbti-chat的超时时间改到 30 秒,同时利用config.json里的定时触发器保持函数热状态。提醒一句:超时时间改长之后,如果调用方没有自己的超时打断机制,用户端会白等很久,所以在api.js里额外加了一层 10 秒的 Promise 超时,超时后提示用户点击重试。
5.5 现象:真机预览时云函数调用被拒绝,提示环境不匹配
开发工具里跑得好好的,一上真机就报环境错误。原因通常是云开发环境 ID 没有统一。开发工具默认使用的是默认环境,真机预览如果切换了调试基础库或没登录同一个微信账号,环境 ID 对不上,调用就会失败。
解决方法是把环境 ID 显式写在app.js里初始化,不要依赖工具默认值。wx.cloud.init({ env: '你的环境ID' }),替换成你自己的云环境 ID 即可。另外确认project.config.json里的cloudfunctionRoot路径正确,否则本地调试时云函数不会上传。
6. 进阶玩法:流式输出与结果报告导出
基础版跑通之后,有两个优化能显著提升答辩观感:一是把 DeepSeek 的回复改成流式输出,二是把结果页的报告导出成图片或 PDF。流式输出的体验是,模型边生成边显示,用户看到逐字弹出的“下一题”,而不是转圈等 3 秒再一整块蹦出来。
微信小程序的云函数不支持直接跑 SSE 流,但可以在云函数里把stream: true的响应完整等齐再返回,前端模拟打字机效果。真正要快,就自建后端用 WebSocket 转发,成本高一些,但体验有质的飞跃。毕设场景我建议先做打字机动画,代码量不大,理由充分。
报告导出推荐用wx.canvasToTempFilePath或后端生成图片返回。最简单的方式是在结果页渲染一个定制卡片,然后调用截图 API 生成临时文件。源码包里结果页的数据结构已经支持扩展,mbti-result云函数返回的报告对象里含有人格类型简称、维度得分、描述文本三部分,前端拼个海报模板完全够用。
// pages/result/result.js 节选 const report = res.data.report const canvasWidth = 300 const canvasHeight = 480 // 在 canvas 上绘制人格类型、得分条形图与描述摘要 drawReport(report).then(() => { wx.canvasToTempFilePath({ canvas: canvasRef, success: (r) => { wx.saveImageToPhotosAlbum({ filePath: r.tempFilePath }) } }) })这段代码的思路是:把报告渲染到画布,然后截图保存到相册。维度得分用条形图表达,条形长度由各维度占比计算,视觉效果比纯文字丰富很多。canvas 绘制时注意wx.canvasToTempFilePath要在 canvas 绘制完成后的回调里调用,否则截到的是空白画布。
从那以后,每次拿到这类 AI 对话类毕设项目,我都会强制检查三个位置:模型参数里的temperature、云函数的超时配置、前端setData的粒度。这三处只要有一个偷懒,线上体验就会翻车。这套源码的价值在于它把“测试题引擎”和“模型生成话术”解耦开,这种架构思路比单纯调 API 值得学得多。希望帮到你。
本文还有配套的精品资源,点击获取