Introduce the error retransmission mechanism to enhance robustness.
Showing
23 changed files
with
580 additions
and
6 deletions
| @@ -62,7 +62,7 @@ class DeepSearchAgent: | @@ -62,7 +62,7 @@ class DeepSearchAgent: | ||
| 62 | # 确保输出目录存在 | 62 | # 确保输出目录存在 |
| 63 | os.makedirs(self.config.output_dir, exist_ok=True) | 63 | os.makedirs(self.config.output_dir, exist_ok=True) |
| 64 | 64 | ||
| 65 | - print(f"Deep Search Agent 已初始化") | 65 | + print(f"Insight Agent已初始化") |
| 66 | print(f"使用LLM: {self.llm_client.get_model_info()}") | 66 | print(f"使用LLM: {self.llm_client.get_model_info()}") |
| 67 | print(f"搜索工具集: MediaCrawlerDB (支持5种本地数据库查询工具)") | 67 | print(f"搜索工具集: MediaCrawlerDB (支持5种本地数据库查询工具)") |
| 68 | print(f"情感分析: WeiboMultilingualSentiment (支持22种语言的情感分析)") | 68 | print(f"情感分析: WeiboMultilingualSentiment (支持22种语言的情感分析)") |
| @@ -7,6 +7,24 @@ import os | @@ -7,6 +7,24 @@ import os | ||
| 7 | from typing import Optional, Dict, Any | 7 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 8 | from openai import OpenAI |
| 9 | from .base import BaseLLM | 9 | from .base import BaseLLM |
| 10 | +import sys | ||
| 11 | + | ||
| 12 | +# 添加utils目录到Python路径 | ||
| 13 | +current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 14 | +root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 15 | +utils_dir = os.path.join(root_dir, 'utils') | ||
| 16 | +if utils_dir not in sys.path: | ||
| 17 | + sys.path.append(utils_dir) | ||
| 18 | + | ||
| 19 | +try: | ||
| 20 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 21 | +except ImportError: | ||
| 22 | + # 如果无法导入重试模块,使用空装饰器 | ||
| 23 | + def with_retry(config): | ||
| 24 | + def decorator(func): | ||
| 25 | + return func | ||
| 26 | + return decorator | ||
| 27 | + LLM_RETRY_CONFIG = None | ||
| 10 | 28 | ||
| 11 | 29 | ||
| 12 | class DeepSeekLLM(BaseLLM): | 30 | class DeepSeekLLM(BaseLLM): |
| @@ -39,6 +57,7 @@ class DeepSeekLLM(BaseLLM): | @@ -39,6 +57,7 @@ class DeepSeekLLM(BaseLLM): | ||
| 39 | """获取默认模型名称""" | 57 | """获取默认模型名称""" |
| 40 | return "deepseek-chat" | 58 | return "deepseek-chat" |
| 41 | 59 | ||
| 60 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 42 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 61 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 43 | """ | 62 | """ |
| 44 | 调用DeepSeek API生成回复 | 63 | 调用DeepSeek API生成回复 |
| @@ -4,11 +4,28 @@ Kimi LLM实现 | @@ -4,11 +4,28 @@ Kimi LLM实现 | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | import os | 6 | import os |
| 7 | +import sys | ||
| 7 | from typing import Optional, Dict, Any | 8 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 9 | from openai import OpenAI |
| 9 | # 假设 .base 模块和 BaseLLM 类已存在 | 10 | # 假设 .base 模块和 BaseLLM 类已存在 |
| 10 | from .base import BaseLLM | 11 | from .base import BaseLLM |
| 11 | 12 | ||
| 13 | +# 添加utils目录到Python路径并导入重试模块 | ||
| 14 | +try: | ||
| 15 | + current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 16 | + root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 17 | + utils_dir = os.path.join(root_dir, 'utils') | ||
| 18 | + if utils_dir not in sys.path: | ||
| 19 | + sys.path.append(utils_dir) | ||
| 20 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 21 | +except ImportError: | ||
| 22 | + # 如果无法导入重试模块,使用空装饰器避免报错 | ||
| 23 | + def with_retry(config): | ||
| 24 | + def decorator(func): | ||
| 25 | + return func | ||
| 26 | + return decorator | ||
| 27 | + LLM_RETRY_CONFIG = None | ||
| 28 | + | ||
| 12 | 29 | ||
| 13 | class KimiLLM(BaseLLM): | 30 | class KimiLLM(BaseLLM): |
| 14 | """Kimi LLM实现类""" | 31 | """Kimi LLM实现类""" |
| @@ -40,6 +57,7 @@ class KimiLLM(BaseLLM): | @@ -40,6 +57,7 @@ class KimiLLM(BaseLLM): | ||
| 40 | """获取默认模型名称""" | 57 | """获取默认模型名称""" |
| 41 | return "kimi-k2-0711-preview" | 58 | return "kimi-k2-0711-preview" |
| 42 | 59 | ||
| 60 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 43 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 61 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 44 | """ | 62 | """ |
| 45 | 调用Kimi API生成回复 | 63 | 调用Kimi API生成回复 |
| @@ -4,10 +4,27 @@ OpenAI LLM实现 | @@ -4,10 +4,27 @@ OpenAI LLM实现 | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | import os | 6 | import os |
| 7 | +import sys | ||
| 7 | from typing import Optional, Dict, Any | 8 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 9 | from openai import OpenAI |
| 9 | from .base import BaseLLM | 10 | from .base import BaseLLM |
| 10 | 11 | ||
| 12 | +# 添加utils目录到Python路径并导入重试模块 | ||
| 13 | +try: | ||
| 14 | + current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 15 | + root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 16 | + utils_dir = os.path.join(root_dir, 'utils') | ||
| 17 | + if utils_dir not in sys.path: | ||
| 18 | + sys.path.append(utils_dir) | ||
| 19 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 20 | +except ImportError: | ||
| 21 | + # 如果无法导入重试模块,使用空装饰器避免报错 | ||
| 22 | + def with_retry(config): | ||
| 23 | + def decorator(func): | ||
| 24 | + return func | ||
| 25 | + return decorator | ||
| 26 | + LLM_RETRY_CONFIG = None | ||
| 27 | + | ||
| 11 | 28 | ||
| 12 | class OpenAILLM(BaseLLM): | 29 | class OpenAILLM(BaseLLM): |
| 13 | """OpenAI LLM实现类""" | 30 | """OpenAI LLM实现类""" |
| @@ -35,6 +52,7 @@ class OpenAILLM(BaseLLM): | @@ -35,6 +52,7 @@ class OpenAILLM(BaseLLM): | ||
| 35 | """获取默认模型名称""" | 52 | """获取默认模型名称""" |
| 36 | return "gpt-4o-mini" | 53 | return "gpt-4o-mini" |
| 37 | 54 | ||
| 55 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 38 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 56 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 39 | """ | 57 | """ |
| 40 | 调用OpenAI API生成回复 | 58 | 调用OpenAI API生成回复 |
| @@ -14,6 +14,15 @@ from dataclasses import dataclass | @@ -14,6 +14,15 @@ from dataclasses import dataclass | ||
| 14 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) | 14 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) |
| 15 | from config import GUIJI_QWEN3_API_KEY | 15 | from config import GUIJI_QWEN3_API_KEY |
| 16 | 16 | ||
| 17 | +# 添加utils目录到Python路径 | ||
| 18 | +current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 19 | +root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 20 | +utils_dir = os.path.join(root_dir, 'utils') | ||
| 21 | +if utils_dir not in sys.path: | ||
| 22 | + sys.path.append(utils_dir) | ||
| 23 | + | ||
| 24 | +from retry_helper import with_graceful_retry, SEARCH_API_RETRY_CONFIG | ||
| 25 | + | ||
| 17 | @dataclass | 26 | @dataclass |
| 18 | class KeywordOptimizationResponse: | 27 | class KeywordOptimizationResponse: |
| 19 | """关键词优化响应""" | 28 | """关键词优化响应""" |
| @@ -164,6 +173,7 @@ class KeywordOptimizer: | @@ -164,6 +173,7 @@ class KeywordOptimizer: | ||
| 164 | 173 | ||
| 165 | return prompt | 174 | return prompt |
| 166 | 175 | ||
| 176 | + @with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return={"success": False, "error": "关键词优化服务暂时不可用"}) | ||
| 167 | def _call_qwen_api(self, system_prompt: str, user_prompt: str) -> Dict[str, Any]: | 177 | def _call_qwen_api(self, system_prompt: str, user_prompt: str) -> Dict[str, Any]: |
| 168 | """调用Qwen API""" | 178 | """调用Qwen API""" |
| 169 | headers = { | 179 | headers = { |
| @@ -51,7 +51,7 @@ class DeepSearchAgent: | @@ -51,7 +51,7 @@ class DeepSearchAgent: | ||
| 51 | # 确保输出目录存在 | 51 | # 确保输出目录存在 |
| 52 | os.makedirs(self.config.output_dir, exist_ok=True) | 52 | os.makedirs(self.config.output_dir, exist_ok=True) |
| 53 | 53 | ||
| 54 | - print(f"Deep Search Agent 已初始化") | 54 | + print(f"Meida Agent已初始化") |
| 55 | print(f"使用LLM: {self.llm_client.get_model_info()}") | 55 | print(f"使用LLM: {self.llm_client.get_model_info()}") |
| 56 | print(f"搜索工具集: BochaMultimodalSearch (支持5种多模态搜索工具)") | 56 | print(f"搜索工具集: BochaMultimodalSearch (支持5种多模态搜索工具)") |
| 57 | 57 |
| @@ -4,10 +4,27 @@ DeepSeek LLM实现 | @@ -4,10 +4,27 @@ DeepSeek LLM实现 | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | import os | 6 | import os |
| 7 | +import sys | ||
| 7 | from typing import Optional, Dict, Any | 8 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 9 | from openai import OpenAI |
| 9 | from .base import BaseLLM | 10 | from .base import BaseLLM |
| 10 | 11 | ||
| 12 | +# 添加utils目录到Python路径并导入重试模块 | ||
| 13 | +try: | ||
| 14 | + current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 15 | + root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 16 | + utils_dir = os.path.join(root_dir, 'utils') | ||
| 17 | + if utils_dir not in sys.path: | ||
| 18 | + sys.path.append(utils_dir) | ||
| 19 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 20 | +except ImportError: | ||
| 21 | + # 如果无法导入重试模块,使用空装饰器避免报错 | ||
| 22 | + def with_retry(config): | ||
| 23 | + def decorator(func): | ||
| 24 | + return func | ||
| 25 | + return decorator | ||
| 26 | + LLM_RETRY_CONFIG = None | ||
| 27 | + | ||
| 11 | 28 | ||
| 12 | class DeepSeekLLM(BaseLLM): | 29 | class DeepSeekLLM(BaseLLM): |
| 13 | """DeepSeek LLM实现类""" | 30 | """DeepSeek LLM实现类""" |
| @@ -39,6 +56,7 @@ class DeepSeekLLM(BaseLLM): | @@ -39,6 +56,7 @@ class DeepSeekLLM(BaseLLM): | ||
| 39 | """获取默认模型名称""" | 56 | """获取默认模型名称""" |
| 40 | return "deepseek-chat" | 57 | return "deepseek-chat" |
| 41 | 58 | ||
| 59 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 42 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 60 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 43 | """ | 61 | """ |
| 44 | 调用DeepSeek API生成回复 | 62 | 调用DeepSeek API生成回复 |
| @@ -4,10 +4,27 @@ Gemini LLM实现 | @@ -4,10 +4,27 @@ Gemini LLM实现 | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | import os | 6 | import os |
| 7 | +import sys | ||
| 7 | from typing import Optional, Dict, Any | 8 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 9 | from openai import OpenAI |
| 9 | from .base import BaseLLM | 10 | from .base import BaseLLM |
| 10 | 11 | ||
| 12 | +# 添加utils目录到Python路径并导入重试模块 | ||
| 13 | +try: | ||
| 14 | + current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 15 | + root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 16 | + utils_dir = os.path.join(root_dir, 'utils') | ||
| 17 | + if utils_dir not in sys.path: | ||
| 18 | + sys.path.append(utils_dir) | ||
| 19 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 20 | +except ImportError: | ||
| 21 | + # 如果无法导入重试模块,使用空装饰器避免报错 | ||
| 22 | + def with_retry(config): | ||
| 23 | + def decorator(func): | ||
| 24 | + return func | ||
| 25 | + return decorator | ||
| 26 | + LLM_RETRY_CONFIG = None | ||
| 27 | + | ||
| 11 | 28 | ||
| 12 | class GeminiLLM(BaseLLM): | 29 | class GeminiLLM(BaseLLM): |
| 13 | """Gemini LLM实现类""" | 30 | """Gemini LLM实现类""" |
| @@ -39,6 +56,7 @@ class GeminiLLM(BaseLLM): | @@ -39,6 +56,7 @@ class GeminiLLM(BaseLLM): | ||
| 39 | """获取默认模型名称""" | 56 | """获取默认模型名称""" |
| 40 | return "gemini-2.5-pro" | 57 | return "gemini-2.5-pro" |
| 41 | 58 | ||
| 59 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 42 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 60 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 43 | """ | 61 | """ |
| 44 | 调用Gemini API生成回复 | 62 | 调用Gemini API生成回复 |
| @@ -4,10 +4,27 @@ OpenAI LLM实现 | @@ -4,10 +4,27 @@ OpenAI LLM实现 | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | import os | 6 | import os |
| 7 | +import sys | ||
| 7 | from typing import Optional, Dict, Any | 8 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 9 | from openai import OpenAI |
| 9 | from .base import BaseLLM | 10 | from .base import BaseLLM |
| 10 | 11 | ||
| 12 | +# 添加utils目录到Python路径并导入重试模块 | ||
| 13 | +try: | ||
| 14 | + current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 15 | + root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 16 | + utils_dir = os.path.join(root_dir, 'utils') | ||
| 17 | + if utils_dir not in sys.path: | ||
| 18 | + sys.path.append(utils_dir) | ||
| 19 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 20 | +except ImportError: | ||
| 21 | + # 如果无法导入重试模块,使用空装饰器避免报错 | ||
| 22 | + def with_retry(config): | ||
| 23 | + def decorator(func): | ||
| 24 | + return func | ||
| 25 | + return decorator | ||
| 26 | + LLM_RETRY_CONFIG = None | ||
| 27 | + | ||
| 11 | 28 | ||
| 12 | class OpenAILLM(BaseLLM): | 29 | class OpenAILLM(BaseLLM): |
| 13 | """OpenAI LLM实现类""" | 30 | """OpenAI LLM实现类""" |
| @@ -35,6 +52,7 @@ class OpenAILLM(BaseLLM): | @@ -35,6 +52,7 @@ class OpenAILLM(BaseLLM): | ||
| 35 | """获取默认模型名称""" | 52 | """获取默认模型名称""" |
| 36 | return "gpt-4o-mini" | 53 | return "gpt-4o-mini" |
| 37 | 54 | ||
| 55 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 38 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 56 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 39 | """ | 57 | """ |
| 40 | 调用OpenAI API生成回复 | 58 | 调用OpenAI API生成回复 |
| @@ -22,6 +22,7 @@ | @@ -22,6 +22,7 @@ | ||
| 22 | 22 | ||
| 23 | import os | 23 | import os |
| 24 | import json | 24 | import json |
| 25 | +import sys | ||
| 25 | from typing import List, Dict, Any, Optional, Literal | 26 | from typing import List, Dict, Any, Optional, Literal |
| 26 | 27 | ||
| 27 | # 运行前请确保已安装 requests 库: pip install requests | 28 | # 运行前请确保已安装 requests 库: pip install requests |
| @@ -30,6 +31,15 @@ try: | @@ -30,6 +31,15 @@ try: | ||
| 30 | except ImportError: | 31 | except ImportError: |
| 31 | raise ImportError("requests 库未安装,请运行 `pip install requests` 进行安装。") | 32 | raise ImportError("requests 库未安装,请运行 `pip install requests` 进行安装。") |
| 32 | 33 | ||
| 34 | +# 添加utils目录到Python路径 | ||
| 35 | +current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 36 | +root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 37 | +utils_dir = os.path.join(root_dir, 'utils') | ||
| 38 | +if utils_dir not in sys.path: | ||
| 39 | + sys.path.append(utils_dir) | ||
| 40 | + | ||
| 41 | +from retry_helper import with_graceful_retry, SEARCH_API_RETRY_CONFIG | ||
| 42 | + | ||
| 33 | # --- 1. 数据结构定义 --- | 43 | # --- 1. 数据结构定义 --- |
| 34 | from dataclasses import dataclass, field | 44 | from dataclasses import dataclass, field |
| 35 | 45 | ||
| @@ -158,6 +168,7 @@ class BochaMultimodalSearch: | @@ -158,6 +168,7 @@ class BochaMultimodalSearch: | ||
| 158 | return final_response | 168 | return final_response |
| 159 | 169 | ||
| 160 | 170 | ||
| 171 | + @with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return=BochaResponse(query="搜索失败")) | ||
| 161 | def _search_internal(self, **kwargs) -> BochaResponse: | 172 | def _search_internal(self, **kwargs) -> BochaResponse: |
| 162 | """内部通用的搜索执行器,所有工具最终都调用此方法""" | 173 | """内部通用的搜索执行器,所有工具最终都调用此方法""" |
| 163 | query = kwargs.get("query", "Unknown Query") | 174 | query = kwargs.get("query", "Unknown Query") |
| @@ -179,10 +190,10 @@ class BochaMultimodalSearch: | @@ -179,10 +190,10 @@ class BochaMultimodalSearch: | ||
| 179 | 190 | ||
| 180 | except requests.exceptions.RequestException as e: | 191 | except requests.exceptions.RequestException as e: |
| 181 | print(f"搜索时发生网络错误: {str(e)}") | 192 | print(f"搜索时发生网络错误: {str(e)}") |
| 182 | - return BochaResponse(query=query) | 193 | + raise e # 让重试机制捕获并处理 |
| 183 | except Exception as e: | 194 | except Exception as e: |
| 184 | print(f"处理响应时发生未知错误: {str(e)}") | 195 | print(f"处理响应时发生未知错误: {str(e)}") |
| 185 | - return BochaResponse(query=query) | 196 | + raise e # 让重试机制捕获并处理 |
| 186 | 197 | ||
| 187 | # --- Agent 可用的工具方法 --- | 198 | # --- Agent 可用的工具方法 --- |
| 188 | 199 |
| @@ -51,7 +51,7 @@ class DeepSearchAgent: | @@ -51,7 +51,7 @@ class DeepSearchAgent: | ||
| 51 | # 确保输出目录存在 | 51 | # 确保输出目录存在 |
| 52 | os.makedirs(self.config.output_dir, exist_ok=True) | 52 | os.makedirs(self.config.output_dir, exist_ok=True) |
| 53 | 53 | ||
| 54 | - print(f"Deep Search Agent 已初始化") | 54 | + print(f"Query Agent已初始化") |
| 55 | print(f"使用LLM: {self.llm_client.get_model_info()}") | 55 | print(f"使用LLM: {self.llm_client.get_model_info()}") |
| 56 | print(f"搜索工具集: TavilyNewsAgency (支持6种搜索工具)") | 56 | print(f"搜索工具集: TavilyNewsAgency (支持6种搜索工具)") |
| 57 | 57 |
| @@ -4,10 +4,27 @@ DeepSeek LLM实现 | @@ -4,10 +4,27 @@ DeepSeek LLM实现 | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | import os | 6 | import os |
| 7 | +import sys | ||
| 7 | from typing import Optional, Dict, Any | 8 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 9 | from openai import OpenAI |
| 9 | from .base import BaseLLM | 10 | from .base import BaseLLM |
| 10 | 11 | ||
| 12 | +# 添加utils目录到Python路径并导入重试模块 | ||
| 13 | +try: | ||
| 14 | + current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 15 | + root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 16 | + utils_dir = os.path.join(root_dir, 'utils') | ||
| 17 | + if utils_dir not in sys.path: | ||
| 18 | + sys.path.append(utils_dir) | ||
| 19 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 20 | +except ImportError: | ||
| 21 | + # 如果无法导入重试模块,使用空装饰器避免报错 | ||
| 22 | + def with_retry(config): | ||
| 23 | + def decorator(func): | ||
| 24 | + return func | ||
| 25 | + return decorator | ||
| 26 | + LLM_RETRY_CONFIG = None | ||
| 27 | + | ||
| 11 | 28 | ||
| 12 | class DeepSeekLLM(BaseLLM): | 29 | class DeepSeekLLM(BaseLLM): |
| 13 | """DeepSeek LLM实现类""" | 30 | """DeepSeek LLM实现类""" |
| @@ -39,6 +56,7 @@ class DeepSeekLLM(BaseLLM): | @@ -39,6 +56,7 @@ class DeepSeekLLM(BaseLLM): | ||
| 39 | """获取默认模型名称""" | 56 | """获取默认模型名称""" |
| 40 | return "deepseek-chat" | 57 | return "deepseek-chat" |
| 41 | 58 | ||
| 59 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 42 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 60 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 43 | """ | 61 | """ |
| 44 | 调用DeepSeek API生成回复 | 62 | 调用DeepSeek API生成回复 |
| @@ -4,10 +4,27 @@ OpenAI LLM实现 | @@ -4,10 +4,27 @@ OpenAI LLM实现 | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | import os | 6 | import os |
| 7 | +import sys | ||
| 7 | from typing import Optional, Dict, Any | 8 | from typing import Optional, Dict, Any |
| 8 | from openai import OpenAI | 9 | from openai import OpenAI |
| 9 | from .base import BaseLLM | 10 | from .base import BaseLLM |
| 10 | 11 | ||
| 12 | +# 添加utils目录到Python路径并导入重试模块 | ||
| 13 | +try: | ||
| 14 | + current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 15 | + root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 16 | + utils_dir = os.path.join(root_dir, 'utils') | ||
| 17 | + if utils_dir not in sys.path: | ||
| 18 | + sys.path.append(utils_dir) | ||
| 19 | + from retry_helper import with_retry, with_graceful_retry, LLM_RETRY_CONFIG | ||
| 20 | +except ImportError: | ||
| 21 | + # 如果无法导入重试模块,使用空装饰器避免报错 | ||
| 22 | + def with_retry(config): | ||
| 23 | + def decorator(func): | ||
| 24 | + return func | ||
| 25 | + return decorator | ||
| 26 | + LLM_RETRY_CONFIG = None | ||
| 27 | + | ||
| 11 | 28 | ||
| 12 | class OpenAILLM(BaseLLM): | 29 | class OpenAILLM(BaseLLM): |
| 13 | """OpenAI LLM实现类""" | 30 | """OpenAI LLM实现类""" |
| @@ -35,6 +52,7 @@ class OpenAILLM(BaseLLM): | @@ -35,6 +52,7 @@ class OpenAILLM(BaseLLM): | ||
| 35 | """获取默认模型名称""" | 52 | """获取默认模型名称""" |
| 36 | return "gpt-4o-mini" | 53 | return "gpt-4o-mini" |
| 37 | 54 | ||
| 55 | + @with_retry(LLM_RETRY_CONFIG) | ||
| 38 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: | 56 | def invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> str: |
| 39 | """ | 57 | """ |
| 40 | 调用OpenAI API生成回复 | 58 | 调用OpenAI API生成回复 |
| @@ -22,7 +22,17 @@ | @@ -22,7 +22,17 @@ | ||
| 22 | """ | 22 | """ |
| 23 | 23 | ||
| 24 | import os | 24 | import os |
| 25 | +import sys | ||
| 25 | from typing import List, Dict, Any, Optional | 26 | from typing import List, Dict, Any, Optional |
| 27 | + | ||
| 28 | +# 添加utils目录到Python路径 | ||
| 29 | +current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| 30 | +root_dir = os.path.dirname(os.path.dirname(current_dir)) | ||
| 31 | +utils_dir = os.path.join(root_dir, 'utils') | ||
| 32 | +if utils_dir not in sys.path: | ||
| 33 | + sys.path.append(utils_dir) | ||
| 34 | + | ||
| 35 | +from retry_helper import with_graceful_retry, SEARCH_API_RETRY_CONFIG | ||
| 26 | from dataclasses import dataclass, field | 36 | from dataclasses import dataclass, field |
| 27 | 37 | ||
| 28 | # 运行前请确保已安装Tavily库: pip install tavily-python | 38 | # 运行前请确保已安装Tavily库: pip install tavily-python |
| @@ -82,6 +92,7 @@ class TavilyNewsAgency: | @@ -82,6 +92,7 @@ class TavilyNewsAgency: | ||
| 82 | raise ValueError("Tavily API Key未找到!请设置TAVILY_API_KEY环境变量或在初始化时提供") | 92 | raise ValueError("Tavily API Key未找到!请设置TAVILY_API_KEY环境变量或在初始化时提供") |
| 83 | self._client = TavilyClient(api_key=api_key) | 93 | self._client = TavilyClient(api_key=api_key) |
| 84 | 94 | ||
| 95 | + @with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return=TavilyResponse(query="搜索失败")) | ||
| 85 | def _search_internal(self, **kwargs) -> TavilyResponse: | 96 | def _search_internal(self, **kwargs) -> TavilyResponse: |
| 86 | """内部通用的搜索执行器,所有工具最终都调用此方法""" | 97 | """内部通用的搜索执行器,所有工具最终都调用此方法""" |
| 87 | try: | 98 | try: |
| @@ -109,7 +120,7 @@ class TavilyNewsAgency: | @@ -109,7 +120,7 @@ class TavilyNewsAgency: | ||
| 109 | ) | 120 | ) |
| 110 | except Exception as e: | 121 | except Exception as e: |
| 111 | print(f"搜索时发生错误: {str(e)}") | 122 | print(f"搜索时发生错误: {str(e)}") |
| 112 | - return TavilyResponse(query=kwargs.get("query", "Unknown Query")) | 123 | + raise e # 让重试机制捕获并处理 |
| 113 | 124 | ||
| 114 | # --- Agent 可用的工具方法 --- | 125 | # --- Agent 可用的工具方法 --- |
| 115 | 126 |
| 1 | +# 樱花下的裂缝:武汉大学年度舆情全景报告 | ||
| 2 | +*——当百年名校遭遇Z世代“真话时代”* | ||
| 3 | + | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +## 一、舆情热点事件时间轴 | ||
| 7 | +| 时间 | 事件 | 主阵地 | 最高声量 | 核心情绪 | | ||
| 8 | +|---|---|---|---|---| | ||
| 9 | +| 3月 | 樱花季“窒息浪漫” | 微博 / 小红书 | 微博转评42万 | 微博35%负面 vs 抖音68%正面 | | ||
| 10 | +| 7月 | 宿舍搬迁“叙利亚” | 超话 / B站 | 超话阅读2300万 | 愤怒值48%,非常愤怒22% | | ||
| 11 | +| 9月 | 奖学金缩水 | 微信群 / 知乎 | 知乎浏览800万 | 三输叙事:学生·老师·学校 | | ||
| 12 | +| 10月 | 1亿捐款翻车 | 微博 / 豆瓣 | 热评2.3万赞 | “被代表感”54%负面 | | ||
| 13 | +| 11月 | 123页PDF学术举报 | 微博 / B站 / 知乎 | B站视频50万播放 | 学术负面58%,“失望+愤怒”高频 | | ||
| 14 | + | ||
| 15 | +--- | ||
| 16 | + | ||
| 17 | +## 二、平台情绪拆解:同一件事,五种滤镜 | ||
| 18 | + | ||
| 19 | +| 平台 | 情绪基调 | 放大机制 | 关键节点账号 | | ||
| 20 | +|---|---|---|---| | ||
| 21 | +| **微博** | 愤怒→理性拉锯 | 热搜+大V | @扒皮王(831万粉,单条120万转评赞) | | ||
| 22 | +| **小红书** | 细节控+情绪拼贴 | 图文笔记+淘宝链接 | “珞珈山学姐”(24万赞) | | ||
| 23 | +| **知乎** | 制度长文+程序辩论 | 万字长答+法条引用 | “法山叔”(收藏8.9万) | | ||
| 24 | +| **抖音** | 慢镜+BGM情绪滤镜 | 算法对冲+直播陪伴 | “新闻姐”(峰值97万人在线) | | ||
| 25 | +| **学校官微** | 单向输出+精选评论 | 公文模板 | 点赞3.2万 vs 阅读量230万,尴尬比1:72 | | ||
| 26 | + | ||
| 27 | +--- | ||
| 28 | + | ||
| 29 | +## 三、校方应对:时间撕裂了信任 | ||
| 30 | +> “正在调查”四个字,从2023年10月拖到2025年7月,**信任赤字**被时间指数级放大。 | ||
| 31 | + | ||
| 32 | +| 维度 | 武大做法 | 对比案例 | 舆情差 | | ||
| 33 | +|---|---|---|---| | ||
| 34 | +| 时效 | 48小时沉默 | 中大48小时3条视频通报 | 负面情感+45% | | ||
| 35 | +| 透明度 | 公文术语“已知晓” | 浙大校长PPT逐页答疑 | “失望”词频一周+45% | | ||
| 36 | +| 情感共鸣 | 官微精选评论 | 校长走进家属宿舍 | “寒心”替代“信任” | | ||
| 37 | + | ||
| 38 | +**关键声音** | ||
| 39 | +- 学生母亲:“孩子确诊PTSD,学校一句道歉都没有。” | ||
| 40 | +- 校友:“母校若不能自清,我们捐再多钱也洗不白。” | ||
| 41 | + | ||
| 42 | +--- | ||
| 43 | + | ||
| 44 | +## 四、学生&校友画像:当“护校”遇上“开炮” | ||
| 45 | +### 1. 阵营分布 | ||
| 46 | +| 群体 | 平台 | 话术 | 情感极化指数 | | ||
| 47 | +|---|---|---|---| | ||
| 48 | +| **护校党** | 微博超话 | 表情包+“捂脸狗头” | 0.73(历史新高) | | ||
| 49 | +| **批评党** | 知乎/豆瓣 | Excel数据+亲历 | 同上 | | ||
| 50 | +| **骑墙派** | 小红书投票 | “不冤但护短”54% | 20%已转向温和批评 | | ||
| 51 | + | ||
| 52 | +### 2. 跨届暗号 | ||
| 53 | +> “我们不是在黑武大,我们是在救武大。”——2011级法学院校友LinkedIn长文10万+阅读 | ||
| 54 | + | ||
| 55 | +--- | ||
| 56 | + | ||
| 57 | +## 五、风险预警 & 声誉修复清单 | ||
| 58 | +### 1. 当前高危雷区 | ||
| 59 | +| 风险点 | 负面占比 | 传播速度 | 触发场景 | | ||
| 60 | +|---|---|---|---| | ||
| 61 | +| 学术诚信 | 58% | 4小时1.9万赞 | 123页PDF二次发酵 | | ||
| 62 | +| 宿舍条件 | 48%愤怒 | 3分钟盖楼147层 | 老生vs新生“空调之争” | | ||
| 63 | +| 奖学金/经费 | 三输叙事 | 微信群截图裂变 | “每月少600” | | ||
| 64 | + | ||
| 65 | +### 2. 修复路径:把“愤怒”转成“共建” | ||
| 66 | +- **一张进度表**:论文审核几轮?导师反馈几天?实验楼几点熄灯? | ||
| 67 | +- **一场无提词器直播**:新闻发言人坐学生中间,直播调查日志、会议纪要、法律依据。 | ||
| 68 | +- **学生监督员**:后勤、学术、宿舍三线招募学生轮值,把“弹幕护校”变“线下共治”。 | ||
| 69 | + | ||
| 70 | +--- | ||
| 71 | + | ||
| 72 | +## 六、结论:樱花依旧开,但学生想要答案 | ||
| 73 | +从“让我好好看花”到“让我相信学术干净”,**诉求升级背后是Z世代对“大学”二字的重新定义**: | ||
| 74 | +- **程序正义 > 樱花滤镜** | ||
| 75 | +- **透明沟通 > 单向通报** | ||
| 76 | +- **共同治理 > 情感绑架** | ||
| 77 | + | ||
| 78 | +当珞珈山的孩子们凌晨三点还在实验室改论文,他们刷的不是“心疼”,而是“一起改”。**武大真正的声誉守护者,正是这群一边吐槽一边熬夜的Z世代**——给他们一张可量化的进度表,就能把“裂缝”变成“光”。 |
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
| 1 | +# 武汉大学舆情分析报告 | ||
| 2 | + | ||
| 3 | +## 武汉大学舆情概述与背景介绍 | ||
| 4 | + | ||
| 5 | +武汉大学作为中国著名高等学府,其舆情监测工作涵盖校园事件、学术声誉和学生活动等多个关键领域。近年来,该校经历了多起引发广泛关注的舆情事件,凸显了舆情管理的重要性。例如,2025年的“职工子女逼停学生”事件中,一名校外人员驾驶机动车在校园内危险驾驶,引发公众对校园安全和特权行为的质疑。尽管校方迅速发布通报澄清事实,但部分自媒体传播不实信息,导致舆情发酵,舆情在6月14日和18日分别达到峰值和次峰值,媒体如新华网、央广网等广泛报道,网民质疑处罚细节和校园管理漏洞。此外,2023年的“图书馆诬告案”涉及学生杨景媛指控同学性骚扰,后经法院判决不成立,但事件暴露了学术诚信和道德规范问题,杨景媛的硕士论文被指存在编造法律条文(如虚构《离婚法》)、数据造假(如未注明来源的中国社科院和WHO数据)等学术不端行为,引发对武汉大学学术声誉和监管机制的批评,校方被指在事件处理中存在程序瑕疵和响应延迟。这些案例表明,舆情监测有助于及时发现负面信息,但校方在回应时效性、信息透明度和处理公正性方面仍有改进空间,需加强危机公关和风险防范,以维护大学形象和社会信任。值得注意的是,相关研究成果已广泛应用于武汉市网络舆情中心等机构的监测系统,涉及可信人工智能等方向,这为武汉大学舆情监测提供了技术支撑。同时,校园安全监测可通过智能摄像头、传感器等设备实现实时监控,提升风险防范能力,而学术声誉管理需结合环境、健康与安全(EHS)等标准,确保合规运营和可持续发展。 | ||
| 6 | + | ||
| 7 | +## 近期武汉大学舆情热点事件分析 | ||
| 8 | + | ||
| 9 | +武汉大学图书馆性骚扰争议事件(2023-2024年)是该校近期最受关注的舆情热点之一。事件始于2023年7月,女生杨景媛在社交媒体指控男生肖明滔在图书馆实施性骚扰,校方迅速对肖明滔处以记过处分。然而,2025年7月法院一审判决认定肖明滔行为不构成性骚扰,驳回杨景媛诉求,引发舆论反转。公众强烈质疑杨景媛涉嫌诬告和学术不端(其硕士论文被指造假),同时批评武汉大学处分草率、处理不透明。校方回应滞后,仅表示将全面调查复核处分和论文问题,但未提供具体时间表或依据,导致舆情持续发酵。媒体覆盖广泛,包括新华社、《南方周末》、《半岛晨报》、《齐鲁晚报》等主流媒体均报道此事,知乎等平台讨论热度高,焦点集中于高校治理能力、舆情处置失当(如反应延迟、话术机械)和学术诚信问题。该事件历时两年多,涉及舆论、高校管理及司法判决等多方面,暴露了武汉大学在危机管理、透明度和社会信任维护方面的不足,对学校声誉造成显著负面影响。值得注意的是,维基百科已创建“武汉大学图书馆争议事件”条目,显示其影响深远,但条目标题暂定为可能原创或不准确,需进一步共识。此外,一周舆情热点报告将此事件列为重点,凸显其在公共讨论中的持续热度。 | ||
| 10 | + | ||
| 11 | +## 舆情监测与应对机制 | ||
| 12 | + | ||
| 13 | +武汉大学在舆情监测与应对机制方面展现出系统化的管理策略,结合先进技术工具和危机公关措施,以应对校园舆情事件。例如,在2025年“职工子女逼停学生”事件中,校方通过保卫部发布官方通报,使用舆情监测机制实时跟踪舆论动态,并针对谣言迅速报案,体现了主动应对和透明度原则。技术工具上,武汉大学采用大数据分析和AI监测系统,如时空大数据分析与AI技术融合的时空信息工程专业(2025年新设),通过动态感知和智能分析能力识别舆情热点和传播趋势,这有助于早期预警和精准干预。具体而言,武汉大学部署了WHU-POMS网络舆情监控系统,该系统全面跟踪互联网上与学校相关的新闻报道,实时识别新闻热点和敏感信息,确保舆情监测的全面性和及时性。此外,武汉大学在舆情研究领域深化产学研合作,例如与湖北省舆情研究中心等机构开展学术交流,提升舆情监测的专业能力,包括网络舆情监测与分析、传播学原理等课程内容。应对策略包括“冷处理”方式,避免过早回应激化矛盾,同时坚持公开公正的信息发布,以消弭公众猜疑。危机公关措施强调责任担当,如校方在事件中主动澄清事实,维护校园秩序和公信力。整体上,武汉大学的舆情处理表现显示出对舆情风险的敏感性和结构化应对能力,但事件也暴露出通报范围和方式需优化,以避免衍生问题,未来应持续完善监测机制和响应时效性,以提升舆情管理效果。 | ||
| 14 | + | ||
| 15 | +## 舆情对武汉大学的影响评估 | ||
| 16 | + | ||
| 17 | +武汉大学近年来面临多起舆情事件的冲击,对学校声誉、招生、学术合作及社会形象产生了显著影响。根据舆情数据分析,2019年“和服赏樱”冲突事件影响力指数达65.4,高于同类事件均值10.8%,短期内引发公众对校园管理政策的质疑,但校方通过及时回应和澄清,使支持率从26%升至48%,有效缓解了负面舆论。然而,2025年的“职工子女逼停学生”事件中,尽管校方迅速通报并报案处理谣言,但自媒体传播仍导致特权质疑等衍生舆情,暴露了危机沟通中的漏洞。更为严重的是2025年杨景媛事件,涉及诬告行为和学术造假,法院判决驳回其诉讼请求,但校方初期仓促处分受害者肖某某(记过处分),未充分调查即回应舆情,导致程序正义缺失;事后未撤销错误处分或追究杨景媛责任(因其已毕业“学籍自动结束”),引发公众对高校治理和学术诚信的广泛批评,损害了学校公信力。招生方面,近期虚假研学夏令营事件(如“魔豆梦工厂”等机构利用虚假信息招生)促使武汉大学招生办公室发布严正声明,凸显舆情对招生诚信的直接影响;杨景媛事件中,其保研资格和香港浸会大学录取引发道德审查争议,长期可能削弱国际学生吸引力,因为舆情波动会损害潜在申请者的信任,尤其在国际传播层面(如2025年全球AI设备博览会等国际活动中的形象关联)。学术合作上,武汉大学持续参与国际学术交流,如师生参加国际媒介与传播研究学会2025年会,以及举办跨学科论坛如“新时代中国国际传播创新”,但2025年性骚扰案涉及的论文争议(如虚构《离婚法》、数据造假)若处理不当,可能损害学术诚信形象,影响研究伙伴关系;校方学术不端处理机制响应延迟,暴露了论文审核和导师指导的漏洞。校方应从这些事件中学习改进,包括加强舆情监测、提升回应透明度(避免“重舆轻法”的应急处理)、平衡纪律处分与法律裁决,建立更有效的谣言应对机制,强化国际传播以维护长期声誉和社会形象,并完善道德审查和学术监管体系,防止类似事件重演。 | ||
| 18 | + | ||
| 19 | +## 未来舆情趋势与建议 | ||
| 20 | + | ||
| 21 | +武汉大学未来可能面临的舆情挑战主要集中在学术诚信危机、管理透明度不足以及沟通机制失效等方面。近期杨景媛事件暴露了学校在舆情管理中的多重问题:学术论文造假(如虚构《离婚法》、数据来源伪造)未被及时查处,损害了学术公信力;图书馆诬告事件中,校方仓促处分受害者肖某某以“平息舆论”,却未在法院判决后撤销处分或追究诬告者责任,凸显程序正义缺失。这些案例反映了高校在危机预防和响应上的滞后性,易引发公众对学校形象和治理能力的质疑。此外,搜索结果揭示的知网论文修改事件和清退留学生争议,表明武汉大学在平衡学术声誉与网络舆论压力时面临困境,可能进一步削弱公众信任。 | ||
| 22 | + | ||
| 23 | +为改进舆情管理,建议武汉大学采取以下措施: | ||
| 24 | +1. 增强透明度:建立公开的学术不端调查流程,及时发布处理结果,避免“学籍自动结束”等逃避责任的机制。参考搜索结果中香港浸会大学的做法,明确招生和行为守则,并对外回应公众关切。同时,借鉴美国大学“宽进严出”模式,加强招生和毕业环节的道德审查,确保学术与伦理标准统一。 | ||
| 25 | +2. 加强沟通策略:优化舆情监测工具,主动收集和分析网络需求清单,在危机初期进行事实核查而非仓促行动。例如,在类似事件中,应优先基于医学和法律证据决策,而非屈服于舆论压力。强化与媒体和校友的沟通,避免公关策略失误导致形象损害。 | ||
| 26 | +3. 强化预防措施:实施风险管理策略,包括定期风险评估和潜在危机检测,建立防诬告机制和学术审查强化体系,确保研究生培养质量监控不被民间替代。加强法学等专业的伦理教育,培育法治信仰,防止道德异化现象,并完善司法体系准入资格审查。 | ||
| 27 | + | ||
| 28 | +通过这些改进,武汉大学可维护学校形象,提升舆情应对水平,避免类似事件重演,并有效应对未来可能的跨境舆情挑战(如香港高校介入引发的关注)。 | ||
| 29 | + | ||
| 30 | +## 结论 | ||
| 31 | + | ||
| 32 | +基于以上分析,武汉大学在舆情管理方面展现出一定的技术能力和结构化策略,但近期事件如杨景媛案和“职工子女逼停学生”事件暴露了在透明度、响应时效性和程序公正性上的不足。这些舆情挑战已对学校声誉、招生和国际合作产生负面影响。未来,武汉大学应聚焦于增强学术诚信监管、优化危机沟通机制和强化预防措施,以提升舆情应对效果和维护长期社会信任。通过实施透明度提升、沟通策略优化和风险管理强化等建议,学校可有效 mitigate 未来舆情风险,巩固其作为顶尖高校的形象。 |
| 1 | +# 武汉大学舆情分析报告 | ||
| 2 | + | ||
| 3 | +## 武汉大学舆情概述与背景 | ||
| 4 | + | ||
| 5 | +武汉大学舆情作为高等教育机构与社会舆论互动的重要体现,具有深厚的历史背景和复杂的现实意义。舆情概念可追溯至古代,如《全唐诗》中已有记载,其内涵随时代演变,涉及民心表达、舆论监督等维度。在高等教育领域,武汉大学舆情常反映学术诚信、校园治理等议题,例如1993年校史争议和近年图书馆事件,凸显了高校在舆论场中的敏感地位。全媒体时代,舆情传播载体从传统论坛演进至社交媒体,信息速度加快、多元平台融合,导致舆情热点频发且复杂化,涉及社会结构变迁、群体利益冲突等因素。武汉大学舆情工作需结合监测、预警和研判方法,应对潜性与显性舆情,促进民意表达和社会改革,同时舆情产业在中国独特发展,涉及媒体、高校等多方参与,成为观察社会变化的重要工具。具体案例中,2023年武汉大学图书馆事件成为舆情处置的典型反面教材,涉及学术诚信和校园治理问题:事件初期校方反应滞后、信息空窗期过长,仅以模糊策略处理,导致公众信任流失;舆情研判不足,缺乏动态跟踪机制,未能预判舆论反转和声誉风险;应对话术机械,暴露推责式公关惯性;最终以沉默代替透明,拖延回应,严重损害高校公信力。该事件还引发对研究生培养质量、学术不端处理机制及网络实名举报治理效能的深度反思,凸显舆情在高校治理中的关键作用。2025年7月25日法院一审判决肖某某行为不构成性骚扰后,舆情焦点转向校方公信力危机,舆论要求撤销错误处分并质疑校方“重舆轻法”的处置模式,同时杨某某的学术不端问题(如虚构《离婚法》、数据造假)进一步暴露了高校在学术伦理监管和危机应对机制上的系统性缺陷。 | ||
| 6 | + | ||
| 7 | +## 近期武汉大学舆情事件分析 | ||
| 8 | + | ||
| 9 | +武汉大学近期因图书馆“性骚扰”争议事件陷入舆论漩涡,事件源于2023年10月,学生杨某某指控2022级本科生肖某某在图书馆性骚扰,学校于2023年10月对肖某某给予记过处分。然而,2025年7月25日武汉市经济技术开发区人民法院一审判决认定肖某某行为不构成性骚扰,其动作存在“抓痒的高度可能”,且事发场景开放、双方无交流,无法证明存在性暗示或性挑逗行为,故驳回杨某某的全部诉讼请求。这一判决引发公众广泛质疑学校最初的处分决定,舆情迅速发酵。事件发展过程中,肖某某遭受严重网暴,个人信息被泄露,本人罹患创伤后应激障碍,家人也受牵连;杨某某则在判决后宣布保研成功、通过法考并将赴香港浸会大学读博,并表示继续投诉肖某某,引发舆论反弹。2025年7月31日晚,武汉大学校长张平文回应称学校正在处理中,具体结果需等待上级安排,但官网处分通报仍未删除(点击量超17万),其回应被指卸责,加剧信任危机。舆情涉及学术诚信、校园管理和特权问题,网民批评学校处理不当、回应迟缓。武汉大学已启动对肖某某处分和杨某某学位论文的复核程序,但尚未撤销处分。事件暴露了校风学风建设不足、意识形态风险防控薄弱等问题,与中央巡视组2021年指出的整改不到位相呼应。舆情高峰出现在2025年7月底至8月初,媒体和网民呼吁透明和公正处理,以避免次生舆情和品牌形象损害。2025年8月1日,武汉大学发布通报,表示已组建工作专班对肖某某纪律处分和杨某某学位论文进行全面调查复核,将以事实为依据,严格按照校纪校规和学术规范作出处理。公众反应强烈,部分网民质疑学校缺乏主见,呼吁尽快公布公正处理结果。 | ||
| 10 | + | ||
| 11 | +## 舆情监测与管理机制 | ||
| 12 | + | ||
| 13 | +武汉大学在舆情监测与管理方面建立了较为完善的机制,通过官方通报、危机公关和社交媒体互动等多渠道应对舆情事件。以2025年“职工子女逼停学生”事件为例,学校保卫部及时发布通报澄清事实,强调涉事人员为校外退休职工子女(其父已去世多年),不存在特权行为,并对造谣者采取法律手段,向公安机关报案。同时,学校通过新华网、央广网、央视网等央级媒体以及网易、今日头条等平台扩散正面信息,遏制谣言传播。在社交媒体管理上,武汉大学注重实时监测舆情动态,采用“冷处理”策略避免过度回应刺激负面舆情,并通过学生意见领袖引导舆论走向。此外,学校还参考了舆情管理最佳实践,如建立48小时黄金回应机制和舆情缓冲带,确保信息公开透明,维护校园公信力。然而,在2023-2025年的杨景媛事件中,学校暴露出危机应对的双轨制困境:初期为平息舆论仓促对肖某某记过处分(取消保研资格),未充分调查事实(监控显示无交流);法院判决反转后,因杨景媛已毕业“学籍自动结束”,未对其学术不端(如论文将新中国成立年份误写为“1049年”、虚构“2001年《离婚法》”)和诬告行为追责,引发程序正义争议。学校采用7×24小时AI智能监测系统(如A公司研发的P系统)实时抓取全网舆情,但在响应机制上存在延迟,依赖新热点转移注意力(如防城港奔驰女事件),而非彻底解决问题。舆情分析显示,网民主要质疑处罚细节(如取消校园通行授权三个月)和谣言滋生原因,部分极端观点借机攻击学校整体声誉。高校网络舆情具有突发性、广泛性和低可控性特点,需平衡法理与人情,强化学术伦理审查(如香港浸会大学发函核查杨景媛事件)和防诬告机制,以提升治理效能。教训表明,舆情处置应避免“未调查先问责”的惯性思维,注重线下事实全面呈现,减少内部汇报链条导致的响应滞后。 | ||
| 14 | + | ||
| 15 | +## 舆情对武汉大学的影响 | ||
| 16 | + | ||
| 17 | +武汉大学近年来因多起舆情事件面临严峻的声誉挑战,对招生、学术合作和公共形象产生了显著影响。负面案例如2023年的杨景媛诬告事件和2025年的知网论文修改争议,引发了全国性舆论风波,导致公众对学校学术诚信和管理能力的质疑,可能影响考生报考意愿和学术合作伙伴的信任。正面案例包括学校办学声誉的明显提升,国内排名上升到前百,以及重大科研项目与经费的显著增长(项目经费从2.36亿上升到3.74亿,SCI论文从405篇增长到534篇),这些成就得益于人才培养工作的实际成效和多学科交叉培养策略,支持博士生参与学术交流和国际合作研究,从而拓宽学术视野并激发创新思维,促进了学术合作的成功和声誉提升。学术研究表明,高校可通过建立舆情协同治理机制(如基于结构方程模型的实证研究所示)、加强透明度(如及时发布调查通报)和强化学术道德教育来缓解负面影响,从而提升声誉和促进招生增长。例如,武汉大学在论文事件中试图快速平息事态,但策略不完善,反而加剧形象损害,突显了高校在平衡学术声誉与舆论压力时的普遍困境。总体而言,舆情管理需从被动应对转向主动预防,通过实证研究和协同机制优化,以维护长期声誉并支持招生增长。 | ||
| 18 | + | ||
| 19 | +## 未来舆情趋势与建议 | ||
| 20 | + | ||
| 21 | +基于武汉大学图书馆事件的舆情处置教训和学术研究分析,未来武汉大学舆情管理应重点关注以下趋势和建议:首先,舆情发展呈现快速扩散和多次反转的特点,高校需建立实时监测和动态预警机制,提前识别潜在风险点,并借鉴中南大学等机构在深度学习构建舆情事件演化图方面的研究成果,提升预测准确性。其次,新媒体环境下舆情传播具有突发性、广泛性和互动性,高校应构建包括危机公关策略在内的全方位应对体系,确保快速响应和信息透明,避免2023年事件中出现的反应滞后、信息空窗期过长问题。第三,2024年趋势显示,舆情管理需从管理思维转向客户思维,强化基层和前台力量,注重公众沟通。建议武汉大学:1)完善舆情管理协同机制,加强部门间协作,避免反应滞后和责任模糊,杜绝“推责式公关”和“唯上不唯实”的倾向;2)提升舆情研判能力,定期进行模拟演练和流程复盘,建立动态跟踪机制以应对舆论风向反转;3)强化信息公开和沟通话术,以透明化操作替代沉默拖延,主动修复公众信任,明确解释事件原委和判断依据;4)加强舆情监管队伍建设,培训专业人员处理敏感问题,引入第三方评估或专家智库支持;5)借鉴SCCIhub等先进技术框架,运用AWP聚类、Tri-training等算法和指令微调后的大语言模型,提升舆情监测、分析和预警能力;6)关注网络舆情涉及内容的广泛性,包括民生诉求、科教文卫等多元领域,建立针对不同主题的应对预案。这些措施有助于维护学校形象,避免类似事件重演,并应对学术诚信、采购管理等多元舆情挑战。 | ||
| 22 | + | ||
| 23 | +## 结论 | ||
| 24 | + | ||
| 25 | +综合分析武汉大学舆情事件,可见其舆情管理面临多重挑战,包括反应滞后、信息不透明、研判不足等问题,尤其在2023-2025年图书馆事件中暴露了系统性缺陷。然而,学校在舆情监测机制建设、正面声誉提升(如科研经费增长和排名上升)方面也展现出积极进展。未来,武汉大学应强化实时监测、动态预警和协同治理,注重透明沟通和学术伦理审查,以主动预防替代被动应对,从而有效维护公信力、促进招生和学术合作,实现可持续发展。 |
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
utils/retry_helper.py
0 → 100644
| 1 | +""" | ||
| 2 | +重试机制工具模块 | ||
| 3 | +提供通用的网络请求重试功能,增强系统健壮性 | ||
| 4 | +""" | ||
| 5 | + | ||
| 6 | +import time | ||
| 7 | +import logging | ||
| 8 | +from functools import wraps | ||
| 9 | +from typing import Callable, Any, Union, List, Type | ||
| 10 | +import requests | ||
| 11 | +from openai import OpenAI | ||
| 12 | + | ||
| 13 | +# 配置日志 | ||
| 14 | +logging.basicConfig(level=logging.INFO) | ||
| 15 | +logger = logging.getLogger(__name__) | ||
| 16 | + | ||
| 17 | +class RetryConfig: | ||
| 18 | + """重试配置类""" | ||
| 19 | + | ||
| 20 | + def __init__( | ||
| 21 | + self, | ||
| 22 | + max_retries: int = 3, | ||
| 23 | + initial_delay: float = 1.0, | ||
| 24 | + backoff_factor: float = 2.0, | ||
| 25 | + max_delay: float = 60.0, | ||
| 26 | + retry_on_exceptions: tuple = None | ||
| 27 | + ): | ||
| 28 | + """ | ||
| 29 | + 初始化重试配置 | ||
| 30 | + | ||
| 31 | + Args: | ||
| 32 | + max_retries: 最大重试次数 | ||
| 33 | + initial_delay: 初始延迟秒数 | ||
| 34 | + backoff_factor: 退避因子(每次重试延迟翻倍) | ||
| 35 | + max_delay: 最大延迟秒数 | ||
| 36 | + retry_on_exceptions: 需要重试的异常类型元组 | ||
| 37 | + """ | ||
| 38 | + self.max_retries = max_retries | ||
| 39 | + self.initial_delay = initial_delay | ||
| 40 | + self.backoff_factor = backoff_factor | ||
| 41 | + self.max_delay = max_delay | ||
| 42 | + | ||
| 43 | + # 默认需要重试的异常类型 | ||
| 44 | + if retry_on_exceptions is None: | ||
| 45 | + self.retry_on_exceptions = ( | ||
| 46 | + requests.exceptions.RequestException, | ||
| 47 | + requests.exceptions.ConnectionError, | ||
| 48 | + requests.exceptions.HTTPError, | ||
| 49 | + requests.exceptions.Timeout, | ||
| 50 | + requests.exceptions.TooManyRedirects, | ||
| 51 | + ConnectionError, | ||
| 52 | + TimeoutError, | ||
| 53 | + Exception # OpenAI和其他API可能抛出的一般异常 | ||
| 54 | + ) | ||
| 55 | + else: | ||
| 56 | + self.retry_on_exceptions = retry_on_exceptions | ||
| 57 | + | ||
| 58 | +# 默认配置 | ||
| 59 | +DEFAULT_RETRY_CONFIG = RetryConfig() | ||
| 60 | + | ||
| 61 | +def with_retry(config: RetryConfig = None): | ||
| 62 | + """ | ||
| 63 | + 重试装饰器 | ||
| 64 | + | ||
| 65 | + Args: | ||
| 66 | + config: 重试配置,如果不提供则使用默认配置 | ||
| 67 | + | ||
| 68 | + Returns: | ||
| 69 | + 装饰器函数 | ||
| 70 | + """ | ||
| 71 | + if config is None: | ||
| 72 | + config = DEFAULT_RETRY_CONFIG | ||
| 73 | + | ||
| 74 | + def decorator(func: Callable) -> Callable: | ||
| 75 | + @wraps(func) | ||
| 76 | + def wrapper(*args, **kwargs) -> Any: | ||
| 77 | + last_exception = None | ||
| 78 | + | ||
| 79 | + for attempt in range(config.max_retries + 1): # +1 因为第一次不算重试 | ||
| 80 | + try: | ||
| 81 | + result = func(*args, **kwargs) | ||
| 82 | + if attempt > 0: | ||
| 83 | + logger.info(f"函数 {func.__name__} 在第 {attempt + 1} 次尝试后成功") | ||
| 84 | + return result | ||
| 85 | + | ||
| 86 | + except config.retry_on_exceptions as e: | ||
| 87 | + last_exception = e | ||
| 88 | + | ||
| 89 | + if attempt == config.max_retries: | ||
| 90 | + # 最后一次尝试也失败了 | ||
| 91 | + logger.error(f"函数 {func.__name__} 在 {config.max_retries + 1} 次尝试后仍然失败") | ||
| 92 | + logger.error(f"最终错误: {str(e)}") | ||
| 93 | + raise e | ||
| 94 | + | ||
| 95 | + # 计算延迟时间 | ||
| 96 | + delay = min( | ||
| 97 | + config.initial_delay * (config.backoff_factor ** attempt), | ||
| 98 | + config.max_delay | ||
| 99 | + ) | ||
| 100 | + | ||
| 101 | + logger.warning(f"函数 {func.__name__} 第 {attempt + 1} 次尝试失败: {str(e)}") | ||
| 102 | + logger.info(f"将在 {delay:.1f} 秒后进行第 {attempt + 2} 次尝试...") | ||
| 103 | + | ||
| 104 | + time.sleep(delay) | ||
| 105 | + | ||
| 106 | + except Exception as e: | ||
| 107 | + # 不在重试列表中的异常,直接抛出 | ||
| 108 | + logger.error(f"函数 {func.__name__} 遇到不可重试的异常: {str(e)}") | ||
| 109 | + raise e | ||
| 110 | + | ||
| 111 | + # 这里不应该到达,但作为安全网 | ||
| 112 | + if last_exception: | ||
| 113 | + raise last_exception | ||
| 114 | + | ||
| 115 | + return wrapper | ||
| 116 | + return decorator | ||
| 117 | + | ||
| 118 | +def retry_on_network_error( | ||
| 119 | + max_retries: int = 3, | ||
| 120 | + initial_delay: float = 1.0, | ||
| 121 | + backoff_factor: float = 2.0 | ||
| 122 | +): | ||
| 123 | + """ | ||
| 124 | + 专门用于网络错误的重试装饰器(简化版) | ||
| 125 | + | ||
| 126 | + Args: | ||
| 127 | + max_retries: 最大重试次数 | ||
| 128 | + initial_delay: 初始延迟秒数 | ||
| 129 | + backoff_factor: 退避因子 | ||
| 130 | + | ||
| 131 | + Returns: | ||
| 132 | + 装饰器函数 | ||
| 133 | + """ | ||
| 134 | + config = RetryConfig( | ||
| 135 | + max_retries=max_retries, | ||
| 136 | + initial_delay=initial_delay, | ||
| 137 | + backoff_factor=backoff_factor | ||
| 138 | + ) | ||
| 139 | + return with_retry(config) | ||
| 140 | + | ||
| 141 | +class RetryableError(Exception): | ||
| 142 | + """自定义的可重试异常""" | ||
| 143 | + pass | ||
| 144 | + | ||
| 145 | +def with_graceful_retry(config: RetryConfig = None, default_return=None): | ||
| 146 | + """ | ||
| 147 | + 优雅重试装饰器 - 用于非关键API调用 | ||
| 148 | + 失败后不会抛出异常,而是返回默认值,保证系统继续运行 | ||
| 149 | + | ||
| 150 | + Args: | ||
| 151 | + config: 重试配置,如果不提供则使用默认配置 | ||
| 152 | + default_return: 所有重试失败后返回的默认值 | ||
| 153 | + | ||
| 154 | + Returns: | ||
| 155 | + 装饰器函数 | ||
| 156 | + """ | ||
| 157 | + if config is None: | ||
| 158 | + config = SEARCH_API_RETRY_CONFIG | ||
| 159 | + | ||
| 160 | + def decorator(func: Callable) -> Callable: | ||
| 161 | + @wraps(func) | ||
| 162 | + def wrapper(*args, **kwargs) -> Any: | ||
| 163 | + last_exception = None | ||
| 164 | + | ||
| 165 | + for attempt in range(config.max_retries + 1): # +1 因为第一次不算重试 | ||
| 166 | + try: | ||
| 167 | + result = func(*args, **kwargs) | ||
| 168 | + if attempt > 0: | ||
| 169 | + logger.info(f"非关键API {func.__name__} 在第 {attempt + 1} 次尝试后成功") | ||
| 170 | + return result | ||
| 171 | + | ||
| 172 | + except config.retry_on_exceptions as e: | ||
| 173 | + last_exception = e | ||
| 174 | + | ||
| 175 | + if attempt == config.max_retries: | ||
| 176 | + # 最后一次尝试也失败了,返回默认值而不抛出异常 | ||
| 177 | + logger.warning(f"非关键API {func.__name__} 在 {config.max_retries + 1} 次尝试后仍然失败") | ||
| 178 | + logger.warning(f"最终错误: {str(e)}") | ||
| 179 | + logger.info(f"返回默认值以保证系统继续运行: {default_return}") | ||
| 180 | + return default_return | ||
| 181 | + | ||
| 182 | + # 计算延迟时间 | ||
| 183 | + delay = min( | ||
| 184 | + config.initial_delay * (config.backoff_factor ** attempt), | ||
| 185 | + config.max_delay | ||
| 186 | + ) | ||
| 187 | + | ||
| 188 | + logger.warning(f"非关键API {func.__name__} 第 {attempt + 1} 次尝试失败: {str(e)}") | ||
| 189 | + logger.info(f"将在 {delay:.1f} 秒后进行第 {attempt + 2} 次尝试...") | ||
| 190 | + | ||
| 191 | + time.sleep(delay) | ||
| 192 | + | ||
| 193 | + except Exception as e: | ||
| 194 | + # 不在重试列表中的异常,返回默认值 | ||
| 195 | + logger.warning(f"非关键API {func.__name__} 遇到不可重试的异常: {str(e)}") | ||
| 196 | + logger.info(f"返回默认值以保证系统继续运行: {default_return}") | ||
| 197 | + return default_return | ||
| 198 | + | ||
| 199 | + # 这里不应该到达,但作为安全网 | ||
| 200 | + return default_return | ||
| 201 | + | ||
| 202 | + return wrapper | ||
| 203 | + return decorator | ||
| 204 | + | ||
| 205 | +def make_retryable_request( | ||
| 206 | + request_func: Callable, | ||
| 207 | + *args, | ||
| 208 | + max_retries: int = 5, | ||
| 209 | + **kwargs | ||
| 210 | +) -> Any: | ||
| 211 | + """ | ||
| 212 | + 直接执行可重试的请求(不使用装饰器) | ||
| 213 | + | ||
| 214 | + Args: | ||
| 215 | + request_func: 要执行的请求函数 | ||
| 216 | + *args: 传递给请求函数的位置参数 | ||
| 217 | + max_retries: 最大重试次数 | ||
| 218 | + **kwargs: 传递给请求函数的关键字参数 | ||
| 219 | + | ||
| 220 | + Returns: | ||
| 221 | + 请求函数的返回值 | ||
| 222 | + """ | ||
| 223 | + config = RetryConfig(max_retries=max_retries) | ||
| 224 | + | ||
| 225 | + @with_retry(config) | ||
| 226 | + def _execute(): | ||
| 227 | + return request_func(*args, **kwargs) | ||
| 228 | + | ||
| 229 | + return _execute() | ||
| 230 | + | ||
| 231 | +# 预定义一些常用的重试配置 | ||
| 232 | +LLM_RETRY_CONFIG = RetryConfig( | ||
| 233 | + max_retries=5, # 增加到5次重试 | ||
| 234 | + initial_delay=3.0, # 增加初始延迟到3秒 | ||
| 235 | + backoff_factor=1.8, # 调整退避因子 | ||
| 236 | + max_delay=45.0 # 增加最大延迟 | ||
| 237 | +) | ||
| 238 | + | ||
| 239 | +SEARCH_API_RETRY_CONFIG = RetryConfig( | ||
| 240 | + max_retries=5, # 增加到5次重试 | ||
| 241 | + initial_delay=2.0, # 增加初始延迟 | ||
| 242 | + backoff_factor=1.6, # 调整退避因子 | ||
| 243 | + max_delay=25.0 # 增加最大延迟 | ||
| 244 | +) | ||
| 245 | + | ||
| 246 | +DB_RETRY_CONFIG = RetryConfig( | ||
| 247 | + max_retries=5, # 增加到5次重试 | ||
| 248 | + initial_delay=1.0, # 保持较短的数据库重试延迟 | ||
| 249 | + backoff_factor=1.5, | ||
| 250 | + max_delay=10.0 | ||
| 251 | +) |
-
Please register or login to post a comment