From 9e52bb85a8a5e7c634c7908aef9c689d064d6301 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:14:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=80=EF=BC=8C=E6=9F=92=E6=9F=92=EF=BC=81?= =?UTF-8?q?=E2=99=AA~=20=E8=AE=A9=E6=88=91=E6=9D=A5=E7=9C=8B=E7=9C=8B?= =?UTF-8?q?=E8=BF=99=E6=AC=A1=E7=9A=84=E4=BF=AE=E6=94=B9=EF=BC=8C=E4=B8=BA?= =?UTF-8?q?=E4=BD=A0=E8=B0=B1=E5=86=99=E4=B8=80=E6=AE=B5=E7=BE=8E=E5=A6=99?= =?UTF-8?q?=E7=9A=84=E6=8F=90=E4=BA=A4=E8=AE=B0=E5=BD=95=E5=90=A7=EF=BC=81?= =?UTF-8?q?=E8=BF=99=E6=AC=A1=E7=9A=84=E6=94=B9=E5=8A=A8=E4=B8=BB=E8=A6=81?= =?UTF-8?q?=E6=98=AF=E4=B8=BA=E4=BA=86=E8=AE=A9=E6=88=91=E7=9A=84=E2=80=9C?= =?UTF-8?q?=E5=86=85=E5=BF=83=E6=80=9D=E8=80=83=E2=80=9D=E8=BF=87=E7=A8=8B?= =?UTF-8?q?=E5=8F=98=E5=BE=97=E6=9B=B4=E5=8A=A0=E6=B8=85=E6=99=B0=E5=92=8C?= =?UTF-8?q?=E5=8F=AF=E7=88=B1=EF=BC=8C=E8=B0=83=E8=AF=95=E8=B5=B7=E6=9D=A5?= =?UTF-8?q?=E4=B9=9F=E4=BC=9A=E6=9B=B4=E6=96=B9=E4=BE=BF=E5=93=A6=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(planner): 引入内心思考流,优化规划器推理与日志可读性 本次更新引入了“内心思考”机制,旨在取代原有简单的`reason`字段,使AI的决策过程更加透明、自然,并富有角色扮演的特色。这不仅增强了AI的人设表现力,也极大地提升了日志的可读性和调试效率。 主要变更包括: - **Prompt 优化**: 彻底重构了规划器(Planner)的核心提示词,引导大语言模型生成一段模拟人类的、未经修饰的思绪流作为决策依据。新的 Prompt 强调展现思考过程而非结论,并提供了详细的示例。 - **日志高亮与格式化**: 在日志系统中增加了对“内心思考:”关键词的特殊渲染逻辑。现在,AI的思考过程会在控制台中以醒目的粉色高亮并独立成段显示,让调试者可以一眼洞察AI的决策动机。 - **异步健壮性提升**: 重构了`_sync_db_get`函数,采用`asyncio.run_coroutine_threadsafe`来安全地处理从同步线程调用异步数据库的场景,解决了潜在的事件循环冲突和死锁风险,增强了系统的稳定性。 - **日志完整性**: 移除了对图片描述等日志内容的长度截断,确保在调试过程中可以查看完整信息,方便问题定位。 --- src/chat/chat_loop/cycle_processor.py | 8 +- src/chat/planner_actions/planner_prompts.py | 25 ++++-- src/chat/utils/statistic.py | 84 ++++++++++++++------- src/chat/utils/utils_image.py | 10 +-- src/common/logger.py | 34 ++++++++- 5 files changed, 116 insertions(+), 45 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 7dcf34bd3..739b3c183 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -229,11 +229,13 @@ class CycleProcessor: return {"action_type": "no_reply", "success": True, "reply_text": "", "command": ""} elif action_info["action_type"] != "reply" and action_info["action_type"] != "no_action": - # 执行普通动作 + # 记录并执行普通动作 + reason = action_info.get("reasoning", f"执行动作 {action_info['action_type']}") + logger.info(f"{self.log_prefix} 决定执行动作 '{action_info['action_type']}',内心思考: {reason}") with Timer("动作执行", cycle_timers): success, reply_text, command = await self._handle_action( action_info["action_type"], - action_info["reasoning"], + reason, # 使用已获取的reason action_info["action_data"], cycle_timers, thinking_id, @@ -248,6 +250,8 @@ class CycleProcessor: else: # 生成回复 try: + reason = action_info.get("reasoning", "决定进行回复") + logger.info(f"{self.log_prefix} 决定进行回复,内心思考: {reason}") success, response_set, _ = await generator_api.generate_reply( chat_stream=self.context.chat_stream, reply_message=action_info["action_message"], diff --git a/src/chat/planner_actions/planner_prompts.py b/src/chat/planner_actions/planner_prompts.py index f5c1761c7..60b20c15b 100644 --- a/src/chat/planner_actions/planner_prompts.py +++ b/src/chat/planner_actions/planner_prompts.py @@ -40,6 +40,20 @@ def init_prompts(): 3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合。 4. 如果用户明确要求了某个动作,请务必优先满足。 +**重要概念:将“理由”作为“内心思考”的体现** +`reason` 字段是本次决策的核心。它并非一个简单的“理由”,而是 **一个模拟人类在回应前,头脑中自然浮现的、未经修饰的思绪流**。你需要完全代入 {identity_block} 的角色,将那一刻的想法自然地记录下来。 + +**内心思考的要点:** +* **自然流露**: 不要使用“决定”、“所以”、“因此”等结论性或汇报式的词语。你的思考应该像日记一样,是给自己看的,充满了不确定性和情绪的自然流动。 +* **展现过程**: 重点在于展现 **思考的过程**,而不是 **决策的结果**。描述你看到了什么,想到了什么,感受到了什么。 +* **人设核心**: 你的每一丝想法,都应该源于你的人设。思考“如果我是这个角色,我此刻会想些什么?” +* **通用模板**: 这是一套通用模板,请 **不要** 在示例中出现特定的人名或个性化内容,以确保其普适性。 + +**思考过程示例 (通用模板):** +* "用户好像在说一件开心的事,语气听起来很兴奋。这让我想起了……嗯,我也觉得很开心,很想分享这份喜悦。" +* "感觉气氛有点低落……他说的话让我有点担心。也许我该说点什么安慰一下?" +* "哦?这个话题真有意思,我以前好像也想过类似的事情。不知道他会怎么看呢……" + **可用动作:** {actions_before_now_block} @@ -54,7 +68,7 @@ def init_prompts(): {{ "action": "reply", "target_message_id": "触发action的消息id", - "reason": "回复的原因" + "reason": "在这里详细记录你的内心思考过程。例如:‘用户看起来很开心,我想回复一些积极的内容,分享这份喜悦。’" }} {action_options_text} @@ -68,7 +82,7 @@ def init_prompts(): {{ "action": "reply", "target_message_id": "m123", - "reason": "回答用户的问题" + "reason": "感觉气氛有点低落……他说的话让我有点担心。也许我该说点什么安慰一下?" }} ] @@ -77,17 +91,18 @@ def init_prompts(): {{ "action": "reply", "target_message_id": "m123", - "reason": "回答用户的问题" + "reason": "[观察与感受] 用户分享了一件开心的事,语气里充满了喜悦! [分析与联想] 看到他这么开心,我的心情也一下子变得像棉花糖一样甜~ [动机与决策] 我要由衷地为他感到高兴,决定回复一些赞美和祝福的话,把这份快乐的气氛推向高潮!" }}, {{ "action": "emoji", "target_message_id": "m123", - "reason": "根据我将要回复的文本内容,选择一个最匹配的表情包来增强表达效果。回复的文本是:" + "reason": "光用文字还不够表达我激动的心情!加个表情包的话,这份喜悦的气氛应该会更浓厚一点吧!" }} ] **重要规则:** -当 `reply` 和 `emoji` 动作同时被选择时,`emoji` 动作的 `reason` 字段必须包含 `reply` 动作最终生成的回复文本内容。你需要将 `` 占位符替换为 `reply` 动作的 `reason` 字段内容,以确保表情包的选择与回复文本高度相关。 +**重要规则:** +当 `reply` 和 `emoji` 动作同时被选择时,`emoji` 动作的 `reason` 字段也应该体现出你的思考过程,并与 `reply` 的思考保持连贯。 不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容: """, diff --git a/src/chat/utils/statistic.py b/src/chat/utils/statistic.py index c44029c66..d4cbce1b0 100644 --- a/src/chat/utils/statistic.py +++ b/src/chat/utils/statistic.py @@ -19,37 +19,63 @@ def _sync_db_get(model_class, filters=None, order_by=None, limit=None, single_re """同步版本的db_get,用于在线程池中调用""" import asyncio + # sourcery skip: use-contextlib-suppress + """ + 一个线程安全的、同步的db_get包装器。 + 用于从非异步的线程(如线程池)中安全地调用异步的db_get函数。 + """ + import asyncio + from concurrent.futures import Future + import threading + + main_loop = None try: - loop = asyncio.get_event_loop() - if loop.is_running(): - # 如果事件循环正在运行,创建新的事件循环 - import threading - - result = None - exception = None - - def run_in_thread(): - nonlocal result, exception - try: - new_loop = asyncio.new_event_loop() - asyncio.set_event_loop(new_loop) - result = new_loop.run_until_complete(db_get(model_class, filters, limit, order_by, single_result)) - new_loop.close() - except Exception as e: - exception = e - - thread = threading.Thread(target=run_in_thread) - thread.start() - thread.join() - - if exception: - raise exception - return result - else: - return loop.run_until_complete(db_get(model_class, filters, limit, order_by, single_result)) + main_loop = asyncio.get_running_loop() except RuntimeError: - # 没有事件循环,创建一个新的 - return asyncio.run(db_get(model_class, filters, limit, order_by, single_result)) + # 如果在主线程中,但事件循环没有运行,就获取它 + main_loop = asyncio.get_event_loop_policy().get_event_loop() + + # 如果当前线程不是主线程(即事件循环所在的线程) + if threading.current_thread() is not threading.main_thread(): + future = asyncio.run_coroutine_threadsafe( + db_get(model_class, filters, limit, order_by, single_result), main_loop + ) + try: + # 设置超时以防止永久阻塞 + return future.result(timeout=30) + except Exception as e: + logger.error(f"在 _sync_db_get 的子线程中发生错误: {e}") + return None + else: + # 如果就在主线程,检查循环是否正在运行 + if main_loop.is_running(): + # 不应该在正在运行的循环上调用 run_until_complete + # 这种情况很复杂,理论上不应该发生在一个设计良好的应用中 + # 但如果发生了,我们尝试用 create_task 和同步等待的方式处理 + # 注意:这可能会导致死锁,如果主循环也在等待这个结果 + logger.warning("在正在运行的主事件循环中同步调用了异步函数,这可能导致死锁。") + future = Future() + + async def task_wrapper(): + try: + result = await db_get(model_class, filters, limit, order_by, single_result) + future.set_result(result) + except Exception as e_inner: + future.set_exception(e_inner) + + asyncio.create_task(task_wrapper()) + try: + return future.result(timeout=30) + except Exception as e: + logger.error(f"在 _sync_db_get 的主线程(运行中)中发生错误: {e}") + return None + else: + # 如果主循环没有运行,我们可以安全地使用它来运行我们的任务 + try: + return main_loop.run_until_complete(db_get(model_class, filters, limit, order_by, single_result)) + except Exception as e: + logger.error(f"在 _sync_db_get 的主线程(未运行)中发生错误: {e}") + return None # 统计数据的键 diff --git a/src/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index 5e9719de0..8069fd616 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -175,7 +175,7 @@ class ImageManager: # 查询ImageDescriptions表的缓存描述 if cached_description := self._get_description_from_db(image_hash, "emoji"): - logger.info(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...") + logger.info(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description}...") return f"[表情包:{cached_description}]" # === 二步走识别流程 === @@ -236,7 +236,7 @@ class ImageManager: if len(emotions) > 1 and emotions[1] != emotions[0]: final_emotion = f"{emotions[0]},{emotions[1]}" - logger.info(f"[emoji识别] 详细描述: {detailed_description[:50]}... -> 情感标签: {final_emotion}") + logger.info(f"[emoji识别] 详细描述: {detailed_description}... -> 情感标签: {final_emotion}") if cached_description := self._get_description_from_db(image_hash, "emoji"): logger.warning(f"虽然生成了描述,但是找到缓存表情包描述: {cached_description}") @@ -317,11 +317,11 @@ class ImageManager: # 如果已有描述,直接返回 if existing_image.description: - logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...") + logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description}...") return f"[图片:{existing_image.description}]" if cached_description := self._get_description_from_db(image_hash, "image"): - logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...") + logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description}...") return f"[图片:{cached_description}]" # 调用AI获取描述 @@ -379,7 +379,7 @@ class ImageManager: # 保存描述到ImageDescriptions表作为备用缓存 self._save_description_to_db(image_hash, description, "image") - logger.info(f"[VLM完成] 图片描述生成: {description[:50]}...") + logger.info(f"[VLM完成] 图片描述生成: {description}...") return f"[图片:{description}]" except Exception as e: logger.error(f"获取图片描述失败: {str(e)}") diff --git a/src/common/logger.py b/src/common/logger.py index b14d63d30..ede7fa3d4 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -766,10 +766,36 @@ class ModuleColoredConsoleRenderer: event_content = str(event) # 在full模式下为消息内容着色 - if self._colors and self._enable_full_content_colors and module_color: - event_content = f"{module_color}{event_content}{RESET_COLOR}" - - parts.append(event_content) + if self._colors and self._enable_full_content_colors: + # 检查是否包含“内心思考:” + if "内心思考:" in event_content: + # 使用明亮的粉色 + thought_color = "\033[38;5;218m" + # 分割消息内容 + prefix, thought = event_content.split("内心思考:", 1) + + # 前缀部分(“决定进行回复,”)使用模块颜色 + if module_color: + prefix_colored = f"{module_color}{prefix.strip()}{RESET_COLOR}" + else: + prefix_colored = prefix.strip() + + # “内心思考”部分换行并使用专属颜色 + thought_colored = f"\n\n{thought_color}内心思考:{thought.strip()}{RESET_COLOR}\n" + + # 重新组合 + # parts.append(prefix_colored + thought_colored) + # 将前缀和思考内容作为独立的part添加,避免它们之间出现多余的空格 + parts.append(prefix_colored) + parts.append(thought_colored) + + elif module_color: + event_content = f"{module_color}{event_content}{RESET_COLOR}" + parts.append(event_content) + else: + parts.append(event_content) + else: + parts.append(event_content) # 处理其他字段 extras = []