diff --git a/backend/app/services/plot_analyzer.py b/backend/app/services/plot_analyzer.py
index 350fedf..c13992a 100644
--- a/backend/app/services/plot_analyzer.py
+++ b/backend/app/services/plot_analyzer.py
@@ -38,7 +38,8 @@ class PlotAnalyzer:
db: AsyncSession = None,
max_retries: int = 3,
existing_foreshadows: Optional[List[Dict[str, Any]]] = None,
- on_retry: Optional[OnRetryCallback] = None
+ on_retry: Optional[OnRetryCallback] = None,
+ characters_info: str = ""
) -> Optional[Dict[str, Any]]:
"""
分析单章内容(带重试机制)
@@ -53,6 +54,7 @@ class PlotAnalyzer:
max_retries: 最大重试次数,默认3次
existing_foreshadows: 已埋入的伏笔列表(用于回收匹配)
on_retry: 重试时的回调函数,参数为 (当前重试次数, 最大重试次数, 等待秒数, 错误原因)
+ characters_info: 项目角色信息文本(用于角色名称匹配)
Returns:
分析结果字典,失败返回None
@@ -83,7 +85,8 @@ class PlotAnalyzer:
title=title,
word_count=word_count,
content=analysis_content,
- existing_foreshadows=foreshadows_text
+ existing_foreshadows=foreshadows_text,
+ characters_info=characters_info if characters_info else "(暂无角色信息)"
)
last_error = None
@@ -187,12 +190,13 @@ class PlotAnalyzer:
def _format_existing_foreshadows(self, foreshadows: Optional[List[Dict[str, Any]]]) -> str:
"""
- 格式化已有伏笔列表,用于注入到分析提示词中(智能分类版)
+ 格式化已有伏笔列表,用于注入到分析提示词中
- 核心策略:
- 1. 必须回收的伏笔 - 明确标注,要求AI识别回收
- 2. 超期的伏笔 - 提醒AI尽快回收
- 3. 未到期的伏笔 - 明确标注禁止提前回收
+ 核心策略(重构版):
+ - 分层展示所有已埋入伏笔,让AI能识别"自然回收"
+ - 第1层:本章必须回收的伏笔(最详细)
+ - 第2层:超期伏笔(较详细)
+ - 第3层:其他已埋入伏笔(精简信息,供AI判断是否自然回收了)
Args:
foreshadows: 伏笔列表,每个包含 id, title, content, plant_chapter_number, resolve_status 等
@@ -203,98 +207,60 @@ class PlotAnalyzer:
if not foreshadows:
return "(暂无已埋入的伏笔)"
- # 按回收状态分类
- must_resolve = [] # 本章必须回收
- overdue = [] # 已超期
- not_yet = [] # 尚未到期
- no_plan = [] # 无明确计划
-
- for fs in foreshadows:
- status = fs.get('resolve_status', 'no_plan')
- if status == 'must_resolve_now':
- must_resolve.append(fs)
- elif status == 'overdue':
- overdue.append(fs)
- elif status == 'not_yet':
- not_yet.append(fs)
- else:
- no_plan.append(fs)
+ # 分类伏笔
+ must_resolve = [fs for fs in foreshadows if fs.get('resolve_status') == 'must_resolve_now']
+ overdue = [fs for fs in foreshadows if fs.get('resolve_status') == 'overdue']
+ others = [fs for fs in foreshadows if fs.get('resolve_status') not in ('must_resolve_now', 'overdue')]
lines = []
- # 1. 本章必须回收的伏笔(最高优先级)
+ # === 第1层:本章必须回收的伏笔(最详细)===
if must_resolve:
- lines.append("=" * 50)
- lines.append("【🎯 本章必须回收的伏笔 - 请务必识别回收】")
- lines.append("=" * 50)
+ lines.append("=" * 40)
+ lines.append("【🎯 本章必须回收的伏笔】")
+ lines.append("=" * 40)
for i, fs in enumerate(must_resolve, 1):
fs_id = fs.get('id', 'unknown')
fs_title = fs.get('title', '未命名伏笔')
- fs_content = fs.get('content', '')[:150]
+ fs_content = fs.get('content', '')[:200]
plant_chapter = fs.get('plant_chapter_number', '?')
+ hint_text = fs.get('hint_text', '')
lines.append(f"{i}. 【ID: {fs_id}】{fs_title}")
- lines.append(f" ⚠️ 回收要求:必须在本章回收此伏笔")
lines.append(f" 埋入章节:第{plant_chapter}章")
- lines.append(f" 伏笔内容:{fs_content}{'...' if len(fs.get('content', '')) > 150 else ''}")
- lines.append(f" 回收时请在 reference_foreshadow_id 中填写: {fs_id}")
+ lines.append(f" 伏笔内容:{fs_content}{'...' if len(fs.get('content', '')) > 200 else ''}")
+ if hint_text:
+ lines.append(f" 埋入暗示:{hint_text[:100]}")
+ lines.append(f" ⚠️ 回收时 reference_foreshadow_id 填写: {fs_id}")
lines.append("")
- # 2. 超期的伏笔(需要尽快处理)
+ # === 第2层:超期伏笔 ===
if overdue:
- lines.append("-" * 50)
- lines.append("【⚠️ 超期待回收伏笔 - 建议尽快回收】")
- lines.append("-" * 50)
- for i, fs in enumerate(overdue, 1):
+ lines.append("【⚠️ 超期未回收伏笔 - 如章节内容回收了请标记】")
+ for fs in overdue[:5]:
fs_id = fs.get('id', 'unknown')
- fs_title = fs.get('title', '未命名伏笔')
- fs_content = fs.get('content', '')[:100]
+ fs_title = fs.get('title', '')
plant_chapter = fs.get('plant_chapter_number', '?')
- hint = fs.get('resolve_hint', '')
-
- lines.append(f"{i}. 【ID: {fs_id}】{fs_title}")
- lines.append(f" 状态:{hint}")
- lines.append(f" 埋入章节:第{plant_chapter}章")
- lines.append(f" 内容:{fs_content}{'...' if len(fs.get('content', '')) > 100 else ''}")
- lines.append("")
-
- # 3. 尚未到期的伏笔(禁止提前回收,仅作参考)
- if not_yet:
- lines.append("-" * 50)
- lines.append("【📋 尚未到期的伏笔 - 仅供参考,请勿在本章回收】")
- lines.append("-" * 50)
- lines.append("⚠️ 以下伏笔尚未到计划回收时间,请勿提前回收!")
+ lines.append(f"- 【ID: {fs_id}】{fs_title}(第{plant_chapter}章埋入)")
lines.append("")
- for i, fs in enumerate(not_yet[:5], 1): # 最多显示5个
- fs_title = fs.get('title', '未命名伏笔')
- target_chapter = fs.get('target_resolve_chapter_number', '?')
- hint = fs.get('resolve_hint', '')
-
- lines.append(f"{i}. {fs_title}")
- lines.append(f" 计划回收章节:第{target_chapter}章 | {hint}")
- lines.append("")
-
- if len(not_yet) > 5:
- lines.append(f" ... 还有 {len(not_yet) - 5} 个未到期伏笔")
- lines.append("")
- # 4. 无明确计划的伏笔(可根据剧情自然回收)
- if no_plan:
- lines.append("-" * 50)
- lines.append("【📝 无明确计划的伏笔 - 可根据剧情自然回收】")
- lines.append("-" * 50)
- for i, fs in enumerate(no_plan[:3], 1): # 最多显示3个
+ # === 第3层:其他已埋入伏笔(精简)===
+ if others:
+ lines.append("【📋 其他已埋入伏笔 - 如章节内容自然回收了请标记】")
+ for fs in others[:10]:
fs_id = fs.get('id', 'unknown')
- fs_title = fs.get('title', '未命名伏笔')
- fs_content = fs.get('content', '')[:80]
+ fs_title = fs.get('title', '')
plant_chapter = fs.get('plant_chapter_number', '?')
-
- lines.append(f"{i}. 【ID: {fs_id}】{fs_title}")
- lines.append(f" 埋入章节:第{plant_chapter}章")
- lines.append(f" 内容:{fs_content}{'...' if len(fs.get('content', '')) > 80 else ''}")
- lines.append("")
+ lines.append(f"- 【ID: {fs_id}】{fs_title}(第{plant_chapter}章埋入)")
+ if len(others) > 10:
+ lines.append(f" ... 还有{len(others) - 10}个伏笔未列出")
+ lines.append("")
- return "\n".join(lines) if lines else "(暂无已埋入的伏笔)"
+ # 操作指引
+ lines.append("提示:如果章节内容回收了上述任一伏笔,请在 foreshadows 数组中")
+ lines.append("添加 type='resolved' 的记录,并在 reference_foreshadow_id 填写对应ID。")
+
+ return "\n".join(lines)
def _parse_analysis_response(self, response: str) -> Optional[Dict[str, Any]]:
"""
diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py
index 61dea2a..af89ff6 100644
--- a/backend/app/services/prompt_service.py
+++ b/backend/app/services/prompt_service.py
@@ -324,27 +324,37 @@ class PromptService:
[
{{
- "chapter_number": 1,
- "title": "章节标题",
- "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折",
- "scenes": ["场景1描述", "场景2描述", "场景3描述"],
- "characters": ["涉及角色1", "涉及角色2"],
- "key_points": ["情节要点1", "情节要点2"],
- "emotion": "本章情感基调",
- "goal": "本章叙事目标"
- }},
- {{
- "chapter_number": 2,
- "title": "章节标题",
- "summary": "章节概要...",
- "scenes": ["场景1", "场景2"],
- "characters": ["角色1", "角色2"],
- "key_points": ["要点1", "要点2"],
- "emotion": "情感基调",
- "goal": "叙事目标"
- }}
+ "chapter_number": 1,
+ "title": "章节标题",
+ "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折",
+ "scenes": ["场景1描述", "场景2描述", "场景3描述"],
+ "characters": [
+ {{"name": "角色名1", "type": "character"}},
+ {{"name": "组织/势力名1", "type": "organization"}}
+ ],
+ "key_points": ["情节要点1", "情节要点2"],
+ "emotion": "本章情感基调",
+ "goal": "本章叙事目标"
+ }},
+ {{
+ "chapter_number": 2,
+ "title": "章节标题",
+ "summary": "章节概要...",
+ "scenes": ["场景1", "场景2"],
+ "characters": [
+ {{"name": "角色名2", "type": "character"}},
+ {{"name": "组织名2", "type": "organization"}}
+ ],
+ "key_points": ["要点1", "要点2"],
+ "emotion": "情感基调",
+ "goal": "叙事目标"
+ }}
]
+【characters字段说明】
+- type为"character"表示个人角色,type为"organization"表示组织/势力/门派/帮派等
+- 必须区分角色和组织,不要把组织当作角色
+
【格式规范】
- 纯JSON数组输出,无markdown标记
- 内容描述中严禁使用特殊符号
@@ -435,27 +445,37 @@ class PromptService:
[
{{
- "chapter_number": {start_chapter},
- "title": "章节标题",
- "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折",
- "scenes": ["场景1描述", "场景2描述", "场景3描述"],
- "characters": ["涉及角色1", "涉及角色2"],
- "key_points": ["情节要点1", "情节要点2"],
- "emotion": "本章情感基调",
- "goal": "本章叙事目标"
- }},
- {{
- "chapter_number": {start_chapter} + 1,
- "title": "章节标题",
- "summary": "章节概要...",
- "scenes": ["场景1", "场景2"],
- "characters": ["角色1", "角色2"],
- "key_points": ["要点1", "要点2"],
- "emotion": "情感基调",
- "goal": "叙事目标"
- }}
+ "chapter_number": {start_chapter},
+ "title": "章节标题",
+ "summary": "章节概要(500-1000字):主要情节、角色互动、关键事件、冲突与转折",
+ "scenes": ["场景1描述", "场景2描述", "场景3描述"],
+ "characters": [
+ {{"name": "角色名1", "type": "character"}},
+ {{"name": "组织/势力名1", "type": "organization"}}
+ ],
+ "key_points": ["情节要点1", "情节要点2"],
+ "emotion": "本章情感基调",
+ "goal": "本章叙事目标"
+ }},
+ {{
+ "chapter_number": {start_chapter} + 1,
+ "title": "章节标题",
+ "summary": "章节概要...",
+ "scenes": ["场景1", "场景2"],
+ "characters": [
+ {{"name": "角色名2", "type": "character"}},
+ {{"name": "组织名2", "type": "organization"}}
+ ],
+ "key_points": ["要点1", "要点2"],
+ "emotion": "情感基调",
+ "goal": "叙事目标"
+ }}
]
+【characters字段说明】
+- type为"character"表示个人角色,type为"organization"表示组织/势力/门派/帮派等
+- 必须区分角色和组织,不要把组织当作角色
+
【格式规范】
- 纯JSON数组输出,无markdown标记
- 内容描述中严禁使用特殊符号
@@ -505,21 +525,45 @@ class PromptService:
-【本章角色】
+【本章角色 - 请严格遵循角色设定】
{characters_info}
+
+⚠️ 角色互动须知:
+- 角色之间的对话和行为必须符合其关系设定(如师徒、敌对等)
+- 涉及组织的情节须体现角色在组织中的身份和职位
+- 角色的能力表现须符合其职业和阶段设定
+
+【本章职业】
+{chapter_careers}
+
+
+
+【🎯 伏笔提醒】
+{foreshadow_reminders}
+
+
+
+【相关记忆】
+{relevant_memories}
+
+
【必须遵守】
✅ 严格按照大纲推进情节
✅ 保持角色性格、说话方式一致
+✅ 角色互动须符合关系设定(师徒、朋友、敌对等)
+✅ 组织相关情节须体现成员身份和职位层级
✅ 字数控制在目标范围内
+✅ 如有伏笔提醒,请在本章中适当埋入或回收相应伏笔
【禁止事项】
❌ 输出章节标题、序号等元信息
❌ 使用"总之"、"综上所述"等AI常见总结语
❌ 在结尾处使用开放式反问
❌ 添加作者注释或创作说明
+❌ 角色行为超出其职业阶段的能力范围