"""知识库消费指标(交付报告 P2-3)。 三个核心指标,全部基于既有 KnowledgeUsage / Segment.references / Segment 落表, 不引入新的数据采集: - 命中率 hit_rate:生产会话中拿到知识片段的比例(检索到片段 ÷ 检索请求) - 重复消费率 repeat_rate:同一 Agent 反复使用同一段落导致重新开放的比例 - 引用覆盖率 reference_coverage:交付段中带引用来源的比例 """ from __future__ import annotations from sqlmodel import Session, select from .models import Agent, Campaign, KnowledgeBase, KnowledgeUsage, Segment def kb_metrics(session: Session, campaign_id: int = 0, *, owner_user_id: int | None = None, organization_id: int | None = None, kb_ids: set[int] | None = None) -> dict: """返回平台级(campaign_id=0)或单任务的消费质量指标。""" usage_rows = session.exec(select(KnowledgeUsage)).all() if campaign_id: usage_rows = [row for row in usage_rows if row.campaign_id == campaign_id] if kb_ids is not None: usage_rows = [row for row in usage_rows if int(row.kb_id or 0) in kb_ids] if organization_id is not None or owner_user_id is not None: campaign_ids = {int(row.campaign_id or 0) for row in usage_rows if row.campaign_id} campaigns = {item.id: item for item in session.exec( select(Campaign).where(Campaign.id.in_(campaign_ids))).all()} if campaign_ids else {} usage_rows = [row for row in usage_rows if ( campaigns.get(row.campaign_id) is not None and (organization_id is None or int(campaigns[row.campaign_id].organization_id or 1) == int(organization_id)) and (owner_user_id is None or int(campaigns[row.campaign_id].owner_user_id or 0) == int(owner_user_id)) )] # ---- 命中率:reserved/consumed(含已释放后再次使用)都算命中 ---- # 一个 (agent_id, segment_id) 组合代表一次生产检索请求。 requests: dict[tuple[int, int], list[KnowledgeUsage]] = {} for row in usage_rows: if row.segment_id: requests.setdefault((row.agent_id, row.segment_id), []).append(row) hit_requests = len(requests) # 重复消费:同一 agent + 同一 chunk 出现多条记录(说明曾被重新开放再用) pair_counts: dict[tuple[int, str], int] = {} for row in usage_rows: key = (row.agent_id, row.chunk_id) pair_counts[key] = pair_counts.get(key, 0) + 1 repeated_pairs = sum(1 for count in pair_counts.values() if count > 1) repeat_rate = round(repeated_pairs / len(pair_counts), 4) if pair_counts else 0.0 # ---- 引用覆盖率:按任务统计 ---- seg_query = select(Segment) if campaign_id: seg_query = seg_query.where(Segment.campaign_id == campaign_id) if organization_id is not None or owner_user_id is not None: campaign_ids = {int(item.id) for item in session.exec(select(Campaign).where( *(([Campaign.organization_id == int(organization_id)] if organization_id is not None else []) + ([Campaign.owner_user_id == int(owner_user_id)] if owner_user_id is not None else [])) )).all() if item.id} seg_query = seg_query.where(Segment.campaign_id.in_(campaign_ids or {-1})) segments = session.exec(seg_query).all() total_segments = len(segments) referenced = sum(1 for seg in segments if seg.references) reference_coverage = round(referenced / total_segments, 4) if total_segments else 0.0 # 检索总量:每次任务运行的检索请求数 = 有消耗记录的 (agent, segment) 数; # 无消耗记录的生产段视为一次未命中的检索。 production_segments = [seg for seg in segments if seg.status in ("approved", "done", "qc", "human_review", "composing_ready", "rework")] miss_requests = 0 for seg in production_segments: if not any(key[1] == seg.id for key in requests): miss_requests += 1 total_requests = hit_requests + miss_requests hit_rate = round(hit_requests / total_requests, 4) if total_requests else 0.0 return { "hit_rate": hit_rate, "repeat_rate": repeat_rate, "reference_coverage": reference_coverage, "detail": { "search_requests": total_requests, "hit_requests": hit_requests, "miss_requests": miss_requests, "consumed_chunks": len(usage_rows), "repeated_pairs": repeated_pairs, "distinct_chunks": len(pair_counts), "segments_total": total_segments, "segments_with_references": referenced, }, } def kb_metrics_overview(session: Session, *, owner_user_id: int | None = None, organization_id: int | None = None, kb_ids: set[int] | None = None) -> dict: """知识库页展示:按库拆分的消费热力 + 平台级三指标。""" allowed_kb_ids = None if kb_ids is None else {int(value) for value in kb_ids} overall = kb_metrics(session, owner_user_id=owner_user_id, organization_id=organization_id, kb_ids=allowed_kb_ids) usage_rows = session.exec(select(KnowledgeUsage)).all() if organization_id is not None or owner_user_id is not None: campaign_ids = {int(item.id) for item in session.exec(select(Campaign).where( *(([Campaign.organization_id == int(organization_id)] if organization_id is not None else []) + ([Campaign.owner_user_id == int(owner_user_id)] if owner_user_id is not None else [])) )).all() if item.id} usage_rows = [row for row in usage_rows if int(row.campaign_id or 0) in campaign_ids] if allowed_kb_ids is not None: usage_rows = [row for row in usage_rows if int(row.kb_id or 0) in allowed_kb_ids] selected_kb_ids = allowed_kb_ids if allowed_kb_ids is not None else {int(row.kb_id) for row in usage_rows} kb_query = select(KnowledgeBase) if organization_id is not None: kb_query = kb_query.where(KnowledgeBase.organization_id.in_([0, int(organization_id)])) kbs = session.exec(kb_query).all() kbs = [kb for kb in kbs if int(kb.id or 0) in selected_kb_ids] by_kb = [] for kb in kbs: rows = [row for row in usage_rows if row.kb_id == kb.id] consumed = sum(1 for row in rows if row.status == "consumed") released = sum(1 for row in rows if row.status == "available") by_kb.append({ "kb_id": kb.id, "name": kb.name, "file_count": kb.file_count, "chunk_count": kb.chunk_count, "consumed": consumed, "released": released, "consumption_ratio": round(consumed / kb.chunk_count, 4) if kb.chunk_count else 0.0, }) by_kb.sort(key=lambda item: -item["consumption_ratio"]) # 最常用片段 Top5(按被不同 Agent 消费次数) chunk_agents: dict[str, set[int]] = {} chunk_label: dict[str, dict] = {} for row in usage_rows: chunk_agents.setdefault(row.chunk_id, set()).add(row.agent_id) chunk_label[row.chunk_id] = {"file": row.file_name, "heading": row.heading, "kb_id": row.kb_id, "excerpt": (row.excerpt or "")[:80]} top_chunks = sorted( ({"chunk_id": cid, "agents": len(agents), **chunk_label.get(cid, {})} for cid, agents in chunk_agents.items()), key=lambda item: -item["agents"])[:5] return {**overall, "by_kb": by_kb, "top_chunks": top_chunks} __all__ = ["kb_metrics", "kb_metrics_overview"]