From fcffe7d35ab6c6081ce29d5229ddb290c6a85bb4 Mon Sep 17 00:00:00 2001 From: SXP-Simon Date: Mon, 23 Mar 2026 13:38:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(quality=5Fsummary):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=A2=9E=E9=87=8F=E5=88=86=E6=9E=90=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _conf_schema.json | 11 +- .../services/analysis_application_service.py | 63 +++++++- src/domain/entities/incremental_state.py | 3 + .../repositories/analysis_repository.py | 10 ++ .../services/incremental_merge_service.py | 6 +- .../analyzers/chat_quality_analyzer.py | 147 ++++++++++++++++-- src/infrastructure/analysis/llm_analyzer.py | 13 ++ src/infrastructure/config/config_manager.py | 26 +++- 8 files changed, 252 insertions(+), 27 deletions(-) diff --git a/_conf_schema.json b/_conf_schema.json index 0c0915a..1aa564c 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -435,12 +435,19 @@ "type": "object", "hint": "聊天质量分析提示词模板,可自定义修改", "items": { - "quality_prompt": { + "quality_v2_prompt": { "description": "默认聊天质量分析提示词", "type": "text", "editor_mode": true, "editor_language": "markdown", - "default": "你是一个毒舌且幽默的群聊质量分析师,请生成聊天质量锐评内容。你需要按照下面的格式返回结果。\n\n分析接下来的群聊内容,给出多个维度的分析(3-6个,所有维度的占比加起来小于等于100%)和汇总内容。\n\n群聊记录:\n{messages_text}\n\n### 返回格式示例:\n\n```json\n{{\n \"title\": \"群聊质量分析报告\",\n \"subtitle\": \"今天的群里发生了什么?\",\n \"dimensions\": [\n {{\n \"name\": \"水群闲聊\",\n \"percentage\": 50.0,\n \"comment\": \"群友们今天也在努力划水呢。\"\n }}\n ],\n \"summary\": \"今天也是元气满满的一天。\"\n}}\n```" + "default": "你是一个毒舌且幽默的群聊质量分析师。\n请分析以下群聊记录,输出一份\"聊天质量锐评\"。\n\n## 任务目标:\n1. **维度划分**:将聊天内容划分为 3-6 个【高层级、抽象、泛化】的维度(例如:就业焦虑、生涯规划、技术方案研究、情感树洞、无意义水群等)。\n2. **严禁在维度名称(name)中出现任何具体的群聊人物名、项目名、具体的报错内容或细碎的事件点。标题必须保持高度抽象且字数简练(2-6个字)。**\n3. 为每个维度计算一个大致的百分比占位(总和小于等于 100%)。\n4. **点评内容**:为每个维度写一句犀利、幽默、毒舌或温情的点评。具体的吐槽内容、具体的细节事件描述请放在这里。\n5. **全群表现**:给出一句总结性的评价,作为总结标题对应的“金句”。\n6. **主题设定**:设定一个本次报告的主题标题和副标题。\n\n## 点评风格指南:\n- 语言要接地气,多用互联网黑话。吐槽要精准,避重就轻。\n- **只有维度名称(name)需要抽象,点评(comment)和总结(summary)可以非常具体和生动。**\n\n## 返回格式要求:\n必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。\n\n```json\n{{\n \"title\": \"今日群聊主题\",\n \"subtitle\": \"副标题\",\n \"dimensions\": [\n {{\n \"name\": \"抽象维度名\",\n \"percentage\": 比例,\n \"comment\": \"维度的毒舌点评\"\n }}\n ],\n \"summary\": \"一句总结性的金句\"\n}}\n```\n\n群聊记录:\n{messages_text}" + }, + "quality_summary_prompt": { + "description": "增量汇总聊天质量分析提示词", + "type": "text", + "editor_mode": true, + "editor_language": "markdown", + "default": "你是一个毒舌且幽默的群聊质量分析师。\n你现在有一份今天全天分散时间段的多个“增量批次点评笔记”。\n你的任务是将这些分散的笔记汇总成一份最终的“全天聊天质量终极锐评”。\n\n## 任务目标:\n1. **全局抽象维度**:根据各批次的维度表现,平衡权重,提取出 3-6 个覆盖全天的【核心、上层抽象】课题维度(如:职场/行业风向、技术架构演进、社畜心理博弈等)。\n2. **严禁在维度名称(name)中出现具体的批次细节。标题必须代表全天的某种趋势。**\n3. **百分比融合**:根据全天笔记的频率和强度,给出一个代表全天整体分布的比例(总和不超过100%)。\n4. **终极点评**:为每个汇总维度写出一句升华后的全天总结性点评。可以融合具体批次中的有趣槽点。\n5. **终极总结**:拟定全天的大型主题标题、副标题,并给出一句霸气的全天表现总结。\n\n## 风格要求:\n- 只有维度名称(name)需要高度概括抽象。\n- 点评(comment)和总结(summary)请尽量生动、毒舌、具体,要把一整天的梗串联起来。\n\n## 返回格式要求:\n必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。\n\n```json\n{{\n \"title\": \"今日群聊主题\",\n \"subtitle\": \"副标题\",\n \"dimensions\": [\n {{\n \"name\": \"抽象大类标题\",\n \"percentage\": 比例,\n \"comment\": \"维度的全天锐评\"\n }}\n ],\n \"summary\": \"全天总结金句\"\n}}\n```\n\n增量汇总数据:\n{reviews_text}" } } } diff --git a/src/application/services/analysis_application_service.py b/src/application/services/analysis_application_service.py index e26df81..4000ee7 100644 --- a/src/application/services/analysis_application_service.py +++ b/src/application/services/analysis_application_service.py @@ -583,21 +583,18 @@ class AnalysisApplicationService: if not adapter: raise ValueError(f"未找到平台 {platform_id} 的适配器") - # 6. 执行用户称号 LLM 分析 + # 6. 执行分析相关的变量准备 user_titles = [] user_title_enabled = self.config_manager.get_user_title_analysis_enabled() + unified_msg_origin = ( + f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id + ) if user_title_enabled and state.user_activities: max_user_titles = self.config_manager.get_max_user_titles() # 从合并后的 user_activities 中取出 top 用户 top_users = state.get_user_activity_ranking(max_user_titles) - unified_msg_origin = ( - f"{platform_id}:GroupMessage:{group_id}" - if platform_id - else group_id - ) - try: async with self.llm_semaphore: logger.debug(f"[LLM] 已进入称号分析队列 (群: {group_id})") @@ -628,6 +625,58 @@ class AnalysisApplicationService: except Exception as e: logger.error(f"增量最终报告用户称号分析失败: {e}", exc_info=True) + # 6.5 执行聊天质量汇总分析 (如果有多个批次的质量报告) + if ( + self.config_manager.get_chat_quality_analysis_enabled() + and state.all_quality_reviews + ): + try: + async with self.llm_semaphore: + logger.debug( + f"[LLM] 已进入聊天质量汇总分析队列 (群: {group_id})" + ) + ( + summarized_review, + quality_token_usage, + ) = await self.llm_analyzer.summarize_quality_reviews( + batch_reviews=state.all_quality_reviews, + umo=unified_msg_origin, + ) + if summarized_review: + # 更新 state 中的 review 为汇总后的结果 + # 这里我们需要将 QualityReview 对象存回 dict 或直接在后续处理中使用 + # build_analysis_result 会使用 state.chat_quality_review + state.chat_quality_review = { + "title": summarized_review.title, + "subtitle": summarized_review.subtitle, + "dimensions": [ + { + "name": d.name, + "percentage": d.percentage, + "comment": d.comment, + "color": d.color, + } + for d in summarized_review.dimensions + ], + "summary": summarized_review.summary, + } + + # 累加 Token + state.total_token_usage["prompt_tokens"] = ( + state.total_token_usage.get("prompt_tokens", 0) + + quality_token_usage.prompt_tokens + ) + state.total_token_usage["completion_tokens"] = ( + state.total_token_usage.get("completion_tokens", 0) + + quality_token_usage.completion_tokens + ) + state.total_token_usage["total_tokens"] = ( + state.total_token_usage.get("total_tokens", 0) + + quality_token_usage.total_tokens + ) + except Exception as e: + logger.error(f"增量最终报告聊天质量汇总失败: {e}", exc_info=True) + # 7. 构建 analysis_result analysis_result = self.incremental_merge_service.build_analysis_result( state, user_titles diff --git a/src/domain/entities/incremental_state.py b/src/domain/entities/incremental_state.py index ab9833b..5d3c424 100644 --- a/src/domain/entities/incremental_state.py +++ b/src/domain/entities/incremental_state.py @@ -175,6 +175,9 @@ class IncrementalState: topics: list[dict] = field(default_factory=list) golden_quotes: list[dict] = field(default_factory=list) chat_quality_review: dict[str, Any] | None = None + all_quality_reviews: list[dict] = field( + default_factory=list + ) # 存储所有批次的质量锐评,用于最终报告时的汇总分析 # 合并后的统计数据(按小时) hourly_message_counts: dict[str, int] = field(default_factory=dict) diff --git a/src/domain/repositories/analysis_repository.py b/src/domain/repositories/analysis_repository.py index f3ac3d5..d6100ce 100644 --- a/src/domain/repositories/analysis_repository.py +++ b/src/domain/repositories/analysis_repository.py @@ -85,3 +85,13 @@ class IAnalysisProvider(ABC): ) -> tuple[list[SummaryTopic], list[GoldenQuote], TokenUsage, QualityReview | None]: """增量模式并发分析""" pass + + @abstractmethod + async def summarize_quality_reviews( + self, + batch_reviews: list[dict], + umo: str | None = None, + session_id: str | None = None, + ) -> tuple[QualityReview | None, TokenUsage]: + """汇总多个聊天质量报告(增量模式使用)""" + pass diff --git a/src/domain/services/incremental_merge_service.py b/src/domain/services/incremental_merge_service.py index 57851d3..1ffbdbf 100644 --- a/src/domain/services/incremental_merge_service.py +++ b/src/domain/services/incremental_merge_service.py @@ -174,10 +174,14 @@ class IncrementalMergeService: # 合并参与者 ID(取并集) state.all_participant_ids.update(batch.participant_ids) + # 收集所有批次的质量锐评(用于最终汇总) + if batch.chat_quality_review: + state.all_quality_reviews.append(batch.chat_quality_review) + # 记录最后分析消息时间戳(取最大值) if batch.last_message_timestamp > state.last_analyzed_message_timestamp: state.last_analyzed_message_timestamp = batch.last_message_timestamp - # 更新锐评为最新批次的 (如果有) + # 更新锐评为最新批次的 (如果没有汇总分析,则作为兜底) if batch.chat_quality_review: state.chat_quality_review = batch.chat_quality_review diff --git a/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py b/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py index 7307117..43ad8e3 100644 --- a/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py +++ b/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py @@ -90,29 +90,28 @@ class ChatQualityAnalyzer(BaseAnalyzer): 请分析以下群聊记录,输出一份"聊天质量锐评"。 ## 任务目标: -1. 将聊天内容划分为 3-6 个不同的维度/类别(如:技术探讨、水群闲聊、就业焦虑、深夜发情等)。 -2. 为每个维度计算一个大致的百分比占位(总和小于等于 100%)。 -3. 为每个维度写一句犀利、幽默、毒舌或温情的点评。 -4. 给出一句总结性的全群表现评价。 -5. 设定一个本次报告的主题标题和副标题。 +1. **维度划分**:将聊天内容划分为 3-6 个【高层级、抽象、泛化】的维度(例如:就业焦虑、生涯规划、技术方案研究、情感树洞、无意义水群等)。 +2. **严禁在维度名称(name)中出现任何具体的群聊人物名、项目名、具体的报错内容或细碎的事件点。标题必须保持高度抽象且字数简练(2-6个字)。** +3. 为每个维度计算一个大致的百分比占位(总和小于等于 100%)。 +4. **点评内容**:为每个维度写一句犀利、幽默、毒舌或温情的点评。具体的吐槽内容、具体的细节事件描述请放在这里。 +5. **全群表现**:给出一句总结性的评价,作为总结标题对应的“金句”。 +6. **主题设定**:设定一个本次报告的主题标题和副标题。 ## 点评风格指南: -- 语言要接地气,多用互联网黑话。 -- 吐槽要精准,避重就轻。 -- 如果群友在认真讨论技术,可以夸两句但也要带点调侃。 -- 如果群友在无意义水群,请狠狠吐槽。 +- 语言要接地气,多用互联网黑话。吐槽要精准,避重就轻。 +- **只有维度名称(name)需要抽象,点评(comment)和总结(summary)可以非常具体和生动。** ## 返回格式要求: 必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。 ```json {{ - "title": "主题标题 (如: 互联网难民收容所)", - "subtitle": "副标题 (如: 只要不工作,我们就是最好的朋友)", + "title": "今日群聊主题", + "subtitle": "副标题", "dimensions": [ {{ - "name": "维度名称", - "percentage": 25.5, + "name": "抽象维度名", + "percentage": 比例, "comment": "维度的毒舌点评" }} ], @@ -198,6 +197,120 @@ class ChatQualityAnalyzer(BaseAnalyzer): summary=data.get("summary", "今天也是充满活力的一天。"), ) + async def summarize_batch_reviews( + self, + batch_reviews: list[dict], + umo: str | None = None, + session_id: str | None = None, + ) -> tuple[QualityReview | None, TokenUsage]: + """ + 汇总多个增量批次的质量报告,生成最终的每日全天总评。 + """ + if not batch_reviews: + return None, TokenUsage() + + if len(batch_reviews) == 1: + return self._build_review_from_dict(batch_reviews[0]), TokenUsage() + + try: + # 构建汇总用的提示词 + reviews_text = "" + for i, rev in enumerate(batch_reviews): + title = rev.get("title", "未命名") + summary = rev.get("summary", "") + dims = ", ".join( + [ + f"{d.get('name')}({d.get('percentage')}%)" + for d in rev.get("dimensions", []) + ] + ) + reviews_text += f"\n批次 {i + 1} [{title}]:\n- 维度表现: {dims}\n- 核心摘要: {summary}\n" + + prompt_template = self.config_manager.get_quality_summary_prompt() + + if not prompt_template: + prompt_template = """你是一个毒舌且幽默的群聊质量分析师。 +你现在有一份今天全天分散时间段的多个“增量批次点评笔记”。 +你的任务是将这些分散的笔记汇总成一份最终的“全天聊天质量终极锐评”。 + +## 任务目标: +1. **全局抽象维度**:根据各批次的维度表现,平衡权重,提取出 3-6 个覆盖全天的【核心、上层抽象】课题维度(如:职场/行业风向、技术架构演进、社畜心理博弈等)。 +2. **严禁在维度名称(name)中出现具体的批次细节。标题必须代表全天的某种趋势。** +3. **百分比融合**:根据全天笔记的频率和强度,给出一个代表全天整体分布的比例(总和不超过100%)。 +4. **终极点评**:为每个汇总维度写出一句升华后的全天总结性点评。可以融合具体批次中的有趣槽点。 +5. **终极总结**:拟定全天的大型主题标题、副标题,并给出一句霸气的全天表现总结。 + +## 风格要求: +- 只有维度名称(name)需要高度概括抽象。 +- 点评(comment)和总结(summary)请尽量生动、毒舌、具体,要把一整天的梗串联起来。 + +## 返回格式要求: +必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。 + +```json +{{ + "title": "今日群聊主题", + "subtitle": "副标题", + "dimensions": [ + {{ + "name": "抽象大类标题", + "percentage": 比例, + "comment": "维度的全天锐评" + }} + ], + "summary": "全天总结金句" +}} +``` +""" + prompt = prompt_template.format(reviews_text=reviews_text) + + # 调用 LLM 进行汇总 + system_prompt = await self._build_system_prompt(umo) + + response = await call_provider_with_retry( + self.context, + self.config_manager, + prompt=prompt, + max_tokens=self.get_max_tokens(), + temperature=0.7, + umo=umo, + provider_id_key=self.get_provider_id_key(), + system_prompt=system_prompt, + ) + + if response is None: + return None, TokenUsage() + + token_usage_dict = extract_token_usage(response) + usage = TokenUsage( + prompt_tokens=token_usage_dict["prompt_tokens"], + completion_tokens=token_usage_dict["completion_tokens"], + total_tokens=token_usage_dict["total_tokens"], + ) + + result_text = extract_response_text(response) + if not result_text: + return None, usage + + success, parsed_data, error_msg = parse_json_object_response( + result_text, "汇总质量分析" + ) + + if success and parsed_data: + review = self._build_review_from_dict(parsed_data) + logger.info( + f"聊天质量汇总分析成功,解析到 {len(review.dimensions)} 个汇总维度" + ) + return review, usage + + # 降级:如果汇总失败,返回最新的一个 + logger.warning(f"聊天质量汇总分析失败,降级使用最新批次: {error_msg}") + return self._build_review_from_dict(batch_reviews[-1]), usage + + except Exception as e: + logger.error(f"聊天质量汇总分析异常: {e}", exc_info=True) + return self._build_review_from_dict(batch_reviews[-1]), TokenUsage() + async def analyze_quality( self, messages: list[dict], @@ -258,7 +371,9 @@ class ChatQualityAnalyzer(BaseAnalyzer): if success and parsed_data: review = self._build_review_from_dict(parsed_data) - logger.info(f"聊天质量分析成功,解析到 {len(review.dimensions)} 个维度") + logger.debug( + f"聊天质量分析成功,解析到 {len(review.dimensions)} 个维度" + ) return review, usage # 7. 正则降级(使用 extract_quality_with_regex) @@ -267,7 +382,7 @@ class ChatQualityAnalyzer(BaseAnalyzer): if regex_data: review = self._build_review_from_dict(regex_data) - logger.info( + logger.debug( f"聊天质量正则提取成功,获得 {len(review.dimensions)} 个维度" ) return review, usage @@ -277,7 +392,7 @@ class ChatQualityAnalyzer(BaseAnalyzer): return None, usage except Exception as e: - logger.error(f"聊天质量分析解析失败: {e}", exc_info=True) + logger.error(f"聊天质量分析失败: {e}", exc_info=True) return None, TokenUsage() # Override analyze to bridge the base class interface diff --git a/src/infrastructure/analysis/llm_analyzer.py b/src/infrastructure/analysis/llm_analyzer.py index 7eb9f56..04188ff 100644 --- a/src/infrastructure/analysis/llm_analyzer.py +++ b/src/infrastructure/analysis/llm_analyzer.py @@ -164,6 +164,19 @@ class LLMAnalyzer(IAnalysisProvider): logger.error(f"金句分析失败: {e}") return [], TokenUsage() + async def summarize_quality_reviews( + self, + batch_reviews: list[dict], + umo: str | None = None, + session_id: str | None = None, + ) -> tuple[QualityReview | None, TokenUsage]: + """ + 汇总多个质量分析报告(增量模式使用) + """ + return await self.chat_quality_analyzer.summarize_batch_reviews( + batch_reviews, umo, session_id + ) + async def analyze_all_concurrent( self, messages: list[dict], diff --git a/src/infrastructure/config/config_manager.py b/src/infrastructure/config/config_manager.py index 537bcb2..1c5c959 100644 --- a/src/infrastructure/config/config_manager.py +++ b/src/infrastructure/config/config_manager.py @@ -291,7 +291,7 @@ class ConfigManager: return prompt return "" - def get_quality_analysis_prompt(self, style: str = "quality_prompt") -> str: + def get_quality_analysis_prompt(self, style: str = "quality_v2_prompt") -> str: """获取聊天质量分析提示词模板""" prompts_config = self._get_group("prompts").get("quality_analysis_prompts", {}) prompt = prompts_config.get(style, "") @@ -299,6 +299,22 @@ class ConfigManager: return prompt return "" + def set_quality_analysis_prompt(self, prompt: str): + """设置聊天质量分析提示词模板""" + prompts = self._ensure_group("prompts") + if "quality_analysis_prompts" not in prompts: + prompts["quality_analysis_prompts"] = {} + prompts["quality_analysis_prompts"]["quality_v2_prompt"] = prompt + self.config.save_config() + + def get_quality_summary_prompt(self, style: str = "quality_summary_prompt") -> str: + """获取聊天质量汇总分析提示词模板""" + prompts_config = self._get_group("prompts").get("quality_analysis_prompts", {}) + prompt = prompts_config.get(style, "") + if prompt: + return prompt + return "" + def set_topic_analysis_prompt(self, prompt: str): """设置话题分析提示词模板""" prompts = self._ensure_group("prompts") @@ -307,6 +323,14 @@ class ConfigManager: prompts["topic_analysis_prompts"]["topic_prompt"] = prompt self.config.save_config() + def set_quality_summary_prompt(self, prompt: str): + """设置聊天质量汇总分析提示词模板""" + prompts = self._ensure_group("prompts") + if "quality_analysis_prompts" not in prompts: + prompts["quality_analysis_prompts"] = {} + prompts["quality_analysis_prompts"]["quality_summary_prompt"] = prompt + self.config.save_config() + def set_user_title_analysis_prompt(self, prompt: str): """设置用户称号分析提示词模板""" prompts = self._ensure_group("prompts")