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

@@ -26,9 +26,9 @@ use crate::{
prompt::runtime_chat::{
NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT, NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT,
NpcChatTurnPromptInput, build_deterministic_chat_suggestions,
build_deterministic_npc_reply, build_fallback_function_suggestions,
build_fallback_npc_chat_suggestions, build_npc_chat_turn_reply_prompt,
build_npc_chat_turn_suggestion_prompt,
build_deterministic_hostile_breakoff_reply, build_deterministic_npc_reply,
build_fallback_function_suggestions, build_fallback_npc_chat_suggestions,
build_npc_chat_turn_reply_prompt, build_npc_chat_turn_suggestion_prompt,
},
request_context::RequestContext,
state::AppState,
@@ -137,16 +137,26 @@ pub async fn stream_runtime_npc_chat_turn(
let (npc_reply, suggestions, function_suggestions, force_exit) = match llm_result {
Some(result) => result,
None => {
let npc_reply = build_deterministic_npc_reply(
npc_name.as_str(),
player_message.as_str(),
payload.npc_initiates_conversation,
);
let force_exit = should_force_chat_exit(payload.chat_directive.as_ref())
|| should_hostile_chat_breakoff_deterministically(
let deterministic_hostile_breakoff =
should_hostile_chat_breakoff_deterministically(
player_message.as_str(),
payload.chat_directive.as_ref(),
Some(&payload.npc_state),
);
let force_exit = should_force_chat_exit(payload.chat_directive.as_ref())
|| deterministic_hostile_breakoff;
let npc_reply = if deterministic_hostile_breakoff {
build_deterministic_hostile_breakoff_reply(
npc_name.as_str(),
player_message.as_str(),
)
} else {
build_deterministic_npc_reply(
npc_name.as_str(),
player_message.as_str(),
payload.npc_initiates_conversation,
)
};
let suggestions = if force_exit {
Vec::new()
} else {
@@ -272,6 +282,7 @@ where
|| should_hostile_chat_breakoff_deterministically(
payload.player_message.as_str(),
payload.chat_directive.as_ref(),
Some(&payload.npc_state),
);
if force_exit {
@@ -618,6 +629,7 @@ fn is_hostile_model_chat(chat_directive: Option<&Value>) -> bool {
fn should_hostile_chat_breakoff_deterministically(
player_message: &str,
chat_directive: Option<&Value>,
npc_state: Option<&Value>,
) -> bool {
if !is_hostile_model_chat(chat_directive) {
return false;
@@ -631,6 +643,14 @@ fn should_hostile_chat_breakoff_deterministically(
return true;
}
// 中文注释:模型建议不可用时,后端兜底仍按敌对聊天口径避免负面挑衅被拖成闲聊。
if npc_state
.and_then(|state| read_number_field(state, "chattedCount"))
.is_some_and(|chatted_count| chatted_count >= 4.0)
{
return true;
}
let hostile_break_words = [
"动手",
"开战",
@@ -640,6 +660,18 @@ fn should_hostile_chat_breakoff_deterministically(
"闭嘴",
"少废话",
"别挡路",
"废话",
"威胁",
"找死",
"送死",
"住口",
"让开",
"滚开",
"不退",
"不会退",
"别装",
"骗子",
"叛徒",
];
count_keyword_matches(player_message, &hostile_break_words) > 0
}
@@ -812,6 +844,51 @@ mod tests {
);
}
#[test]
fn hostile_chat_breakoff_fallback_triggers_on_negative_words() {
let chat_directive = json!({
"terminationMode": "hostile_model",
"isHostileChat": true,
});
let npc_state = json!({ "chattedCount": 1 });
assert!(should_hostile_chat_breakoff_deterministically(
"少废话,让开,不然现在就动手。",
Some(&chat_directive),
Some(&npc_state),
));
}
#[test]
fn hostile_chat_breakoff_fallback_triggers_after_four_turns() {
let chat_directive = json!({
"terminationMode": "hostile_model",
"isHostileChat": true,
});
let npc_state = json!({ "chattedCount": 4 });
assert!(should_hostile_chat_breakoff_deterministically(
"我还想再问一个问题。",
Some(&chat_directive),
Some(&npc_state),
));
}
#[test]
fn hostile_chat_breakoff_fallback_ignores_non_hostile_chat() {
let chat_directive = json!({
"terminationMode": "none",
"isHostileChat": false,
});
let npc_state = json!({ "chattedCount": 6 });
assert!(!should_hostile_chat_breakoff_deterministically(
"少废话,让开。",
Some(&chat_directive),
Some(&npc_state),
));
}
#[tokio::test]
async fn npc_chat_turn_prefers_request_snapshot_over_persisted_session() {
let state = AppState::new(AppConfig::default()).expect("state should build");