From e4a391aa085f40481008d78bc87aea12cd5707a5 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Mon, 13 Jul 2026 19:37:12 +0800 Subject: [PATCH] fix(agent): serialize retrieval trace responses --- .../apps/admin-web/src/services/api.ts | 16 ++++++++++++++-- .../backend/app/core/exception_handlers.py | 4 ++++ .../app/services/knowledge_agent_service.py | 16 ++++++++++++++-- .../backend/tests/test_knowledge_agent.py | 19 +++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts index 0c61dd2..9b55f65 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts @@ -46,13 +46,25 @@ async function request(path: string, init: RequestInit = {}): Promise { const token = getToken(); if (token) headers.set("Authorization", `Bearer ${token}`); const response = await fetch(`${API_BASE}${path}`, { ...init, headers }); - const body = (await response.json()) as ApiResponse & { detail?: string }; + const body = await readApiResponse(response); if (!response.ok || body.code !== 0) { throw new Error(body.detail || body.message || `${response.status} ${response.statusText}`); } return body.data; } +async function readApiResponse(response: Response): Promise & { detail?: string }> { + const text = await response.text(); + if (!text.trim()) { + throw new Error(response.ok ? "接口返回为空" : `服务暂时不可用(${response.status})`); + } + try { + return JSON.parse(text) as ApiResponse & { detail?: string }; + } catch { + throw new Error(response.ok ? "接口返回格式不正确" : `服务暂时不可用(${response.status})`); + } +} + async function download(path: string, filename: string) { const headers = new Headers(); const token = getToken(); @@ -73,7 +85,7 @@ async function upload(path: string, formData: FormData): Promise { const token = getToken(); if (token) headers.set("Authorization", `Bearer ${token}`); const response = await fetch(`${API_BASE}${path}`, { method: "POST", body: formData, headers }); - const body = (await response.json()) as ApiResponse & { detail?: string }; + const body = await readApiResponse(response); if (!response.ok || body.code !== 0) { throw new Error(body.message || body.detail || `${response.status} ${response.statusText}`); } diff --git a/ai_knowledge_base_v2/apps/backend/app/core/exception_handlers.py b/ai_knowledge_base_v2/apps/backend/app/core/exception_handlers.py index 5ac4f15..fb3d969 100644 --- a/ai_knowledge_base_v2/apps/backend/app/core/exception_handlers.py +++ b/ai_knowledge_base_v2/apps/backend/app/core/exception_handlers.py @@ -16,6 +16,10 @@ def register_exception_handlers(app: FastAPI) -> None: async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: return JSONResponse(status_code=422, content=api_error(10000, "请求参数不正确", exc.errors())) + @app.exception_handler(Exception) + async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: + return JSONResponse(status_code=500, content=api_error(50000, "系统内部错误,请稍后重试")) + def _code_from_status(status_code: int) -> int: if status_code in {401, 403}: diff --git a/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py b/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py index 5009020..4ca3489 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py @@ -127,7 +127,7 @@ class KnowledgeAgentService: log.selected_knowledge_ids = ",".join(str(item) for item in selected_ids) log.query_terms = json.dumps(terms, ensure_ascii=False) log.final_section_ids = ",".join(str(item.section.id) for item in selected) - log.tool_trace = json.dumps(trace, ensure_ascii=False) + log.tool_trace = _dump_trace(trace) log.tool_cost_ms = int((perf_counter() - started) * 1000) log.status = "prepared" db.add(log) @@ -152,7 +152,7 @@ class KnowledgeAgentService: except Exception as exc: log.status = "failed" log.error_message = str(exc) - log.tool_trace = json.dumps(trace, ensure_ascii=False) + log.tool_trace = _dump_trace(trace) log.tool_cost_ms = int((perf_counter() - started) * 1000) db.add(log) raise @@ -431,6 +431,18 @@ class KnowledgeAgentService: return content if len(content) <= 3200 else content[:3200].rstrip() + "\n[章节内容已按保护规则截断]" +def _dump_trace(trace: list[dict]) -> str: + """Persist tool traces without failing on database datetime values.""" + return json.dumps(trace, ensure_ascii=False, default=_json_trace_value) + + +def _json_trace_value(value): + isoformat = getattr(value, "isoformat", None) + if callable(isoformat): + return isoformat() + return str(value) + + def _extract_json(value: str) -> str: match = re.search(r"\{[\s\S]*\}", value) if not match: diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py b/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py index aa193c6..e6ed151 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py @@ -1,6 +1,8 @@ from __future__ import annotations import asyncio +import json +from datetime import datetime from sqlalchemy import create_engine from sqlalchemy.orm import Session @@ -11,6 +13,7 @@ from app.models.knowledge import ( Knowledge, KnowledgeChunk, KnowledgeManifest, + KnowledgeRetrievalLog, KnowledgeSection, KnowledgeSourceSnapshot, KnowledgeVersion, @@ -140,6 +143,22 @@ def test_course_question_searches_chunk_and_reads_parent_section(): assert any(item["tool"] == "read_knowledge_sections" for item in result.tool_trace) +def test_tool_trace_serializes_published_datetime(): + with _database() as db: + knowledge = _add_published_knowledge(db, knowledge_id=1, name="亲子课程") + version = db.get(KnowledgeVersion, knowledge.current_version_id) + assert version is not None + version.published_at = datetime(2026, 7, 13, 10, 30, 0) + db.commit() + + result = asyncio.run(KnowledgeAgentService.build_result(db, question="课程里如何进行亲子沟通?")) + log = db.get(KnowledgeRetrievalLog, result.retrieval_log_id) + trace = json.loads(log.tool_trace) + + catalog_call = next(item for item in trace if item["tool"] == "get_knowledge_catalog") + assert catalog_call["items"][0]["publishedAt"] == "2026-07-13T10:30:00" + + def test_contextual_follow_up_is_rewritten_before_agent_decision(): with _database() as db: history = [