一個把大型語言模型變成「不會亂講話」的心理健康日記平台:日記情緒偏低時,AI 的回饋必須引用 WHO、APA 等臨床來源;同時用 Random Forest 從過去 14 天的行為 預測 3 天後的情緒,提前給出壓力預警。獨立開發的畢業專題,從機器學習、後端、 前端、行動端到基礎設施一手完成。
線上體驗與原始碼
線上版本:heartbox.tw 無需註冊,直接用測試帳號登入。
三組帳號分別展示系統在不同情緒情境下的行為:
| 帳號 | 密碼 | 情境 | 適合看什麼 |
|---|---|---|---|
test1 | test1 | 波動型 | 情緒起伏大,看圖表如何呈現波動、身心關聯如何找出睡眠與活動的影響 |
test2 | test2 | 正向型 | 長期正向累積,看「我的進展」如何呈現成長軌跡 |
test3 | test3 | 負向型 | 情緒持續低落時,系統如何主動啟動關懷機制與知識庫建議 |
原始碼:github.com/alanlin0604/HeartBox
Problem
全球約 5.7% 的人口受憂鬱症影響(WHO),台灣青年的心理困擾比例也逐年上升。 但求助這件事本身就有門檻:諮商資源不足、候診時間長,而污名化讓很多人根本不會 踏出第一步。
市面上的心情記錄 App 填不了這個空缺,原因不只是功能少:
- 回饋停留在表面 —— 記錄了情緒,但沒有任何分析
- AI 會編造內容 —— 通用 LLM 給的心理建議沒有依據,在這個領域是有實際 傷害的
- 敏感資料多為明文儲存 —— 心情日記是最私密的文字之一
- 只能事後回顧 —— 看得到過去的低潮,看不到即將到來的
HeartBox 要處理的是中間這一段:在還沒到需要專業介入之前,提供一個有依據、 可信任、且看得到未來趨勢的自我覺察工具。 它不取代諮商,而是降低「發現自己 不對勁」的門檻。
Constraints
這些限制決定了後面所有的技術選擇:
- 每位使用者的資料極少。 不是幾萬筆,是幾百筆。任何需要大量資料才能訓練 的方法一開始就出局。
- 資料極度敏感。 日記內容是使用者最私密的文字,把它送到第三方 API 是很難 說服自己的決定。
- 建議不能是編造的。 一般聊天機器人講錯話的代價是尷尬;心理健康場景講錯 話的代價不是。
- 回饋必須即時。 使用者寫完日記就要看到分析,不能等。
- 個體差異大到無法跨人比較。 有人的壓力來源是失眠,有人是缺乏運動, 「平均使用者」在這裡沒有意義。
- 一個人開發與維運。 沒有團隊可以分攤複雜度,任何需要持續調參或照顧的 方案都會變成負債。
Key decisions
這一節是整頁的重點。每個決策都寫成「情境 → 選項 → 取捨 → 決定」。
為什麼用 RAG,而不是直接讓 LLM 回答?
- 情境
- 使用者寫下低落的日記後,系統要給出回應。通用 LLM 能產出看起來很專業的心理建議,但那些建議可能完全是編造的 —— 而且因為語氣溫暖、結構完整,反而更容易被相信。
- 考慮過的選項
- 直接呼叫 LLM,靠 prompt 要求它「謹慎回答」
- 改用固定的罐頭回覆,完全不讓模型自由生成
- 檢索增強生成:先從臨床文件找出相關段落,要求模型必須基於這些段落回答
- 取捨
- RAG 讓每次回饋多了一次向量檢索與更長的 prompt,回應變慢、實作也更複雜;而且知識庫的涵蓋範圍會直接變成回答品質的天花板。罐頭回覆雖然絕對安全,但沒有針對性,使用者兩次就不看了。
- 決定
- 採用 RAG。知識庫收錄 7 份國際機構文件 —— WHO 的壓力管理指南、心理健康行動方案與《Doing What Matters in Times of Stress》、APA 的壓力管理技巧與復原力建構、NHS 的心理福祉建議、NIMH 的壓力因應指南 —— 以 BGE-M3 embeddings 建立向量索引,檢索 top-3 段落,並要求模型的建議必須落在檢索到的內容範圍內。代價是延遲與複雜度,換到的是「這句建議出自哪裡」可以被追溯。
RAG 的完整流程是五個步驟:
- 使用者寫日記
- 模型判讀情緒,給出 −1 到 +1 的情緒分數
- 分數低於 −0.4 才觸發知識庫查詢
- ChromaDB 從 7 份臨床文件中檢索最相關的 top-3 段落
- 模型結合檢索內容產出回饋
為什麼把觸發門檻設在 −0.4?
- 情境
- 不是每篇日記都需要搬出臨床文件。「今天有點累」跟「什麼都不想做了」需要的回應層級完全不同,但兩者都會被判為負面情緒。門檻定在哪裡,決定了系統是恰到好處還是惱人。
- 考慮過的選項
- 不設門檻,每篇日記都跑 RAG
- 門檻設高(例如 −0.2),寧可多觸發
- 門檻設低(例如 −0.6),只在明顯低落時介入
- 取捨
- 門檻太高會讓系統對輕微的日常抱怨也端出心理學文獻,顯得說教而且很快就被忽略 —— 這是「狼來了」的反面:不是漏報,是報得太多而失去意義。門檻太低則會錯過那些寫得含蓄、但其實需要被接住的內容。每篇都跑則同時付出延遲與 GPU 成本。
- 決定
- 設在 −0.4。這個位置讓明確的負面情緒會觸發、一般的日常抱怨不會,在誤觸發與漏接之間取得平衡。必須誠實說明:這個數字是根據實際日記樣本的判斷結果訂出來的,沒有做系統性的敏感度分析 —— 這是我會優先補上的實驗。
為什麼選 Random Forest,而不是 XGBoost 或 LSTM?
- 情境
- 情緒預測本質上是時間序列問題,直覺會想用 LSTM。但這裡的條件很特殊:每位使用者只有幾百筆資料、預測要在寫完日記的當下就回傳、而且使用者會問「為什麼系統覺得我壓力會變高」。
- 考慮過的選項
- 線性迴歸 —— 最簡單、最好解釋
- 單一決策樹 —— 規則完全透明
- Random Forest —— 100 棵樹集成
- XGBoost —— 調參後通常準確度最高
- LSTM —— 最適合時間序列的直覺選擇
- 取捨
- 放棄 LSTM 等於放棄它可能捕捉到的長期時間結構,也放棄了資料量長大之後的準確度上限。放棄 XGBoost 等於放棄調參後通常能再多拿到的那幾個百分點。換來的是:幾百筆就能訓練、純 CPU 50ms 內完成推論、可以用特徵重要度解釋、而且幾乎不需要調參。
- 決定
- 選 Random Forest,100 棵樹。迴歸任務(情緒分數、壓力指數)取 100 棵樹的平均,分類任務(高壓日預警)取多數決。四項本專題的關鍵需求它同時滿足:資料量少、可解釋、CPU 即時推論、抗過擬合且開箱即用。
| 演算法 | 資料量需求 | 可解釋性 | 推論成本 | 過擬合風險 | 準確度與穩定度 |
|---|---|---|---|---|---|
| 線性迴歸 | 少量即可 | 高(係數) | 極低 | 易欠擬合 | 受限於線性假設 |
| 單一決策樹 | 少量即可 | 高(規則) | 極低 | 高(易過擬) | 不穩定 |
| Random Forest★ 選用 | 幾百筆即可訓練 | 中高(特徵重要度) | 低・純 CPU <50ms | 低(Bagging 天然抗過擬) | 高・穩定且幾乎免調參 |
| XGBoost | 中等需求 | 中高(特徵重要度) | 低 | 中(需調參) | 高(調參後常略勝) |
| LSTM | 需大量(萬筆+) | 低(黑箱) | 高(需 GPU) | 高(資料少時) | 高(資料足時) |
為什麼把 recall 調到 88%,而不是追求 precision?
- 情境
- 高壓日預警是一個分類問題,而分類的門檻要往哪邊偏,取決於兩種錯誤的代價哪個比較高。
- 考慮過的選項
- 平衡 precision 與 recall,追求最高 F1
- 偏向 precision —— 只在很確定時才警示,避免打擾
- 偏向 recall —— 寧可多警示,也不要漏掉
- 取捨
- 偏向 recall 的直接代價是誤報變多:使用者可能收到「未來幾天壓力可能升高」的提醒,結果什麼事也沒發生。誤報多了會降低提醒的份量,這是真實的成本,不能假裝沒有。
- 決定
- 偏向 recall,最終落在 88%。理由是兩種錯誤的代價不對稱:誤報的代價是一次多餘的提醒,漏報的代價是錯過一個本來可以被接住的低潮。在心理健康場景,這個不對稱大到足以決定門檻要往哪邊偏。系統的措辭也配合這個選擇 —— 提醒寫成「可以留意一下」而不是警告,降低誤報時的壓迫感。
為什麼自架模型推論,而不是呼叫商用 API?
- 情境
- 呼叫商用 API 是最省事的做法:不用管 GPU、不用管模型部署、品質通常更好。但這個系統處理的是使用者的心情日記。
- 考慮過的選項
- 呼叫商用 LLM API,最快也最省維運
- 呼叫商用 API,但先把日記做去識別化處理
- 自架開放權重模型,資料完全不出可控環境
- 取捨
- 自架的代價很實在:要自己準備 GPU、自己處理模型載入與推論服務、自己承擔服務掛掉的後果,而且開放權重的 7B 模型在中文生成品質上比不過商用大模型。去識別化聽起來是折衷,但日記內容本身就是識別資訊 —— 一個人寫的生活細節無法真正匿名。
- 決定
- 自架。使用開放權重模型 —— LLaVA-v1.6-Mistral-7B 負責視覺理解,TAIDE-LX-7B 則是針對繁體中文調校的開放權重模型 —— 架在自己的 GPU 上,以 FastAPI 對外,透過 Cloudflare Tunnel 連回後端。生成品質的差距是明確的代價,但「日記內容從來沒有離開過我能控制的環境」這件事,在這個產品上是不能妥協的前提。
為什麼把同意流程拆成三個獨立步驟?
- 情境
- 這個系統會收集日記內容、AI 分析出的情緒與壓力分數,以及選擇性的健康與睡眠資料。GDPR Art. 7 要求同意必須針對特定目的,而且必須能夠獨立拒絕。台灣個資法第 7 條也有類似要求。一份「我全部同意」的勾選框無法滿足這個標準。
- 考慮過的選項
- 單一同意書,一次勾選全部
- 兩段式:服務條款 + 資料使用
- 三段分離:資料使用 → AI 訓練同意 → 年齡確認
- 取捨
- 流程從一次點擊變成三個步驟,註冊轉換率一定會下降 —— 而且允許使用者拒絕 AI 訓練,等於主動放棄一部分可用的訓練資料。這兩個都是真實的損失。
- 決定
- 三段分離。關鍵設計是:AI 訓練同意獨立勾選,而且拒絕的人仍然可以使用全部功能 —— 如果拒絕會導致功能受限,那個同意在法律上就不是自由給予的。13 至 17 歲的使用者另外走家長驗證:系統寄出確認連結,家長點擊後才解鎖 AI 功能。轉換率的損失是這個決定的代價,我認為值得。
為什麼用「自己跟自己比」,而不是跨使用者的基準?
- 情境
- 做完預測模型之後的下一個問題是:怎麼讓使用者知道自己有沒有變好?直覺做法是跟其他使用者的平均比較,但心理健康的個體差異大到讓這種比較幾乎沒有意義 —— 有人的基線情緒天生就比較低,那不代表他需要被警示。
- 考慮過的選項
- 跨使用者百分位:「你的情緒分數高於 70% 的使用者」
- 固定的絕對門檻:「情緒低於 −0.3 就算不好」
- 個人基準:以使用者自己的首週為基準線
- 取捨
- 個人基準無法回答「我跟別人比起來如何」,也無法在使用者剛註冊時提供任何比較 —— 資料不足時只能什麼都不顯示,這在產品體驗上是明顯的空窗。
- 決定
- 採用個人基準。以使用者首週為基準線,所有進步指標都是跟自己的過去比;「向上箭頭」一律代表進步,不需要讓使用者記憶方向。資料未滿 14 天時,介面顯示「再過 X 天解鎖」而不是硬算一個沒有統計意義的數字。這是整個專案裡我最滿意的一個決定 —— 它承認了資料量不足這件事,而不是用一個看起來很專業的數字蓋過去。
Results
- 情緒分數 MAE
- 0.22情緒分數 MAE分數範圍 −1 到 +1
- 壓力指數 MAE
- 1.04壓力指數 MAE指數範圍 0 到 10
- 高壓日 AUC
- 0.948高壓日 AUC排序能力接近完美
- 高壓日 Recall
- 88%高壓日 Recall刻意偏向不漏抓
5-fold 交叉驗證。迴歸任務 22,796 列,高壓日分類任務 31,720 列。
特徵工程的部分是 53 個特徵:12 個行為與生理指標(情緒平均、壓力平均與最高 值、日記數、睡眠時數與品質、深睡比例、步數、運動分鐘、HRV、靜止心率、就寢時間 標準差)各自展開成 1、3、7、14 天四個時間窗口,共 48 個 lag 特徵,再加上 5 個 calendar 特徵(星期幾、連續寫日記天數等)。
這些數字代表什麼,以及不代表什麼。 它們是離線交叉驗證的結果,證明模型能從 歷史行為中學到有預測力的模式。它們不是臨床效度的證據 —— 沒有做過對照試驗, 也沒有證據顯示這些預警真的改善了任何人的心理健康。這個區分很重要,不應該被漂亮 的數字蓋過去。
What I’d do differently
-
驗證方式應該用時間序列切分,而不是隨機 5-fold。 特徵裡有大量 lag 變數, 隨機切分會讓同一段時間的資訊同時出現在訓練與驗證集,指標可能因此偏樂觀。 改成「用前面的時間訓練、預測後面的時間」才是誠實的評估。這是整個專案在方法 學上最大的弱點,如果重做我會第一個修。
-
−0.4 的觸發門檻沒有做敏感度分析。 它是根據實際樣本判斷出來的合理值,但 我沒有量化過門檻在 −0.3 或 −0.5 時的誤觸發率變化。這是可以補做的實驗,只是 當時把時間花在功能上了。
-
沒有評估檢索本身的品質。 整個 RAG 的價值建立在「檢索到的段落確實相關」 這個前提上,但我沒有建立檢索評估集,也沒有量測過 top-3 的命中率。模型引用了 來源不等於引用了對的來源。
-
功能做得太多,稀釋了核心。 社群、好友、課程、成就系統、呼吸冥想 —— 這些 都是完整的功能,但它們跟「RAG 讓 AI 不亂講話」和「預測未來情緒」這兩件真正 有價值的事沒有關係。如果重做,我會把同樣的時間投在檢索品質評估與前瞻性驗證 上,而不是第 103 個成就徽章。
-
低估了自架推論的維運成本。 決定本身我仍然認為是對的,但 GPU 服務掛掉的 時候整個 AI 功能就沒了,而我沒有準備降級路徑。至少應該做到「推論服務不可用 時,回退到不含 AI 分析的純記錄模式」而不是讓使用者看到錯誤。
Stack
| 層 | 技術 |
|---|---|
| 前端 | React ・ Vite ・ Tailwind CSS ・ Recharts |
| 後端 | Django REST Framework ・ Django Channels(WebSocket) |
| AI | LLaVA-v1.6-Mistral-7B ・ TAIDE-LX-7B ・ BGE-M3 ・ LangChain ・ ChromaDB |
| 機器學習 | scikit-learn(Random Forest) |
| 資料庫 | PostgreSQL,日記欄位以 Fernet 加密(AES-128-CBC + HMAC-SHA256) |
| 行動端 | Capacitor(Android) |
| 部署 | Cloudflare Pages ・ Cloudflare Tunnel ・ Google Cloud Run |
安全與隱私的部分:日記內容以 Fernet 對稱加密儲存,金鑰由環境變數注入且與資料庫 分離 —— 資料庫外洩本身不足以還原日記內容。登入支援兩步驟驗證,全站 HTTPS。 日記、AI 聊天與匿名社群三處都有危機字眼偵測,觸發時即時顯示求助資源(1925 安心 專線、1995 生命線、1980 張老師專線)。