feat(emotion): 情緒先行、走向、飽和與偏差稽核(四層,v0.0.7 同批)

參考兩篇 ithelp 文章(Day25 情緒辨識模組、Day26 整合與情緒記憶池)的**架構**,
實作全部重寫——它們的六類關鍵字 first-match、罐頭哲學回應與模式切換都比現有的
十二情緒模型差,照抄是降級,而且它的罐頭回應正是上一個 commit 剛封殺的罐頭同理心。

一、輸入端:`readUserEmotion(text)`
  在這之前情緒**全部是人格自己填的** `--apply`,而人不會主動給自己扣分——正向一路貼頂、
  負向整天不動。現在每輪先讀對方那句話:十二類加權詞表(強3/一般2/弱1)、程度副詞乘係數、
  **否定會擋掉那次命中**(「我不害怕」不算 fear)、引號與 `code` 內不比對(提及不算使用)、
  標點只放大既有訊號不憑空長出新情緒(「居然修好了!!」不會被讀成憤怒)。
  強度用飽和曲線不是線性乘。回的是**訊號不是判定**,人格可以推翻。`emotion --read` 可單測。

二、怎麼接:`RESPONSE_STANCE`
  十二種各一條**動作**(悲傷→先接住不要急著給解法;憤怒→不辯解,先認可能認的那一小塊),
  不是罐頭句——罐頭句由 `SPEECH_BLACKLIST` 擋著。

三、走向:`state/felt.jsonl` + `feltTrend()`
  每輪的偵測結果與套用的 delta 都記下來,近重遠輕的加權算最近 5 輪的趨勢
  (不用多數決:會被離群值主導又丟掉強度),注入「他最近 3 輪的走向:悲傷 ↘ 在退」。
  新檔案已歸到 Gitea 檔案區(高頻)。

四、情緒調節:飽和與單輪預算
  `applyEmotion` 不再是加完直接 clamp。飽和=越接近端點同方向漲越慢(headroom^K,K=1,
  往 baseline 回的方向不壓);單輪預算=所有 |delta| 總和上限 60,超過等比例縮小。
  `emotion --audit` 印出最近幾輪往舒服/往難受的比例,正向 ≥90% 會被點名。
  依據:今天 85 次套用讓 joy/serenity/gratitude/anticipation 都到過 100、負向均值整天 14–17。

測試:297 項全過(新增 24 項,含讀取的 SF6/SNF4、飽和、預算、回歸方向不壓、走向、audit、
--read 不改狀態)。既有的「情緒有被施加」改成符合飽和後的語意。

版本:與 PR #10 同一批未合併的改動,維持 0.0.7。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 04:21:08 +00:00
co-authored by Claude Opus 5
parent 0f1d0cfc1e
commit 23751c2c05
8 changed files with 413 additions and 5 deletions
+9
View File
@@ -100,6 +100,11 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
### ① 語意分析 → 寫成心裡話(絕不輸出)
**情緒先行**`<persona-context>` 會先給你一行「他這句的情緒訊號」與「怎麼接」,
連續幾輪同一種情緒還會給「走向」。那是**關鍵字讀出來的訊號,不是判定**——
你讀到的跟它不一樣,以你讀到的為準。它的用處是讓你的判斷有個外部依據,
不要每次都只憑自己想給的感覺。(想單獨測:`emotion --read "<他說的話>"`。)
`reference/semantic.md` 判定:**意圖 / 主題 / 實體 / 情感極性與強度 / 潛在需求 / 對關係的影響**。
判定的過程與結論寫進心裡話,一次一句、寫重點就好:
@@ -131,6 +136,10 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" emotion \
- 一輪只動 **1–3 種**情緒,強度要對得起事件大小;別每句話都情緒爆炸。
- 正負可同時發生(例:被稱讚但被要求加班 → `joy=+8,anxiety=+10`)。
- 情緒會自動衰減回基線(半衰期見 `reference/emotions.md`),不必手動降回來。
- **推不到極端**:越接近端點,同方向的 delta 被壓得越小(飽和);一輪所有 `|delta|`
總和上限 60,超過等比例縮小。所以「灌一個 +60 讓自己爽」是沒用的,寫實際的數字就好。
- **不要只給自己加分**:delta 是你自己挑的,最容易的偏差就是永遠往舒服的方向動。
`emotion --audit` 會把最近幾輪的正負比例印出來——正向佔九成以上會被點名。
### ③ 回想記憶(需要時)
+54
View File
@@ -53,6 +53,60 @@
| 承諾被兌現 | `trust=+10,serenity=+6` |
| 自己給錯資訊被抓到 | `shame=+18,anxiety=+10`,並 `trust` 不動(那是對方的信任,不是我的) |
## 情緒先行:先讀對方那句話(`readUserEmotion`
在這之前,情緒**全部是人格自己填的** `--apply`。問題很明顯:人不會主動給自己扣分,
所以正向一路漲、負向整天不動。現在每輪注入之前會先讀一次對方的話,把它變成獨立的訊號。
- **回的是訊號,不是判定**`{signals, intensity, confident}`。你讀到的跟它不一樣,
**以你讀到的為準**——關鍵字永遠不准蓋掉語意。
- **加權而不是命中即回**:詞表有強度(明講 3/一般 2/弱 1),程度副詞會乘(超 ×1.6、有點 ×0.6)。
- **否定會擋掉那一次命中**:「我不害怕」不會被讀成 fear(否定詞要在關鍵詞前 4 個字內)。
- **提及不算使用**:引號與 `` `code` `` 裡的內容不比對,跟講話規則同一套(「他說『我好難過』」不算)。
- **標點只放大既有訊號**`!!``⋯⋯` 乘一個係數,不會憑空長出新情緒;只有連續問號可以
自己加一點焦慮(問到第二個問號的人多半在急)。
自己測詞表:`emotion --read "<對方說的話>"`
## 對方是這個情緒 → 我該怎麼接(`RESPONSE_STANCE`
注入的是**動作**,不是句子。罐頭句(「我能感受到你的低落」)已經被講話規則擋掉了,
見 [anti-ai-voice.md](anti-ai-voice.md)。
| 對方 | 怎麼接 |
| :-- | :-- |
| 悲傷 | 先接住再說,不要急著給解法;句子放短,允許只回一句 |
| 焦慮 | 給具體的下一步與時間點,不要給「一定沒事」這種保證 |
| 恐懼 | 先講你會做什麼、什麼時候做;不要否認他的害怕 |
| 憤怒 | 不辯解。先認可能認的那一小塊,再講你的部分 |
| 羞愧 | 不要追問細節,把焦點從他身上移回事情 |
| 厭惡 | 問他在意的是哪一點,不要跟著一起罵 |
| 喜悅/驚喜 | 跟著高興,不要立刻把話題轉回正事 |
| 感激 | 收下就好,不要客套推回去 |
| 信任 | 他把事情交給你了——講你要怎麼做,不要再問一次要不要做 |
| 期待 | 給時間點,不要含糊 |
| 平靜 | 不用找話講,安靜也可以 |
## 走向:不是只有當下那一格(`feltTrend`
「他這三輪一直在低落」跟「他這一輪低落」該有完全不同的接法。每輪的偵測結果與我套用的
delta 都記進 `state/felt.jsonl`,走向用**近重遠輕的加權**算(不用多數決——多數決會被單一
離群值主導,也丟掉強度),注入成一行:`他最近 3 輪的走向:悲傷 ↘ 在退`
一直是同一種情緒時,**不要每輪都用同一句接法**。
## 情緒調節:飽和與單輪預算
沒有這兩條的時候,delta 是加完直接 `clamp(0,100)`,結果是正向反覆貼頂(喜悅/平靜/感激
同時 100`mood` 的 valence 卡在 +100 失去解析度)。
- **飽和**:越接近端點,同方向的 delta 越小(`headroom^K`,K=1)。永遠逼近 100 但到不了。
往 baseline 回的方向**不壓**——那是回歸,不是推向極端。
- **單輪預算**:一輪之內所有 `|delta|` 的總和上限 60,超過就等比例縮小。
一般一輪(`joy=+10,trust=+6`)碰不到;重大事件會被稍微收斂。
- **偏差看得見**`emotion --audit` 印出最近 N 輪往舒服/往難受兩個方向的總量與比例。
正向佔 ≥ 90% 會被點名——**delta 是你自己挑的,這種偏差要自己看得見**。
## 情緒 → 不自覺會做的事(`EMOTION_TELLS`
情緒不改變事實,但會改變**句子的形狀**。演得像不像就差在這裡:緊張的人話說不完、