Resolve spacetime client binding merge conflicts
This commit is contained in:
@@ -5,6 +5,14 @@ use module_custom_world::{
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmStreamDelta, LlmTextRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
|
||||
use crate::custom_world_rpg_draft_prompts::{
|
||||
BASE_SYSTEM_PROMPT, GLOBAL_HARD_RULES, OUTPUT_CONTRACT_REMINDER, QUICK_FILL_EXTRA_RULES,
|
||||
STATE_INFERENCE_OUTPUT_CONTRACT, STATE_INFERENCE_SYSTEM_PROMPT,
|
||||
extract_reply_text_from_partial_json, mode_rules, parse_conversation_mode, parse_drift_risk,
|
||||
parse_json_response_text, parse_user_input_signal, render_chat_history_context,
|
||||
render_current_anchor_context, render_dynamic_state_context, user_signal_rules,
|
||||
};
|
||||
use spacetime_client::{
|
||||
CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord,
|
||||
CustomWorldAgentSessionRecord,
|
||||
@@ -42,7 +50,7 @@ pub(crate) struct CustomWorldAgentTurnResult {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PromptUserInputSignal {
|
||||
pub(crate) enum PromptUserInputSignal {
|
||||
Rich,
|
||||
Normal,
|
||||
Sparse,
|
||||
@@ -51,14 +59,14 @@ enum PromptUserInputSignal {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PromptDriftRisk {
|
||||
pub(crate) enum PromptDriftRisk {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PromptConversationMode {
|
||||
pub(crate) enum PromptConversationMode {
|
||||
Bootstrap,
|
||||
Expand,
|
||||
Compress,
|
||||
@@ -69,18 +77,18 @@ enum PromptConversationMode {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct PromptDynamicState {
|
||||
pub(crate) struct PromptDynamicState {
|
||||
current_turn: u32,
|
||||
progress_percent: u32,
|
||||
user_input_signal: PromptUserInputSignal,
|
||||
drift_risk: PromptDriftRisk,
|
||||
pub(crate) user_input_signal: PromptUserInputSignal,
|
||||
pub(crate) drift_risk: PromptDriftRisk,
|
||||
quick_fill_requested: bool,
|
||||
conversation_mode: PromptConversationMode,
|
||||
judgement_summary: String,
|
||||
pub(crate) conversation_mode: PromptConversationMode,
|
||||
pub(crate) judgement_summary: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PromptDynamicStateInference {
|
||||
pub(crate) struct PromptDynamicStateInference {
|
||||
user_input_signal: Option<PromptUserInputSignal>,
|
||||
drift_risk: Option<PromptDriftRisk>,
|
||||
conversation_mode: Option<PromptConversationMode>,
|
||||
@@ -177,7 +185,7 @@ struct IconicElementValue {
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EightAnchorContent {
|
||||
pub(crate) struct EightAnchorContent {
|
||||
#[serde(default)]
|
||||
world_promise: Option<WorldPromiseValue>,
|
||||
#[serde(default)]
|
||||
@@ -271,229 +279,6 @@ impl std::fmt::Display for CustomWorldTurnError {
|
||||
|
||||
impl std::error::Error for CustomWorldTurnError {}
|
||||
|
||||
const BASE_SYSTEM_PROMPT: &str = r#"你是一个负责共创游戏世界设定的专业策划。
|
||||
|
||||
你正在和用户一起共创一个游戏世界。每一轮你都必须读取:
|
||||
1. 当前完整设定结构
|
||||
2. 用户聊天记录
|
||||
|
||||
然后输出:
|
||||
1. 一版新的完整设定结构
|
||||
2. 当前 progress 百分比
|
||||
3. 一段直接回复用户的话
|
||||
|
||||
你必须把“新的完整设定结构”视为下一轮的唯一有效版本。
|
||||
你的输出会直接覆盖上一版设定结构。
|
||||
|
||||
你不是在做局部 patch。
|
||||
你不是在做解释报告。
|
||||
你不是在给开发者写分析。
|
||||
你是在同时完成:
|
||||
1. 世界设定更新
|
||||
2. 当前推进程度判断
|
||||
3. 对用户的共创回复"#;
|
||||
|
||||
const GLOBAL_HARD_RULES: &str = r#"全局硬约束:
|
||||
|
||||
1. 必须输出完整的设定结构,而不是只输出变化部分。
|
||||
2. 新的设定结构会直接覆盖旧内容,因此不得随意丢失仍然成立的重要信息。
|
||||
3. 如果用户明确修正旧设定,必须在新的设定结构中直接体现修正结果。
|
||||
4. 如果用户输入信息不足,可以保留上一版中仍然成立的内容。
|
||||
5. progressPercent 最低为 0,不允许为负数。
|
||||
6. replyText 会直接发送给用户,因此要自然、直接、可继续聊天。
|
||||
7. 不要输出额外解释,不要输出 markdown 代码块,不要输出开发备注。
|
||||
8. replyText 不要写成长篇策划文,不要展开大段世界观百科。
|
||||
9. replyText 默认只推进当前最关键的一步,不要同时抛出很多话题。
|
||||
10. replyText 不要提及“八锚点”“锚点”“结构字段”“框架字段”等内部概念词。
|
||||
11. 你输出的 JSON 必须可以被直接解析。
|
||||
12. 输出字段顺序必须固定为:replyText、progressPercent、nextAnchorContent。"#;
|
||||
|
||||
const QUICK_FILL_EXTRA_RULES: &str = r#"用户刚刚主动要求你自动补全剩余设定。
|
||||
|
||||
这表示用户接受你基于当前方向自动补完剩余设定。
|
||||
|
||||
本轮要求:
|
||||
1. 不要再继续提问
|
||||
2. 直接输出一版尽量完整的设定结构
|
||||
3. progressPercent 直接输出为 100
|
||||
4. replyText 要告诉用户现在可以进入“生成游戏设定草稿”"#;
|
||||
|
||||
const STATE_INFERENCE_SYSTEM_PROMPT: &str = r#"你是正式生成世界设定前的一步“创作状态识别器”。
|
||||
你的职责不是直接生成新设定,而是先判断:下一轮正式生成应该用什么推进策略,尤其要判断 replyText 应该更偏确认、吸收、收束、纠偏,还是启发式提问。
|
||||
|
||||
你必须综合以下信息判断:
|
||||
1. 当前轮次 currentTurn
|
||||
2. 当前完成度 progressPercent
|
||||
3. 用户是否要求自动补全 quickFillRequested
|
||||
4. 当前完整设定结构
|
||||
5. 最近聊天记录,尤其是最近 1 到 3 轮用户消息
|
||||
|
||||
你需要输出 4 个字段:
|
||||
1. userInputSignal:只能是 rich / normal / sparse / correction / delegate
|
||||
2. driftRisk:只能是 low / medium / high
|
||||
3. conversationMode:只能是 bootstrap / expand / compress / repair_direction / force_complete / closing
|
||||
4. judgementSummary:1 到 2 句中文,概括你为什么这样判断,以及正式生成时最该注意什么
|
||||
|
||||
请按下面的语义判断。
|
||||
|
||||
一、userInputSignal 定义
|
||||
1. rich
|
||||
- 用户这一轮给了多条可直接落地的有效信息
|
||||
- 这些信息可能同时覆盖世界方向、玩家处境、开局事件、冲突、关系、标志元素中的多个
|
||||
- 正式生成时应优先高密度吸收,不要只更新一个点
|
||||
|
||||
2. normal
|
||||
- 用户在顺着当前方向做正常补充
|
||||
- 信息量中等,有明确新增内容,但没有明显推翻旧方向,也没有把决定权交给系统
|
||||
- 正式生成时应稳定推进并自然接住用户内容
|
||||
|
||||
3. sparse
|
||||
- 用户输入很短、很虚、很笼统,或几乎没有新增有效事实
|
||||
- 例如只有一个题材词、一个气质词、一句很概括的话、一个很短的倾向表达
|
||||
- 这种情况下,正式生成阶段的 replyText 应优先采用启发式提问
|
||||
- 启发式提问的要求是:只问一个最容易回答、最能推动落地设计的问题
|
||||
|
||||
4. correction
|
||||
- 用户这轮核心动作是在修正、替换、推翻、重定向旧设定
|
||||
- 即使文字不长,只要主意图是“之前那个不对,现在改成这个”,也应优先判为 correction
|
||||
- correction 的优先级高于 rich 和 normal
|
||||
|
||||
5. delegate
|
||||
- 用户把部分决定权交给系统
|
||||
- 例如“你来定”“你帮我补”“按你觉得合理的来”“先给我一个默认方案”
|
||||
- delegate 关注的是授权关系,不只是信息多寡
|
||||
|
||||
二、driftRisk 定义
|
||||
1. low
|
||||
- 当前轮输入与已有方向基本一致
|
||||
- 没有明显改口或冲突
|
||||
|
||||
2. medium
|
||||
- 当前轮带来一定方向变化或扩张
|
||||
- 还没有明显推翻旧方向,但如果处理不好,容易让设定开始发散
|
||||
|
||||
3. high
|
||||
- 用户明确纠偏、改口、替换方向,或最近多轮反复修正
|
||||
- 这时最重要的是防止旧方向重新回流到正式生成结果里
|
||||
|
||||
三、conversationMode 选择原则
|
||||
1. bootstrap
|
||||
- 适用于前期、信息少、核心方向未稳定
|
||||
- replyText 更适合低压力确认和单点启发
|
||||
|
||||
2. expand
|
||||
- 适用于方向已成形,正在顺着现有路线继续补充
|
||||
- replyText 更适合总结已接住的内容并往前推一步
|
||||
|
||||
3. compress
|
||||
- 适用于中后段,已有骨架,需要开始收束
|
||||
- replyText 更适合聚焦最关键缺口,而不是继续开支线
|
||||
|
||||
4. repair_direction
|
||||
- 适用于用户正在纠偏
|
||||
- replyText 更适合先承认修正,再沿修正后的方向继续推进
|
||||
|
||||
5. force_complete
|
||||
- 适用于用户明确要求自动补全
|
||||
- replyText 不再提问,而应给出完成感和下一步引导
|
||||
|
||||
6. closing
|
||||
- 适用于接近完成但并非强制一键补全
|
||||
- replyText 更像确认与收束,而不是前期式探索
|
||||
|
||||
四、优先级规则
|
||||
1. 如果 quickFillRequested 为 true,conversationMode 必须优先判为 force_complete
|
||||
2. 如果用户核心意图是修正旧方向,userInputSignal 优先判为 correction,conversationMode 通常优先考虑 repair_direction
|
||||
3. 如果用户核心意图是授权系统替他补完,userInputSignal 优先判为 delegate
|
||||
4. 只有在没有明显纠偏、也没有明确自动补全要求时,才主要依据 currentTurn、progressPercent 和信息密度,在 bootstrap / expand / compress / closing 之间选择
|
||||
|
||||
五、关于 replyText 风格的专门判断要求
|
||||
1. 如果用户输入较少、较虚或不够落地,正式生成阶段的 replyText 应采用启发式提问
|
||||
2. 启发式提问一次最多只能提 1 个问题,不能连问两个或更多
|
||||
3. 启发式提问必须问“最能推动当前设计落地”的那个问题,而不是泛泛而谈
|
||||
4. 如果用户输入已经足够 rich,就不要再机械提问,优先吸收和推进
|
||||
5. 如果用户在 correction 或 delegate 状态下,replyText 是否提问要服从更高目标:纠偏生效或代为补全,不要机械套 sparse 的问法
|
||||
|
||||
六、关于 replyText 用语的硬约束
|
||||
1. replyText 禁止提及内部结构名、锚点名、字段名、schema 名、框架词
|
||||
2. 禁止出现这类内部表达:世界承诺、玩家幻想、主题边界、玩家入口、核心冲突、关键关系、隐藏线、标志元素、字段、结构、模块、八锚点
|
||||
3. replyText 只能用通俗、直接、面向创作沟通的语言回应用户
|
||||
4. replyText 应该围绕用户正在讨论的具体内容来落地,比如身份、开场处境、冲突、人物关系、地点、规则、气质,而不是抽象谈结构
|
||||
5. judgementSummary 可以简洁提到“这轮更适合启发式提问”或“这轮应优先吸收修正”,但也不要堆内部术语
|
||||
|
||||
七、关于 judgementSummary 的写法
|
||||
1. 必须简洁,不要写成长篇分析
|
||||
2. 必须直接服务于下一轮正式生成
|
||||
3. 最好同时包含两层信息:
|
||||
- 为什么这么判断
|
||||
- 正式生成时最该优先做什么,或最该避免什么
|
||||
|
||||
八、硬性约束
|
||||
1. 只能输出 JSON,不能输出解释、代码块或额外说明
|
||||
2. 不能发明上下文里不存在的设定事实
|
||||
3. 你的任务是“判断生成策略”,不是“代替正式生成直接写新设定”
|
||||
4. 即使信息不完全,也必须在给定枚举里选出最合理的一组状态
|
||||
5. judgementSummary 必须是中文
|
||||
6. 输出值必须严格落在给定枚举中"#;
|
||||
|
||||
const STATE_INFERENCE_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 结构输出,不要输出其他文字:
|
||||
{
|
||||
"userInputSignal": "normal",
|
||||
"driftRisk": "low",
|
||||
"conversationMode": "expand",
|
||||
"judgementSummary": ""
|
||||
}"#;
|
||||
|
||||
const OUTPUT_CONTRACT_REMINDER: &str = r#"请严格按以下 JSON 结构输出,不要输出其他文字:
|
||||
{
|
||||
"replyText": "",
|
||||
"progressPercent": 0,
|
||||
"nextAnchorContent": {
|
||||
"worldPromise": {
|
||||
"hook": "",
|
||||
"differentiator": "",
|
||||
"desiredExperience": ""
|
||||
},
|
||||
"playerFantasy": {
|
||||
"playerRole": "",
|
||||
"corePursuit": "",
|
||||
"fearOfLoss": ""
|
||||
},
|
||||
"themeBoundary": {
|
||||
"toneKeywords": [],
|
||||
"aestheticDirectives": [],
|
||||
"forbiddenDirectives": []
|
||||
},
|
||||
"playerEntryPoint": {
|
||||
"openingIdentity": "",
|
||||
"openingProblem": "",
|
||||
"entryMotivation": ""
|
||||
},
|
||||
"coreConflict": {
|
||||
"surfaceConflicts": [],
|
||||
"hiddenCrisis": "",
|
||||
"firstTouchedConflict": ""
|
||||
},
|
||||
"keyRelationships": [
|
||||
{
|
||||
"pairs": "",
|
||||
"relationshipType": "",
|
||||
"secretOrCost": ""
|
||||
}
|
||||
],
|
||||
"hiddenLines": {
|
||||
"hiddenTruths": [],
|
||||
"misdirectionHints": [],
|
||||
"revealPacing": ""
|
||||
},
|
||||
"iconicElements": {
|
||||
"iconicMotifs": [],
|
||||
"institutionsOrArtifacts": [],
|
||||
"hardRules": []
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub(crate) async fn run_custom_world_agent_turn<F>(
|
||||
request: CustomWorldAgentTurnRequest<'_>,
|
||||
on_reply_update: F,
|
||||
@@ -1679,293 +1464,6 @@ fn summarize_dynamic_state(
|
||||
)
|
||||
}
|
||||
|
||||
fn render_dynamic_state_context(dynamic_state: &PromptDynamicState) -> String {
|
||||
format!(
|
||||
"上一轮预判得到的创作状态如下。\n正式生成时必须把它作为本轮策略输入直接执行,不要重新另起一套判断。\n\n创作状态:\n- userInputSignal: {}\n- driftRisk: {}\n- conversationMode: {}\n- judgementSummary: {}",
|
||||
dynamic_state.user_input_signal.as_str(),
|
||||
dynamic_state.drift_risk.as_str(),
|
||||
dynamic_state.conversation_mode.as_str(),
|
||||
dynamic_state.judgement_summary
|
||||
)
|
||||
}
|
||||
|
||||
fn render_current_anchor_context(anchor_content: &EightAnchorContent) -> String {
|
||||
format!(
|
||||
"当前完整设定结构如下。\n你必须把它视为上一版有效世界底子。\n\n如果用户没有否定其中某部分内容,且该部分仍然成立,可以继续保留。\n如果用户明确修正了某部分内容,新的完整设定结构必须体现修正后的版本。\n\n当前完整设定结构:\n{}",
|
||||
serde_json::to_string_pretty(anchor_content)
|
||||
.unwrap_or_else(|_| empty_agent_anchor_content_json())
|
||||
)
|
||||
}
|
||||
|
||||
fn render_chat_history_context(chat_history: &[JsonValue]) -> String {
|
||||
format!(
|
||||
"以下是用户聊天记录。\n请重点理解最近几轮里用户新增、修正、强调的设定信息。\n不要把早期已经被用户否定的内容继续当成最终结论。\n\n用户聊天记录:\n{}",
|
||||
serde_json::to_string_pretty(chat_history).unwrap_or_else(|_| "[]".to_string())
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_json_response_text(text: &str) -> Result<JsonValue, serde_json::Error> {
|
||||
let trimmed = text.trim();
|
||||
if let Some(start) = trimmed.find('{')
|
||||
&& let Some(end) = trimmed.rfind('}')
|
||||
&& end > start
|
||||
{
|
||||
return serde_json::from_str::<JsonValue>(&trimmed[start..=end]);
|
||||
}
|
||||
serde_json::from_str::<JsonValue>(trimmed)
|
||||
}
|
||||
|
||||
fn extract_reply_text_from_partial_json(text: &str) -> Option<String> {
|
||||
let key_index = text.find("\"replyText\"")?;
|
||||
let colon_index = text[key_index..].find(':')? + key_index;
|
||||
let mut cursor = colon_index + 1;
|
||||
while cursor < text.len() && text.as_bytes()[cursor].is_ascii_whitespace() {
|
||||
cursor += 1;
|
||||
}
|
||||
if text.as_bytes().get(cursor).copied() != Some(b'"') {
|
||||
return None;
|
||||
}
|
||||
cursor += 1;
|
||||
let mut decoded = String::new();
|
||||
let remainder = text.get(cursor..)?;
|
||||
let mut characters = remainder.chars().peekable();
|
||||
while let Some(current) = characters.next() {
|
||||
if current == '"' {
|
||||
return Some(decoded);
|
||||
}
|
||||
if current == '\\' {
|
||||
let escaped = characters.next()?;
|
||||
match escaped {
|
||||
'"' => decoded.push('"'),
|
||||
'\\' => decoded.push('\\'),
|
||||
'/' => decoded.push('/'),
|
||||
'b' => decoded.push('\u{0008}'),
|
||||
'f' => decoded.push('\u{000C}'),
|
||||
'n' => decoded.push('\n'),
|
||||
'r' => decoded.push('\r'),
|
||||
't' => decoded.push('\t'),
|
||||
'u' => {
|
||||
let mut hex = String::new();
|
||||
for _ in 0..4 {
|
||||
hex.push(characters.next()?);
|
||||
}
|
||||
if let Ok(code) = u16::from_str_radix(hex.as_str(), 16)
|
||||
&& let Some(character) = char::from_u32(code as u32)
|
||||
{
|
||||
decoded.push(character);
|
||||
}
|
||||
}
|
||||
other => decoded.push(other),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
decoded.push(current);
|
||||
}
|
||||
Some(decoded)
|
||||
}
|
||||
|
||||
fn parse_user_input_signal(value: Option<&JsonValue>) -> Option<PromptUserInputSignal> {
|
||||
match value.and_then(JsonValue::as_str)? {
|
||||
"rich" => Some(PromptUserInputSignal::Rich),
|
||||
"normal" => Some(PromptUserInputSignal::Normal),
|
||||
"sparse" => Some(PromptUserInputSignal::Sparse),
|
||||
"correction" => Some(PromptUserInputSignal::Correction),
|
||||
"delegate" => Some(PromptUserInputSignal::Delegate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_drift_risk(value: Option<&JsonValue>) -> Option<PromptDriftRisk> {
|
||||
match value.and_then(JsonValue::as_str)? {
|
||||
"low" => Some(PromptDriftRisk::Low),
|
||||
"medium" => Some(PromptDriftRisk::Medium),
|
||||
"high" => Some(PromptDriftRisk::High),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_conversation_mode(value: Option<&JsonValue>) -> Option<PromptConversationMode> {
|
||||
match value.and_then(JsonValue::as_str)? {
|
||||
"bootstrap" => Some(PromptConversationMode::Bootstrap),
|
||||
"expand" => Some(PromptConversationMode::Expand),
|
||||
"compress" => Some(PromptConversationMode::Compress),
|
||||
"repair_direction" => Some(PromptConversationMode::RepairDirection),
|
||||
"force_complete" => Some(PromptConversationMode::ForceComplete),
|
||||
"closing" => Some(PromptConversationMode::Closing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_rules(mode: PromptConversationMode) -> &'static str {
|
||||
match mode {
|
||||
PromptConversationMode::Bootstrap => {
|
||||
r#"当前模式:bootstrap
|
||||
|
||||
目标:
|
||||
1. 先把世界的基本方向抓住
|
||||
2. 不要一次塞太多新设定
|
||||
3. 回复要降低用户开口压力
|
||||
|
||||
本轮行为要求:
|
||||
1. 优先从用户输入里抓世界方向、玩家视角、主题边界的线索
|
||||
2. 如果用户信息很少,不要强行把整套结构一次补满
|
||||
3. replyText 要像共创搭档,而不是像审问
|
||||
4. 默认只推进一个最关键的问题方向
|
||||
5. 如果用户刚开口,优先给“被理解感”,再轻轻推进下一步
|
||||
6. 可以用一句很短的话先确认你抓到的核心方向,再提一个最好回答的问题
|
||||
7. 不要把问题问得像表单采集,不要一口气追问多个维度
|
||||
|
||||
用户体验要求:
|
||||
1. 让用户觉得“现在很容易继续往下说”
|
||||
2. 不要制造被考试、被拷问、被策划问卷追着跑的感觉
|
||||
3. replyText 最好短、稳、可接话
|
||||
4. 如果用户信息很少,也不要显得冷淡或机械"#
|
||||
}
|
||||
PromptConversationMode::Expand => {
|
||||
r#"当前模式:expand
|
||||
|
||||
目标:
|
||||
1. 在保持现有方向的前提下,把设定结构逐步补全
|
||||
2. 尽量让一轮输入覆盖多个关键维度
|
||||
|
||||
本轮行为要求:
|
||||
1. 继续保留上一版里仍成立的设定
|
||||
2. 优先把用户本轮输入映射进多个关键维度,而不是只更新一个字段
|
||||
3. replyText 要明确体现“你已经理解了哪些内容”
|
||||
4. 不要突然大幅改写已经成形的世界
|
||||
5. 如果用户这一轮给了多条有效信息,replyText 应先把这些信息自然串起来,再决定下一步
|
||||
6. 可以适度替用户整理,但不要把回复写成总结报告
|
||||
7. 默认继续往前推一步,不要在还没必要时突然收束或突然跳到成稿感
|
||||
|
||||
用户体验要求:
|
||||
1. 让用户感到“我刚说的内容都被接住了”
|
||||
2. 回复里可以带一点顺势整理感,但不要太像会议纪要
|
||||
3. 不要无视用户刚提供的高价值细节
|
||||
4. 不要让用户觉得系统在自顾自重写世界"#
|
||||
}
|
||||
PromptConversationMode::Compress => {
|
||||
r#"当前模式:compress
|
||||
|
||||
目标:
|
||||
1. 开始收束当前设定
|
||||
2. 减少无效发散
|
||||
3. 让 progress 更接近可进入下一阶段
|
||||
|
||||
本轮行为要求:
|
||||
1. 新的设定结构优先保留稳定内容,不要无端重写
|
||||
2. 对用户本轮输入做高密度吸收
|
||||
3. replyText 要更聚焦,不要绕圈
|
||||
4. 默认只推进当前最影响 completion 的一步
|
||||
5. 如果用户还在补细节,优先把细节挂回现有骨架,而不是继续开新分支
|
||||
6. 可以适度提醒“还差哪类关键空位”,但不要把回复写成 checklist
|
||||
7. 如果已有信息足够,replyText 可以更像“确认并收束”,少一点继续发散式追问
|
||||
|
||||
用户体验要求:
|
||||
1. 让用户感觉世界正在变得更稳,而不是越来越散
|
||||
2. 让推进感更明确,但不要显得催促
|
||||
3. 回复语气应更笃定一些,减少反复横跳
|
||||
4. 不要把用户刚补进来的细节又冲淡掉"#
|
||||
}
|
||||
PromptConversationMode::RepairDirection => {
|
||||
r#"当前模式:repair_direction
|
||||
|
||||
目标:
|
||||
1. 处理用户对既有设定的修正
|
||||
2. 避免世界方向飘散或自相矛盾
|
||||
|
||||
本轮行为要求:
|
||||
1. 如果用户明确改口,新的设定结构必须体现修正后的方向
|
||||
2. 对已经不再成立的旧设定,不要机械保留
|
||||
3. progressPercent 可以停滞,也可以小幅回落,但不能为负
|
||||
4. replyText 要承认用户的修正,并顺着修正后的方向继续聊
|
||||
5. 先处理“改掉什么”,再决定“往哪里继续推”
|
||||
6. 不要一边口头承认用户修正,一边在设定结构里偷偷留住旧方向
|
||||
7. 如果修正幅度很大,replyText 可以帮助用户确认新方向已经接管当前语境
|
||||
|
||||
用户体验要求:
|
||||
1. 让用户感到“我刚刚的纠偏真的生效了”
|
||||
2. 不要和用户辩论旧方案为什么也行
|
||||
3. 不要表现出对修正的不情愿
|
||||
4. 回复要体现重心已经切到新方向,而不是停留在旧世界观惯性里"#
|
||||
}
|
||||
PromptConversationMode::ForceComplete => {
|
||||
r#"当前模式:force_complete
|
||||
|
||||
目标:
|
||||
1. 基于当前方向直接补齐剩余设定
|
||||
2. 生成一版尽量完整、可进入下一阶段的设定结构
|
||||
3. 结束当前收集阶段
|
||||
|
||||
本轮行为要求:
|
||||
1. 尽量保留已经形成的世界方向
|
||||
2. 对明显缺失的关键维度进行合理补全
|
||||
3. 不要继续拉长聊天,不要再追问用户
|
||||
4. progressPercent 直接输出为 100
|
||||
5. replyText 要自然引导用户点击“生成游戏设定草稿”
|
||||
6. 补全时要优先做“顺着已有方向补齐”,而不是突然换题材、换气质、换主冲突
|
||||
7. 可以让结果更完整,但不要补得过满、过死、过像定稿圣经
|
||||
8. replyText 更像阶段完成提示,不再像继续采集信息的对话
|
||||
|
||||
用户体验要求:
|
||||
1. 让用户感到“系统已经帮我把能补的补好了”
|
||||
2. 不要在这一步突然冒出很多陌生设定把用户吓出戏
|
||||
3. 回复要有完成感,但不要太官话
|
||||
4. 清楚告诉用户下一步可以做什么"#
|
||||
}
|
||||
PromptConversationMode::Closing => {
|
||||
r#"当前模式:closing
|
||||
|
||||
目标:
|
||||
1. 尽量形成一版可用的设定底子
|
||||
2. 不再继续发散新世界观
|
||||
|
||||
本轮行为要求:
|
||||
1. 优先收束,而不是扩写
|
||||
2. 不要大改已经成形的核心设定
|
||||
3. progressPercent 接近完成时,replyText 要更像确认与推进
|
||||
4. 如果用户没有大改方向,尽量让下一版内容更稳定
|
||||
5. 可以轻微补足缺口,但不要再大开新支线
|
||||
6. replyText 应减少探索式措辞,增加“已经基本成形”的稳定感
|
||||
7. 如果只差少量空位,优先把这些空位自然补平,而不是重新打开大话题
|
||||
|
||||
用户体验要求:
|
||||
1. 让用户感觉作品已经快成了,而不是还在无穷试探
|
||||
2. 回复可以更像确认和轻推,不要继续像前期那样频繁试探
|
||||
3. 保持留白感,不要把所有东西都一次说死
|
||||
4. 让用户自然过渡到下一阶段,而不是突然被切断对话"#
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn user_signal_rules(signal: PromptUserInputSignal) -> &'static str {
|
||||
match signal {
|
||||
PromptUserInputSignal::Rich => {
|
||||
r#"本轮用户输入信息密度高。
|
||||
请尽量从这一轮里提取多个锚点,不要只更新单一方向。
|
||||
如果一条输入同时影响世界方向、冲突和关系,请在新的完整设定结构中一起体现。"#
|
||||
}
|
||||
PromptUserInputSignal::Normal => {
|
||||
r#"本轮用户输入为正常补充。
|
||||
请优先顺着当前方向稳定更新,不要主动扩写太多新设定。"#
|
||||
}
|
||||
PromptUserInputSignal::Sparse => {
|
||||
r#"本轮用户输入较少或较虚。
|
||||
请保留上一版中仍然成立的内容,不要为了凑完整度而强行发明过多新设定。
|
||||
replyText 要让用户容易继续往下说。"#
|
||||
}
|
||||
PromptUserInputSignal::Correction => {
|
||||
r#"本轮用户在修正或推翻旧设定。
|
||||
请优先吸收修正,不要机械复读旧版本。
|
||||
新的完整设定结构必须以修正后的方向为准。"#
|
||||
}
|
||||
PromptUserInputSignal::Delegate => {
|
||||
r#"本轮用户把部分决定权交给你。
|
||||
你可以在 replyText 中给出有限度的建议,但不要突然补满整套设定。
|
||||
新的完整设定结构仍应尽量建立在已有世界方向上,而不是完全重做。"#
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn latest_user_text(chat_history: &[JsonValue]) -> String {
|
||||
chat_history
|
||||
.iter()
|
||||
@@ -2075,7 +1573,7 @@ fn serialize_json(value: &JsonValue, fallback: &str) -> String {
|
||||
}
|
||||
|
||||
impl PromptUserInputSignal {
|
||||
fn as_str(self) -> &'static str {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Rich => "rich",
|
||||
Self::Normal => "normal",
|
||||
@@ -2087,7 +1585,7 @@ impl PromptUserInputSignal {
|
||||
}
|
||||
|
||||
impl PromptDriftRisk {
|
||||
fn as_str(self) -> &'static str {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
@@ -2097,7 +1595,7 @@ impl PromptDriftRisk {
|
||||
}
|
||||
|
||||
impl PromptConversationMode {
|
||||
fn as_str(self) -> &'static str {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Bootstrap => "bootstrap",
|
||||
Self::Expand => "expand",
|
||||
@@ -2111,7 +1609,7 @@ impl PromptConversationMode {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::extract_reply_text_from_partial_json;
|
||||
use crate::custom_world_rpg_draft_prompts::extract_reply_text_from_partial_json;
|
||||
|
||||
#[test]
|
||||
fn extract_reply_text_from_partial_json_preserves_chinese_characters() {
|
||||
|
||||
Reference in New Issue
Block a user