update:优化章节上下文构建系统和大纲查找逻辑,优先使用outline_id直接关联
This commit is contained in:
@@ -13,6 +13,7 @@ from app.models.character import Character
|
||||
from app.models.career import Career, CharacterCareer
|
||||
from app.models.memory import StoryMemory
|
||||
from app.models.foreshadow import Foreshadow
|
||||
from app.models.relationship import CharacterRelationship, Organization, OrganizationMember
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -24,14 +25,15 @@ class OneToManyContext:
|
||||
1-N模式章节上下文数据结构
|
||||
|
||||
采用RTCO框架的分层设计:
|
||||
- P0-核心:大纲、衔接锚点、字数要求
|
||||
- P1-重要:角色、情感基调、风格
|
||||
- P2-参考:记忆、故事骨架、伏笔提醒
|
||||
- P0-核心:大纲(含最近10章规划)、衔接锚点(500字+摘要)、字数要求
|
||||
- P1-重要:角色(完整版含关系/组织/职业)、职业详情、情感基调
|
||||
- P2-参考:记忆(始终启用,相关度>0.6)、伏笔提醒
|
||||
"""
|
||||
|
||||
# === P0-核心信息 ===
|
||||
chapter_outline: str = "" # 本章大纲(从expansion_plan构建)
|
||||
continuation_point: Optional[str] = None # 衔接锚点
|
||||
recent_chapters_context: Optional[str] = None # 最近10章expansion_plan摘要
|
||||
continuation_point: Optional[str] = None # 衔接锚点(统一500字)
|
||||
previous_chapter_summary: Optional[str] = None # 上一章剧情摘要
|
||||
previous_chapter_events: Optional[List[str]] = None # 上一章关键事件
|
||||
target_word_count: int = 3000
|
||||
@@ -49,13 +51,12 @@ class OneToManyContext:
|
||||
theme: str = ""
|
||||
|
||||
# === P1-重要信息 ===
|
||||
chapter_characters: str = "" # 从character_focus筛选的角色
|
||||
chapter_characters: str = "" # 完整版角色信息(含年龄、外貌、背景、关系、组织)
|
||||
chapter_careers: Optional[str] = None # 独立的职业详情(含完整阶段体系)
|
||||
emotional_tone: str = ""
|
||||
style_instruction: str = ""
|
||||
|
||||
# === P2-参考信息 ===
|
||||
relevant_memories: Optional[str] = None
|
||||
story_skeleton: Optional[str] = None # 50章+启用
|
||||
relevant_memories: Optional[str] = None # 始终启用(相关度>0.6)
|
||||
foreshadow_reminders: Optional[str] = None
|
||||
|
||||
# === 元信息 ===
|
||||
@@ -64,9 +65,10 @@ class OneToManyContext:
|
||||
def get_total_context_length(self) -> int:
|
||||
"""计算总上下文长度"""
|
||||
total = 0
|
||||
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters',
|
||||
'relevant_memories', 'story_skeleton', 'style_instruction',
|
||||
'foreshadow_reminders', 'previous_chapter_summary']:
|
||||
for field_name in ['chapter_outline', 'recent_chapters_context', 'continuation_point',
|
||||
'chapter_characters', 'chapter_careers',
|
||||
'relevant_memories', 'foreshadow_reminders',
|
||||
'previous_chapter_summary']:
|
||||
value = getattr(self, field_name, None)
|
||||
if value:
|
||||
total += len(value)
|
||||
@@ -102,6 +104,7 @@ class OneToOneContext:
|
||||
|
||||
# === P1-重要信息 ===
|
||||
continuation_point: Optional[str] = None # 上一章最后500字
|
||||
previous_chapter_summary: Optional[str] = None # 上一章剧情摘要
|
||||
chapter_characters: str = "" # 从structure.characters获取
|
||||
chapter_careers: Optional[str] = None # 本章涉及的职业完整信息
|
||||
|
||||
@@ -115,8 +118,9 @@ class OneToOneContext:
|
||||
def get_total_context_length(self) -> int:
|
||||
"""计算总上下文长度"""
|
||||
total = 0
|
||||
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters',
|
||||
'chapter_careers', 'foreshadow_reminders', 'relevant_memories']:
|
||||
for field_name in ['chapter_outline', 'continuation_point', 'previous_chapter_summary',
|
||||
'chapter_characters', 'chapter_careers', 'foreshadow_reminders',
|
||||
'relevant_memories']:
|
||||
value = getattr(self, field_name, None)
|
||||
if value:
|
||||
total += len(value)
|
||||
@@ -129,23 +133,20 @@ class OneToManyContextBuilder:
|
||||
"""
|
||||
1-N模式上下文构建器
|
||||
|
||||
实现动态裁剪逻辑,根据章节序号自动调整上下文复杂度:
|
||||
- 第1章:无前置上下文,仅提供大纲和角色
|
||||
- 第2-10章:上一章结尾300字 + 涉及角色
|
||||
- 第11-50章:上一章结尾500字 + 相关记忆3条
|
||||
- 第51章+:上一章结尾500字 + 故事骨架 + 智能记忆5条
|
||||
上下文构建策略:
|
||||
- 章节大纲:本章expansion_plan + 最近10章expansion_plan摘要
|
||||
- 衔接锚点:统一上一章末尾500字 + 摘要
|
||||
- 角色信息:完整版(含年龄、外貌、背景、关系、组织、职业)
|
||||
- 职业详情:独立的chapter_careers字段,含完整阶段体系
|
||||
- 相关记忆:始终启用(相关度>0.6)
|
||||
- 伏笔提醒:始终启用
|
||||
"""
|
||||
|
||||
# 配置常量
|
||||
ENDING_LENGTH_SHORT = 300 # 1-10章:短衔接
|
||||
ENDING_LENGTH_NORMAL = 500 # 11章+:标准衔接
|
||||
MEMORY_COUNT_LIGHT = 3 # 11-50章:轻量记忆
|
||||
MEMORY_COUNT_FULL = 5 # 51章+:完整记忆
|
||||
SKELETON_THRESHOLD = 50 # 启用故事骨架的章节阈值
|
||||
SKELETON_SAMPLE_INTERVAL = 10 # 故事骨架采样间隔
|
||||
MEMORY_IMPORTANCE_THRESHOLD = 0.7 # 记忆重要性阈值
|
||||
STYLE_MAX_LENGTH = 200 # 风格描述最大长度
|
||||
MAX_CONTEXT_LENGTH = 3000 # 总上下文最大字符数
|
||||
ENDING_LENGTH = 500 # 统一衔接长度500字
|
||||
MEMORY_COUNT = 10 # 记忆条数
|
||||
MEMORY_SIMILARITY_THRESHOLD = 0.6 # 记忆相关度阈值
|
||||
RECENT_CHAPTERS_COUNT = 10 # 最近章节规划数量
|
||||
|
||||
def __init__(self, memory_service=None, foreshadow_service=None):
|
||||
"""
|
||||
@@ -178,7 +179,7 @@ class OneToManyContextBuilder:
|
||||
outline: 大纲对象(可选)
|
||||
user_id: 用户ID
|
||||
db: 数据库会话
|
||||
style_content: 写作风格内容(可选)
|
||||
style_content: 写作风格内容(可选,不再使用,保留参数兼容性)
|
||||
target_word_count: 目标字数
|
||||
temp_narrative_perspective: 临时叙事视角(可选,覆盖项目默认)
|
||||
|
||||
@@ -211,59 +212,47 @@ class OneToManyContextBuilder:
|
||||
# === P0-核心信息(始终构建)===
|
||||
context.chapter_outline = self._build_chapter_outline_1n(chapter, outline)
|
||||
|
||||
# === 衔接锚点(根据章节调整长度,增强版含摘要和事件)===
|
||||
# === 最近10章expansion_plan摘要 ===
|
||||
if chapter_number > 1:
|
||||
context.recent_chapters_context = await self._build_recent_chapters_context(
|
||||
chapter, project.id, db
|
||||
)
|
||||
logger.info(f" ✅ 最近章节规划: {len(context.recent_chapters_context or '')}字符")
|
||||
|
||||
# === 衔接锚点(统一500字 + 摘要)===
|
||||
if chapter_number == 1:
|
||||
context.continuation_point = None
|
||||
context.previous_chapter_summary = None
|
||||
context.previous_chapter_events = None
|
||||
logger.info(" ✅ 第1章无需衔接锚点")
|
||||
elif chapter_number <= 10:
|
||||
ending_info = await self._get_last_ending_enhanced(
|
||||
chapter, db, self.ENDING_LENGTH_SHORT
|
||||
)
|
||||
context.continuation_point = ending_info.get('ending_text')
|
||||
context.previous_chapter_summary = ending_info.get('summary')
|
||||
context.previous_chapter_events = ending_info.get('key_events')
|
||||
logger.info(f" ✅ 衔接锚点(短): {len(context.continuation_point or '')}字符")
|
||||
else:
|
||||
ending_info = await self._get_last_ending_enhanced(
|
||||
chapter, db, self.ENDING_LENGTH_NORMAL
|
||||
chapter, db, self.ENDING_LENGTH
|
||||
)
|
||||
context.continuation_point = ending_info.get('ending_text')
|
||||
context.previous_chapter_summary = ending_info.get('summary')
|
||||
context.previous_chapter_events = ending_info.get('key_events')
|
||||
logger.info(f" ✅ 衔接锚点(标准): {len(context.continuation_point or '')}字符")
|
||||
logger.info(f" ✅ 衔接锚点: {len(context.continuation_point or '')}字符")
|
||||
|
||||
# === P1-重要信息 ===
|
||||
context.chapter_characters = await self._build_chapter_characters_1n(
|
||||
# 角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情
|
||||
characters_info, careers_info = await self._build_chapter_characters_1n(
|
||||
chapter, project, outline, db
|
||||
)
|
||||
context.chapter_characters = characters_info
|
||||
context.chapter_careers = careers_info
|
||||
context.emotional_tone = self._extract_emotional_tone(chapter, outline)
|
||||
logger.info(f" ✅ 角色信息: {len(context.chapter_characters)}字符")
|
||||
logger.info(f" ✅ 职业信息: {len(context.chapter_careers or '')}字符")
|
||||
|
||||
# 写作风格(摘要化)
|
||||
if style_content:
|
||||
context.style_instruction = self._summarize_style(style_content)
|
||||
|
||||
# === P2-参考信息(条件触发)===
|
||||
if chapter_number > 10 and self.memory_service:
|
||||
memory_limit = (
|
||||
self.MEMORY_COUNT_LIGHT if chapter_number <= 50
|
||||
else self.MEMORY_COUNT_FULL
|
||||
)
|
||||
context.relevant_memories = await self._get_relevant_memories(
|
||||
user_id, project.id, chapter_number,
|
||||
context.chapter_outline,
|
||||
limit=memory_limit
|
||||
# === P2-参考信息(始终启用)===
|
||||
if self.memory_service:
|
||||
context.relevant_memories = await self._get_relevant_memories_enhanced(
|
||||
user_id, project.id, chapter_number,
|
||||
context.chapter_outline, db
|
||||
)
|
||||
logger.info(f" ✅ 相关记忆: {len(context.relevant_memories or '')}字符")
|
||||
|
||||
# 故事骨架(50章+)
|
||||
if chapter_number > self.SKELETON_THRESHOLD:
|
||||
context.story_skeleton = await self._build_story_skeleton(
|
||||
project.id, chapter_number, db
|
||||
)
|
||||
logger.info(f" ✅ 故事骨架: {len(context.story_skeleton or '')}字符")
|
||||
|
||||
# === P2-伏笔提醒===
|
||||
if self.foreshadow_service:
|
||||
context.foreshadow_reminders = await self._get_foreshadow_reminders(
|
||||
@@ -279,8 +268,9 @@ class OneToManyContextBuilder:
|
||||
"has_continuation": context.continuation_point is not None,
|
||||
"continuation_length": len(context.continuation_point or ""),
|
||||
"characters_length": len(context.chapter_characters),
|
||||
"careers_length": len(context.chapter_careers or ""),
|
||||
"recent_context_length": len(context.recent_chapters_context or ""),
|
||||
"memories_length": len(context.relevant_memories or ""),
|
||||
"skeleton_length": len(context.story_skeleton or ""),
|
||||
"foreshadow_length": len(context.foreshadow_reminders or ""),
|
||||
"total_length": context.get_total_context_length()
|
||||
}
|
||||
@@ -321,16 +311,21 @@ class OneToManyContextBuilder:
|
||||
project: Project,
|
||||
outline: Optional[Outline],
|
||||
db: AsyncSession
|
||||
) -> str:
|
||||
"""构建1-N模式的角色信息(从expansion_plan提取character_focus)"""
|
||||
) -> tuple[str, Optional[str]]:
|
||||
"""构建1-N模式的角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情"""
|
||||
from sqlalchemy import or_
|
||||
|
||||
# 获取所有角色
|
||||
characters_result = await db.execute(
|
||||
select(Character).where(Character.project_id == project.id)
|
||||
)
|
||||
characters = characters_result.scalars().all()
|
||||
all_characters = characters_result.scalars().all()
|
||||
|
||||
if not characters:
|
||||
return "暂无角色信息"
|
||||
if not all_characters:
|
||||
return "暂无角色信息", None
|
||||
|
||||
# 构建全局角色名称映射(用于关系查询)
|
||||
all_char_map = {c.id: c.name for c in all_characters}
|
||||
|
||||
# 从expansion_plan中提取角色焦点
|
||||
filter_character_names = None
|
||||
@@ -342,26 +337,338 @@ class OneToManyContextBuilder:
|
||||
pass
|
||||
|
||||
# 筛选角色
|
||||
characters = all_characters
|
||||
if filter_character_names:
|
||||
characters = [c for c in characters if c.name in filter_character_names]
|
||||
characters = [c for c in all_characters if c.name in filter_character_names]
|
||||
|
||||
if not characters:
|
||||
return "暂无相关角色"
|
||||
return "暂无相关角色", None
|
||||
|
||||
# 构建精简的角色信息
|
||||
char_lines = []
|
||||
for c in characters[:10]:
|
||||
role_type = "主角" if c.role_type == "protagonist" else (
|
||||
"反派" if c.role_type == "antagonist" else "配角"
|
||||
# 限制最多10个角色
|
||||
characters = characters[:10]
|
||||
character_ids = [c.id for c in characters]
|
||||
|
||||
# === 批量查询关系数据 ===
|
||||
rels_result = await db.execute(
|
||||
select(CharacterRelationship).where(
|
||||
CharacterRelationship.project_id == project.id,
|
||||
or_(
|
||||
CharacterRelationship.character_from_id.in_(character_ids),
|
||||
CharacterRelationship.character_to_id.in_(character_ids)
|
||||
)
|
||||
)
|
||||
personality_brief = ""
|
||||
if c.personality:
|
||||
personality_brief = c.personality[:50]
|
||||
if len(c.personality) > 50:
|
||||
personality_brief += "..."
|
||||
char_lines.append(f"- {c.name}({role_type}): {personality_brief}")
|
||||
)
|
||||
all_rels = rels_result.scalars().all()
|
||||
|
||||
return "\n".join(char_lines)
|
||||
# 按角色ID分组关系
|
||||
char_rels_map: Dict[str, List] = {cid: [] for cid in character_ids}
|
||||
for r in all_rels:
|
||||
if r.character_from_id in char_rels_map:
|
||||
char_rels_map[r.character_from_id].append(r)
|
||||
if r.character_to_id in char_rels_map:
|
||||
char_rels_map[r.character_to_id].append(r)
|
||||
|
||||
# === 批量查询组织成员数据 ===
|
||||
non_org_ids = [c.id for c in characters if not c.is_organization]
|
||||
org_memberships_map: Dict[str, List] = {cid: [] for cid in non_org_ids}
|
||||
|
||||
if non_org_ids:
|
||||
member_result = await db.execute(
|
||||
select(OrganizationMember, Character.name).join(
|
||||
Organization, OrganizationMember.organization_id == Organization.id
|
||||
).join(
|
||||
Character, Organization.character_id == Character.id
|
||||
).where(OrganizationMember.character_id.in_(non_org_ids))
|
||||
)
|
||||
for m, org_name in member_result.all():
|
||||
if m.character_id in org_memberships_map:
|
||||
org_memberships_map[m.character_id].append((m, org_name))
|
||||
|
||||
# === 批量查询职业关联数据(CharacterCareer)===
|
||||
char_career_result = await db.execute(
|
||||
select(CharacterCareer).where(CharacterCareer.character_id.in_(character_ids))
|
||||
)
|
||||
all_char_careers = char_career_result.scalars().all()
|
||||
|
||||
# 收集所有职业ID
|
||||
career_ids = set()
|
||||
for cc in all_char_careers:
|
||||
career_ids.add(cc.career_id)
|
||||
# 也加入 main_career_id
|
||||
for c in characters:
|
||||
if not c.is_organization and c.main_career_id:
|
||||
career_ids.add(c.main_career_id)
|
||||
|
||||
careers_map: Dict[str, Career] = {}
|
||||
if career_ids:
|
||||
careers_result = await db.execute(
|
||||
select(Career).where(Career.id.in_(list(career_ids)))
|
||||
)
|
||||
careers_map = {c.id: c for c in careers_result.scalars().all()}
|
||||
|
||||
# 构建角色ID到职业关联的映射
|
||||
char_career_relations: Dict[str, Dict[str, List]] = {}
|
||||
for cc in all_char_careers:
|
||||
if cc.character_id not in char_career_relations:
|
||||
char_career_relations[cc.character_id] = {'main': [], 'sub': []}
|
||||
if cc.career_type == 'main':
|
||||
char_career_relations[cc.character_id]['main'].append(cc)
|
||||
else:
|
||||
char_career_relations[cc.character_id]['sub'].append(cc)
|
||||
|
||||
# === 查询组织角色的成员列表 ===
|
||||
org_chars = [c for c in characters if c.is_organization]
|
||||
org_members_map: Dict[str, List] = {}
|
||||
|
||||
if org_chars:
|
||||
org_char_ids = [c.id for c in org_chars]
|
||||
orgs_result = await db.execute(
|
||||
select(Organization).where(Organization.character_id.in_(org_char_ids))
|
||||
)
|
||||
orgs = orgs_result.scalars().all()
|
||||
|
||||
if orgs:
|
||||
org_id_to_char_id = {o.id: o.character_id for o in orgs}
|
||||
org_ids = [o.id for o in orgs]
|
||||
|
||||
members_result = await db.execute(
|
||||
select(OrganizationMember, Character.name).join(
|
||||
Character, OrganizationMember.character_id == Character.id
|
||||
).where(OrganizationMember.organization_id.in_(org_ids))
|
||||
)
|
||||
for m, member_name in members_result.all():
|
||||
char_id = org_id_to_char_id.get(m.organization_id)
|
||||
if char_id:
|
||||
if char_id not in org_members_map:
|
||||
org_members_map[char_id] = []
|
||||
org_members_map[char_id].append((m, member_name))
|
||||
|
||||
# === 构建完整版角色信息 ===
|
||||
characters_info_parts = []
|
||||
for c in characters:
|
||||
entity_type = '组织' if c.is_organization else '角色'
|
||||
role_type_map = {
|
||||
'protagonist': '主角',
|
||||
'antagonist': '反派',
|
||||
'supporting': '配角'
|
||||
}
|
||||
role_type = role_type_map.get(c.role_type, c.role_type or '配角')
|
||||
|
||||
info_lines = [f"【{c.name}】({entity_type}, {role_type})"]
|
||||
|
||||
# 详细属性
|
||||
if c.age:
|
||||
info_lines.append(f" 年龄: {c.age}")
|
||||
if c.gender:
|
||||
info_lines.append(f" 性别: {c.gender}")
|
||||
if c.appearance:
|
||||
appearance_preview = c.appearance[:100] if len(c.appearance) > 100 else c.appearance
|
||||
info_lines.append(f" 外貌: {appearance_preview}")
|
||||
if c.personality:
|
||||
personality_preview = c.personality[:100] if len(c.personality) > 100 else c.personality
|
||||
info_lines.append(f" 性格: {personality_preview}")
|
||||
if c.background:
|
||||
background_preview = c.background[:150] if len(c.background) > 150 else c.background
|
||||
info_lines.append(f" 背景: {background_preview}")
|
||||
|
||||
# 职业信息
|
||||
if c.id in char_career_relations:
|
||||
career_rel = char_career_relations[c.id]
|
||||
if career_rel['main']:
|
||||
for cc in career_rel['main']:
|
||||
career = careers_map.get(cc.career_id)
|
||||
if career:
|
||||
try:
|
||||
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
|
||||
stage_name = f'第{cc.current_stage}阶'
|
||||
for stage in (stages or []):
|
||||
if stage.get('level') == cc.current_stage:
|
||||
stage_name = stage.get('name', stage_name)
|
||||
break
|
||||
except (json.JSONDecodeError, AttributeError, TypeError):
|
||||
stage_name = f'第{cc.current_stage}阶'
|
||||
info_lines.append(f" 主职业: {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})")
|
||||
if career_rel['sub']:
|
||||
for cc in career_rel['sub']:
|
||||
career = careers_map.get(cc.career_id)
|
||||
if career:
|
||||
try:
|
||||
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
|
||||
stage_name = f'第{cc.current_stage}阶'
|
||||
for stage in (stages or []):
|
||||
if stage.get('level') == cc.current_stage:
|
||||
stage_name = stage.get('name', stage_name)
|
||||
break
|
||||
except (json.JSONDecodeError, AttributeError, TypeError):
|
||||
stage_name = f'第{cc.current_stage}阶'
|
||||
info_lines.append(f" 副职业: {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})")
|
||||
elif not c.is_organization and c.main_career_id:
|
||||
career = careers_map.get(c.main_career_id)
|
||||
if career:
|
||||
stage = c.main_career_stage or 1
|
||||
info_lines.append(f" 主职业: {career.name}(第{stage}阶段)")
|
||||
|
||||
# 角色关系
|
||||
if not c.is_organization and c.id in char_rels_map:
|
||||
rels = char_rels_map[c.id]
|
||||
if rels:
|
||||
rel_parts = []
|
||||
for r in rels:
|
||||
if r.character_from_id == c.id:
|
||||
target_name = all_char_map.get(r.character_to_id, "未知")
|
||||
else:
|
||||
target_name = all_char_map.get(r.character_from_id, "未知")
|
||||
rel_name = r.relationship_name or "相关"
|
||||
rel_parts.append(f"与{target_name}:{rel_name}")
|
||||
info_lines.append(f" 关系网络: {';'.join(rel_parts)}")
|
||||
|
||||
# 组织归属
|
||||
if not c.is_organization and c.id in org_memberships_map:
|
||||
memberships = org_memberships_map[c.id]
|
||||
if memberships:
|
||||
org_parts = [f"{org_name}({m.position})" for m, org_name in memberships[:2]]
|
||||
info_lines.append(f" 组织归属: {'、'.join(org_parts)}")
|
||||
|
||||
# 组织特有信息
|
||||
if c.is_organization:
|
||||
if c.organization_type:
|
||||
info_lines.append(f" 组织类型: {c.organization_type}")
|
||||
if c.organization_purpose:
|
||||
info_lines.append(f" 组织目的: {c.organization_purpose[:100]}")
|
||||
if c.id in org_members_map:
|
||||
members = org_members_map[c.id]
|
||||
if members:
|
||||
member_parts = [f"{name}({m.position})" for m, name in members[:5]]
|
||||
info_lines.append(f" 组织成员: {'、'.join(member_parts)}")
|
||||
|
||||
characters_info_parts.append("\n".join(info_lines))
|
||||
|
||||
characters_result_str = "\n\n".join(characters_info_parts)
|
||||
logger.info(f" ✅ [1-N完整版] 构建了 {len(characters_info_parts)} 个角色信息,总长度: {len(characters_result_str)} 字符")
|
||||
|
||||
# === 构建独立职业详情 ===
|
||||
careers_info_parts = []
|
||||
if careers_map:
|
||||
for career_id, career in careers_map.items():
|
||||
career_lines = [f"{career.name} ({career.type}职业)"]
|
||||
if career.description:
|
||||
career_lines.append(f" 描述: {career.description}")
|
||||
if career.category:
|
||||
career_lines.append(f" 分类: {career.category}")
|
||||
try:
|
||||
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
|
||||
if stages:
|
||||
career_lines.append(f" 阶段体系: (共{career.max_stage}阶)")
|
||||
for stage in stages:
|
||||
level = stage.get('level', '?')
|
||||
name = stage.get('name', '未命名')
|
||||
desc = stage.get('description', '')
|
||||
career_lines.append(f" {level}阶-{name}: {desc}")
|
||||
except (json.JSONDecodeError, AttributeError, TypeError):
|
||||
career_lines.append(f" 阶段体系: 共{career.max_stage}阶")
|
||||
if career.special_abilities:
|
||||
career_lines.append(f" 特殊能力: {career.special_abilities}")
|
||||
careers_info_parts.append("\n".join(career_lines))
|
||||
|
||||
careers_result_str = None
|
||||
if careers_info_parts:
|
||||
careers_result_str = "\n\n".join(careers_info_parts)
|
||||
logger.info(f" ✅ [1-N完整版] 构建了 {len(careers_map)} 个职业详情,总长度: {len(careers_result_str)} 字符")
|
||||
|
||||
return characters_result_str, careers_result_str
|
||||
|
||||
async def _build_recent_chapters_context(
|
||||
self,
|
||||
chapter: Chapter,
|
||||
project_id: str,
|
||||
db: AsyncSession
|
||||
) -> Optional[str]:
|
||||
"""构建最近10章的expansion_plan摘要"""
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Chapter.chapter_number, Chapter.title, Chapter.expansion_plan, Chapter.summary)
|
||||
.where(Chapter.project_id == project_id)
|
||||
.where(Chapter.chapter_number < chapter.chapter_number)
|
||||
.order_by(Chapter.chapter_number.desc())
|
||||
.limit(self.RECENT_CHAPTERS_COUNT)
|
||||
)
|
||||
recent_chapters = result.all()
|
||||
|
||||
if not recent_chapters:
|
||||
return None
|
||||
|
||||
# 按章节号正序排列
|
||||
recent_chapters = sorted(recent_chapters, key=lambda x: x[0])
|
||||
|
||||
lines = ["【最近章节规划】"]
|
||||
for ch_num, ch_title, expansion_plan, summary in recent_chapters:
|
||||
if expansion_plan:
|
||||
try:
|
||||
plan = json.loads(expansion_plan)
|
||||
plot_summary = plan.get('plot_summary', '')
|
||||
key_events = plan.get('key_events', [])
|
||||
events_str = ';'.join(key_events[:3]) if key_events else ''
|
||||
line = f"第{ch_num}章《{ch_title}》:{plot_summary}"
|
||||
if events_str:
|
||||
line += f"(关键事件:{events_str})"
|
||||
lines.append(line)
|
||||
except json.JSONDecodeError:
|
||||
if summary:
|
||||
lines.append(f"第{ch_num}章《{ch_title}》:{summary[:100]}")
|
||||
elif summary:
|
||||
lines.append(f"第{ch_num}章《{ch_title}》:{summary[:100]}")
|
||||
|
||||
if len(lines) <= 1:
|
||||
return None
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 构建最近章节上下文失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _get_relevant_memories_enhanced(
|
||||
self,
|
||||
user_id: str,
|
||||
project_id: str,
|
||||
chapter_number: int,
|
||||
chapter_outline: str,
|
||||
db: AsyncSession
|
||||
) -> Optional[str]:
|
||||
"""获取相关记忆(始终启用,相关度>0.6)"""
|
||||
if not self.memory_service:
|
||||
return None
|
||||
|
||||
try:
|
||||
query_text = chapter_outline[:500].replace('\n', ' ')
|
||||
|
||||
relevant_memories = await self.memory_service.search_memories(
|
||||
user_id=user_id,
|
||||
project_id=project_id,
|
||||
query=query_text,
|
||||
limit=15,
|
||||
min_importance=0.0
|
||||
)
|
||||
|
||||
# 过滤相关度>0.6
|
||||
filtered_memories = [
|
||||
mem for mem in relevant_memories
|
||||
if mem.get('similarity', 0) > self.MEMORY_SIMILARITY_THRESHOLD
|
||||
]
|
||||
|
||||
if not filtered_memories:
|
||||
return None
|
||||
|
||||
memory_lines = ["【相关记忆】"]
|
||||
for mem in filtered_memories[:self.MEMORY_COUNT]:
|
||||
similarity = mem.get('similarity', 0)
|
||||
content = mem.get('content', '')[:100]
|
||||
memory_lines.append(f"- (相关度:{similarity:.2f}) {content}")
|
||||
|
||||
return "\n".join(memory_lines) if len(memory_lines) > 1 else None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取相关记忆失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _get_last_ending_enhanced(
|
||||
self,
|
||||
@@ -379,11 +686,13 @@ class OneToManyContextBuilder:
|
||||
if chapter.chapter_number <= 1:
|
||||
return result_info
|
||||
|
||||
# 查询上一章
|
||||
# 查询上一章:不假设序号连续,取 chapter_number < 当前章 中最大的
|
||||
result = await db.execute(
|
||||
select(Chapter)
|
||||
.where(Chapter.project_id == chapter.project_id)
|
||||
.where(Chapter.chapter_number == chapter.chapter_number - 1)
|
||||
.where(Chapter.chapter_number < chapter.chapter_number)
|
||||
.order_by(Chapter.chapter_number.desc())
|
||||
.limit(1)
|
||||
)
|
||||
prev_chapter = result.scalar_one_or_none()
|
||||
|
||||
@@ -475,7 +784,12 @@ class OneToManyContextBuilder:
|
||||
chapter_outline: str,
|
||||
limit: int = 3
|
||||
) -> Optional[str]:
|
||||
"""获取与本章最相关的记忆"""
|
||||
"""
|
||||
获取与本章最相关的记忆
|
||||
|
||||
注意:伏笔相关信息统一由 _get_foreshadow_reminders() 通过 foreshadow_service 提供,
|
||||
此方法只负责获取故事记忆,不再从旧的 memory_service 获取伏笔信息。
|
||||
"""
|
||||
if not self.memory_service:
|
||||
return None
|
||||
|
||||
@@ -488,80 +802,33 @@ class OneToManyContextBuilder:
|
||||
min_importance=self.MEMORY_IMPORTANCE_THRESHOLD
|
||||
)
|
||||
|
||||
foreshadows = await self._get_due_foreshadows(
|
||||
user_id, project_id, chapter_number,
|
||||
lookahead=5
|
||||
)
|
||||
|
||||
return self._format_memories(relevant, foreshadows, max_length=500)
|
||||
return self._format_memories(relevant, max_length=500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取相关记忆失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _get_due_foreshadows(
|
||||
self,
|
||||
user_id: str,
|
||||
project_id: str,
|
||||
chapter_number: int,
|
||||
lookahead: int = 5
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取即将需要回收的伏笔"""
|
||||
if not self.memory_service:
|
||||
return []
|
||||
|
||||
try:
|
||||
foreshadows = await self.memory_service.find_unresolved_foreshadows(
|
||||
user_id, project_id, chapter_number
|
||||
)
|
||||
|
||||
due_foreshadows = []
|
||||
for fs in foreshadows:
|
||||
meta = fs.get('metadata', {})
|
||||
fs_chapter = meta.get('chapter_number', 0)
|
||||
if chapter_number - fs_chapter >= lookahead:
|
||||
due_foreshadows.append({
|
||||
'chapter': fs_chapter,
|
||||
'content': fs.get('content', '')[:60],
|
||||
'importance': meta.get('importance', 0.5)
|
||||
})
|
||||
|
||||
return due_foreshadows[:2]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取待回收伏笔失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def _format_memories(
|
||||
self,
|
||||
relevant: List[Dict[str, Any]],
|
||||
foreshadows: List[Dict[str, Any]],
|
||||
max_length: int = 500
|
||||
) -> str:
|
||||
"""格式化记忆为简洁文本"""
|
||||
lines = []
|
||||
"""格式化记忆为简洁文本(纯记忆,不含伏笔)"""
|
||||
if not relevant:
|
||||
return None
|
||||
|
||||
lines = ["【相关记忆】"]
|
||||
current_length = 0
|
||||
|
||||
if foreshadows:
|
||||
lines.append("【待回收伏笔】")
|
||||
for fs in foreshadows[:2]:
|
||||
text = f"- 第{fs['chapter']}章埋下:{fs['content']}"
|
||||
if current_length + len(text) > max_length:
|
||||
break
|
||||
lines.append(text)
|
||||
current_length += len(text)
|
||||
for mem in relevant:
|
||||
content = mem.get('content', '')[:80]
|
||||
text = f"- {content}"
|
||||
if current_length + len(text) > max_length:
|
||||
break
|
||||
lines.append(text)
|
||||
current_length += len(text)
|
||||
|
||||
if relevant and current_length < max_length:
|
||||
lines.append("【相关记忆】")
|
||||
for mem in relevant:
|
||||
content = mem.get('content', '')[:80]
|
||||
text = f"- {content}"
|
||||
if current_length + len(text) > max_length:
|
||||
break
|
||||
lines.append(text)
|
||||
current_length += len(text)
|
||||
|
||||
return "\n".join(lines) if lines else None
|
||||
return "\n".join(lines) if len(lines) > 1 else None
|
||||
|
||||
async def _get_foreshadow_reminders(
|
||||
self,
|
||||
@@ -764,12 +1031,15 @@ class OneToOneContextBuilder:
|
||||
logger.info(f" ✅ P0-大纲信息: {len(context.chapter_outline)}字符")
|
||||
|
||||
# === P1-重要信息 ===
|
||||
# 1. 获取上一章内容的最后500字
|
||||
# 1. 获取上一章内容的最后500字和上一章摘要
|
||||
if chapter_number > 1:
|
||||
# 查找前一章:不假设序号连续,取 chapter_number < 当前章 中最大的
|
||||
prev_chapter_result = await db.execute(
|
||||
select(Chapter)
|
||||
.where(Chapter.project_id == chapter.project_id)
|
||||
.where(Chapter.chapter_number == chapter_number - 1)
|
||||
.where(Chapter.chapter_number < chapter_number)
|
||||
.order_by(Chapter.chapter_number.desc())
|
||||
.limit(1)
|
||||
)
|
||||
prev_chapter = prev_chapter_result.scalar_one_or_none()
|
||||
|
||||
@@ -780,11 +1050,33 @@ class OneToOneContextBuilder:
|
||||
else:
|
||||
context.continuation_point = content[-500:]
|
||||
logger.info(f" ✅ P1-上一章内容(最后500字): {len(context.continuation_point)}字符")
|
||||
|
||||
# 获取上一章摘要(优先从记忆系统获取,其次使用章节摘要)
|
||||
summary_result = await db.execute(
|
||||
select(StoryMemory.content)
|
||||
.where(StoryMemory.project_id == chapter.project_id)
|
||||
.where(StoryMemory.chapter_id == prev_chapter.id)
|
||||
.where(StoryMemory.memory_type == 'chapter_summary')
|
||||
.limit(1)
|
||||
)
|
||||
summary_mem = summary_result.scalar_one_or_none()
|
||||
|
||||
if summary_mem:
|
||||
context.previous_chapter_summary = summary_mem[:300]
|
||||
logger.info(f" ✅ P1-上一章摘要(记忆): {len(context.previous_chapter_summary)}字符")
|
||||
elif prev_chapter.summary:
|
||||
context.previous_chapter_summary = prev_chapter.summary[:300]
|
||||
logger.info(f" ✅ P1-上一章摘要(章节): {len(context.previous_chapter_summary)}字符")
|
||||
else:
|
||||
context.previous_chapter_summary = None
|
||||
logger.info(f" ⚠️ P1-上一章摘要: 无")
|
||||
else:
|
||||
context.continuation_point = None
|
||||
context.previous_chapter_summary = None
|
||||
logger.info(f" ⚠️ P1-上一章内容: 无")
|
||||
else:
|
||||
context.continuation_point = None
|
||||
context.previous_chapter_summary = None
|
||||
logger.info(f" ✅ P1-第1章无需上一章内容")
|
||||
|
||||
# 2. 根据structure中的characters获取角色信息(含职业)
|
||||
@@ -792,7 +1084,12 @@ class OneToOneContextBuilder:
|
||||
if outline and outline.structure:
|
||||
try:
|
||||
structure = json.loads(outline.structure)
|
||||
character_names = structure.get('characters', [])
|
||||
raw_characters = structure.get('characters', [])
|
||||
# characters可能是字符串列表或字典列表,统一提取为名称字符串列表
|
||||
character_names = [
|
||||
c['name'] if isinstance(c, dict) else c
|
||||
for c in raw_characters
|
||||
]
|
||||
logger.info(f" 📋 从structure提取角色: {character_names}")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -853,7 +1150,7 @@ class OneToOneContextBuilder:
|
||||
min_importance=0.0
|
||||
)
|
||||
|
||||
# 降低相关度阈值到0.4,提高召回率
|
||||
# 过滤相关度阈值为0.6
|
||||
filtered_memories = [
|
||||
mem for mem in relevant_memories
|
||||
if mem.get('similarity', 0) > 0.6
|
||||
@@ -867,7 +1164,7 @@ class OneToOneContextBuilder:
|
||||
memory_lines.append(f"- (相关度:{similarity:.2f}) {content}")
|
||||
|
||||
context.relevant_memories = "\n".join(memory_lines)
|
||||
logger.info(f" ✅ P2-相关记忆: {len(filtered_memories)}条 (相关度>0.4, 共搜索{len(relevant_memories)}条)")
|
||||
logger.info(f" ✅ P2-相关记忆: {len(filtered_memories)}条 (相关度>0.6, 共搜索{len(relevant_memories)}条)")
|
||||
else:
|
||||
context.relevant_memories = None
|
||||
logger.info(f" ⚠️ P2-相关记忆: 无符合条件的记忆 (共搜索到{len(relevant_memories)}条)")
|
||||
@@ -885,6 +1182,7 @@ class OneToOneContextBuilder:
|
||||
"chapter_number": chapter_number,
|
||||
"has_previous_content": context.continuation_point is not None,
|
||||
"previous_content_length": len(context.continuation_point or ""),
|
||||
"previous_summary_length": len(context.previous_chapter_summary or ""),
|
||||
"outline_length": len(context.chapter_outline),
|
||||
"characters_length": len(context.chapter_characters),
|
||||
"careers_length": len(context.chapter_careers or ""),
|
||||
@@ -1093,14 +1391,61 @@ class OneToOneContextBuilder:
|
||||
# 副职业也只显示引用
|
||||
info_lines.append(f" - {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})")
|
||||
|
||||
# === 角色关系信息 ===
|
||||
if not c.is_organization:
|
||||
from sqlalchemy import or_
|
||||
rels_result = await db.execute(
|
||||
select(CharacterRelationship).where(
|
||||
CharacterRelationship.project_id == project_id,
|
||||
or_(
|
||||
CharacterRelationship.character_from_id == c.id,
|
||||
CharacterRelationship.character_to_id == c.id
|
||||
)
|
||||
)
|
||||
)
|
||||
rels = rels_result.scalars().all()
|
||||
if rels:
|
||||
related_ids = set()
|
||||
for r in rels:
|
||||
related_ids.add(r.character_from_id)
|
||||
related_ids.add(r.character_to_id)
|
||||
related_ids.discard(c.id)
|
||||
if related_ids:
|
||||
names_result = await db.execute(
|
||||
select(Character.id, Character.name).where(Character.id.in_(related_ids))
|
||||
)
|
||||
name_map = {row.id: row.name for row in names_result}
|
||||
rel_parts = []
|
||||
for r in rels:
|
||||
if r.character_from_id == c.id:
|
||||
target_name = name_map.get(r.character_to_id, "未知")
|
||||
else:
|
||||
target_name = name_map.get(r.character_from_id, "未知")
|
||||
rel_name = r.relationship_name or "相关"
|
||||
rel_parts.append(f"与{target_name}:{rel_name}")
|
||||
info_lines.append(f" 关系网络: {';'.join(rel_parts)}")
|
||||
|
||||
# === 组织特有信息 ===
|
||||
if c.is_organization:
|
||||
if c.organization_type:
|
||||
info_lines.append(f" 组织类型: {c.organization_type}")
|
||||
if c.organization_purpose:
|
||||
info_lines.append(f" 组织目的: {c.organization_purpose[:100]}")
|
||||
if c.organization_members:
|
||||
info_lines.append(f" 组织成员: {c.organization_members[:100]}")
|
||||
# 从 OrganizationMember 表动态查询组织成员
|
||||
org_result = await db.execute(
|
||||
select(Organization).where(Organization.character_id == c.id)
|
||||
)
|
||||
org = org_result.scalar_one_or_none()
|
||||
if org:
|
||||
members_result = await db.execute(
|
||||
select(OrganizationMember, Character.name).join(
|
||||
Character, OrganizationMember.character_id == Character.id
|
||||
).where(OrganizationMember.organization_id == org.id)
|
||||
)
|
||||
members = members_result.all()
|
||||
if members:
|
||||
member_parts = [f"{name}({m.position})" for m, name in members]
|
||||
info_lines.append(f" 组织成员: {'、'.join(member_parts)[:100]}")
|
||||
|
||||
# 组合完整信息
|
||||
full_info = "\n".join(info_lines)
|
||||
|
||||
Reference in New Issue
Block a user