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:
@@ -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);
|
||||
|
||||
@@ -413,6 +413,22 @@ const lintCheck = JSON.parse(cli(["said", "check", "--persona", "alpha", "--sess
|
||||
"--text", "換句話說,我等一下再過去。", "--json"]).stdout);
|
||||
check("said check 也會報講話的樣子",
|
||||
lintCheck.ok === false && lintCheck.lint.some((i) => i.kind === "explain"), JSON.stringify(lintCheck.lint));
|
||||
// 情緒的破口:緊張會斷句疊字、彆扭會嘴硬——這些要跟著情緒一起注入。
|
||||
cli(["emotion", "--persona", "alpha", "--session", S_SPEAK, "--apply", "anxiety=+45,shame=+25",
|
||||
"--trigger", "被問到還沒做完的事"]);
|
||||
const tells = pl.emotionTells("alpha");
|
||||
check("emotionTells 取主導情緒裡強度夠的那幾種",
|
||||
tells.some((t) => t.key === "anxiety" && t.tells.some((s) => s.includes("疊字"))) &&
|
||||
tells.every((t) => t.level >= 40) && tells.length <= 2, JSON.stringify(tells));
|
||||
check("低於門檻的情緒不給破口(強度不到就不演)",
|
||||
pl.emotionTells("alpha", null, { min: 99 }).length === 0);
|
||||
check("情緒的破口有注入,且說明是演出來不是講出來", (() => {
|
||||
const ctxTell = pl.turnContext("alpha", S_SPEAK, "那件事做完了嗎");
|
||||
return ctxTell.includes("此刻不自覺會出現的") && ctxTell.includes("疊字") &&
|
||||
ctxTell.includes("不要用旁白說明") && ctxTell.includes("一輪最多露一個破口");
|
||||
})(), pl.turnContext("alpha", S_SPEAK, "那件事做完了嗎").slice(0, 900));
|
||||
check("十二情緒每一種都有破口可演",
|
||||
pl.EMOTION_KEYS.every((k) => (pl.EMOTION_TELLS[k] || []).length >= 3));
|
||||
check("說話規則有注入「短句/日常用詞/看得見的東西/不要解釋」", (() => {
|
||||
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
||||
return ["短句", "日常用詞", "看得見的東西", "不要解釋自己剛講的話"].every((w) => ctxSay.includes(w));
|
||||
|
||||
Reference in New Issue
Block a user