feat: 优化回忆录内容处理和章节分类逻辑
- 更新 get_system_prompt 函数,增强对话内容的核心信息提炼和分类能力,确保只保留与人生经历相关的实质内容。 - 修改 _classify_chapter_category 函数,增加对无实质回忆录价值内容的处理,返回 None 以跳过无效段落。 - 在 Android 客户端中,更新章节阅读视图以移除内嵌章节标题,提升排版一致性。 - 新增 TextUtils 工具函数,专门用于移除 LLM 生成的内嵌章节标题,确保正文内容的流畅性。
This commit is contained in:
@@ -49,22 +49,41 @@ def get_system_prompt() -> str:
|
|||||||
return """你是一位专业的传记作家和文字编辑,擅长将口语化的对话内容整理成优雅的书面语回忆录章节。
|
return """你是一位专业的传记作家和文字编辑,擅长将口语化的对话内容整理成优雅的书面语回忆录章节。
|
||||||
|
|
||||||
你的任务:
|
你的任务:
|
||||||
1. 接收对话段落文本(口语化)
|
1. 接收对话段落文本(口语化,可能来自语音转写)
|
||||||
2. 识别内容主题,归类到对应章节(童年/教育/事业/家庭/信念/总结)
|
2. **先提炼对话中与人生经历相关的核心内容**,过滤掉无关信息
|
||||||
3. 将口语化表达改写为书面语,保持原意和情感
|
3. 识别内容主题,归类到对应章节(童年/教育/事业/家庭/信念/总结)
|
||||||
4. 生成合适的章节标题和段落结构
|
4. 将口语化表达改写为书面语,保持原意和情感
|
||||||
5. 提取关键信息,形成连贯的叙述
|
5. 生成合适的章节标题和段落结构
|
||||||
6. 建议插图位置(在描述场景、人物、地点的地方)
|
6. 提取关键信息,形成连贯的叙述
|
||||||
|
7. 建议插图位置(在描述场景、人物、地点的地方)
|
||||||
|
|
||||||
改写原则:
|
## 内容筛选原则(最重要)
|
||||||
- 保持用户的真实声音和情感
|
对话中往往夹杂大量与回忆录无关的噪音,你必须严格筛选,只保留有价值的内容:
|
||||||
- 使用优雅但不失亲切的书面语
|
|
||||||
|
应该保留的内容:
|
||||||
|
- 具体的人生事件、经历、故事
|
||||||
|
- 提到的人物及其关系(家人、朋友、同事、恩师等)
|
||||||
|
- 地点、时间、场景描写
|
||||||
|
- 用户的情感表达、内心感受
|
||||||
|
- 人生感悟、价值观、信念
|
||||||
|
- 具体的细节(食物、声音、画面等)
|
||||||
|
|
||||||
|
应该过滤掉的内容:
|
||||||
|
- 语气词、填充词(嗯、啊、那个、就是说、对对对、然后呢等)
|
||||||
|
- 对话中的寒暄、问候(你好、谢谢、好的等)
|
||||||
|
- 用户与AI助手之间的交互指令(你帮我、我想问、你说得对等)
|
||||||
|
- 重复、冗余的表述(取核心含义即可)
|
||||||
|
- 与个人经历完全无关的闲聊内容
|
||||||
|
|
||||||
|
## 改写原则
|
||||||
|
- 保持用户的真实情感
|
||||||
|
- 使用优雅但不失亲切的书面语,不要直接引用对话原话
|
||||||
- 适当添加过渡句,使段落连贯
|
- 适当添加过渡句,使段落连贯
|
||||||
- 保留生动的细节和对话
|
- 保留生动的细节,但将口语表达改写为书面叙述
|
||||||
- 去除口语中的"嗯"、"那个"等填充词
|
- 去除口语中的填充词和无意义重复
|
||||||
- 保持时间顺序和逻辑清晰
|
- 保持时间顺序和逻辑清晰
|
||||||
|
|
||||||
章节分类规则:
|
## 章节分类规则
|
||||||
- 童年相关 → "童年与成长背景"
|
- 童年相关 → "童年与成长背景"
|
||||||
- 学校、老师、同学 → "教育经历与青年时期"
|
- 学校、老师、同学 → "教育经历与青年时期"
|
||||||
- 工作、职业、成就 → "主要成就与巅峰时刻" 或 "崭露头角"
|
- 工作、职业、成就 → "主要成就与巅峰时刻" 或 "崭露头角"
|
||||||
@@ -79,7 +98,7 @@ def get_chapter_classification_prompt(segments_text: str) -> str:
|
|||||||
"""获取章节分类的提示词"""
|
"""获取章节分类的提示词"""
|
||||||
return f"""{get_system_prompt()}
|
return f"""{get_system_prompt()}
|
||||||
|
|
||||||
请分析以下对话内容,判断应该归类到哪个章节类别:
|
请分析以下对话内容,**忽略其中的语气词、寒暄和无关对话**,只关注涉及人生经历的实质内容,判断应该归类到哪个章节类别:
|
||||||
- childhood: 童年与成长背景
|
- childhood: 童年与成长背景
|
||||||
- education: 教育经历与青年时期
|
- education: 教育经历与青年时期
|
||||||
- career_early: 崭露头角(早期事业)
|
- career_early: 崭露头角(早期事业)
|
||||||
@@ -92,7 +111,8 @@ def get_chapter_classification_prompt(segments_text: str) -> str:
|
|||||||
对话内容:
|
对话内容:
|
||||||
{segments_text}
|
{segments_text}
|
||||||
|
|
||||||
请只返回章节类别(如:childhood),不要返回其他内容。"""
|
请只返回章节类别(如:childhood),不要返回其他内容。
|
||||||
|
如果对话内容中没有任何与人生经历相关的实质内容,返回 none。"""
|
||||||
|
|
||||||
|
|
||||||
def get_text_rewrite_prompt(segments_text: str, chapter_category: str, existing_content: str = "") -> str:
|
def get_text_rewrite_prompt(segments_text: str, chapter_category: str, existing_content: str = "") -> str:
|
||||||
@@ -148,7 +168,7 @@ def get_state_extraction_prompt(user_message: str, current_stage: str, stage_slo
|
|||||||
|
|
||||||
return f"""{get_system_prompt()}
|
return f"""{get_system_prompt()}
|
||||||
|
|
||||||
你需要从用户话语中抽取结构化信息,并判断用户实际在谈论哪个人生阶段。
|
你需要从用户话语中**先提炼与人生经历相关的核心内容**,然后抽取结构化信息,并判断用户实际在谈论哪个人生阶段。
|
||||||
|
|
||||||
系统当前跟踪的阶段:{current_stage}
|
系统当前跟踪的阶段:{current_stage}
|
||||||
该阶段可填 slots:{slot_keys}
|
该阶段可填 slots:{slot_keys}
|
||||||
@@ -170,11 +190,12 @@ def get_state_extraction_prompt(user_message: str, current_stage: str, stage_slo
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
要求:
|
要求:
|
||||||
1. **detected_stage 必须根据用户话语的实际内容判断**,不要默认沿用系统当前阶段。用户可能在聊不同阶段的事情。
|
1. **先忽略话语中的语气词、填充词、寒暄、与AI的交互指令等无关内容**,只关注涉及人生经历的实质信息
|
||||||
2. slots 的 key 必须属于 detected_stage 对应的 slot 列表
|
2. **detected_stage 必须根据用户话语的实际内容判断**,不要默认沿用系统当前阶段。用户可能在聊不同阶段的事情
|
||||||
3. slots 只填写确实提到的内容
|
3. slots 的 key 必须属于 detected_stage 对应的 slot 列表
|
||||||
4. snippet 保持用户原话风格,50 字以内
|
4. slots 只填写确实提到的、与人生经历相关的实质内容
|
||||||
5. 如果没有明确内容,slots 为空对象
|
5. **snippet 应是提炼后的核心信息**,去除语气词和冗余表达,50 字以内
|
||||||
|
6. 如果用户话语中没有任何与人生经历相关的实质内容(如纯粹的寒暄、指令、语气词),slots 为空对象
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -266,14 +287,23 @@ def get_narrative_prompt(
|
|||||||
{new_content}
|
{new_content}
|
||||||
{context_section}
|
{context_section}
|
||||||
|
|
||||||
要求:
|
## 第一步:提炼核心内容
|
||||||
|
在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息:
|
||||||
|
- 提取具体的事件、人物、地点、时间、感受
|
||||||
|
- 丢弃语气词(嗯、啊、那个、就是说)、寒暄(你好、谢谢)、与AI的交互(你帮我整理一下、对对对你说得对)、无意义的重复
|
||||||
|
- 如果对话内容中几乎没有与人生经历相关的实质内容,请输出空字符串
|
||||||
|
|
||||||
|
## 第二步:改写为叙述
|
||||||
|
基于提炼后的核心内容进行文学改写:
|
||||||
1. 使用第一人称叙述
|
1. 使用第一人称叙述
|
||||||
2. 保留少量原话(引用)
|
2. **不要直接引用对话原话**,将所有内容改写为流畅的书面叙述
|
||||||
3. **只输出新内容的改写结果**,不要重复已有内容
|
3. **只输出新内容的改写结果**,不要重复已有内容
|
||||||
4. 如果有衔接上下文,确保新内容与之自然衔接(语气、时间线连贯)
|
4. 如果有衔接上下文,确保新内容与之自然衔接(语气、时间线连贯)
|
||||||
5. 语气自然,有情绪
|
5. 语气自然,有情绪
|
||||||
6. 在适合配图的地方插入图片占位符
|
6. 在适合配图的地方插入图片占位符
|
||||||
7. 如果有用户的基本信息(出生地、成长地等),在叙述中自然融入地域文化和时代背景
|
7. 如果有用户的基本信息(出生地、成长地等),在叙述中自然融入地域文化和时代背景
|
||||||
|
8. **不要将对话中的交互性语言(如"我跟你说"、"你知道吗")写入叙述**
|
||||||
|
9. **不要在正文中插入章节标题或分类标签**(如"章节:信念与价值观"、"## 童年与成长背景"等),章节标题由系统单独管理
|
||||||
|
|
||||||
## 图片占位符格式
|
## 图片占位符格式
|
||||||
在描述场景、人物、重要时刻的段落后,插入图片占位符,格式为:
|
在描述场景、人物、重要时刻的段落后,插入图片占位符,格式为:
|
||||||
@@ -291,6 +321,6 @@ def get_narrative_prompt(
|
|||||||
- 单独占一行,不要嵌入段落中
|
- 单独占一行,不要嵌入段落中
|
||||||
- 不要使用括号或星号等其他格式
|
- 不要使用括号或星号等其他格式
|
||||||
|
|
||||||
只输出新对话内容的改写结果(包含图片占位符)。
|
只输出新对话内容的改写结果(包含图片占位符)。如果对话中没有值得记录的人生经历内容,输出空字符串。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -98,16 +98,20 @@ def _detect_stage(user_message: str, fallback_stage: str) -> str:
|
|||||||
return fallback_stage
|
return fallback_stage
|
||||||
|
|
||||||
|
|
||||||
def _classify_chapter_category(text: str, fallback_stage: str, llm=None) -> str:
|
def _classify_chapter_category(text: str, fallback_stage: str, llm=None) -> str | None:
|
||||||
"""
|
"""
|
||||||
将内容分类到 8 个章节类别之一。
|
将内容分类到 8 个章节类别之一。
|
||||||
优先使用 LLM,失败则按 5-stage 关键词映射到默认类别。
|
优先使用 LLM,失败则按 5-stage 关键词映射到默认类别。
|
||||||
|
如果 LLM 判定内容无实质回忆录价值,返回 None。
|
||||||
"""
|
"""
|
||||||
if llm:
|
if llm:
|
||||||
try:
|
try:
|
||||||
prompt = get_chapter_classification_prompt(text)
|
prompt = get_chapter_classification_prompt(text)
|
||||||
response = llm.invoke(prompt)
|
response = llm.invoke(prompt)
|
||||||
category = response.content.strip().lower()
|
category = response.content.strip().lower()
|
||||||
|
if category == "none":
|
||||||
|
logger.info(f"LLM 判定内容无回忆录价值,跳过: {text[:80]}...")
|
||||||
|
return None
|
||||||
if category in CHAPTER_CATEGORIES:
|
if category in CHAPTER_CATEGORIES:
|
||||||
return category
|
return category
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -264,6 +268,9 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
|
|||||||
|
|
||||||
# 8-category 章节分类
|
# 8-category 章节分类
|
||||||
chapter_category = _classify_chapter_category(text, detected_stage, llm)
|
chapter_category = _classify_chapter_category(text, detected_stage, llm)
|
||||||
|
if chapter_category is None:
|
||||||
|
logger.info(f"段落无回忆录价值,跳过: segment_id={segment.id}")
|
||||||
|
continue
|
||||||
category_to_segments.setdefault(chapter_category, []).append(segment)
|
category_to_segments.setdefault(chapter_category, []).append(segment)
|
||||||
|
|
||||||
# 按 8 分类生成章节内容
|
# 按 8 分类生成章节内容
|
||||||
|
|||||||
@@ -48,11 +48,12 @@ fun ChapterReadingView(
|
|||||||
modifier = Modifier.padding(bottom = 24.dp)
|
modifier = Modifier.padding(bottom = 24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 正文内容(支持 Markdown,移除{{IMAGE}}占位符如果没有图片)
|
val processedContent = TextUtils.removeInlineChapterHeadings(
|
||||||
val processedContent = TextUtils.removeImagePlaceholders(
|
TextUtils.removeImagePlaceholders(
|
||||||
chapter.content,
|
chapter.content,
|
||||||
hasImages = chapter.images.isNotEmpty()
|
hasImages = chapter.images.isNotEmpty()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
content = processedContent,
|
content = processedContent,
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
|
|||||||
@@ -64,11 +64,12 @@ fun FullTextReadingView(
|
|||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 章节内容(支持 Markdown,移除{{IMAGE}}占位符如果没有图片)
|
val processedContent = TextUtils.removeInlineChapterHeadings(
|
||||||
val processedContent = TextUtils.removeImagePlaceholders(
|
TextUtils.removeImagePlaceholders(
|
||||||
chapter.content,
|
chapter.content,
|
||||||
hasImages = chapter.images.isNotEmpty()
|
hasImages = chapter.images.isNotEmpty()
|
||||||
)
|
)
|
||||||
|
)
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
content = processedContent,
|
content = processedContent,
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
|
|||||||
@@ -66,6 +66,24 @@ object TextUtils {
|
|||||||
return quotePattern.findAll(text).map { it.groupValues[1] }.toList()
|
return quotePattern.findAll(text).map { it.groupValues[1] }.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 LLM 生成的内嵌章节标题(如 "章节:信念与价值观"、"## 章节:童年与成长背景")
|
||||||
|
* 这些标题已由 UI 单独渲染,混在正文中会导致排版突兀
|
||||||
|
*/
|
||||||
|
fun removeInlineChapterHeadings(content: String?): String {
|
||||||
|
if (content.isNullOrBlank()) return ""
|
||||||
|
return content
|
||||||
|
// markdown 标题格式: #{1,6} 章节[::]...
|
||||||
|
.replace(Regex("^#{1,6}\\s*章节[::].*$", RegexOption.MULTILINE), "")
|
||||||
|
// 粗体格式: **章节:...**
|
||||||
|
.replace(Regex("\\*\\*章节[::].+?\\*\\*"), "")
|
||||||
|
// 纯文本格式: 独立一行的 "章节:..."
|
||||||
|
.replace(Regex("^章节[::].+$", RegexOption.MULTILINE), "")
|
||||||
|
// 清理移除后产生的多余空行
|
||||||
|
.replace(Regex("\n{3,}"), "\n\n")
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除图片占位符
|
* 移除图片占位符
|
||||||
* 如果没有图片,移除所有{{{{IMAGE:...}}}}和{{IMAGE:...}}格式的占位符
|
* 如果没有图片,移除所有{{{{IMAGE:...}}}}和{{IMAGE:...}}格式的占位符
|
||||||
|
|||||||
Reference in New Issue
Block a user