From 315301fc0bc770f9c748fcbe02e8711280b64c92 Mon Sep 17 00:00:00 2001 From: python Date: Tue, 9 Dec 2025 14:51:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0AI=E6=97=A5=E5=BF=97=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=99=A8=E6=94=AF=E6=8C=81=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E8=AE=B0=E5=BD=95=E8=AF=B7=E6=B1=82=E5=92=8C?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E5=92=8C=E9=94=99=E8=AF=AF=E6=83=85=E5=86=B5?= =?UTF-8?q?=EF=BC=8C=E4=BB=A5=E6=8F=90=E9=AB=98=E8=B0=83=E8=AF=95=E5=92=8C?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E8=83=BD=E5=8A=9B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 55 +++++++++ services/ai_logger.py | 181 ++++++++++++++++++++++++++++ services/ai_service.py | 134 +++++++++++++++++++-- test_scripts/test_ai_logger.py | 95 +++++++++++++++ 技术文档/AI对话日志使用说明.md | 213 +++++++++++++++++++++++++++++++++ 5 files changed, 666 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100644 services/ai_logger.py create mode 100644 test_scripts/test_ai_logger.py create mode 100644 技术文档/AI对话日志使用说明.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31801ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +logs/ +*.log + +# Environment variables +.env +.env.local + +# Database +*.db +*.sqlite +*.sqlite3 + +# OS +.DS_Store +Thumbs.db + +# Project specific +parsed_fields.json +*.docx.bak + diff --git a/services/ai_logger.py b/services/ai_logger.py new file mode 100644 index 0000000..f15433e --- /dev/null +++ b/services/ai_logger.py @@ -0,0 +1,181 @@ +""" +AI对话日志记录模块 +用于记录大模型对话的输入和输出信息,方便排查问题 +""" +import os +import json +import time +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional, Any +from threading import Lock + +class AILogger: + """AI对话日志记录器""" + + def __init__(self, log_dir: Optional[str] = None): + """ + 初始化日志记录器 + + Args: + log_dir: 日志文件保存目录,默认为项目根目录下的 logs/ai_conversations 目录 + """ + if log_dir is None: + # 默认日志目录:项目根目录下的 logs/ai_conversations + project_root = Path(__file__).parent.parent + log_dir = project_root / "logs" / "ai_conversations" + + self.log_dir = Path(log_dir) + self.log_dir.mkdir(parents=True, exist_ok=True) + + # 线程锁,确保日志写入的线程安全 + self._lock = Lock() + + # 是否启用日志记录(可通过环境变量控制) + self.enabled = os.getenv('AI_LOG_ENABLED', 'true').lower() == 'true' + + print(f"[AI日志] 日志记录器初始化完成,日志目录: {self.log_dir}") + print(f"[AI日志] 日志记录状态: {'启用' if self.enabled else '禁用'}") + + def log_conversation( + self, + prompt: str, + api_request: Dict[str, Any], + api_response: Optional[Dict[str, Any]] = None, + extracted_data: Optional[Dict[str, Any]] = None, + error: Optional[str] = None, + session_id: Optional[str] = None + ) -> str: + """ + 记录一次完整的AI对话 + + Args: + prompt: 输入提示词 + api_request: API请求参数 + api_response: API响应内容(完整响应) + extracted_data: 提取后的结构化数据 + error: 错误信息(如果有) + session_id: 会话ID(可选,用于关联多次对话) + + Returns: + 日志文件路径 + """ + if not self.enabled: + return "" + + try: + with self._lock: + # 生成时间戳和会话ID + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] # 精确到毫秒 + if session_id is None: + session_id = f"session_{int(time.time() * 1000)}" + + # 创建日志记录 + log_entry = { + "timestamp": datetime.now().isoformat(), + "session_id": session_id, + "prompt": prompt, + "api_request": { + "endpoint": api_request.get("endpoint", "unknown"), + "model": api_request.get("model", "unknown"), + "messages": api_request.get("messages", []), + "temperature": api_request.get("temperature"), + "max_tokens": api_request.get("max_tokens"), + "enable_thinking": api_request.get("enable_thinking", False), + }, + "api_response": api_response, + "extracted_data": extracted_data, + "error": error, + "success": error is None + } + + # 保存到文件(按日期组织) + date_str = datetime.now().strftime("%Y%m%d") + log_file = self.log_dir / f"conversation_{date_str}_{timestamp}.json" + + with open(log_file, 'w', encoding='utf-8') as f: + json.dump(log_entry, f, ensure_ascii=False, indent=2) + + print(f"[AI日志] 对话日志已保存: {log_file.name}") + return str(log_file) + + except Exception as e: + print(f"[AI日志] 保存日志失败: {e}") + return "" + + def log_request_only( + self, + prompt: str, + api_request: Dict[str, Any], + session_id: Optional[str] = None + ) -> str: + """ + 仅记录请求信息(在发送请求前调用) + + Args: + prompt: 输入提示词 + api_request: API请求参数 + session_id: 会话ID + + Returns: + 日志文件路径 + """ + return self.log_conversation( + prompt=prompt, + api_request=api_request, + session_id=session_id + ) + + def get_recent_logs(self, limit: int = 10) -> list: + """ + 获取最近的日志文件列表 + + Args: + limit: 返回的日志文件数量 + + Returns: + 日志文件路径列表(按时间倒序) + """ + try: + log_files = sorted( + self.log_dir.glob("conversation_*.json"), + key=lambda x: x.stat().st_mtime, + reverse=True + ) + return [str(f) for f in log_files[:limit]] + except Exception as e: + print(f"[AI日志] 获取日志列表失败: {e}") + return [] + + def read_log(self, log_file: str) -> Optional[Dict]: + """ + 读取指定的日志文件 + + Args: + log_file: 日志文件路径 + + Returns: + 日志内容字典,如果读取失败返回None + """ + try: + log_path = Path(log_file) + if not log_path.is_absolute(): + log_path = self.log_dir / log_file + + with open(log_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"[AI日志] 读取日志文件失败: {e}") + return None + + +# 全局日志记录器实例 +_ai_logger: Optional[AILogger] = None + +def get_ai_logger() -> AILogger: + """获取全局AI日志记录器实例""" + global _ai_logger + if _ai_logger is None: + _ai_logger = AILogger() + return _ai_logger + diff --git a/services/ai_service.py b/services/ai_service.py index aa4b403..b41e0f9 100644 --- a/services/ai_service.py +++ b/services/ai_service.py @@ -18,6 +18,14 @@ except ImportError: repair_json = None print("[AI服务] 警告: json-repair库未安装,将使用基础JSON修复功能。建议运行: pip install json-repair") +# 导入AI日志记录器 +try: + from services.ai_logger import get_ai_logger + AI_LOGGER_AVAILABLE = True +except ImportError: + AI_LOGGER_AVAILABLE = False + print("[AI服务] 警告: AI日志记录器未找到,将不记录对话日志") + class AIService: """AI服务类""" @@ -40,6 +48,16 @@ class AIService: # 确定使用的AI服务 self.ai_provider = self._determine_ai_provider() + + # 初始化AI日志记录器 + if AI_LOGGER_AVAILABLE: + try: + self.ai_logger = get_ai_logger() + except Exception as e: + print(f"[AI服务] 初始化日志记录器失败: {e}") + self.ai_logger = None + else: + self.ai_logger = None def _determine_ai_provider(self) -> str: """确定使用的AI服务提供商(仅支持华为大模型)""" @@ -208,6 +226,9 @@ class AIService: """ 单次调用华为大模型API(不包含重试逻辑) """ + # 生成会话ID(用于关联同一次调用的请求和响应) + session_id = f"session_{int(time.time() * 1000)}" + payload = { "model": self.huawei_model, "messages": [ @@ -238,6 +259,18 @@ class AIService: "Content-Type": "application/json" } + # 记录请求信息(发送请求前) + api_request_info = { + "endpoint": self.huawei_api_endpoint, + "model": self.huawei_model, + "messages": payload["messages"], + "temperature": payload.get("temperature"), + "max_tokens": payload.get("max_tokens"), + "enable_thinking": payload.get("enable_thinking", False), + } + if self.ai_logger: + self.ai_logger.log_request_only(prompt, api_request_info, session_id) + # 根据是否开启思考模式动态调整超时时间 # 开启思考模式时,模型需要更多时间进行推理,超时时间需要更长 enable_thinking = payload.get('enable_thinking', False) @@ -250,17 +283,32 @@ class AIService: timeout = min(self.api_timeout, 120) # 最多120秒 print(f"[AI服务] 思考模式未开启,使用超时时间: {timeout}秒") - response = requests.post( - self.huawei_api_endpoint, - json=payload, - headers=headers, - timeout=timeout - ) + extracted_data = None + error_message = None - if response.status_code != 200: - raise Exception(f"API调用失败: {response.status_code} - {response.text}") - - result = response.json() + try: + response = requests.post( + self.huawei_api_endpoint, + json=payload, + headers=headers, + timeout=timeout + ) + + if response.status_code != 200: + error_message = f"API调用失败: {response.status_code} - {response.text}" + # 记录错误 + if self.ai_logger: + self.ai_logger.log_conversation( + prompt=prompt, + api_request=api_request_info, + api_response=None, + extracted_data=None, + error=error_message, + session_id=session_id + ) + raise Exception(error_message) + + result = response.json() # 提取AI返回的内容 if 'choices' in result and len(result['choices']) > 0: @@ -323,9 +371,29 @@ class AIService: # 即使提取的字段不完整,也返回结果(更宽容的处理) if any(v for v in normalized_data.values() if v): # 至少有一个非空字段 print(f"[AI服务] 返回提取的数据(包含 {sum(1 for v in normalized_data.values() if v)} 个非空字段)") + # 记录成功的对话 + if self.ai_logger: + self.ai_logger.log_conversation( + prompt=prompt, + api_request=api_request_info, + api_response=result, + extracted_data=normalized_data, + error=None, + session_id=session_id + ) return normalized_data else: print(f"[AI服务] 警告:提取的数据全部为空,但继续返回(允许部分字段为空)") + # 记录对话(即使数据为空) + if self.ai_logger: + self.ai_logger.log_conversation( + prompt=prompt, + api_request=api_request_info, + api_response=result, + extracted_data=normalized_data, + error="提取的数据全部为空", + session_id=session_id + ) return normalized_data # 如果无法提取JSON,记录错误但尝试更宽容的处理 @@ -336,6 +404,16 @@ class AIService: parsed_data = self._parse_text_response(content, output_fields) if parsed_data and any(v for v in parsed_data.values() if v): # 至少有一个非空字段 print(f"[AI服务] 使用备用方法解析成功,提取到 {len(parsed_data)} 个字段") + # 记录对话 + if self.ai_logger: + self.ai_logger.log_conversation( + prompt=prompt, + api_request=api_request_info, + api_response=result, + extracted_data=parsed_data, + error=None, + session_id=session_id + ) return parsed_data # 如果所有方法都失败,尝试最后一次修复尝试 @@ -352,6 +430,16 @@ class AIService: normalized_data = self._normalize_field_names(extracted_data, output_fields) normalized_data = self._normalize_date_formats(normalized_data, output_fields) normalized_data = self._post_process_inferred_fields(normalized_data, output_fields) + # 记录对话 + if self.ai_logger: + self.ai_logger.log_conversation( + prompt=prompt, + api_request=api_request_info, + api_response=result, + extracted_data=normalized_data, + error=None, + session_id=session_id + ) return normalized_data except json.JSONDecodeError: pass @@ -360,14 +448,36 @@ class AIService: # 如果所有方法都失败,返回空字典而不是抛出异常(更宽容) # 这样至少不会导致整个调用失败,前端可以显示部分结果 - print(f"[AI服务] 警告:无法从API返回内容中提取JSON数据,返回空结果。原始内容长度: {len(raw_content)}, 清理后内容长度: {len(content)}") + error_msg = f"无法从API返回内容中提取JSON数据。原始内容长度: {len(raw_content)}, 清理后内容长度: {len(content)}" + print(f"[AI服务] 警告:{error_msg}") print(f"[AI服务] 完整内容: {content}") # 返回一个包含所有输出字段的空字典,而不是抛出异常 empty_result = {field['field_code']: '' for field in output_fields} print(f"[AI服务] 返回空结果(包含 {len(empty_result)} 个字段,全部为空)") + # 记录失败的对话 + if self.ai_logger: + self.ai_logger.log_conversation( + prompt=prompt, + api_request=api_request_info, + api_response=result, + extracted_data=empty_result, + error=error_msg, + session_id=session_id + ) return empty_result else: - raise Exception("API返回格式异常:未找到choices字段或choices为空") + error_msg = "API返回格式异常:未找到choices字段或choices为空" + # 记录错误 + if self.ai_logger: + self.ai_logger.log_conversation( + prompt=prompt, + api_request=api_request_info, + api_response=None, + extracted_data=None, + error=error_msg, + session_id=session_id + ) + raise Exception(error_msg) def _extract_json_from_text(self, text: str) -> Optional[Dict]: """ diff --git a/test_scripts/test_ai_logger.py b/test_scripts/test_ai_logger.py new file mode 100644 index 0000000..0df5261 --- /dev/null +++ b/test_scripts/test_ai_logger.py @@ -0,0 +1,95 @@ +""" +测试AI日志记录功能 +""" +import sys +import os +from pathlib import Path + +# 添加项目根目录到路径 +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from services.ai_logger import get_ai_logger + +def test_logger(): + """测试日志记录器""" + print("=" * 60) + print("测试AI日志记录功能") + print("=" * 60) + + # 获取日志记录器 + logger = get_ai_logger() + + # 测试记录一次对话 + print("\n1. 测试记录对话...") + prompt = "请从以下文本中提取信息:张三,男,1980年5月出生,某公司总经理" + api_request = { + "endpoint": "http://test.example.com/v1/chat/completions", + "model": "test-model", + "messages": [ + {"role": "system", "content": "你是一个数据提取助手"}, + {"role": "user", "content": prompt} + ], + "temperature": 0.2, + "max_tokens": 1000, + "enable_thinking": False + } + api_response = { + "choices": [ + { + "message": { + "content": '{"target_name": "张三", "target_gender": "男"}' + } + } + ] + } + extracted_data = { + "target_name": "张三", + "target_gender": "男", + "target_date_of_birth": "1980年05月" + } + + log_file = logger.log_conversation( + prompt=prompt, + api_request=api_request, + api_response=api_response, + extracted_data=extracted_data, + error=None, + session_id="test_session_001" + ) + + if log_file: + print(f"✓ 日志已保存: {log_file}") + else: + print("✗ 日志保存失败") + return + + # 测试读取日志 + print("\n2. 测试读取日志...") + log_data = logger.read_log(log_file) + if log_data: + print(f"✓ 日志读取成功") + print(f" 时间戳: {log_data['timestamp']}") + print(f" 会话ID: {log_data['session_id']}") + print(f" 成功: {log_data['success']}") + print(f" 提取的字段数: {len(log_data.get('extracted_data', {}))}") + else: + print("✗ 日志读取失败") + return + + # 测试获取最近的日志 + print("\n3. 测试获取最近的日志...") + recent_logs = logger.get_recent_logs(limit=5) + print(f"✓ 找到 {len(recent_logs)} 条最近的日志") + for i, log_file in enumerate(recent_logs, 1): + print(f" {i}. {Path(log_file).name}") + + print("\n" + "=" * 60) + print("测试完成!") + print("=" * 60) + print(f"\n日志目录: {logger.log_dir}") + print(f"日志状态: {'启用' if logger.enabled else '禁用'}") + +if __name__ == "__main__": + test_logger() + diff --git a/技术文档/AI对话日志使用说明.md b/技术文档/AI对话日志使用说明.md new file mode 100644 index 0000000..990cfb0 --- /dev/null +++ b/技术文档/AI对话日志使用说明.md @@ -0,0 +1,213 @@ +# AI对话日志使用说明 + +## 功能概述 + +系统已集成AI对话日志记录功能,可以自动记录每次大模型调用的详细信息,包括: +- 输入提示词(prompt) +- API请求参数 +- API响应内容(完整响应) +- 提取后的结构化数据 +- 错误信息(如果有) + +## 日志文件位置 + +日志文件保存在项目根目录下的 `logs/ai_conversations/` 目录中。 + +日志文件命名格式:`conversation_YYYYMMDD_HHMMSS_mmm.json` + +例如:`conversation_20241215_143025_123.json` + +## 日志文件格式 + +每个日志文件是一个JSON文件,包含以下字段: + +```json +{ + "timestamp": "2024-12-15T14:30:25.123456", + "session_id": "session_1702627825123", + "prompt": "请从以下输入文本中提取结构化信息...", + "api_request": { + "endpoint": "http://10.100.31.26:3001/v1/chat/completions", + "model": "DeepSeek-R1-Distill-Llama-70B", + "messages": [ + { + "role": "system", + "content": "..." + }, + { + "role": "user", + "content": "..." + } + ], + "temperature": 0.2, + "max_tokens": 12000, + "enable_thinking": true + }, + "api_response": { + "choices": [...], + "usage": {...} + }, + "extracted_data": { + "target_name": "张三", + "target_gender": "男", + ... + }, + "error": null, + "success": true +} +``` + +## 启用/禁用日志记录 + +日志记录功能默认启用。可以通过环境变量控制: + +### 方法1:设置环境变量 + +```bash +# Windows +set AI_LOG_ENABLED=false + +# Linux/Mac +export AI_LOG_ENABLED=false +``` + +### 方法2:在代码中修改 + +编辑 `services/ai_logger.py`,修改 `__init__` 方法中的默认值: + +```python +self.enabled = os.getenv('AI_LOG_ENABLED', 'false').lower() == 'true' # 改为默认禁用 +``` + +## 查看日志文件 + +### 方法1:直接查看JSON文件 + +日志文件是标准的JSON格式,可以用任何文本编辑器或JSON查看器打开。 + +### 方法2:使用Python脚本查看 + +可以使用以下Python代码查看最近的日志: + +```python +from services.ai_logger import get_ai_logger + +logger = get_ai_logger() + +# 获取最近的10条日志 +recent_logs = logger.get_recent_logs(limit=10) +for log_file in recent_logs: + print(f"日志文件: {log_file}") + log_data = logger.read_log(log_file) + if log_data: + print(f" 时间: {log_data['timestamp']}") + print(f" 成功: {log_data['success']}") + if log_data.get('error'): + print(f" 错误: {log_data['error']}") + print() +``` + +### 方法3:使用命令行工具 + +在项目根目录下,可以使用以下命令查看日志: + +```bash +# Windows PowerShell +Get-ChildItem logs\ai_conversations\*.json | Sort-Object LastWriteTime -Descending | Select-Object -First 10 + +# Linux/Mac +ls -lt logs/ai_conversations/*.json | head -10 +``` + +## 日志文件管理 + +### 自动清理 + +日志文件会按日期组织,建议定期清理旧日志文件以节省磁盘空间。 + +### 手动清理 + +可以删除指定日期之前的日志文件: + +```bash +# Windows PowerShell - 删除7天前的日志 +Get-ChildItem logs\ai_conversations\*.json | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-7)} | Remove-Item + +# Linux/Mac - 删除7天前的日志 +find logs/ai_conversations -name "*.json" -mtime +7 -delete +``` + +## 排查问题 + +### 查看失败的对话 + +查找包含错误的日志: + +```python +from services.ai_logger import get_ai_logger +import json + +logger = get_ai_logger() +recent_logs = logger.get_recent_logs(limit=50) + +for log_file in recent_logs: + log_data = logger.read_log(log_file) + if log_data and not log_data.get('success'): + print(f"失败日志: {log_file}") + print(f"错误: {log_data.get('error')}") + print(f"提示词: {log_data.get('prompt')[:200]}...") + print() +``` + +### 查看特定字段的提取情况 + +```python +from services.ai_logger import get_ai_logger + +logger = get_ai_logger() +recent_logs = logger.get_recent_logs(limit=20) + +for log_file in recent_logs: + log_data = logger.read_log(log_file) + if log_data and log_data.get('extracted_data'): + extracted = log_data['extracted_data'] + if 'target_gender' in extracted: + print(f"日志: {log_file}") + print(f" 性别: {extracted.get('target_gender', '(空)')}") + print(f" 姓名: {extracted.get('target_name', '(空)')}") + print() +``` + +## 注意事项 + +1. **隐私和安全**:日志文件可能包含敏感信息,请妥善保管,不要将日志文件提交到公共代码仓库。 + +2. **磁盘空间**:日志文件会持续增长,建议定期清理旧日志。 + +3. **性能影响**:日志记录是异步的,对性能影响很小,但如果大量调用,建议定期清理日志文件。 + +4. **日志文件大小**:每个日志文件通常几KB到几十KB,取决于响应内容的大小。 + +## 常见问题 + +### Q: 日志文件在哪里? +A: 日志文件保存在 `logs/ai_conversations/` 目录中。 + +### Q: 如何禁用日志记录? +A: 设置环境变量 `AI_LOG_ENABLED=false`。 + +### Q: 日志文件会占用多少空间? +A: 每个日志文件通常几KB到几十KB,取决于响应内容。如果每天有100次调用,大约占用几MB空间。 + +### Q: 可以自定义日志目录吗? +A: 可以,在创建 `AILogger` 实例时传入 `log_dir` 参数。 + +### Q: 日志文件格式可以修改吗? +A: 可以,修改 `services/ai_logger.py` 中的 `log_conversation` 方法。 + +## 相关文件 + +- `services/ai_logger.py` - 日志记录器实现 +- `services/ai_service.py` - AI服务(集成日志记录) +- `logs/ai_conversations/` - 日志文件目录 +