From f39e51a4d43ae687a4b2fd45fb2031195dfdfe6f Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Thu, 9 Jul 2026 10:29:58 +0800 Subject: [PATCH] Show retrieved knowledge chunks in audit logs --- .../apps/admin-web/src/App.vue | 26 ++++++++++ .../apps/admin-web/src/styles.css | 52 +++++++++++++++++++ .../apps/admin-web/src/types/api.ts | 10 ++++ .../versions/0003_ai_log_retrieved_chunks.py | 17 ++++++ .../apps/backend/app/api/admin_records.py | 29 +++++++++++ .../apps/backend/app/models/logs.py | 1 + .../app/services/ai_request_log_service.py | 25 +++++++++ .../apps/backend/app/services/chat_service.py | 9 ++-- .../app/services/chat_stream_service.py | 6 +++ 9 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 ai_knowledge_base_v2/apps/backend/alembic/versions/0003_ai_log_retrieved_chunks.py diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue index 29eaca6..0ccac4a 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -1463,6 +1463,21 @@ function buildChatQuery() { + + + @@ -1528,6 +1543,17 @@ function buildChatQuery() { 耗时:{{ log.costMs || '-' }}ms 时间:{{ log.createdAt }} +
+
+
+ #{{ chunk.index }} {{ chunk.knowledgeName || '未知知识库' }} + {{ chunk.title || '未命名片段' }} +
+
{{ chunk.content }}
+ 查看来源 +
+
+
{{ log.prompt || '无 Prompt 记录' }}

{{ log.errorMessage }}

diff --git a/ai_knowledge_base_v2/apps/admin-web/src/styles.css b/ai_knowledge_base_v2/apps/admin-web/src/styles.css index 19fb904..135ea72 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/styles.css +++ b/ai_knowledge_base_v2/apps/admin-web/src/styles.css @@ -712,6 +712,58 @@ textarea { font-size: 13px; } +.retrieval-chunks { + display: grid; + gap: 12px; + margin: 12px 0; +} + +.retrieval-chunk { + padding: 12px; + border: 1px solid #dce8e4; + border-radius: 6px; + background: #fbfdfc; +} + +.chunk-head { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + align-items: baseline; + margin-bottom: 8px; + color: #203832; +} + +.chunk-head strong { + color: #0f735d; +} + +.chunk-head span { + color: #60746d; + font-size: 13px; +} + +.retrieval-chunk pre { + max-height: 260px; + margin: 0; + padding: 10px; + overflow: auto; + border-radius: 6px; + background: #f4f7f6; + color: #263832; + white-space: pre-wrap; + word-break: break-word; + font-family: "SFMono-Regular", Consolas, "PingFang SC", monospace; + line-height: 1.65; +} + +.retrieval-chunk a { + display: inline-flex; + margin-top: 8px; + color: #0f735d; + text-decoration: none; +} + .error-text { color: #b42318; } diff --git a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts index b6efa2b..6e07058 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts @@ -133,6 +133,7 @@ export interface AiLogRecord { modelName?: string | null; knowledgeIds?: string | null; retrieveCount: number; + retrievedChunks: RetrievedKnowledgeChunk[]; inputToken?: number | null; outputToken?: number | null; totalToken?: number | null; @@ -143,6 +144,15 @@ export interface AiLogRecord { createdAt: string; } +export interface RetrievedKnowledgeChunk { + index: number; + knowledgeId?: number | null; + knowledgeName: string; + title: string; + content: string; + sourceUrl?: string | null; +} + export interface ChatDetail { session: ChatRecord; messages: ChatMessageRecord[]; diff --git a/ai_knowledge_base_v2/apps/backend/alembic/versions/0003_ai_log_retrieved_chunks.py b/ai_knowledge_base_v2/apps/backend/alembic/versions/0003_ai_log_retrieved_chunks.py new file mode 100644 index 0000000..6b86a28 --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/alembic/versions/0003_ai_log_retrieved_chunks.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "0003_ai_log_retrieved_chunks" +down_revision = "0002_expand_model_config" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("sys_ai_request_log", sa.Column("retrieved_chunks", sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("sys_ai_request_log", "retrieved_chunks") diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_records.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_records.py index 944ebfb..3cae58c 100644 --- a/ai_knowledge_base_v2/apps/backend/app/api/admin_records.py +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_records.py @@ -1,8 +1,10 @@ from __future__ import annotations import csv +import json from datetime import datetime from io import StringIO +from typing import Any from fastapi import APIRouter, Depends, Query from fastapi.responses import Response @@ -236,6 +238,7 @@ def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False) -> dict: "modelName": log.model_name, "knowledgeIds": log.knowledge_ids, "retrieveCount": log.retrieve_count, + "retrievedChunks": _parse_retrieved_chunks(log.retrieved_chunks), "inputToken": log.input_token, "outputToken": log.output_token, "totalToken": log.total_token, @@ -247,3 +250,29 @@ def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False) -> dict: if include_prompt: data["prompt"] = log.prompt return data + + +def _parse_retrieved_chunks(raw: str | None) -> list[dict[str, Any]]: + if not raw: + return [] + try: + chunks = json.loads(raw) + except json.JSONDecodeError: + return [] + if not isinstance(chunks, list): + return [] + normalized: list[dict[str, Any]] = [] + for index, item in enumerate(chunks, start=1): + if not isinstance(item, dict): + continue + normalized.append( + { + "index": item.get("index") or index, + "knowledgeId": item.get("knowledgeId"), + "knowledgeName": item.get("knowledgeName") or "", + "title": item.get("title") or "", + "content": item.get("content") or "", + "sourceUrl": item.get("sourceUrl") or None, + } + ) + return normalized diff --git a/ai_knowledge_base_v2/apps/backend/app/models/logs.py b/ai_knowledge_base_v2/apps/backend/app/models/logs.py index 2075bcd..a0ebfa4 100644 --- a/ai_knowledge_base_v2/apps/backend/app/models/logs.py +++ b/ai_knowledge_base_v2/apps/backend/app/models/logs.py @@ -19,6 +19,7 @@ class AiRequestLog(Base): prompt: Mapped[str | None] = mapped_column(Text, nullable=True) knowledge_ids: Mapped[str | None] = mapped_column(String(500), nullable=True) retrieve_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + retrieved_chunks: Mapped[str | None] = mapped_column(Text, nullable=True) input_token: Mapped[int | None] = mapped_column(Integer, nullable=True) output_token: Mapped[int | None] = mapped_column(Integer, nullable=True) total_token: Mapped[int | None] = mapped_column(Integer, nullable=True) diff --git a/ai_knowledge_base_v2/apps/backend/app/services/ai_request_log_service.py b/ai_knowledge_base_v2/apps/backend/app/services/ai_request_log_service.py index bd773ae..fd70f80 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/ai_request_log_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/ai_request_log_service.py @@ -1,8 +1,12 @@ from __future__ import annotations +import json +from collections.abc import Sequence + from sqlalchemy.orm import Session from app.models.logs import AiRequestLog +from app.services.rag_service import RetrievedChunk class AiRequestLogService: @@ -20,6 +24,7 @@ class AiRequestLogService: input_token: int, output_token: int, cost_ms: int, + retrieved_chunks: Sequence[RetrievedChunk] | None = None, ) -> None: db.add( AiRequestLog( @@ -30,6 +35,7 @@ class AiRequestLogService: prompt=prompt, knowledge_ids=knowledge_ids, retrieve_count=retrieve_count, + retrieved_chunks=_dump_retrieved_chunks(retrieved_chunks), input_token=input_token, output_token=output_token, total_token=input_token + output_token, @@ -51,6 +57,7 @@ class AiRequestLogService: retrieve_count: int, cost_ms: int, error_message: str, + retrieved_chunks: Sequence[RetrievedChunk] | None = None, ) -> None: db.add( AiRequestLog( @@ -61,8 +68,26 @@ class AiRequestLogService: prompt=prompt, knowledge_ids=knowledge_ids, retrieve_count=retrieve_count, + retrieved_chunks=_dump_retrieved_chunks(retrieved_chunks), cost_ms=cost_ms, status="FAILED", error_message=error_message, ) ) + + +def _dump_retrieved_chunks(chunks: Sequence[RetrievedChunk] | None) -> str | None: + if not chunks: + return None + payload = [ + { + "index": index, + "knowledgeId": chunk.knowledge_id, + "knowledgeName": chunk.knowledge_name, + "title": chunk.title, + "content": chunk.content, + "sourceUrl": chunk.source_url, + } + for index, chunk in enumerate(chunks, start=1) + ] + return json.dumps(payload, ensure_ascii=False) diff --git a/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py b/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py index b4b9c15..c17900d 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py @@ -87,6 +87,7 @@ class ChatService: db.flush() started_at = perf_counter() + rag_result = None try: # 获取历史消息(不含刚插入的 user_message,它还没 flush id) history = list( @@ -121,11 +122,12 @@ class ChatService: message_id=user_message.id, user_id=user.id, model_name=None, - prompt=normalized_question, - knowledge_ids=None, - retrieve_count=0, + prompt=rag_result.prompt if rag_result is not None else normalized_question, + knowledge_ids=rag_result.knowledge_ids if rag_result is not None else None, + retrieve_count=len(rag_result.chunks) if rag_result is not None else 0, cost_ms=cost_ms, error_message=str(exc), + retrieved_chunks=rag_result.chunks if rag_result is not None else None, ) db.commit() raise HTTPException( @@ -168,6 +170,7 @@ class ChatService: input_token=completion.input_token, output_token=completion.output_token, cost_ms=cost_ms, + retrieved_chunks=rag_result.chunks, ) db.commit() return completion.answer diff --git a/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py b/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py index 52ba8f2..be7dbc3 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py @@ -80,6 +80,7 @@ class ChatStreamService: retrieve_count=len(rag_result.chunks) if rag_result is not None else 0, cost_ms=cost_ms, error_message=str(exc), + retrieved_chunks=rag_result.chunks if rag_result is not None else None, ) db.commit() raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc @@ -98,6 +99,7 @@ class ChatStreamService: retrieve_count=len(rag_result.chunks) if rag_result is not None else 0, cost_ms=cost_ms, error_message="模型未返回有效内容", + retrieved_chunks=rag_result.chunks if rag_result is not None else None, ) db.commit() raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="模型未返回有效内容") @@ -137,6 +139,7 @@ class ChatStreamService: input_token=model_response.input_token if model_response is not None else 0, output_token=_rough_token_count(answer), cost_ms=cost_ms, + retrieved_chunks=rag_result.chunks if rag_result is not None else None, ) db.commit() @@ -205,6 +208,7 @@ class ChatStreamService: retrieve_count=len(rag_result.chunks) if rag_result is not None else 0, cost_ms=cost_ms, error_message=str(exc), + retrieved_chunks=rag_result.chunks if rag_result is not None else None, ) db.commit() raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc @@ -223,6 +227,7 @@ class ChatStreamService: retrieve_count=len(rag_result.chunks) if rag_result is not None else 0, cost_ms=cost_ms, error_message="模型未返回有效内容", + retrieved_chunks=rag_result.chunks if rag_result is not None else None, ) db.commit() raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="模型未返回有效内容") @@ -295,6 +300,7 @@ def _write_success( input_token=model_response.input_token if model_response is not None else 0, output_token=_rough_token_count(answer), cost_ms=cost_ms, + retrieved_chunks=rag_result.chunks if rag_result is not None else None, ) db.commit()