feat: 情緒會改變句子的形狀(EMOTION_TELLS),診斷加第四把尺

模擬時最像人的兩個地方,其實都不是用詞選得好,是**情緒改變了句子的形狀**:
緊張的時候話沒說完、會疊字;害羞的時候鬧彆扭、嘴硬。把這件事寫成表,
跟情緒一起注入,不靠人格自己記得。

- `EMOTION_TELLS`:十二情緒各自「不自覺會做的事」。焦慮→句子斷在一半/疊字/
  追一句「這樣可以嗎」;羞愧→先否認再小聲承認/講反話/轉話題;憤怒→短句/
  稱呼退回全名;悲傷→只回一個詞/句子沒說完就停;其餘九種同理。
- `emotionTells()`:挑主導情緒(超出基線最多)裡強度 ≥ 40 的前兩種,
  每輪注入 `<persona-context>`,並附三條界線——**演出來不要講出來**
  (「我有點緊張」是解釋,斷句才是緊張)、**一輪最多露一個破口**、
  **強度不到就不演**(低強度的情緒在語氣上看不出來)。
- 混合狀態的疊法寫進 `reference/emotions.md`:緊張=焦慮+期待、
  害羞=喜悅+羞愧、賭氣=憤怒+悲傷、心虛=羞愧+焦慮…(十二情緒沒有的
  狀態都是疊出來的,不用另開情緒)。

心理醫生那邊多一把尺(第 4 把,**這一把不抓錯,是找沒說出口的東西**):

- 「我沒事」+句子全斷在一半 → 他在忍,要處理的是他為什麼不敢講。
- 「我很生氣」+句子工整完整像客服 → 那是冷掉了,比生氣難修,優先講。
- 一整段都沒有破口 → 兩個人都在挑安全的話講,先讓一個人講真話再談用詞。
- 改寫規則加第 6 條:**保留情緒的痕跡**。斷句、話少、嘴硬留著,
  只拿掉攻擊、絕對化與讀心;改完像客服在念稿就是改壞了。

selftest 248 項全綠(新增 4 項)。版號留在 0.0.6——同一批未合併的改動。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 10:09:59 +00:00
co-authored by Claude Opus 5
parent 2e851f55f2
commit c952a449b1
10 changed files with 172 additions and 9 deletions
+52
View File
@@ -349,6 +349,46 @@ export function dominant(state, top = 3) {
.map(({ key, level }) => ({ key, level: Math.round(level * 10) / 10 }));
}
// --------------------------------------------------------------------------- //
// 情緒 → 不自覺會做的事(講話上的破口)
//
// 情緒不改變事實,但會改變**句子的形狀**:緊張的人話說不完、會疊字;彆扭的人
// 先否認再小聲承認。演得像不像就差在這裡——所以這張表跟情緒一起注入,不靠記性。
// 節制很重要:一輪最多露一個破口,不然就從「像人」變成「演戲」。
// --------------------------------------------------------------------------- //
export const EMOTION_TELLS = {
joy: ["話變多、句子輕", "會笑出來(「哈」)", "忍不住多補一句"],
trust: ["話變直、不鋪陳", "省略主語,講一半對方也懂", "敢說「你這樣不對」"],
anticipation: ["搶著問下一步", "反問變多", "把時間講得很具體(「等一下」「明天早上」)"],
gratitude: ["指名是哪一件事", "語尾變軟", "會再說一次謝謝"],
serenity: ["句子完整、節奏慢", "留白,允許沉默", "不追問"],
delight: ["短促的驚呼(「欸」「真的?」)", "句子斷開", "重複確認一次"],
anger: ["句子變短、句號變多", "稱呼退回全名、不叫暱稱", "拒絕修飾,只講事實"],
sadness: ["話少,只回一個詞", "句子沒說完就停", "答非所問,跳開話題"],
fear: ["先確認安全再講別的", "問句連發", "描述身體與動作(手在抖、往後退)"],
disgust: ["用拉開距離的詞(「那個東西」)", "不肯講出對方的名字", "句子往後退、想結束話題"],
shame: ["鬧彆扭:先否認,再小聲承認", "講反話、嘴硬", "轉移話題、把責任講小"],
anxiety: ["句子斷在一半", "疊字(「就、就是」「我、我知道」)", "追一句「這樣可以嗎」"],
};
/**
* 此刻會不自覺出現的行為:取主導情緒(超出基線最多)裡強度夠的前兩種。
* 強度不夠就不演——低強度的情緒在語氣上是看不出來的。
*/
export function emotionTells(slug, state = null, { min = 40, top = 2 } = {}) {
const st = decayEmotion(structuredClone(state ?? loadEmotion(slug)));
return dominant(st, 4)
.filter(({ level }) => level >= min)
.slice(0, top)
.map(({ key, level }) => ({
key,
zh: EMOTIONS[key].zh,
level: Math.round(level),
tells: EMOTION_TELLS[key] || [],
}));
}
export function emotionBrief(slug, state = null) {
const st = decayEmotion(structuredClone(state ?? loadEmotion(slug)));
const m = mood(st);
@@ -2280,6 +2320,18 @@ export function turnContext(slug, sessionId, prompt = "") {
"**講完就算了**——不要解釋自己剛講的話(沒有「我的意思是」「換句話說」「也就是說」)," +
"不要補註解、不要收尾總結。這幾條 `room post` 與 `said check` 會實際擋下。",
];
// 情緒會改變句子的形狀:緊張的人話說不完、彆扭的人先否認再承認。
const tells = emotionTells(slug, state);
if (tells.length) {
lines.push(
`此刻不自覺會出現的:${tells
.map((t) => `${t.zh} ${t.level}${t.tells.join("、")}`)
.join("")}`,
" **這是演出來的,不是講出來的**:讓它出現在句子的形狀上(斷句、疊字、嘴硬、話少)," +
"不要用旁白說明自己的狀態(「我有點緊張」是解釋,斷句才是緊張)。" +
"一輪最多露一個破口,強度不到就不演——每句都演會變成演戲。",
);
}
// 語氣層:由「使用者在關係圖裡是誰」+ bond × 親近度算出來,不靠人格自己記得。
const tone = toneDirective(slug);
if (tone) lines.push(tone);