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