2026-09-07 02:41:42 +08:00
|
|
|
|
"""记忆服务(T-06 最小闭环):Redis 会话窗口 + MySQL 权威落盘。
|
|
|
|
|
|
|
|
|
|
|
|
口径(redis-keys §2.1):`sess:{agent}:{session_id}:msgs` List,TTL 2h,
|
|
|
|
|
|
最近 N≤20 条 JSON;Redis 只保窗口、丢失可重建(miss/异常回源 MySQL,
|
|
|
|
|
|
MySQL 为权威)。L1/L2/L3 画像读写归后续任务(画像不得覆盖 L0 正式测评)。
|
|
|
|
|
|
落盘策略:「每条消息同步或 5s 内异步写 MySQL」——最小闭环同步写(api/chat
|
|
|
|
|
|
双条落库),异步化归后续优化。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
from app.config.settings import settings
|
|
|
|
|
|
from app.repository.session_repository import SessionRepository
|
|
|
|
|
|
from app.service.risk.redis_gateway import get_gateway
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
SESSION_TTL_SECONDS = 2 * 3600
|
|
|
|
|
|
WINDOW_SIZE = 20
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def window_key(agent_type: str, session_id: str) -> str:
|
|
|
|
|
|
"""redis-keys §2.1:sess:{agent}:{session_id}:msgs。"""
|
|
|
|
|
|
return f"sess:{agent_type}:{session_id}:msgs"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _session_repo() -> SessionRepository:
|
|
|
|
|
|
"""回源仓储入口(测试 monkeypatch 点,与 api 模式一致)。"""
|
|
|
|
|
|
return SessionRepository()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_recent(agent_type: str, session_id: str, limit: int = WINDOW_SIZE) -> list[dict]:
|
|
|
|
|
|
"""最近 N 轮消息([{role, content}]);Redis miss/异常回源 MySQL。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
raw = get_gateway().lrange(window_key(agent_type, session_id), -limit, -1)
|
|
|
|
|
|
if raw:
|
|
|
|
|
|
return [json.loads(item) for item in raw]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
logger.warning("session window read failed, fallback to MySQL: %s", session_id, exc_info=True)
|
|
|
|
|
|
return _session_repo().list_messages(session_id, limit=limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def append_window(
|
|
|
|
|
|
agent_type: str,
|
|
|
|
|
|
session_id: str,
|
|
|
|
|
|
messages: list[dict],
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""追加窗口(RPUSH + LTRIM 保留最近 N 条 + TTL 续期);失败降级不阻塞。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
key = window_key(agent_type, session_id)
|
|
|
|
|
|
client = get_gateway()
|
|
|
|
|
|
client.rpush(key, *[json.dumps(m, ensure_ascii=False) for m in messages])
|
|
|
|
|
|
client.ltrim(key, -WINDOW_SIZE, -1)
|
|
|
|
|
|
client.expire(key, SESSION_TTL_SECONDS)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# 窗口丢失可重建(下次读回源 MySQL),不阻塞对话主链路
|
|
|
|
|
|
logger.warning("session window append failed (degrade): %s", session_id, exc_info=True)
|