diff --git a/backend/app/api/inspiration.py b/backend/app/api/inspiration.py index ffa577f..3bad1e7 100644 --- a/backend/app/api/inspiration.py +++ b/backend/app/api/inspiration.py @@ -17,10 +17,10 @@ logger = get_logger(__name__) INSPIRATION_PROMPTS = { "title": { "system": """你是一位专业的小说创作顾问。 -用户想写的小说:{description} +用户的原始想法:{initial_idea} 请根据用户的想法,生成6个吸引人的书名建议,要求: -1. 符合用户的故事构思 +1. 紧扣用户的原始想法和核心故事构思 2. 富有创意和吸引力 3. 涵盖不同的风格倾向 @@ -31,55 +31,61 @@ INSPIRATION_PROMPTS = { }} 只返回纯JSON,不要有其他文字。""", - "user": "用户的想法:{description}\n请生成6个书名建议" + "user": "用户的想法:{initial_idea}\n请生成6个书名建议" }, "description": { "system": """你是一位专业的小说创作顾问。 -用户已经确定了书名:{title} +用户的原始想法:{initial_idea} +已确定的书名:{title} 请生成6个精彩的小说简介,要求: -1. 符合书名风格 -2. 简洁有力,每个50-100字 -3. 包含核心冲突 -4. 涵盖不同的故事走向 +1. 必须紧扣用户的原始想法,确保简介是原始想法的具体展开 +2. 符合已确定的书名风格 +3. 简洁有力,每个50-100字 +4. 包含核心冲突 +5. 涵盖不同的故事走向,但都基于用户的原始构思 返回JSON格式: {{"prompt":"选择一个简介:","options":["简介1","简介2","简介3","简介4","简介5","简介6"]}} 只返回纯JSON,不要有其他文字,不要换行。""", - "user": "书名是:{title},请生成6个简介选项" + "user": "原始想法:{initial_idea}\n书名:{title}\n请生成6个简介选项" }, "theme": { "system": """你是一位专业的小说创作顾问。 -用户的小说信息: +用户的原始想法:{initial_idea} +小说信息: - 书名:{title} - 简介:{description} 请生成6个深刻的主题选项,要求: -1. 符合书名和简介的风格 -2. 有深度和思想性 -3. 每个50-150字 -4. 涵盖不同角度(如:成长、复仇、救赎、探索等) +1. 必须与用户的原始想法保持高度一致 +2. 符合书名和简介的风格 +3. 有深度和思想性 +4. 每个50-150字 +5. 涵盖不同角度(如:成长、复仇、救赎、探索等),但都围绕用户的核心构思 返回JSON格式: {{"prompt":"这本书的核心主题是什么?","options":["主题1","主题2","主题3","主题4","主题5","主题6"]}} 只返回纯JSON,不要有其他文字,不要换行。""", - "user": "书名:{title}\n简介:{description}\n请生成6个主题选项" + "user": "原始想法:{initial_idea}\n书名:{title}\n简介:{description}\n请生成6个主题选项" }, "genre": { "system": """你是一位专业的小说创作顾问。 -用户的小说信息: +用户的原始想法:{initial_idea} +小说信息: - 书名:{title} - 简介:{description} - 主题:{theme} 请生成6个合适的类型标签(每个2-4字),要求: -1. 符合小说整体风格 -2. 可以多选组合 +1. 必须符合用户原始想法中暗示的类型倾向 +2. 符合小说整体风格 +3. 可以多选组合 常见类型:玄幻、都市、科幻、武侠、仙侠、历史、言情、悬疑、奇幻、修仙等 @@ -87,11 +93,20 @@ INSPIRATION_PROMPTS = { {{"prompt":"选择类型标签(可多选):","options":["类型1","类型2","类型3","类型4","类型5","类型6"]}} 只返回紧凑的纯JSON,不要换行,不要有其他文字。""", - "user": "书名:{title}\n简介:{description}\n主题:{theme}\n请生成6个类型标签" + "user": "原始想法:{initial_idea}\n书名:{title}\n简介:{description}\n主题:{theme}\n请生成6个类型标签" } } +# 不同阶段的temperature设置(递减以保持一致性) +TEMPERATURE_SETTINGS = { + "title": 0.8, # 书名阶段可以更有创意 + "description": 0.65, # 简介需要贴合书名和原始想法 + "theme": 0.55, # 主题需要更加贴合 + "genre": 0.45 # 类型应该很明确 +} + + def validate_options_response(result: Dict[str, Any], step: str, max_retries: int = 3) -> tuple[bool, str]: """ 校验AI返回的选项格式是否正确 @@ -179,7 +194,9 @@ async def generate_options( prompt_template = INSPIRATION_PROMPTS[step] # 准备格式化参数(提供默认值避免KeyError) + # 关键改进:保持initial_idea在所有阶段传递,确保内容关联性 format_params = { + "initial_idea": context.get("initial_idea", context.get("description", "")), # 优先使用initial_idea,兼容旧数据 "title": context.get("title", ""), "description": context.get("description", ""), "theme": context.get("theme", "") @@ -194,11 +211,13 @@ async def generate_options( system_prompt += f"\n\n⚠️ 这是第{attempt + 1}次生成,请务必严格按照JSON格式返回,确保options数组包含6个有效选项!" # 调用AI生成选项 - logger.info(f"调用AI生成{step}选项...") + # 关键改进:使用递减的temperature以保持后续阶段与前文的一致性 + temperature = TEMPERATURE_SETTINGS.get(step, 0.7) + logger.info(f"调用AI生成{step}选项... (temperature={temperature})") response = await ai_service.generate_text( prompt=user_prompt, system_prompt=system_prompt, - temperature=0.8 # 提高创造性 + temperature=temperature ) content = response.get("content", "") @@ -327,10 +346,12 @@ async def quick_generate( 请生成完整的小说方案,包含: 1. title: 书名(3-6字,如果用户已提供则保持原样) -2. description: 简介(50-100字) -3. theme: 核心主题(30-50字) +2. description: 简介(50-100字,必须基于用户提供的信息,不要偏离原意) +3. theme: 核心主题(30-50字,必须与用户提供的信息保持一致) 4. genre: 类型标签数组(2-3个) +重要:所有补全的内容都必须与用户提供的信息保持高度关联,确保前后一致性。 + 返回JSON格式: {{ "title": "书名", diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py index 72969cc..2c2cfb2 100644 --- a/backend/app/services/prompt_service.py +++ b/backend/app/services/prompt_service.py @@ -112,42 +112,49 @@ class PromptService: """提示词模板管理""" # 世界构建提示词 - WORLD_BUILDING = """你是一位资深的世界观设计师。请根据以下信息构建一个完整的小说世界观: + WORLD_BUILDING = """你是一位资深的世界观设计师(World-Building Architect)。你的任务是基于输入信息,构建一个高度原创、深度自洽、且充满戏剧冲突的小说世界观。 +# 1. 输入信息 书名:{title} 主题:{theme} 类型:{genre} -请生成包含以下内容的世界构建框架: +# 2. 世界构建框架 +请生成包含以下四个核心板块的世界构建框架。请确保所有板块都围绕【核心概念】展开,并且板块之间【互为因果】。 -1. **时间背景**:具体的时代设定、时间流逝特点、重要历史事件 -2. **地理位置**:主要地点描述、地理环境特征、空间布局 -3. **氛围基调**:整体氛围感觉、情感色彩、视觉风格 -4. **世界规则**:基本运行法则、特殊设定、社会规则和禁忌、权力结构 +1. **时间背景 (time_period)**: + * 具体的时代设定(例如:星际航行晚期、黑铁时代)。 + * 重要的【历史转折事件】(是什么导致了当前的世界面貌?)。 + * 当前的主要【社会矛盾】或【时代议题】。 +2. **地理/空间 (location)**: + * 主要舞台(如城市、星球、位面)的【地理环境特征】。 + * 这些特征如何影响了【文明】的形态和【资源】分布? + * 独特的【空间布局】或【奇观】。 +3. **氛围基调 (atmosphere)**: + * 整体的【情感色彩】(例如:压抑、荒诞、史诗、诡异)。 + * 【视觉风格】(例如:赛博霓虹、蒸汽朋克、哥特式)。 + * 普通居民在日常生活中最常【感受】到什么? +4. **世界规则 (rules)**: + * 【物理法则】或【超自然力量】(如魔法、科技)的【具体运作方式】和【代价】。 + * 【社会规则】和【权力结构】(谁在统治?基于什么?)。 + * 最严重的【社会禁忌】是什么?违反了会怎样? -要求: -- 与主题高度契合 -- 设定要合理自洽 -- 为故事发展提供支撑 -- 具有独特性和吸引力 +# 3. 严格格式要求 +1. **绝对纯净JSON**:你的[唯一]输出必须是一个完整的JSON对象。输出必须以左花括号开始,并以右花括号结束。 +2. **禁止额外字符**:不要在JSON对象之前或之后包含任何说明文字、Markdown标记(如三个反引号加json)、注释或任何其他非JSON字符。 +3. **JSON内部文本规则**:在JSON的value字符串内部: + * 严禁使用任何中文引号(""'')或英文引号来表示强调或引用。 + * 所有【专有名词】(如地点、人物、组织)应使用【】包裹。 + * 所有《作品》或《特殊概念》的标题应使用《》包裹。 +4. **JSON结构**:严格遵守`"key": "value"`的英文双引号结构,并使用下面指定的key。 +5. **内容密度**:每个字段的描述都必须【深入且详实】,提供至少5-7个具体的设定点或细节。 -**重要格式要求:** -1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字 -2. 不要在JSON字符串值中使用中文引号(""''),请使用英文引号或直接省略引号 -3. 专有名词和强调内容可以使用【】或《》标记,不要用引号 - -请严格按照以下JSON格式返回(每个字段为200-300字的文本描述): {{ - "time_period": "时间背景的详细描述,包括时代设定、时间特点、历史事件", - "location": "地理位置的详细描述,包括主要地点、环境特征、空间布局", - "atmosphere": "氛围基调的详细描述,包括整体氛围、情感色彩、视觉风格", - "rules": "世界规则的详细描述,包括运行法则、特殊设定、社会规则、权力结构" -}} - -再次强调: -1. 只返回纯JSON对象,不要有```json```这样的标记 -2. 文本中不要使用中文引号(""),使用【】或《》代替 -3. 不要有任何额外的文字说明""" + "time_period": "(此处填写时间背景的详细描述)", + "location": "(此处填写地理/空间的详细描述)", + "atmosphere": "(此处填写氛围基调的详细描述)", + "rules": "(此处填写世界规则的详细描述)" +}}""" # 批量角色生成提示词 CHARACTERS_BATCH_GENERATION = """你是一位专业的角色设定师。请根据以下世界观和要求,生成{count}个立体丰满的角色和组织: diff --git a/frontend/src/pages/Inspiration.tsx b/frontend/src/pages/Inspiration.tsx index 70cb7de..0623232 100644 --- a/frontend/src/pages/Inspiration.tsx +++ b/frontend/src/pages/Inspiration.tsx @@ -40,6 +40,8 @@ const Inspiration: React.FC = () => { // 收集的数据 const [wizardData, setWizardData] = useState>({}); + // 保存用户的原始想法,用于保持上下文一致性 + const [initialIdea, setInitialIdea] = useState(''); // 项目生成状态 const [projectId, setProjectId] = useState(''); @@ -148,9 +150,15 @@ const Inspiration: React.FC = () => { try { if (currentStep === 'idea') { + // 保存用户的原始想法 + setInitialIdea(userInput); + const requestData = { step: 'title' as const, - context: { description: userInput } + context: { + initial_idea: userInput, + description: userInput + } }; const response = await inspirationApi.generateOptions(requestData); @@ -492,7 +500,10 @@ const Inspiration: React.FC = () => { if (nextStep === 'description') { const requestData = { step: 'description' as const, - context: { title: data.title } + context: { + initial_idea: initialIdea, + title: data.title + } }; const response = await inspirationApi.generateOptions(requestData); @@ -522,7 +533,11 @@ const Inspiration: React.FC = () => { } else if (nextStep === 'theme') { const requestData = { step: 'theme' as const, - context: { title: data.title, description: data.description } + context: { + initial_idea: initialIdea, + title: data.title, + description: data.description + } }; const response = await inspirationApi.generateOptions(requestData); @@ -553,6 +568,7 @@ const Inspiration: React.FC = () => { const requestData = { step: 'genre' as const, context: { + initial_idea: initialIdea, title: data.title, description: data.description, theme: data.theme @@ -596,6 +612,7 @@ const Inspiration: React.FC = () => { } ]); setWizardData({}); + setInitialIdea(''); // 重置原始想法 setSelectedOptions([]); setLoading(false); };