From 8b4a05864020abcf830c80d8a1957a9e89603fea Mon Sep 17 00:00:00 2001 From: penghanyuan Date: Mon, 2 Mar 2026 19:47:32 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=9B=9E=E5=BF=86?= =?UTF-8?q?=E5=BD=95=E5=86=85=E5=AE=B9=E5=A4=84=E7=90=86=E5=92=8C=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E5=88=86=E7=B1=BB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 get_system_prompt 函数,增强对话内容的核心信息提炼和分类能力,确保只保留与人生经历相关的实质内容。 - 修改 _classify_chapter_category 函数,增加对无实质回忆录价值内容的处理,返回 None 以跳过无效段落。 - 在 Android 客户端中,更新章节阅读视图以移除内嵌章节标题,提升排版一致性。 - 新增 TextUtils 工具函数,专门用于移除 LLM 生成的内嵌章节标题,确保正文内容的流畅性。 --- api/agents/prompts/memory_prompts.py | 76 +++++++++++++------ api/tasks/memoir_tasks.py | 9 ++- .../components/memoir/ChapterReadingView.kt | 9 ++- .../components/memoir/FullTextReadingView.kt | 9 ++- .../com/huaga/life_echo/utils/TextUtils.kt | 18 +++++ 5 files changed, 89 insertions(+), 32 deletions(-) diff --git a/api/agents/prompts/memory_prompts.py b/api/agents/prompts/memory_prompts.py index a2b7e90..d80c0c9 100644 --- a/api/agents/prompts/memory_prompts.py +++ b/api/agents/prompts/memory_prompts.py @@ -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( - 单独占一行,不要嵌入段落中 - 不要使用括号或星号等其他格式 -只输出新对话内容的改写结果(包含图片占位符)。 +只输出新对话内容的改写结果(包含图片占位符)。如果对话中没有值得记录的人生经历内容,输出空字符串。 """ diff --git a/api/tasks/memoir_tasks.py b/api/tasks/memoir_tasks.py index c2de954..e1c5b58 100644 --- a/api/tasks/memoir_tasks.py +++ b/api/tasks/memoir_tasks.py @@ -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 分类生成章节内容 diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterReadingView.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterReadingView.kt index 99f470c..9b7add2 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterReadingView.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterReadingView.kt @@ -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, diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/FullTextReadingView.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/FullTextReadingView.kt index 26b3ea0..50ea0b8 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/FullTextReadingView.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/FullTextReadingView.kt @@ -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, diff --git a/app-android/app/src/main/java/com/huaga/life_echo/utils/TextUtils.kt b/app-android/app/src/main/java/com/huaga/life_echo/utils/TextUtils.kt index 812b1d8..76f2435 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/utils/TextUtils.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/utils/TextUtils.kt @@ -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:...}}格式的占位符