添加AI日志记录器支持,增强对话日志记录功能,记录请求和响应信息,包括成功和错误情况,以提高调试和监控能力。

This commit is contained in:
python 2025-12-09 14:51:33 +08:00
parent 8bebc13efe
commit 315301fc0b
5 changed files with 666 additions and 12 deletions

55
.gitignore vendored Normal file
View File

@ -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

181
services/ai_logger.py Normal file
View File

@ -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

View File

@ -18,6 +18,14 @@ except ImportError:
repair_json = None repair_json = None
print("[AI服务] 警告: json-repair库未安装将使用基础JSON修复功能。建议运行: pip install json-repair") 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: class AIService:
"""AI服务类""" """AI服务类"""
@ -40,6 +48,16 @@ class AIService:
# 确定使用的AI服务 # 确定使用的AI服务
self.ai_provider = self._determine_ai_provider() 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: def _determine_ai_provider(self) -> str:
"""确定使用的AI服务提供商仅支持华为大模型""" """确定使用的AI服务提供商仅支持华为大模型"""
@ -208,6 +226,9 @@ class AIService:
""" """
单次调用华为大模型API不包含重试逻辑 单次调用华为大模型API不包含重试逻辑
""" """
# 生成会话ID用于关联同一次调用的请求和响应
session_id = f"session_{int(time.time() * 1000)}"
payload = { payload = {
"model": self.huawei_model, "model": self.huawei_model,
"messages": [ "messages": [
@ -238,6 +259,18 @@ class AIService:
"Content-Type": "application/json" "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) enable_thinking = payload.get('enable_thinking', False)
@ -250,17 +283,32 @@ class AIService:
timeout = min(self.api_timeout, 120) # 最多120秒 timeout = min(self.api_timeout, 120) # 最多120秒
print(f"[AI服务] 思考模式未开启,使用超时时间: {timeout}") print(f"[AI服务] 思考模式未开启,使用超时时间: {timeout}")
response = requests.post( extracted_data = None
self.huawei_api_endpoint, error_message = None
json=payload,
headers=headers,
timeout=timeout
)
if response.status_code != 200: try:
raise Exception(f"API调用失败: {response.status_code} - {response.text}") response = requests.post(
self.huawei_api_endpoint,
result = response.json() 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返回的内容 # 提取AI返回的内容
if 'choices' in result and len(result['choices']) > 0: 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): # 至少有一个非空字段 if any(v for v in normalized_data.values() if v): # 至少有一个非空字段
print(f"[AI服务] 返回提取的数据(包含 {sum(1 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 return normalized_data
else: else:
print(f"[AI服务] 警告:提取的数据全部为空,但继续返回(允许部分字段为空)") 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 return normalized_data
# 如果无法提取JSON记录错误但尝试更宽容的处理 # 如果无法提取JSON记录错误但尝试更宽容的处理
@ -336,6 +404,16 @@ class AIService:
parsed_data = self._parse_text_response(content, output_fields) parsed_data = self._parse_text_response(content, output_fields)
if parsed_data and any(v for v in parsed_data.values() if v): # 至少有一个非空字段 if parsed_data and any(v for v in parsed_data.values() if v): # 至少有一个非空字段
print(f"[AI服务] 使用备用方法解析成功,提取到 {len(parsed_data)} 个字段") 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 return parsed_data
# 如果所有方法都失败,尝试最后一次修复尝试 # 如果所有方法都失败,尝试最后一次修复尝试
@ -352,6 +430,16 @@ class AIService:
normalized_data = self._normalize_field_names(extracted_data, output_fields) normalized_data = self._normalize_field_names(extracted_data, output_fields)
normalized_data = self._normalize_date_formats(normalized_data, output_fields) normalized_data = self._normalize_date_formats(normalized_data, output_fields)
normalized_data = self._post_process_inferred_fields(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 return normalized_data
except json.JSONDecodeError: except json.JSONDecodeError:
pass 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}") print(f"[AI服务] 完整内容: {content}")
# 返回一个包含所有输出字段的空字典,而不是抛出异常 # 返回一个包含所有输出字段的空字典,而不是抛出异常
empty_result = {field['field_code']: '' for field in output_fields} empty_result = {field['field_code']: '' for field in output_fields}
print(f"[AI服务] 返回空结果(包含 {len(empty_result)} 个字段,全部为空)") 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 return empty_result
else: 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]: def _extract_json_from_text(self, text: str) -> Optional[Dict]:
""" """

View File

@ -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()

View File

@ -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/` - 日志文件目录