码道实践:从 0 到 1 打造「心屿」AI 对话心理陪伴助手
码道实践:从 0 到 1 打造「心屿」AI 对话心理陪伴助手
前言
当代社会节奏越来越快,无论是深夜还在赶工加班的职场人,还是被学业压得喘不过气的学生,负面情绪每个人都会有,但并不是每个人都能在情绪来临时找到一个人好好倾诉。传统心理咨询服务虽然专业,却存在预约繁琐、费用较高、私密性不足、沟通门槛高等现实痛点。于是,一个朴素的念头在我心里发芽:能不能用大语言模型,做一个全天候在线、高私密、零门槛的"情绪树洞"?
带着这个想法,我在码道平台上完成了「心屿 · AI 对话心理陪伴助手」这个项目的全部开发。它是一款基于 DeepSeek 大模型的智能对话应用,核心定位是情绪陪伴与正向心理引导,严格区分与医疗诊疗的边界。这篇文章记录下我从需求分析、方案设计,到代码实现、测试交付的完整过程,希望能给同样想做 AI 应用的朋友一些参考。


一、项目定位:做一个有温度的情绪树洞
在动手之前,我首先思考了一个问题:什么样的产品才配叫"心理陪伴助手"?答案是,它必须同时满足三个条件:
第一,懂共情。当用户说"我今天好累"时,AI 不能像搜索引擎一样给出一堆心理学名词,而要先把这份疲惫接住,让用户感到"被看见、被理解"。共情是心理陪伴的第一课,技术上的体现就是:AI 的回复必须先承认情绪、不评判,再考虑下一步。
第二,有边界。心理陪伴不等于心理治疗。AI 不应该去做诊断、开药、下结论,而应该做的是倾听、疏导、给建议、劝求助。尤其当检测到自伤、轻生等高危信号时,必须温柔而坚定地引导用户寻求专业帮助。这条边界我在提示词里写死了,也在界面上用安全提示条做了兜底。
第三,低门槛。不需要下载 App、不需要注册审核、不需要预约排队,打开网页就能说心里话,隐私本地保存,谁都可以用。这决定了技术选型必须轻——一个静态页面是最好的载体。
在需求分析阶段,我还认真调研了目标用户:他们往往是深夜emo的职场人、备考压力的学生、社交中感到孤独的年轻人。这些人有共性——不是"病了",只是"累了",需要有人听、有人懂、有人给一点照亮前路的小光。甚至还有很多用户只是想让 AI 陪自己聊聊天、说说日常,并不需要任何"心理干预",所以产品在"专业疏导"和"日常闲聊"之间必须取得平衡,绝不能每条回复都端着心理学的架子。
基于这样的定位,我梳理出产品的核心功能清单:
- 沉浸式智能对话:具备上下文记忆,适配学习压力、职场焦虑、人际关系、情绪内耗等多种场景;
- 智能情绪识别:实时捕捉对话中的情绪,输出情绪类型、困扰程度与变化趋势;
- 情绪打卡:用户发送消息前可标记"此刻心情",让 AI 更贴近真实状态;
- 结构化建议:每条回复附带 1~3 条可以立即做的小行动;
- 呼吸/接地练习:内置 4-7-8 呼吸引导动画与 5-4-3-2-1 接地练习;
- 内容安全过滤:高危信号自动提示并给出心理援助热线;
- 对话历史:浏览器本地保存、支持导出与回看。
产品名字叫"心屿"——屿,是海中的小岛。它想表达的意象是:在你情绪的大海里,这里有一个永远亮着灯的小岛,随时欢迎你靠岸歇一歇。
二、技术选型:为什么用纯前端
确定功能之后,技术选型是我面临的第一个决策。最初我考虑过 Flask/FastAPI 后端渲染的方案,但转念一想,很多 AI 应用场景其实完全可以用纯前端解决:
- 零部署成本:一个静态网页挂到任何托管平台即可运行,无需服务器、无需数据库;
- 响应迅速:前端直连大模型 API,省去中间层转发,延迟更低;
- 天然私密:对话数据存在用户自己的浏览器 localStorage 里,不经过自建服务器,隐私性反而更强。
因此最终确定技术栈为:HTML5 + CSS3 + 原生 JavaScript,零框架、零构建、零依赖。AI 能力接入 GitCode AI 平台提供的 OpenAI 兼容接口,模型选用 deepseek-ai/DeepSeek-V4-Flash,并通过标准的 SSE(Server-Sent Events)流式接口实现打字机式的输出效果。
项目结构非常轻量:
AIduihuaxinlipeibanzhushou/
├── index.html # 页面骨架:聊天区、情绪打卡、快捷场景、历史抽屉、呼吸弹层
├── css/
│ └── style.css # 全部样式:治愈系渐变、玻璃拟态、交互动画
├── js/
│ └── app.js # 全部逻辑:API 流式调用、富卡片渲染、状态管理、本地存储
└── README.md # 项目文档
三、核心设计:让 AI 说"结构化的话"
做 AI 应用,最重要的一环往往不是界面,而是提示词工程。普通对话机器人只要 AI 输出文本就行,但"心屿"需要在前端渲染出情绪卡片、建议列表、练习引导按钮等多种组件,这就要求 AI 的输出必须是结构化的。
我的方案是:在系统提示词中要求 AI 每次回复只输出一个合法的 JSON 对象,并把字段协议定义得清清楚楚。协议大致如下:
{
"reply_type": "empathy | encourage | question | advice | breathing | grounding | celebration | safety",
"reply_text": "给用户看的核心回复,80~180字,温暖口语化",
"emotion": {
"detected": "识别到的核心情绪,如焦虑/疲惫/孤独",
"level": 0,
"trend": "up | down | stable"
},
"suggestions": ["1~3条具体可立即执行的小建议"],
"guide": {
"title": "引导活动标题(无引导时为 null)",
"type": "breathing | grounding | action | null",
"steps": ["2~4步引导步骤"]
},
"safety": {
"risk": "low | medium | high",
"message": "安全提示内容(risk 非 low 时填写)"
}
}
这套协议有几个精心设计的点:
-
reply_type 决定前端回应的"姿态"。同理心、鼓励、提建议、做练习……不同的回复类型在前端渲染成不同的徽章样式,用户一眼就能感知到 AI 此刻是在"陪你说话"还是在"帮你疏导"。
-
emotion 是情绪洞察卡。
detected告诉你识别到了什么情绪,level(0~10)刻画困扰程度,前端会渲染成一条平滑的进度条动画,trend用箭头提示情绪趋势。这让整个对话过程多了一层"自我觉察"的价值——用户能看到自己的情绪被清晰地命名。 -
suggestions 强调"可立即执行"。心理学上有"行为激活"的干预思路:情绪低落时给一个微小、具体、立刻能做的行动,比讲大道理有效得多。所以我在提示词里明确要求每一条建议都不超过 25 字,且必须是"喝一杯温水"、"给窗外的天空拍张照"这种级别。
-
guide 做"动手引导"。当检测到用户高度焦虑时,AI 会给出
breathing类型的指导,前端弹出一个有 4-7-8 呼吸法动画的引导层,用户跟着圆圈缩放和倒计时就能完成一轮呼吸练习,比单纯读文字更有沉浸感。接地练习(5-4-3-2-1 法)同理。 -
safety 是安全底线。一旦提示词嗅探到自伤、轻生等信号,
risk会置为high,前端渲染红色安全提示条,附上全国心理援助热线 12356,并强调"你很重要,不必独自扛着"。这是产品伦理上必须守住的红线。
值得说明的是,提示词里我用了几处"心智小技巧"来保证输出格式稳定:明确禁止输出 markdown 代码块标记、明确要求"必须能被 JSON.parse 直接解析"、对 guide.steps 直接给出了呼吸练习的示例文本让模型照抄,大幅降低了模型格式跑偏的概率。
这里我把系统提示词的核心片段摘录出来,供大家参考我是怎么"围堵"格式错误的:
【硬性要求】
你每次回复都必须严格输出一个合法的 JSON 对象,不得输出任何多余文字、
解释、markdown 代码块标记(不要用 ```)。输出必须能被 JSON.parse 直接解析。
【对话原则】
1. 永远先共情、再引导:先接纳用户的情绪,再温和地推进。
2. 用词温暖、口语化,多用"你",少用专业术语,不评判、不指责、不说教。
3. 鼓励的表达要具体,避免空洞的"加油"。
4. 用户情绪平稳、轻松闲聊时,可自然陪伴互动,不必每次都切入心理话题。
5. 当检测到自伤/轻生等高危信号时 reply_type 用 "safety",safety.risk 为 "high"……
把"格式约束"放在最前面、把"对话原则"放在后面,是参考了大模型对提示词头部和尾部注意力更强这一规律;同时把所有枚举值、示例步骤直接写在提示词里,等于给了模型一份"标准答案",它在照抄示例时的格式合规率会高非常多。
四、从 Python 到 JavaScript:流式调用的转换
项目需求里给定了一段 Python 参考代码,展示了如何调用 GitCode AI 的流式接口:
def query(payload):
response = requests.post(API_URL, headers=headers, json=payload, stream=True)
for line in response.iter_lines():
if not line.startswith(b"data:"):
continue
if line.strip() in (b"data:[DONE]", b"data: [DONE]"):
return
yield json.loads(line.decode("utf-8").lstrip("data:"))
chunks = query({
"model": "deepseek-ai/DeepSeek-V4-Flash",
"messages": [{"role": "user", "content": "告诉我一个有关宇宙的有趣事实?"}],
"stream": True,
"max_tokens": 2048,
"temperature": 0.6,
"top_p": 0.95,
"frequency_penalty": 0,
"thinking_budget": 2048
})
Python 的 requests.iter_lines() 会按行迭代响应体,逻辑清晰。而在纯前端语境下,等价的工具是 fetch + ReadableStream + TextDecoder。我的转换思路是保持语义一致:一字节一字节地读取底层数据流,用 TextDecoder 增量解码,再把解码文本按行切分处理。整个过程用 reader.read() 的 async 循环实现:
const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // 最后一行可能不完整,留到下一轮
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith("data:")) continue;
const dataStr = trimmed.replace(/^data:\s*/, "");
if (dataStr === "[DONE]") break;
try {
const json = JSON.parse(dataStr);
const delta = json.choices?.[0]?.delta?.content || "";
if (delta) fullText += delta;
} catch (e) { /* 忽略不完整行 */ }
}
}
这里有个细节值得展开:SSE 流在传输过程中,边界不是恰好落在换行符上的,所以我把 buffer 拆行后,总是把最后一段(可能是半个 data: 事件)留到下一次循环再处理,再配合 decoder.decode(value, { stream: true }) 处理多字节 UTF-8 字符的跨 chunk 截断问题。这两个处理是流式解析不出 bug 的关键。
关于 thinking_budget 的一个发现
我在评测中发现,DeepSeek-V4-Flash 这类模型在流式响应时,会先输出一段 reasoning_content(模型内部推理过程),之后才输出真正展示给用户的 content。参考代码里直接 print(chunk["choices"]) 把两类内容都打出来,中间还夹杂了思考过程。
我的前端做了正确的取舍:只提取 delta.content,忽略 reasoning_content。用户的聊天界面是给用户看的,他们不需要看到 AI 的思考草稿,这既保持了对话的干净自然,也是一种性能优化——不用把推理过程也渲染到 DOM 上。
五、前端实现:把 JSON 翻译成有温度的界面
拿到结构化的 JSON 之后,剩下的工作就是把它们渲染成好看的界面。我把渲染逻辑分成两层:
第一层是富内容渲染器 renderRichData,它根据 JSON 按顺序拼装组件:类型徽章 → 正文 → 情绪卡 → 建议卡 → 引导卡 → 安全提示条。第二层是各独立组件函数,比如 renderEmotionCard、renderGuideCard。
情绪卡渲染有个小彩蛋:level 进度条不是静态画上去的,而是先以 0 宽度生成,再在 requestAnimationFrame 里把它平滑过渡到目标宽度,配合 transition: width 0.8s,滚动到这条消息时进度条会"慢慢生长",视觉上很治愈。
引导卡则根据 guide.type 分发到不同的交互体验:
btn.addEventListener("click", () => {
if (guide.type === "breathing") openBreathModal(guide);
else openGuideModal(guide);
});
openBreathModal 实现了真正的 4-7-8 呼吸训练器:一个圆形元素通过 transform: scale() 放大缩小表示"吸气—屏息—呼气"三阶段,配合同步的秒数倒计时与"吸气/屏息/呼气"文字提示,一轮结束后自动进入下一轮,循环 4 次后庆祝完成。这用 CSS 的 transition: 1s ease-in-out 配合 JavaScript 的 setInterval 递减即可实现,没有引入任何动画库。
流式打字机:如何让"加载"也好看
对话体验里还有一个容易被忽略但很影响观感的细节——等待回复的过程。我实现了一个"思考气泡":发送消息后,AI 侧立刻出现一个带三个跳动圆点的气泡,下面还会实时滚动显示已经收到的文本。三个圆点用 CSS animation: blink 1.2s infinite 加错峰 animation-delay 实现跳动;文本则不断从累积的 JSON 缓冲中提取 reply_text 字段的当前片段来更新,形成一种"AI 正在斟酌措辞"的临场感。
如果最终 JSON 解析失败(比如模型偶尔没忍住输出了 markdown 代码块),还有一道兜底逻辑:先剥离代码块围栏,再截取首个 { 到末个 } 之间的内容尝试解析,仍然失败就把原始文本当作普通消息展示,保证对话永远不会中断。
情绪打卡与快捷场景
情绪打卡是产品情感设计的一部分:输入框上方有 5 个表情按钮(难过→开心),用户点选后,这一条消息会带上 [用户此刻心情打卡:😞 低落] 的前缀上下文发送给模型。这样做有两个用意:一是给 AI 提供额外的情绪信号,让响应更贴合;二是帮用户养成"识别并命名自己的情绪"的习惯。
快捷场景则是一组场景标签(学习压力、职场焦虑、人际关系、情绪低落),点击即填入预设的倾诉文本并自动发送。它解决了大多数用户"不知道跟 AI 说什么"的冷启动问题——你先点一个场景,对话就自然开始了。
六、本地存储:给用户一本"心情手账"
对话历史用浏览器的 localStorage 实现,存储结构是"会话列表":
{
id: sessionId, // 会话唯一 ID
time: nowTime(), // 记录时间
title: "会话标题", // 取第一条用户消息前 20 字
messages: [...] // 该会话的完整消息
}
完成一轮对话后自动写入(最多保留 30 条会话),用户可以在右上角的"历史"抽屉里回看任意一场对话,也可以一键导出 JSON(下载一份完整的手账文件),或者清空全部记录。所有数据都存在用户自己的浏览器里,不上传任何服务器——这本身就是"高私密"定位的落地。
七、UI 设计与细节打磨
心理陪伴类产品对视觉有天然的高要求,UI 不能冷冰冰。我定下的设计语言是:“午后窗户边的一杯热牛奶”——柔和、温暖、有包裹感。
- 色彩:薰衣草紫渐变为主色,暖粉色点缀,背景是
#eef1ff → #fdf0f3的柔和渐变,整页没有刺眼的纯白或纯黑; - 玻璃拟态:顶栏、输入区采用半透明背景 +
backdrop-filter: blur(),营造轻盈的悬浮感; - 圆角与阴影:气泡、卡片大量使用 14~20px 圆角,配合柔和的彩色阴影,让每个元素都像"软软的枕头";
- 微动效:消息入场有
msgIn上浮渐显动画,hover 状态有轻微放大的反馈,呼吸圆有长周期缩放——动效量不多,但每一处都服务于"被温柔对待"的感受; - 移动端适配:用媒体查询保证在手机上聊天区自适应,输入框下方用
env(safe-area-inset-bottom)适配全面屏手势区。
八、内容安全:为高危场景守住底线
作为心理陪伴产品,安全设计不是可选项,而是必需品。我在系统提示词里明确要求:
当检测到自伤、轻生、极端绝望等信号时,reply_type 用 “safety”,emotion.level 给高值,safety.risk 为 “high”,message 须包含:理解其痛苦、强调并非孤身一人、强烈建议寻求专业帮助、提供国内 24 小时心理援助热线,并鼓励联系信任的人或就医。
对应到前端,safety.risk !== "low" 时会渲染一条醒目的暖红色提示条,标题是"你很重要,这些想法很沉重,但不必独自扛着"。同时我在 README 的免责声明中明确标注:本项目不提供心理疾病的诊断、治疗或用药建议,如处于危机状态请立即拨打全国心理援助热线 12356。
这是我对"AI 陪伴"边界的理解:陪伴可以轻柔,但底线必须坚硬。
九、测试与交付验证
代码写完之后,验证环节一点都不能含糊。我做的验证包括:
1. 接口连通性验证:先用 curl 直接以小请求测试 GitCode AI 接口,确认鉴权、模型名、流式格式都正确,并确认返回的 delta.content 是最终展示内容、delta.reasoning_content 是推理草稿,验证了前端只取 content 的正确性。
2. SSE 全链路模拟:用脚本驱动一个模拟接口返回完整流,校验前端拼接后的 JSON 能被 JSON.parse 正确解析,[DONE] 能正确终止循环。
3. 浏览器端到端测试(Playwright):启动本地静态服务器,用 Playwright 逐一断言:
- 欢迎卡片正常渲染;
- 情绪打卡选中状态正确切换;
- 点击快捷场景能自动发送消息;
- AI 回复完成后富卡片正常出现(类型徽章存在);
- 情绪卡片、建议卡片、引导卡片均渲染成功;
- 呼吸练习弹层可打开、可关闭;
- 历史抽屉可打开、可关闭;
- 全程控制台无任何 JS 报错。
这一轮测试真的帮我抓到一个 bug:最初 typingBubble.remove() 调用的是对象而非真实 DOM 节点(返回的是 { text } 而误调了 .remove()),正是因为端到端测试断言了"无 JS 错误"才把它揪了出来。修完之后再跑一轮,全绿通过。
4. 安全降级验证:构造了一个"非 JSON"响应,确认前端能正常降级为纯文本展示,对话链路不中断。
十、部署:一条命令跑起来
纯前端项目最大的好处之一就是部署近乎零成本。把这个项目部署起来只需要三步:
第一步,克隆代码(或在本地直接打开 index.html):
git clone https://atomgit.com/2601_96901552/AIduihuaxinlipeibanzhushou.git
cd AIduihuaxinlipeibanzhushou
第二步,启动一个本地静态服务器(推荐,可规避个别浏览器对 file:// 协议的跨域限制):
python3 -m http.server 8080
第三步,浏览器访问 http://localhost:8080,就可以开始对话了。
如果后续要上线公网,可以直接把这个目录推送到任意静态托管平台(GitHub Pages、CloudFlare Pages、Gitee Pages 等),无需任何后端配置。唯一要提醒的是:API Key 目前以明文写在 js/app.js 的 CONFIG 里,仅供本地演示,正式上线前务必将它迁移到后端代理接口,做一次"前端 → 你的后端 → 大模型 API"的转发,并对 Key 做权限与用量管控,避免被他人盗用产生费用或风险。
十一、踩过的坑与复盘
整个开发过程并不是一帆风顺,这里记录几个印象最深的坑,也是新手最容易被绊倒的地方:
坑一:SSE 流的"半行"问题。刚开始我按"每读到一轮数据就完整处理一行"的思路写,结果常常出现 JSON 解析失败。原因在于底层字节流不会恰好按行边界切分,一行 data: {...} 可能被拆成两半分别到达。解决方法是维护一个 buffer,每轮把完整的行取出来处理,不完整的尾部留给下一轮拼接。这个坑几乎每个手写 SSE 解析的人都会踩。
坑二:TextDecoder 的流式中断问题。如果直接 decoder.decode(value),遇到多字节 UTF-8 字符(比如中文、emoji)被拆到两个 chunk 时,会产生乱码。正确写法是 decoder.decode(value, { stream: true }),并在流结束时用 decoder.decode() 冲刷残余字节。中文应用里中文和 emoji 满屏都是,这个细节直接决定输出会不会出现"口口口"。
坑三:demo 里的 DOM 断言。我在端到端测试时写了个断言"控制台无 JS 错误",结果真的抓到一个 bug——打字气泡的返回值是 { text } 对象,而我误以为它返回了 DOM 节点,调用 .remove() 直接报错。这一下让我意识到:自动化测试不是形式主义,它是真的会替你把 bug 从犄角旮旯里拎出来的。
坑四:模型会先"想"后"说"。第一次看到 reasoning_content 字段时我一度把它当成了正式回复直接展示,结果聊天框里出现了一大段密密麻麻的推理过程,用户体验瞬间崩塌。最终处理是只消费 delta.content,reasoning_content 直接忽略。
坑五:localStorage 容量。长时间对话消息越来越多,localStorage 有 5MB 左右的容量上限,超出后 setItem 会抛异常。我在存储处做了 try/catch 兜底,并限制只保留最近 30 条会话,让极端情况下也不会让对话功能崩溃。
坑六:移动端安全区。iPhone 全面屏的底部手势条会遮住输入框,需要在输入区底部加 env(safe-area-inset-bottom) 内边距。这类细节不做,手机用户会直接觉得"这个产品很粗糙"。
十二、总结与展望
从需求萌芽到完成交付,「心屿」这个项目让我完整走了一遍 AI 应用开发的闭环:需求分析 → 方案设计 → 提示词工程 → 流式对接 → 前端呈现 → 安全兜底 → 测试交付。
最大的收获有三个:
一是提示词即产品。在 AI 应用里,提示词不只是"让模型更听话",它就是产品数据结构的一部分。通过精心设计的 JSON 协议,我把一个对话模型变成了一个能输出结构化情绪洞察的"心理陪伴系统"。
二是流式体验决定产品质感。打字机效果、思考气泡、进度条生长动画……这些细节加起来,才让人感觉"对面真的有一个温柔的人在慢慢组织语言"。
三是安全不是功能,是底线。做陪伴类产品,必须对"伤害预防"有真实的敬畏。
未来这个项目还有很大的想象空间:可以把 API Key 迁移到后端代理(如 FastAPI)以提升安全性;可以基于 localStorage 的情绪数据生成情绪周报,让用户看到自己一周的情绪曲线;可以接入更丰富的多模态能力(如语音倾诉);也可以在隐私合规的前提下,引入匿名化的群体情绪洞察。这些小目标,我会在码道平台上继续迭代。
最后,欢迎每一位开发者 clone 这个项目,给它加上你自己的设计和想法。技术是冷的,但我们可以用它做温暖的事。也欢迎你到仓库主页查看完整代码与运行说明:https://atomgit.com/2601_96901552/AIduihuaxinlipeibanzhushou
—— 用代码,温柔地陪伴每一个人。
更多推荐


所有评论(0)