避开这5个坑!LangChain对话历史管理最佳实践指南
最近在帮几个团队重构他们的LangChain对话应用,发现一个挺有意思的现象:大家花了很多心思在模型选型、提示工程上,却往往在对话历史管理这个“基础设施”上栽跟头。我见过最离谱的一个案例,一个客服系统上线第一天就出了事故——所有用户的对话历史全混在一起了,客服机器人对着张三喊李四的名字,场面一度十分尴尬。
对话历史管理听起来简单,不就是存几句话嘛。但真到了生产环境,你会发现这里面门道不少。session_id怎么设计才合理?数据库写入失败怎么优雅处理?中文内容存进去变成乱码怎么办?这些问题不解决,你的对话系统就像建在沙滩上的城堡,看起来漂亮,一涨潮就垮。
今天我就结合自己踩过的坑和实战经验,聊聊LangChain对话历史管理中最容易出问题的五个地方。这些不是理论推导,而是真金白银换来的教训。如果你正在用LangChain构建对话系统,特别是已经遇到了一些奇怪的问题,这篇文章应该能帮你少走不少弯路。
1. 第一个坑:session_id设计不当导致记忆混乱
session_id是对话历史的灵魂,它决定了“谁跟谁在聊天”。但很多开发者在这里犯了两个典型错误:要么用随机生成的值,要么用不够唯一的标识符。
我见过一个教育类应用,他们用学生的user_id作为session_id。听起来合理对吧?问题来了,同一个学生在手机和电脑上同时登录,两个设备的对话历史就混在一起了。更糟的是,如果学生退出后重新登录,系统生成了新的session_id,之前的对话历史就找不到了。
1.1 session_id的最佳实践
一个健壮的session_id应该包含多个维度信息。下面这个表格对比了几种常见方案:
| 方案 | 示例 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 单一用户ID | user_123 | 简单 | 多设备冲突 | 单设备应用 |
| 用户+设备 | user_123_web | 区分设备 | 会话不连续 | 多设备但无需同步 |
| 用户+会话类型 | user_123_customer_service | 区分对话场景 | 实现稍复杂 | 多功能聊天机器人 |
| 复合键 | user_123_20250115_chat | 高度隔离 | 存储冗余 | 需要严格隔离的场景 |
在实际项目中,我推荐使用第三种方案。它既保证了同一用户在不同场景下的对话隔离,又能在需要时实现历史共享。下面是一个具体的实现:
from datetime import datetime
from typing import Optional
from langchain_community.chat_message_histories import SQLChatMessageHistory
def generate_session_id(
user_id: str,
conversation_type: str,
additional_context: Optional[str] = None
) -> str:
"""生成结构化的session_id"""
date_str = datetime.now().strftime("%Y%m%d")
base_id = f"{user_id}_{conversation_type}_{date_str}"
if additional_context:
# 移除特殊字符,避免存储问题
safe_context = "".join(c for c in additional_context if c.isalnum() or c in "_-")
return f"{base_id}_{safe_context}"
return base_id
def get_session_history(session_id: str):
"""获取指定session的历史记录"""
return SQLChatMessageHistory(
session_id=session_id,
connection_string="sqlite:///conversations.db"
)
# 使用示例
user_id = "u1001"
conversation_type = "tech_support"
session_id = generate_session_id(user_id, conversation_type, "issue_network")
print(f"生成的session_id: {session_id}")
# 输出: u1001_tech_support_20250115_issue_network
1.2 动态session_id管理
有些场景下,你可能需要更灵活的session_id管理。比如一个客服系统,用户可能从网页聊天转到电话客服,再转回网页。这时候需要能合并或关联不同的session。
class SessionManager:
def __init__(self):
self.session_mapping = {} # 主session到子session的映射
def create_main_session(self, user_id: str) -> str:
"""创建主会话"""
main_session = f"main_{user_id}_{datetime.now().timestamp()}"
self.session_mapping[main_session] = []
return main_session
def add_sub_session(self, main_session: str, sub_type: str) -> str:
"""为主会话添加子会话"""
if main_session not in self.session_mapping:
raise ValueError(f"主会话 {main_session} 不存在")
sub_session = f"{main_session}_{sub_type}_{len(self.session_mapping[main_session])}"
self.session_mapping[main_session].append(sub_session)
return sub_session
def get_related_sessions(self, session_id: str) -> list:
"""获取所有相关会话"""
# 如果是主会话,返回所有子会话
if session_id in self.session_mapping:
return [session_id] + self.session_mapping[session_id]
# 如果是子会话,找到对应的主会话
for main_session, sub_sessions in self.session_mapping.items():
if session_id in sub_sessions:
return [main_session] + sub_sessions
return [session_id]
# 使用示例
manager = SessionManager()
main_session = manager.create_main_session("u1001")
web_session = manager.add_sub_session(main_session, "web")
phone_session = manager.add_sub_session(main_session, "phone")
print(f"主会话: {main_session}")
print(f"相关会话: {manager.get_related_sessions(web_session)}")
注意:session_id的设计要提前规划好。一旦上线后想改,迁移历史数据会非常痛苦。建议在项目初期就设计好扩展方案,预留足够的字段。
2. 第二个坑:数据库写入失败与权限问题
这是生产环境最常见的问题之一。开发时用内存数据库一切正常,一上线就各种写入失败。我遇到过最诡异的一次是,代码在本地跑得好好的,部署到服务器后历史记录就是存不进去,查了半天才发现是数据库文件权限问题。
2.1 数据库连接的安全处理
先看一个典型的错误示例:
# ❌ 不安全的写法
def get_session_history(session_id: str):
return SQLChatMessageHistory(
session_id=session_id,
connection_string="sqlite:///chat_history.db"
)
这个写法有几个问题:
- 没有错误处理
- 没有连接池管理
- 没有重试机制
- 硬编码数据库路径
下面是一个改进版本:
import sqlite3
from contextlib import contextmanager
from typing import Generator, Optional
from langchain_community.chat_message_histories import SQLChatMessageHistory
class DatabaseManager:
def __init__(self, db_path: str = "chat_history.db"):
self.db_path = db_path
self._init_database()
def _init_database(self):
"""初始化数据库,确保表结构存在"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建消息表
cursor.execute('''
CREATE TABLE IF NOT EXISTS message_store (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
message TEXT NOT NULL,
type TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建索引提高查询性能
cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON message_store(session_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_timestamp ON message_store(timestamp)')
conn.commit()
conn.close()
print(f"数据库初始化完成: {self.db_path}")
except sqlite3.Error as e:
print(f"数据库初始化失败: {e}")
# 这里可以根据需要抛出异常或使用fallback方案
raise
@contextmanager
def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
"""获取数据库连接(带错误处理)"""
conn = None
try:
conn = sqlite3.connect(self.db_path)
conn.execute("PRAGMA journal_mode=WAL") # 启用WAL模式提高并发性能
conn.execute("PRAGMA synchronous=NORMAL") # 平衡性能和数据安全
yield conn
conn.commit()
except sqlite3.Error as e:
if conn:
conn.rollback()
print(f"数据库操作失败: {e}")
# 这里可以添加重试逻辑或fallback到内存存储
raise
finally:
if conn:
conn.close()
def get_session_history(self, session_id: str, max_retries: int = 3) -> SQLChatMessageHistory:
"""获取会话历史(带重试机制)"""
for attempt in range(max_retries):
try:
# 测试数据库连接
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT 1")
# 连接正常,返回历史对象
return SQLChatMessageHistory(
session_id=session_id,
connection_string=f"sqlite:///{self.db_path}"
)
except Exception as e:
if attempt == max_retries - 1:
print(f"数据库连接失败,已重试{max_retries}次: {e}")
# 最后一次重试失败,使用内存fallback
from langchain_community.chat_message_histories import ChatMessageHistory
return ChatMessageHistory()
print(f"数据库连接失败,第{attempt + 1}次重试: {e}")
import time
time.sleep(1 * (attempt + 1)) # 指数退避
def check_health(self) -> dict:
"""检查数据库健康状态"""
try:
with self.get_connection() as conn:
cursor = conn.cursor()
# 检查表是否存在
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='message_store'")
table_exists = cursor.fetchone() is not None
# 检查记录数量
cursor.execute("SELECT COUNT(*) FROM message_store")
record_count = cursor.fetchone()[0]
# 检查最近写入
cursor.execute("SELECT MAX(timestamp) FROM message_store")
last_write = cursor.fetchone()[0]
return {
"status": "healthy",
"table_exists": table_exists,
"record_count": record_count,
"last_write": last_write,
"database": self.db_path
}
except Exception as e:
return {
"status": "unhealthy",
"error": str(e),
"database": self.db_path
}
# 使用示例
db_manager = DatabaseManager("production.db")
# 检查数据库状态
health = db_manager.check_health()
print(f"数据库状态: {health}")
# 获取会话历史(自动处理连接问题)
history = db_manager.get_session_history("test_session")
2.2 写入失败的优雅降级
即使有了完善的错误处理,数据库还是可能暂时不可用。这时候需要有降级方案:
from enum import Enum
from typing import Union
from dataclasses import dataclass
from datetime import datetime
class StorageType(Enum):
MEMORY = "memory"
DATABASE = "database"
FILE = "file"
@dataclass
class StorageStatus:
type: StorageType
healthy: bool
last_check: datetime
error: Optional[str] = None
class FallbackMessageHistory:
def __init__(self, primary_storage, fallback_storage):
self.primary = primary_storage
self.fallback = fallback_storage
self.current_storage = primary_storage
self.storage_status = {
"primary": StorageStatus(StorageType.DATABASE, True, datetime.now()),
"fallback": StorageStatus(StorageType.MEMORY, True, datetime.now())
}
def add_message(self, message):
"""添加消息,自动降级"""
try:
self.current_storage.add_message(message)
# 如果当前是fallback但primary恢复了,尝试同步
if self.current_storage == self.fallback and self._check_primary_health():
self._sync_to_primary()
self.current_storage = self.primary
except Exception as e:
print(f"主存储写入失败,切换到备用存储: {e}")
self.storage_status["primary"].healthy = False
self.storage_status["primary"].error = str(e)
self.storage_status["primary"].last_check = datetime.now()
# 切换到fallback
self.current_storage = self.fallback
self.fallback.add_message(message)
def _check_primary_health(self) -> bool:
"""检查主存储是否恢复"""
try:
# 简单的健康检查,比如执行一个查询
# 这里需要根据实际存储类型实现
return True
except:
return False
def _sync_to_primary(self):
"""将fallback中的数据同步到primary"""
try:
fallback_messages = self.fallback.messages
for msg in fallback_messages:
self.primary.add_message(msg)
self.fallback.clear()
print("数据同步完成")
except Exception as e:
print(f"数据同步失败: {e}")
# 使用示例
from langchain_community.chat_message_histories import (
SQLChatMessageHistory,
ChatMessageHistory
)
primary_storage = SQLChatMessageHistory(
session_id="test",
connection_string="sqlite:///chat.db"
)
fallback_storage = ChatMessageHistory()
fallback_history = FallbackMessageHistory(primary_storage, fallback_storage)
# 添加消息时会自动处理存储失败
fallback_history.add_message("测试消息")
提示:生产环境中,建议定期检查数据库健康状态,并设置监控告警。可以记录以下指标:写入成功率、平均响应时间、连接池使用率、磁盘空间等。
3. 第三个坑:中文与特殊字符处理
这个问题特别容易在团队协作时出现。开发者A用英文测试一切正常,开发者B用中文测试发现存进去的是乱码。更麻烦的是,有些特殊字符(如emoji、数学符号)可能导致数据库操作失败。
3.1 编码问题的根本解决
先看一个会产生乱码的示例:
# ❌ 可能导致乱码的写法
history = SQLChatMessageHistory(
session_id="test",
connection_string="sqlite:///chat.db" # 缺少字符集指定
)
SQLite默认使用UTF-8,但有些情况下(特别是Windows环境)可能不是。最保险的做法是显式指定:
# ✅ 正确的写法
history = SQLChatMessageHistory(
session_id="test",
connection_string="sqlite:///chat.db?charset=utf8"
)
但仅仅这样还不够。我们还需要处理消息内容的编码:
import json
import base64
from typing import Any, Dict
from langchain_core.messages import BaseMessage
class SafeMessageEncoder:
"""安全的消息编码器"""
@staticmethod
def encode_message(message: BaseMessage) -> Dict[str, Any]:
"""编码消息为可安全存储的格式"""
message_dict = message.dict()
# 处理内容字段
if 'content' in message_dict and message_dict['content']:
content = message_dict['content']
# 检查内容类型
if isinstance(content, str):
# 字符串内容,确保是UTF-8
message_dict['content'] = content.encode('utf-8').decode('utf-8')
elif isinstance(content, list):
# 多模态内容(如图片、文件)
encoded_content = []
for item in content:
if isinstance(item, dict) and 'type' in item:
if item['type'] == 'text':
item['text'] = item.get('text', '').encode('utf-8').decode('utf-8')
elif item['type'] in ['image', 'file']:
# 二进制内容需要base64编码
if 'data' in item and item['data']:
if isinstance(item['data'], bytes):
item['data'] = base64.b64encode(item['data']).decode('ascii')
item['encoding'] = 'base64'
encoded_content.append(item)
message_dict['content'] = encoded_content
# 处理其他可能包含特殊字符的字段
for key, value in message_dict.items():
if isinstance(value, str):
# 移除或替换控制字符
message_dict[key] = SafeMessageEncoder._sanitize_string(value)
return message_dict
@staticmethod
def decode_message(message_dict: Dict[str, Any]) -> Dict[str, Any]:
"""解码存储的消息"""
if 'content' in message_dict:
content = message_dict['content']
if isinstance(content, str):
# 已经是解码后的字符串
pass
elif isinstance(content, list):
decoded_content = []
for item in content:
if isinstance(item, dict):
if item.get('encoding') == 'base64' and 'data' in item:
# 解码base64内容
item['data'] = base64.b64decode(item['data'])
del item['encoding']
decoded_content.append(item)
message_dict['content'] = decoded_content
return message_dict
@staticmethod
def _sanitize_string(text: str) -> str:
"""清理字符串中的问题字符"""
if not text:
return text
# 移除控制字符(除了换行和制表符)
import re
# 保留:\n (换行), \t (制表符), \r (回车)
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
# 处理零宽字符
text = re.sub(r'[\u200b-\u200f\u202a-\u202e]', '', text)
# 替换不安全的Unicode字符
replacements = {
'\ufeff': '', # BOM标记
'\ufffe': '', # 非字符
'\uffff': '', # 非字符
}
for old, new in replacements.items():
text = text.replace(old, new)
return text
@staticmethod
def validate_encoding(text: str) -> bool:
"""验证字符串编码是否有效"""
try:
# 尝试用UTF-8编码再解码
encoded = text.encode('utf-8')
decoded = encoded.decode('utf-8')
# 检查是否 round-trip 成功
return decoded == text
except (UnicodeEncodeError, UnicodeDecodeError):
return False
# 使用示例
from langchain_core.messages import HumanMessage
# 创建包含特殊字符的消息
problematic_message = HumanMessage(
content="Hello\u200bWorld\x00测试🎉", # 包含零宽字符和空字符
additional_kwargs={"metadata": "特殊测试"}
)
# 编码消息
encoder = SafeMessageEncoder()
encoded = encoder.encode_message(problematic_message)
print(f"原始内容: {problematic_message.content}")
print(f"编码后: {encoded['content']}")
print(f"编码有效: {encoder.validate_encoding(encoded['content'])}")
# 保存到数据库
import json
safe_json = json.dumps(encoded, ensure_ascii=False)
print(f"JSON安全: {safe_json}")
3.2 数据库层面的字符集配置
不同的数据库需要不同的字符集配置。下面是一个多数据库支持的配置类:
from typing import Dict, Any
from enum import Enum
class DatabaseType(Enum):
SQLITE = "sqlite"
POSTGRESQL = "postgresql"
MYSQL = "mysql"
REDIS = "redis"
class DatabaseCharsetConfig:
"""数据库字符集配置"""
DEFAULT_CONFIGS = {
DatabaseType.SQLITE: {
"charset_param": "charset=utf8",
"collation": None,
"create_table_sql": """
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
content TEXT NOT NULL COLLATE NOCASE,
message_type TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
},
DatabaseType.POSTGRESQL: {
"charset_param": "client_encoding=UTF8",
"collation": "en_US.UTF-8",
"create_table_sql": """
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
message_type VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_content_encoding
CHECK (content ~ '^[\\x00-\\x7F]*$' OR content ~ '^[\\x80-\\xFF]*$')
)
"""
},
DatabaseType.MYSQL: {
"charset_param": "charset=utf8mb4",
"collation": "utf8mb4_unicode_ci",
"create_table_sql": """
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
message_type VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
"""
}
}
@classmethod
def get_connection_string(cls, db_type: DatabaseType, **kwargs) -> str:
"""获取带字符集配置的连接字符串"""
config = cls.DEFAULT_CONFIGS.get(db_type, {})
if db_type == DatabaseType.SQLITE:
db_path = kwargs.get("db_path", "chat.db")
charset = config.get("charset_param", "")
return f"sqlite:///{db_path}?{charset}" if charset else f"sqlite:///{db_path}"
elif db_type == DatabaseType.POSTGRESQL:
host = kwargs.get("host", "localhost")
port = kwargs.get("port", 5432)
database = kwargs.get("database", "chatdb")
username = kwargs.get("username", "postgres")
password = kwargs.get("password", "")
charset = config.get("charset_param", "")
return f"postgresql://{username}:{password}@{host}:{port}/{database}?{charset}"
elif db_type == DatabaseType.MYSQL:
host = kwargs.get("host", "localhost")
port = kwargs.get("port", 3306)
database = kwargs.get("database", "chatdb")
username = kwargs.get("username", "root")
password = kwargs.get("password", "")
charset = config.get("charset_param", "")
return f"mysql+pymysql://{username}:{password}@{host}:{port}/{database}?{charset}"
else:
raise ValueError(f"不支持的数据库类型: {db_type}")
@classmethod
def get_create_table_sql(cls, db_type: DatabaseType) -> str:
"""获取建表SQL"""
return cls.DEFAULT_CONFIGS.get(db_type, {}).get("create_table_sql", "")
# 使用示例
# SQLite配置
sqlite_conn = DatabaseCharsetConfig.get_connection_string(
DatabaseType.SQLITE,
db_path="chinese_chat.db"
)
print(f"SQLite连接字符串: {sqlite_conn}")
# MySQL配置(支持emoji)
mysql_conn = DatabaseCharsetConfig.get_connection_string(
DatabaseType.MYSQL,
host="localhost",
database="chat_app",
username="app_user",
password="secure_password"
)
print(f"MySQL连接字符串: {mysql_conn}")
# 获取建表语句
create_sql = DatabaseCharsetConfig.get_create_table_sql(DatabaseType.MYSQL)
print(f"MySQL建表语句:\n{create_sql}")
3.3 实战:处理用户输入中的各种字符
用户可能输入任何内容,我们需要一个统一的处理流程:
import re
from typing import Optional, Tuple
class InputSanitizer:
"""用户输入 sanitizer"""
# 定义允许的字符范围(根据应用需求调整)
ALLOWED_CHAR_RANGES = [
(0x0020, 0x007E), # 基本拉丁字母
(0x00A0, 0x00FF), # 拉丁字母补充
(0x0100, 0x017F), # 拉丁字母扩展-A
(0x0180, 0x024F), # 拉丁字母扩展-B
(0x0370, 0x03FF), # 希腊字母
(0x0400, 0x04FF), # 西里尔字母
(0x4E00, 0x9FFF), # 中日韩统一表意文字
(0x3400, 0x4DBF), # 中日韩统一表意文字扩展A
(0x20000, 0x2A6DF), # 中日韩统一表意文字扩展B
(0x1F300, 0x1F5FF), # 杂项符号和象形文字
(0x1F600, 0x1F64F), # 表情符号
(0x1F900, 0x1F9FF), # 补充符号和象形文字
]
# 需要特别处理的字符
SPECIAL_HANDLING = {
'\u0000': '[NULL]', # 空字符
'\u0007': '[BELL]', # 响铃
'\u0008': '[BACKSPACE]', # 退格
'\u000B': '[VTAB]', # 垂直制表符
'\u000C': '[FF]', # 换页符
'\u001B': '[ESC]', # 退出
'\u007F': '[DEL]', # 删除
'\u2028': '[LS]', # 行分隔符
'\u2029': '[PS]', # 段落分隔符
}
@classmethod
def sanitize(cls, text: str, max_length: int = 10000) -> Tuple[str, Optional[str]]:
"""
清理用户输入
返回:
Tuple[清理后的文本, 错误信息或None]
"""
if not text:
return "", None
# 检查长度
if len(text) > max_length:
return text[:max_length], f"文本超过{max_length}字符,已截断"
# 处理特殊字符
cleaned_parts = []
issues = []
for char in text:
if char in cls.SPECIAL_HANDLING:
cleaned_parts.append(cls.SPECIAL_HANDLING[char])
issues.append(f"特殊字符替换: {repr(char)} -> {cls.SPECIAL_HANDLING[char]}")
continue
# 检查字符是否在允许范围内
char_code = ord(char)
allowed = False
for start, end in cls.ALLOWED_CHAR_RANGES:
if start <= char_code <= end:
allowed = True
break
if allowed:
cleaned_parts.append(char)
else:
# 不在允许范围内的字符,用Unicode转义表示
cleaned_parts.append(f"[U+{char_code:04X}]")
issues.append(f"非常规字符: {repr(char)} (U+{char_code:04X})")
cleaned_text = ''.join(cleaned_parts)
# 检查编码有效性
try:
cleaned_text.encode('utf-8')
except UnicodeEncodeError as e:
error_msg = f"编码失败: {str(e)}"
# 尝试更激进的清理
cleaned_text = cleaned_text.encode('utf-8', errors='replace').decode('utf-8')
issues.append(error_msg)
# 合并连续的空格和换行
cleaned_text = re.sub(r'\s+', ' ', cleaned_text)
cleaned_text = re.sub(r'\n{3,}', '\n\n', cleaned_text)
error_msg = '; '.join(issues) if issues else None
return cleaned_text.strip(), error_msg
@classmethod
def validate_for_storage(cls, text: str) -> Dict[str, Any]:
"""验证文本是否适合存储"""
sanitized, error = cls.sanitize(text)
result = {
"original_length": len(text),
"sanitized_length": len(sanitized),
"is_valid": error is None or "截断" in error,
"sanitized_text": sanitized,
"issues": []
}
if error:
result["issues"] = error.split('; ')
# 检查是否有潜在的危险模式
dangerous_patterns = [
(r'<script[^>]*>.*?</script>', '脚本标签'),
(r'on\w+\s*=', '事件处理器'),
(r'javascript:', 'JavaScript协议'),
(r'data:', '数据协议'),
(r'vbscript:', 'VBScript协议'),
]
for pattern, description in dangerous_patterns:
if re.search(pattern, text, re.IGNORECASE):
result["issues"].append(f"检测到潜在危险内容: {description}")
result["is_valid"] = False
return result
# 使用示例
test_inputs = [
"正常的文本",
"Hello\x00World", # 包含空字符
"测试🎉表情", # 包含emoji
"<script>alert('xss')</script>", # XSS尝试
"A" * 15000, # 超长文本
"混合内容:Hello 世界 🌍 测试",
]
for test in test_inputs:
result = InputSanitizer.validate_for_storage(test)
print(f"输入: {test[:50]}...")
print(f" 有效: {result['is_valid']}")
print(f" 问题: {result['issues']}")
print(f" 处理后长度: {result['sanitized_length']}")
print("-" * 50)
注意:字符处理策略需要根据具体应用场景调整。比如,一个国际化的客服系统需要支持更多语言字符,而一个内部工具可能可以限制字符范围。关键是要有明确的策略并记录日志,这样出问题时可以追溯。
4. 第四个坑:历史长度管理与性能衰减
随着对话进行,历史记录会越来越长。这不仅影响性能(每次都要加载大量数据),还可能超过模型的上下文窗口限制。我见过一个系统,运行几个月后响应时间从200ms增加到5秒,一查发现每次都要加载上万条历史记录。
4.1 智能历史截断策略
简单的固定窗口截断(只保留最近N条)可能丢失重要信息。我们需要更智能的策略:
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from dataclasses import dataclass
from enum import Enum
from langchain_core.messages import BaseMessage
class TruncationStrategy(Enum):
"""截断策略"""
FIXED_WINDOW = "fixed_window" # 固定条数
TIME_WINDOW = "time_window" # 时间窗口
TOKEN_LIMIT = "token_limit" # Token限制
SEMANTIC = "semantic" # 语义重要性
HYBRID = "hybrid" # 混合策略
@dataclass
class TruncationConfig:
"""截断配置"""
strategy: TruncationStrategy
max_items: Optional[int] = None # 最大条数
max_tokens: Optional[int] = None # 最大token数
time_window_hours: Optional[int] = None # 时间窗口(小时)
keep_important: bool = True # 是否保留重要消息
importance_keywords: List[str] = None # 重要关键词
def __post_init__(self):
if self.importance_keywords is None:
self.importance_keywords = ["名字", "姓名", "地址", "电话", "重要", "记住"]
class SmartHistoryTruncator:
"""智能历史截断器"""
def __init__(self, config: TruncationConfig):
self.config = config
def truncate(self, messages: List[BaseMessage]) -> List[BaseMessage]:
"""根据策略截断消息"""
if not messages:
return messages
if self.config.strategy == TruncationStrategy.FIXED_WINDOW:
return self._fixed_window_truncate(messages)
elif self.config.strategy == TruncationStrategy.TIME_WINDOW:
return self._time_window_truncate(messages)
elif self.config.strategy == TruncationStrategy.TOKEN_LIMIT:
return self._token_limit_truncate(messages)
elif self.config.strategy == TruncationStrategy.SEMANTIC:
return self._semantic_truncate(messages)
elif self.config.strategy == TruncationStrategy.HYBRID:
return self._hybrid_truncate(messages)
else:
return messages
def _fixed_window_truncate(self, messages: List[BaseMessage]) -> List[BaseMessage]:
"""固定窗口截断"""
if self.config.max_items is None or len(messages) <= self.config.max_items:
return messages
if self.config.keep_important:
# 分离重要和普通消息
important, regular = self._categorize_messages(messages)
# 保留所有重要消息
# 从普通消息中取最近的补足到max_items
keep_count = self.config.max_items - len(important)
if keep_count > 0:
recent_regular = regular[-keep_count:]
return important + recent_regular
else:
# 如果重要消息已经超过限制,只保留最重要的
return important[:self.config.max_items]
else:
# 简单截断
return messages[-self.config.max_items:]
def _time_window_truncate(self, messages: List[BaseMessage]) -> List[BaseMessage]:
"""时间窗口截断"""
if self.config.time_window_hours is None:
return messages
cutoff_time = datetime.now() - timedelta(hours=self.config.time_window_hours)
filtered = []
for msg in messages:
# 这里需要根据实际的消息时间戳字段调整
# 假设消息有metadata包含时间戳
msg_time = self._extract_message_time(msg)
if msg_time and msg_time >= cutoff_time:
filtered.append(msg)
elif not msg_time:
# 没有时间戳的消息,根据策略决定是否保留
if self.config.keep_important and self._is_important_message(msg):
filtered.append(msg)
return filtered
def _token_limit_truncate(self, messages: List[BaseMessage]) -> List[BaseMessage]:
"""Token限制截断"""
if self.config.max_tokens is None:
return messages
# 估算token数(这里简化处理,实际应该用tokenizer)
total_tokens = 0
kept_messages = []
# 先保留重要消息
if self.config.keep_important:
important, regular = self._categorize_messages(messages)
for msg in important:
msg_tokens = self._estimate_tokens(msg)
if total_tokens + msg_tokens <= self.config.max_tokens:
kept_messages.append(msg)
total_tokens += msg_tokens
# 从后往前添加普通消息(最近的优先)
for msg in reversed(regular):
if total_tokens >= self.config.max_tokens:
break
msg_tokens = self._estimate_tokens(msg)
if total_tokens + msg_tokens <= self.config.max_tokens:
kept_messages.insert(len(important), msg)
total_tokens += msg_tokens
else:
# 从后往前添加(最近的优先)
for msg in reversed(messages):
msg_tokens = self._estimate_tokens(msg)
if total_tokens + msg_tokens <= self.config.max_tokens:
kept_messages.insert(0, msg)
total_tokens += msg_tokens
return kept_messages
def _semantic_truncate(self, messages: List[BaseMessage]) -> List[BaseMessage]:
"""基于语义重要性的截断"""
# 这里可以实现更复杂的语义分析
# 例如:使用嵌入计算消息重要性,保留与当前对话最相关的消息
# 简化版:基于规则的重要性判断
important_messages = []
regular_messages = []
for msg in messages:
if self._is_important_message(msg):
important_messages.append(msg)
else:
regular_messages.append(msg)
# 保留所有重要消息 + 最近的部分普通消息
max_regular = max(0, (self.config.max_items or 50) - len(important_messages))
recent_regular = regular_messages[-max_regular:] if max_regular > 0 else []
return important_messages + recent_regular
def _hybrid_truncate(self, messages: List[BaseMessage]) -> List[BaseMessage]:
"""混合策略截断"""
# 先用时间窗口过滤
time_filtered = self._time_window_truncate(messages)
# 再用token限制
token_config = TruncationConfig(
strategy=TruncationStrategy.TOKEN_LIMIT,
max_tokens=self.config.max_tokens,
keep_important=self.config.keep_important,
importance_keywords=self.config.importance_keywords
)
token_truncator = SmartHistoryTruncator(token_config)
return token_truncator.truncate(time_filtered)
def _categorize_messages(self, messages: List[BaseMessage]) -> tuple:
"""将消息分为重要和普通"""
important = []
regular = []
for msg in messages:
if self._is_important_message(msg):
important.append(msg)
else:
regular.append(msg)
return important, regular
def _is_important_message(self, message: BaseMessage) -> bool:
"""判断消息是否重要"""
content = self._get_message_content(message)
if not content:
return False
content_lower = content.lower()
# 检查是否包含重要关键词
for keyword in self.config.importance_keywords:
if keyword in content_lower:
return True
# 检查是否是系统消息或包含用户信息
if hasattr(message, 'type'):
if message.type in ['system', 'user_info']:
return True
# 检查是否包含明显的个人信息模式
personal_patterns = [
r'我叫[^\s,。,.!!??]{1,10}',
r'姓名是[^\s,。,.!!??]{1,10}',
r'电话是\d{7,15}',
r'邮箱是[^\s@]+@[^\s@]+\.[^\s@]+',
r'地址是[^\s,。,.!!??]{5,50}',
]
for pattern in personal_patterns:
if re.search(pattern, content):
return True
return False
def _get_message_content(self, message: BaseMessage) -> str:
"""提取消息内容"""
if hasattr(message, 'content'):
if isinstance(message.content, str):
return message.content
elif isinstance(message.content, list):
# 提取文本部分
texts = []
for item in message.content:
if isinstance(item, dict) and item.get('type') == 'text':
texts.append(item.get('text', ''))
return ' '.join(texts)
return ''
def _extract_message_time(self, message: BaseMessage) -> Optional[datetime]:
"""提取消息时间"""
# 这里需要根据实际的消息结构调整
if hasattr(message, 'additional_kwargs'):
metadata = message.additional_kwargs.get('metadata', {})
if isinstance(metadata, dict):
timestamp = metadata.get('timestamp')
if timestamp:
if isinstance(timestamp, (int, float)):
return datetime.fromtimestamp(timestamp)
elif isinstance(timestamp, str):
try:
return datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
except:
pass
return None
def _estimate_tokens(self, message: BaseMessage) -> int:
"""估算消息的token数(简化版)"""
content = self._get_message_content(message)
if not content:
return 0
# 简单估算:英文约4字符=1token,中文约1字符=2token
english_chars = sum(1 for c in content if ord(c) < 128)
chinese_chars = len(content) - english_chars
return (english_chars // 4) + (chinese_chars * 2)
# 使用示例
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
# 创建测试消息
test_messages = [
SystemMessage(content="你是一个客服助手", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 3600*24} # 1天前
}),
HumanMessage(content="你好,我叫张三", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 3600*2} # 2小时前
}),
AIMessage(content="你好张三,有什么可以帮您?", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 3600*1.5}
}),
HumanMessage(content="我想查询订单状态", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 3600*1}
}),
AIMessage(content="请提供订单号", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 1800}
}),
HumanMessage(content="订单号是123456", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 900}
}),
AIMessage(content="订单正在处理中", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 600}
}),
HumanMessage(content="谢谢", additional_kwargs={
"metadata": {"timestamp": datetime.now().timestamp() - 300}
}),
]
# 测试不同截断策略
configs = [
TruncationConfig(strategy=TruncationStrategy.FIXED_WINDOW, max_items=3),
TruncationConfig(strategy=TruncationStrategy.TIME_WINDOW, time_window_hours=1),
TruncationConfig(strategy=TruncationStrategy.TOKEN_LIMIT, max_tokens=50),
TruncationConfig(strategy=TruncationStrategy.SEMANTIC, keep_important=True),
TruncationConfig(strategy=TruncationStrategy.HYBRID, max_tokens=100, time_window_hours=2),
]
for i, config in enumerate(configs):
truncator = SmartHistoryTruncator(config)
truncated = truncator.truncate(test_messages)
print(f"\n策略 {i+1}: {config.strategy.value}")
print(f"原始消息数: {len(test_messages)}")
print(f"截断后消息数: {len(truncated)}")
for msg in truncated:
content = truncator._get_message_content(msg)[:30]
print(f" - {msg.__class__.__name__}: {content}...")
4.2 历史摘要与压缩
对于非常长的对话,我们可以生成摘要而不是保存所有消息:
from typing import List, Dict, Any
from dataclasses import dataclass
from datetime import datetime
@dataclass
class ConversationSummary:
"""对话摘要"""
summary_id: str
original_message_count: int
summary_text: str
key_points: List[str]
participants: List[str]
start_time: datetime
end_time: datetime
metadata: Dict[str, Any]
def to_dict(self) -> Dict[str, Any]:
return {
"summary_id": self.summary_id,
"original_message_count": self.original_message_count,
"summary_text": self.summary_text,
"key_points": self.key_points,
"participants": self.participants,
"start_time": self.start_time.isoformat(),
"end_time": self.end_time.isoformat(),
"metadata": self.metadata
}
class ConversationSummarizer:
"""对话摘要生成器"""
def __init__(self, llm=None):
self.llm = llm
def summarize_conversation(
self,
messages: List[BaseMessage],
max_summary_length: int = 500
) -> ConversationSummary:
"""生成对话摘要"""
# 提取对话信息
participants = self._extract_participants(messages)
time_range = self._extract_time_range(messages)
# 生成摘要文本
if self.llm:
summary_text = self._llm_summarize(messages, max_summary_length)
else:
summary_text = self._rule_based_summarize(messages, max_summary_length)
# 提取关键点
key_points = self._extract_key_points(messages)
# 生成摘要ID
summary_id = self._generate_summary_id(messages)
return ConversationSummary(
summary_id=summary_id,
original_message_count=len(messages),
summary_text=summary_text,
key_points=key_points,
participants=participants,
start_time=time_range[0],
end_time=time_range[1],
metadata=self._extract_metadata(messages)
)
def _extract_participants(self, messages: List[BaseMessage]) -> List[str]:
"""提取参与者"""
participants = set()
for msg in messages:
if hasattr(msg, 'name') and msg.name:
participants.add(msg.name)
elif hasattr(msg, 'type'):
participants.add(msg.type)
return list(participants)
def _extract_time_range(self, messages: List[BaseMessage]) -> tuple:
"""提取时间范围"""
timestamps = []
for msg in messages:
msg_time = self._extract_message_time(msg)
if msg_time:
timestamps.append(msg_time)
if timestamps:
return min(timestamps), max(timestamps)
else:
return datetime.now(), datetime.now()
def _llm_summarize(self, messages: List[BaseMessage], max_length: int) -> str:
"""使用LLM生成摘要"""
# 这里需要实际的LLM调用
# 简化版:返回模拟摘要
return f"对话包含{len(messages)}条消息,讨论了多个主题。"
def _rule_based_summarize(self, messages: List[BaseMessage], max_length: int) -> str:
"""基于规则的摘要生成"""
if len(messages) <= 3:
return "简短的对话交流。"
# 提取开头和结尾
start_msgs = messages[:2]
end_msgs = messages[-2:] if len(messages) > 4 else messages[-1:]
start_summary = self._summarize_message_group(start_msgs)
end_summary = self._summarize_message_group(end_msgs)
# 统计主题
topics = self._extract_topics(messages)
summary_parts = []
if start_summary:
summary_parts.append(f"对话开始于{start_summary}")
if topics:
summary_parts.append(f"讨论了{len(topics)}个主要主题")
if end_summary:
summary_parts.append(f"以{end_summary}结束")
summary = ",".join(summary_parts) + "。"
# 截断到最大长度
if len(summary) > max_length:
summary = summary[:max_length-3] + "..."
return summary
def _summarize_message_group(self, messages: List[BaseMessage]) -> str:
"""摘要消息组"""
if not messages:
return ""
actions = []
for msg in messages:
content = self._get_message_content(msg)
if content:
# 简单提取动作
if any(word in content for word in ["问", "询问", "想知道"]):
actions.append("提问")
elif any(word in content for word in ["回答", "回复", "说"]):
actions.append("回答")
elif any(word in content for word in ["提供", "给出", "发送"]):
actions.append("提供信息")
if actions:
return "、".join(set(actions))
else:
return "交流"
def _extract_key_points(self, messages: List[BaseMessage]) -> List[str]:
"""提取关键点"""
key_points = []
for msg in messages:
content = self._get_message_content(msg)
if not content:
continue
# 检查是否包含关键信息
key_indicators = [
("决定", ["决定", "确定", "选择"]),
("同意", ["同意", "认可", "接受"]),
("拒绝", ["拒绝", "不同意", "反对"]),
("问题", ["问题", "疑问", "困惑"]),
("解决", ["解决", "完成", "处理好"]),
("信息", ["电话", "邮箱", "地址", "订单号"]),
]
for point_type, keywords in key_indicators:
if any(keyword in content for keyword in keywords):
# 提取相关句子
sentences = re.split(r'[。!?!?]', content)
for sentence in sentences:
if any(keyword in sentence for keyword in keywords):
key_point = f"{point_type}: {sentence.strip()[:50]}..."
if key_point not in key_points:
key_points.append(key_point)
return key_points[:5] # 最多返回5个关键点
def _extract_topics(self, messages: List[BaseMessage]) -> List[str]:
"""提取主题"""
# 简单基于关键词的主题识别
topic_keywords = {
"订单": ["订单", "购买", "付款", "发货"],
"账户": ["账户", "登录", "注册", "密码"],
"产品": ["产品", "商品", "规格", "价格"],
"服务": ["客服", "支持", "帮助", "咨询"],
"技术": ["技术", "错误", "bug", "故障"],
}
topic_counts = {topic: 0 for topic in topic_keywords}
for msg in messages:
content = self._get_message_content(msg)
if content:
for topic, keywords in topic_keywords.items():
if any(keyword in content for keyword in keywords):
topic_counts[topic] += 1
# 返回出现次数最多的主题
sorted_topics = sorted(topic_counts.items(), key=lambda x: x[1], reverse=True)
return [topic for topic, count in sorted_topics if count > 0]
def _generate_summary_id(self, messages: List[BaseMessage]) -> str:
"""生成摘要ID"""
if not messages:
return "empty"
# 使用第一条和最后一条消息的时间戳
first_time = self._extract_message_time(messages[0])
last_time = self._extract_message_time(messages[-1])
if first_time and last_time:
first_str = first_time.strftime("%Y%m%d%H%M")
last_str = last_time.strftime("%H%M")
return f"summary_{first_str}_{last_str}"
else:
return f"summary_{datetime.now().strftime('%Y%m%d%H%M%S')}"
def _extract_metadata(self, messages: List[BaseMessage]) -> Dict[str, Any]:
"""提取元数据"""
metadata = {
"message_types": {},
"has_attachments": False,
"has_important_info": False,
}
for msg in messages:
# 统计消息类型
msg_type = msg.__class__.__name__
metadata["message_types"][msg_type] = metadata["message_types"].get(msg_type, 0) + 1
# 检查附件
if hasattr(msg, 'additional_kwargs'):
if 'attachments' in msg.additional_kwargs:
metadata["has_attachments"] = True
# 检查重要信息
content = self._get_message_content(msg)
if content and any(keyword in content for keyword in ["重要", "紧急", "尽快", "立即"]):
metadata["has_important_info"] = True
return metadata
# 辅助方法(与前面相同)
def _get_message_content(self, message: BaseMessage) -> str:
if hasattr(message, 'content'):
if isinstance(message.content, str):
return message.content
return ''
def _extract_message_time(self, message: BaseMessage) -> Optional[datetime]:
if hasattr(message, 'additional_kwargs'):
metadata = message.additional_kwargs.get('metadata', {})
if isinstance(metadata, dict):
timestamp = metadata.get('timestamp')
if timestamp:
if isinstance(timestamp, (int, float)):
return datetime.fromtimestamp(timestamp)
return None
# 使用示例
summarizer = ConversationSummarizer()
# 生成摘要
summary = summarizer.summarize_conversation(test_messages)
print("对话摘要:")
print(f"ID: {summary.summary_id}")
print(f"原始消息数: {summary.original_message_count}")
print(f"摘要: {summary.summary_text}")
print(f"关键点: {summary.key_points}")
print(f"参与者: {summary.participants}")
print(f"时间范围: {summary.start_time} 到 {summary.end_time}")
print(f"元数据: {summary.metadata}")
# 保存摘要
summary_dict = summary.to_dict()
print(f"\n摘要字典: {summary_dict}")
4.3 性能监控与自动清理
建立监控机制,自动发现和处理性能问题:
import time
from typing import Dict, List, Optional
from dataclasses import dataclass
from datetime import datetime, timedelta
from collections import defaultdict
import threading
@dataclass
class PerformanceMetrics:
"""性能指标"""
session_id: str
message_count: int
load_time_ms: float
save_time_ms: float
total_size_bytes: int
avg_message_size: float
timestamp: datetime
def is_problematic(self, thresholds: Dict) -> bool:
"""检查是否达到问题阈值"""
problems = []
if self.message_count > thresholds.get('max_messages', 1000):
problems.append(f"消息数过多: {self.message_count}")
if self.load_time_ms > thresholds.get('max_load_time_ms', 1000):
problems.append(f"加载时间过长: {self.load_time_ms}ms")
if self.total_size_bytes > thresholds.get('max_size_bytes', 10*1024*1024): # 10MB
problems.append(f"总大小过大: {self.total_size_bytes/1024/1024:.2f}MB")
return len(problems) > 0, problems
class HistoryPerformanceMonitor:
"""历史性能监控器"""
def __init__(self, cleanup_thresholds: Optional[Dict] = None):
self.metrics_history = defaultdict(list)
self.cleanup_thresholds = cleanup_thresholds or {
'max_messages': 500,
'max_load_time_ms': 500,
'max_size_bytes': 5*1024*1024, # 5MB
'max_age_days': 30,
}
self.lock = threading.Lock()
self.cleanup_thread = None
self.running = False
def start_monitoring(self, interval_seconds: int = 300):
"""启动监控线程"""
self.running = True
self.cleanup_thread = threading.Thread(
target=self._monitor_loop,
args=(interval_seconds,),
daemon=True
)
self.cleanup_thread.start()
print(f"性能监控已启动,检查间隔: {interval_seconds}秒")
def stop_monitoring(self):
"""停止监控"""
self.running = False
if self.cleanup_thread:
self.cleanup_thread.join(timeout=5)
def _monitor_loop(self, interval_seconds: int):
"""监控循环"""
while self.running:
time.sleep(interval_seconds)
self._check_and_cleanup()
def _check_and_cleanup(self):
"""检查并清理问题会话"""
with self.lock:
now = datetime.now()
sessions_to_cleanup = []
# 分析所有会话的指标
for session_id, metrics_list in self.metrics_history.items():
if not metrics_list:
continue
# 取最近一次指标
latest_metrics = metrics_list[-1]
# 检查是否达到清理阈值
is_problematic, problems = latest_metrics.is_problematic(self.cleanup_thresholds)
# 检查是否过期
age = (now - latest_metrics.timestamp).days
is_expired = age > self.cleanup_thresholds.get('max_age_days', 30)
if is_problematic or is_expired:
sessions_to_cleanup.append({
'session_id': session_id,
'reason': '性能问题' if is_problematic else '过期',
'problems': problems if is_problematic else [f"过期{age}天"],
'metrics': latest_metrics
})
# 执行清理
for session_info in sessions_to_cleanup:
self._cleanup_session(session_info)
def _cleanup_session(self, session_info: Dict):
"""清理问题会话"""
session_id = session_info['session_id']
reason = session_info['reason']
problems = session_info['problems']
metrics = session_info['metrics']
print(f"\n[清理会话] {session_id}")
print(f" 原因: {reason}")
print(f" 问题: {problems}")
print(f" 指标: {metrics.message_count}条消息, "
f"{metrics.load_time_ms:.1f}ms加载, "
f"{metrics.total_size_bytes/1024:.1f}KB大小")
# 这里实现实际的清理逻辑
# 例如:生成摘要、归档、删除等
try:
# 示例:生成摘要后删除详细历史
self._archive_session(session_id, metrics)
print(f" 状态: 已归档")
except Exception as e:
print(f" 错误: 清理失败 - {e}")
def _archive_session(self, session_id: str, metrics: PerformanceMetrics):
"""归档会话"""
# 这里实现归档逻辑
# 例如:保存摘要到另一个数据库,删除原始消息
pass
def record_metrics(self, session_id: str, metrics: PerformanceMetrics):
"""记录性能指标"""
with self.lock:
self.metrics_history[session_id].append(metrics)
# 只保留最近100次记录
if len(self.metrics_history[session_id]) > 100:
self.metrics_history[session_id] = self.metrics_history[session_id][-100:]
def get_session_report(self, session_id: str) -> Optional[Dict]:
"""获取会话报告"""
with self.lock:
if session_id not in self.metrics_history:
return None
metrics_list = self.metrics_history[session_id]
if not metrics_list:
return None
latest = metrics_list[-1]
is_problematic, problems = latest.is_problematic(self.cleanup_thresholds)
# 计算趋势
if len(metrics_list) >= 2:
oldest = metrics_list[0]
growth_rate = (latest.message_count - oldest.message_count) / len(metrics_list)
else:
growth_rate = 0
return {
'session_id': session_id,
'current_metrics': latest,
'is_problematic': is_problematic,
'problems': problems,
'history_count': len(metrics_list),
'growth_rate_per_record': growth_rate,
'suggested_action': self._suggest_action(latest, problems)
}
def _suggest_action(self, metrics: PerformanceMetrics, problems: List[str]) -> str:
"""建议处理动作"""
if not problems:
return "无需操作"
suggestions = []
if "消息数过多" in str(problems):
suggestions.append("启用自动截断(如只保留最近100条)")
if "加载时间过长" in str(problems):
suggestions.append("优化查询索引或启用分页加载")
if "总大小过大" in str(problems):
suggestions.append("压缩历史或迁移到归档存储")
return ";".join(suggestions) if suggestions else "监控观察"
# 使用示例
monitor = HistoryPerformanceMonitor()
# 模拟记录指标
test_metrics = PerformanceMetrics(
session_id="test_session_1",
message_count=1200, # 超过阈值
load_time_ms=1500, # 超过阈值
save_time_ms=200,
total_size_bytes=8*1024*1024, # 8MB
avg_message_size=7000,
timestamp=datetime.now()
)
monitor.record_metrics("test_session_1", test_metrics)
# 获取报告
report = monitor.get_session_report("test_session_1")
if report:
print("会话性能报告:")
print(f"会话ID: {report['session_id']}")
print(f"是否有问题: {report['is_problematic']}")
print(f"具体问题: {report['problems']}")
print(f"建议操作: {report['suggested_action']}")
# 启动监控
monitor.start_monitoring(interval_seconds=10)
# 模拟运行一段时间
print("\n监控已启动,等待检查...")
time.sleep(15)
# 停止监控
monitor.stop_monitoring()
提示:历史管理策略需要根据实际业务需求调整。一个客服系统可能需要保留较长的完整历史,而一个闲聊机器人可能只需要最近几轮对话。关键是要有明确的策略和监控,避免问题积累。
5. 第五个坑:多用户并发与数据隔离
最后一个但同样重要的坑是并发访问。当多个用户同时使用系统时,如果没有处理好数据隔离和并发控制,可能会出现数据错乱、丢失甚至安全问题。
5.1 线程安全的会话管理
import threading
import uuid
from typing import Dict, Optional, Set
from datetime import datetime, timedelta
from dataclasses import dataclass
from contextlib import contextmanager
@dataclass
class SessionInfo:
"""会话信息"""
session_id: str
user_id: str
created_at: datetime
last_accessed: datetime
message_count: int
is_active: bool
lock: threading.RLock # 可重入锁,允许同一线程多次获取
def update_access(self):
"""更新访问时间"""
self.last_accessed = datetime.now()
def increment_message_count(self):
"""增加消息计数"""
self.message_count += 1
class ConcurrentSessionManager:
"""并发会话管理器"""
def __init__(self, session_timeout_minutes: int = 30):
self.sessions: Dict[str, SessionInfo] = {}
self.user_sessions: Dict[str, Set[str]] = defaultdict(set) # 用户到会话的映射
self.lock = threading.RLock()
self.session_timeout = timedelta(minutes=session_timeout_minutes)
self.cleanup_thread = None
self.running = False
def create_session(self, user_id: str, existing_session_id: Optional[str] = None) -> str:
"""创建新会话或恢复现有会话"""
with self.lock:
# 如果提供了现有session_id且有效,则恢复它
if existing_session_id and existing_session_id in self.sessions:
session = self.sessions[existing_session_id]
if session.user_id == user_id and session.is_active:
session.update_access()
return existing_session_id
# 创建新会话
session_id = self._generate_session_id(user_id)
session = SessionInfo(
session_id=session_id,
user_id=user_id,
created_at=datetime.now(),
last_accessed=datetime.now(),
message_count=0,
is_active=True,
lock=threading.RLock()
)
self.sessions[session_id] = session
self.user_sessions[user_id].add(session_id)
print(f"创建新会话: {session_id} (用户: {user_id
507



被折叠的 条评论
为什么被折叠?



