This commit is contained in:
2026-05-01 00:33:39 +08:00
parent 61969c5116
commit fe02603ba1
68 changed files with 4586 additions and 748 deletions

View File

@@ -240,7 +240,10 @@ JSON 结构:
- functionSuggestions 只能从用户提示提供的 functionOptions 中挑选,不要发明 functionId。
- functionSuggestions 的 actionText 必须像玩家可点击动作,不暴露 functionId不写规则说明。
- 非敌对聊天 shouldEndChat 必须为 false。
- 敌对聊天可以随时 shouldEndChat=true,且敌对 NPC 更偏好在话不投机、被威胁、玩家退出、底线被触碰时结束聊天。"#;
- 敌对聊天可以随时 shouldEndChat=true
- 敌对 NPC 感知到玩家负面发言时,例如挑衅、威胁、羞辱、逼问、拒绝退让、直接宣战或强行越界,应倾向立即 shouldEndChat=true。
- 敌对 NPC 已聊天轮次达到 4 轮或以上时,本轮结束后会超过 4 轮,应倾向立即 shouldEndChat=true。
- shouldEndChat=true 时 terminationReason 使用 hostile_breakoffsuggestions 与 functionSuggestions 可以为空。"#;
#[derive(Debug)]
pub(crate) struct NpcChatTurnPromptInput<'a> {
@@ -394,6 +397,19 @@ pub(crate) fn build_npc_chat_turn_reply_prompt(payload: &NpcChatTurnPromptInput<
} else {
None
},
if is_hostile_model_chat {
Some("如果玩家刚才的话被 NPC 感知为负面发言,例如挑衅、威胁、羞辱、逼问、拒绝退让、直接宣战或强行越界,本轮回复应倾向写成最后通牒、驱逐前警告或战斗前狠话。".to_string())
} else {
None
},
if is_hostile_model_chat && chatted_count >= 4.0 {
Some(format!(
"敌对聊天已持续 {} 轮,本轮结束后会超过 4 轮;回复应明显倾向立即收束,像开战前最后一句狠话,而不是继续闲聊。",
format_prompt_number(chatted_count)
))
} else {
None
},
if is_player_exit_turn {
Some("玩家正在主动结束这轮聊天。请对这个收束动作作出回应,并留下自然的下一步入口。回复后聊天会结束。".to_string())
} else {
@@ -474,6 +490,9 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt(
.and_then(|record| read_string(record.get("terminationReason")))
.as_deref()
== Some("player_exit");
let chatted_count = as_record(payload.npc_state)
.and_then(|record| read_number(record.get("chattedCount")))
.unwrap_or(0.0);
let function_options_block = chat_directive
.and_then(|record| record.get("functionOptions"))
.map(describe_function_options)
@@ -498,6 +517,14 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt(
} else {
Some("这是非敌对聊天shouldEndChat 必须为 false。".to_string())
},
if is_hostile_model_chat {
Some(format!(
"敌对聊天判定:已聊天轮次为 {}。若玩家刚才的话可被 NPC 感知为负面发言,或已聊天轮次达到 4 轮及以上,本轮应倾向 shouldEndChat=true并使用 terminationReason=hostile_breakoff。",
format_prompt_number(chatted_count)
))
} else {
None
},
if is_player_exit_turn {
Some("玩家已经选择结束聊天shouldEndChat 必须为 trueterminationReason 必须为 player_exit。".to_string())
} else {
@@ -526,6 +553,20 @@ pub(crate) fn build_deterministic_npc_reply(
format!("{npc_name}听完你的话,回应道:“{player_message}。我明白你的意思,我们继续说。”")
}
pub(crate) fn build_deterministic_hostile_breakoff_reply(
npc_name: &str,
player_message: &str,
) -> String {
// 中文注释:当模型不可用而敌对聊天必须中止时,兜底文案也保持“战斗前狠话”的语气。
let player_signal = player_message.trim();
if player_signal.is_empty() {
return format!("{npc_name}冷声说道:“话已经够多了。再往前一步,就别指望还能全身而退。”");
}
format!(
"{npc_name}冷声说道:“{player_signal}?话已经够多了。再往前一步,就别指望还能全身而退。”"
)
}
pub(crate) fn build_character_chat_reply_fallback(
target_character: &Value,
player_message: &str,
@@ -1066,3 +1107,55 @@ fn format_prompt_number(value: f64) -> String {
value.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hostile_prompt_input(npc_state: Value) -> NpcChatTurnPromptInput<'static> {
NpcChatTurnPromptInput {
world_type: "CUSTOM",
character: Box::leak(Box::new(Value::Null)),
encounter: Box::leak(Box::new(Value::Null)),
monsters: &[],
history: &[],
context: Box::leak(Box::new(Value::Null)),
conversation_history: &[],
dialogue: &[],
combat_context: None,
player_message: "少废话,让开。",
npc_state: Box::leak(Box::new(npc_state)),
npc_initiates_conversation: false,
chat_directive: Some(Box::leak(Box::new(json!({
"terminationMode": "hostile_model",
"isHostileChat": true,
})))),
}
}
#[test]
fn hostile_reply_prompt_mentions_final_threat_after_four_turns() {
let input = hostile_prompt_input(json!({
"affinity": -12,
"chattedCount": 4,
}));
let prompt = build_npc_chat_turn_reply_prompt(&input);
assert!(prompt.contains("已聊天轮次4"));
assert!(prompt.contains("战斗前狠话"));
assert!(prompt.contains("本轮结束后会超过 4 轮"));
}
#[test]
fn hostile_suggestion_prompt_mentions_should_end_chat_signals() {
let input = hostile_prompt_input(json!({
"affinity": -12,
"chattedCount": 4,
}));
let prompt = build_npc_chat_turn_suggestion_prompt(&input, "再往前一步,就别想回头。");
assert!(prompt.contains("shouldEndChat=true"));
assert!(prompt.contains("terminationReason=hostile_breakoff"));
assert!(prompt.contains("已聊天轮次为 4"));
}
}