From da7dd434dd2b3df5820bfb9f370fe4d8bd8f24ef Mon Sep 17 00:00:00 2001 From: sinohqb Date: Mon, 24 Aug 2026 01:52:05 +0800 Subject: [PATCH] =?UTF-8?q?perf(intelligent-eval):=20=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?N+1=20=E6=9F=A5=E8=AF=A2=E5=92=8C=E5=8F=82=E6=95=B0=E5=90=8D?= =?UTF-8?q?=E6=B7=B7=E6=B7=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - expire_stale_running_sessions: 使用单次 JOIN 查询替代 N+1 查询 将每个会话单独查询最后消息时间改为一次性获取所有 running 会话及其最后消息时间 - submit_report/evals_needing_analyst_nudge: 消除 session/sessions 参数名混淆 将局部变量 sessions 重命名为 eval_sessions,避免与数据库会话参数 session 混淆 这些改进提升了查询性能并增强了代码可读性。 --- .../agenteval/intelligent_eval/lifecycle.py | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/backend/agenteval/intelligent_eval/lifecycle.py b/backend/agenteval/intelligent_eval/lifecycle.py index c5324a2..9fe9ffc 100644 --- a/backend/agenteval/intelligent_eval/lifecycle.py +++ b/backend/agenteval/intelligent_eval/lifecycle.py @@ -226,8 +226,8 @@ def submit_report(session: Session, eval_id: str, report: dict[str, Any]) -> Int (不完整证据),不再阻塞报告提交。 """ repo = IntelligentEvalRepository(session) - sessions = IntelligentEvalSessionRepository(session).list_by_eval(eval_id) - if any(s.status == IntelligentEvalSessionStatus.RUNNING for s in sessions): + eval_sessions = IntelligentEvalSessionRepository(session).list_by_eval(eval_id) + if any(s.status == IntelligentEvalSessionStatus.RUNNING for s in eval_sessions): raise IntelligentEvalTransitionError("存在进行中的会话,不能提交报告") # ADR-0011:submit 边界把 scores 归一到 {overall, dimensions} 单一规范结构 if report.get("scores"): @@ -432,29 +432,36 @@ def expire_stale_running_sessions(session: Session) -> int: now = utc_now() # SQLite 读出为 naive datetime,阈值须同为 naive 才能在 Python 侧比较 threshold = now.replace(tzinfo=None) - timedelta(minutes=SESSION_IDLE_EXPIRE_MINUTES) - rows = session.exec( - select(IntelligentEvalSessionDB).where(IntelligentEvalSessionDB.status == "running") - ).all() + + # 单次查询获取所有 running 会话及其最后消息时间(避免 N+1 查询) + stmt = ( + select( + IntelligentEvalSessionDB, + func.max(IntelligentEvalMessageDB.created_at).label("last_message_at"), + ) + .outerjoin( + IntelligentEvalMessageDB, + IntelligentEvalMessageDB.session_id == IntelligentEvalSessionDB.id, + ) + .where(IntelligentEvalSessionDB.status == "running") + .group_by(IntelligentEvalSessionDB.id) + ) + rows = session.exec(stmt).all() expired = 0 - for row in rows: - last_message_at = session.exec( - select(func.max(IntelligentEvalMessageDB.created_at)).where( - IntelligentEvalMessageDB.session_id == row.id - ) - ).one() - last_activity = last_message_at or row.created_at + for session_row, last_message_at in rows: + last_activity = last_message_at or session_row.created_at if last_activity is None or last_activity >= threshold: continue - row.status = IntelligentEvalSessionStatus.EXPIRED.value - row.closed_at = now + session_row.status = IntelligentEvalSessionStatus.EXPIRED.value + session_row.closed_at = now expired += 1 append_decision_log( - row.eval_id, + session_row.eval_id, "session_expired", f"平台兜底:会话 {SESSION_IDLE_EXPIRE_MINUTES} 分钟无新轮次,置为过期(不完整证据)", "platform", - {"platform_supplemented": True, "session_id": row.id, "turn_count": row.turn_count}, + {"platform_supplemented": True, "session_id": session_row.id, "turn_count": session_row.turn_count}, session, ) @@ -674,16 +681,16 @@ def evals_needing_analyst_nudge(session: Session) -> list[str]: needing: list[str] = [] for row in executing: - sessions = session.exec( + eval_sessions = session.exec( select(IntelligentEvalSessionDB).where(IntelligentEvalSessionDB.eval_id == row.id) ).all() - if not sessions or any(s.status not in _TERMINAL_SESSION_STATUSES for s in sessions): + if not eval_sessions or any(s.status not in _TERMINAL_SESSION_STATUSES for s in eval_sessions): continue # 冒烟教训:窗口未结束且会话数未达计划时,未来时段到期后还要建会话, # 此时催促 analyst 会让报告提前收敛(漏掉后续时段的证据) - if _window_has_pending_future_slots(row, sessions, now): + if _window_has_pending_future_slots(row, eval_sessions, now): continue - closed_moments = [s.closed_at for s in sessions if s.closed_at is not None] + closed_moments = [s.closed_at for s in eval_sessions if s.closed_at is not None] if not closed_moments: continue last_closed = max(closed_moments)