从想法到上线,一个人一周做完的 AI 小产品
背景
两周前一个做心理咨询的朋友问我:"能不能做个工具,让用户先跟AI聊聊天,再填标准量表,自动出评估报告?"
需求很清晰:
- AI对话评估 --- 用户像跟咨询师聊天一样描述状态,AI 引导式提问
- 标准量表 --- PHQ-9、GAD-7、SDS、SAS 四大临床量表,自动评分出报告
- 危机干预 --- 检测到自伤/自杀关键词或量表高分,立即弹热线
- 隐私 --- 全部本地部署,数据不留存
于是花了一周,做了个叫「心晴助手」的东西。今天把完整的做法和踩坑记录下来。
一、技术选型为什么这么搭
| 需求 | 选型 | 理由 |
|---|---|---|
| API 层 | FastAPI | 异步原生,自动生成 OpenAPI 文档,pydantic 校验 |
| AI 对话 | DeepSeek API | 便宜(百万token不到1块),兼容 OpenAI 格式,随时能换 |
| 前端 | Streamlit | 纯 Python 搞定 UI,不用写 HTML/JS/Vue |
| 量表数据 | JSON 文件 | 新增量表只需加一个 JSON 文件,零代码改动 |
| 部署 | Docker Compose | 用户拉下来 docker compose up -d 就能跑 |
这套组合最大的好处是:一个人能全栈搞定。
二、核心架构
javascript
┌─────────────┐ HTTP API ┌──────────────┐
│ Streamlit │ ────────────────► │ FastAPI │
│ 前端:8501 │ │ 后端:8001 │
└─────────────┘ └──────┬───────┘
│
┌─────────┴─────────┐
│ chat_agent.py │──► DeepSeek
│ (对话+危机检测) │
├───────────────────┤
│ engine.py │
│ (量表评分引擎) │
└─────────┬─────────┘
│
┌────▼────┐
│ scales/ │
│ JSON量表 │
└─────────┘
API 设计
一共 5 个端点,很克制:
python
GET /api/scales # 量表列表
GET /api/scales/{id} # 量表详情(含题目)
POST /api/scales/{id}/score # 提交答案,返回评分
POST /api/chat # AI 对话
POST /api/summary # 对话结束后生成评估摘要
为什么不做流式?做了,/api/chat/stream 用 SSE 返回,但 Streamlit 的 st.chat_message + 流式渲染在 1.28 版本后体验才够好。非流式在 DeepSeek 下响应时间 1-2 秒,能接受。
三、量表引擎:最花心思的部分
评分逻辑
四种量表评分方式不一样,engine.py 统一处理:
python
# PHQ-9 / GAD-7:直接看原始分
# 0-4 无,5-9 轻,10-14 中,15-21/27 重
# SDS / SAS:先算粗分,转指数
# 指数 = 粗分 / 80 × 100
# 按指数判定:0-49 无,50-59 轻,60-69 中,70-100 重
反向计分,最容易写错的地方
SDS 有 10 道反向题(如"我感到早晨心情最好"------选"没有"反而是抑郁),SAS 有 5 道。
python
# engine.py
if qid in reverse_items:
raw_score = 5 - raw_score
公式是 5 - raw_score,因为选项是 1-4 分。如果选"没有或很少时间"(分=1),反向后变成 4。验证时我算了一遍:
arduino
Q2 "我感到早晨心情最好" → 选"小部分时间"(分=2)
→ 反向计分 5-2=3(分数越高越抑郁 ✓)
危机预警双层防御
arduino
对话层 → check_crisis("想死", "自杀"...) → 匹配则返回热线
量表层 → PHQ-9 第9题 > 0 → alert_rules 弹出危机提示
不依赖 AI 判断,纯规则匹配,宁可误报不可漏报。
四、AI 对话:提示词设计
System Prompt
核心就一个原则:不要让他觉得自己在被审问。
diff
你是一位专业的心理评估师,名叫"心晴助手"。
- 通过自然对话了解用户的心理状态
- 使用情绪、睡眠、社交、压力、自我认知 5 个维度评估
- 语气温和、共情、不评判
- 不要下诊断,只用"可能"、"初步判断"等措辞
- 每次回答控制在 100-200 字
- 自然地引导对话,不要像审问一样连续提问
实测效果:用户说"最近睡不好",AI 回复不是直接甩量表,而是"听起来你最近压力比较大,能具体说说失眠的表现吗?比如入睡困难还是容易醒?"
评估摘要 Prompt
对话 4 轮以上后,用户可以点"生成评估报告"。这里有个坑------最开始我直接复用 chat() 函数,结果 AI 继续以咨询师角色回复,而不是生成摘要。
修复 :新增独立的 summarize() 函数,不注入对话系统提示词:
python
# 新函数,不含 SYSTEM_PROMPT
async def summarize(messages, system_prompt):
full_messages = [{"role": "system", "content": system_prompt}] + messages
# ... 直接调 LLM API
五、踩坑记录
1. Streamlit 的 rerun 陷阱
量表答题是逐题进行的,用户选一题 → 存 session_state → st.rerun() → 显示下一题。但 st.rerun() 后所有未缓存的请求会重新执行,如果不小心在 rerun 前调了 API,后端会被刷爆。
解决 :用 if qid in st.session_state.scale_answers: continue 跳过已答题。
2. 路径问题
一开始 SCALES_DIR 写的是 os.path.join(os.path.dirname(__file__), "..", "scales")。这在开发环境没问题,但 Docker 打包后 __file__ 的路径变了,量表加载失败。
解决 :支持 SCALES_DIR 环境变量覆盖,fallback 到约定路径。
3. PHQ-9 第 8 题的笔误
原 JSON 数据里第 8 题是"动作或说话速度缓慢到别人已经察觉?或正好相反------ Loss 得比平常更多"。这个 Loss 明显是翻译残留。改了。
4. LLM 调用没有重试
最开始直接 httpx.post(),没有重试。DeepSeek 偶尔会有 5xx 或超时,用户对话到一半报错很劝退。
解决 :用 tenacity 加 3 次重试,指数退避,全部失败后返回友好提示。
六、运行效果
ruby
$ curl -X POST http://localhost:8001/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"最近睡不好,心情也很低落"}]}'
{
"reply": "听起来你最近状态不太好,能具体说说吗?比如睡不着是因为脑子里想事情,还是身体上不舒服?"
}
PHQ-9 答题结果示例(总分 8,轻度抑郁):
erlang
📊 PHQ-9 评估结果
总分:8
等级:轻度抑郁
建议:可能有轻度抑郁情绪。建议关注自身情绪变化...
七、Docker 一键部署
bash
git clone https://github.com/tangwenqing123/mood-assistant
cd mood-assistant
cp backend/.env.example backend/.env
# 编辑 .env 填入 DeepSeek API Key
docker compose up -d
# 前端 http://localhost:8501
整个镜像加起来 600MB(Python 3.11-slim + FastAPI + Streamlit),不算大。
八、复盘
做得好的:
- 量表数据 JSON 配置化,加新量表零代码
- 危机检测双层防御
- 前后端分离,API 可复用
可以更好的:
- 没有测试覆盖(纯手测)
- 对话历史存在 Streamlit session_state 里,刷新就丢
- 没有用户系统(当前就是匿名使用)
项目地址:github.com/tangwenqing... 欢迎 Star,欢迎 PR,欢迎拿去改造成自己的版本。