diff --git a/ai_knowledge_base_v2/apps/admin-web/package-lock.json b/ai_knowledge_base_v2/apps/admin-web/package-lock.json
index 19b0348..d8c4093 100644
--- a/ai_knowledge_base_v2/apps/admin-web/package-lock.json
+++ b/ai_knowledge_base_v2/apps/admin-web/package-lock.json
@@ -10,10 +10,12 @@
"dependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"element-plus": "^2.11.0",
+ "markdown-it": "^14.3.0",
"vite": "^7.1.0",
"vue": "^3.5.0"
},
"devDependencies": {
+ "@types/markdown-it": "^14.1.2",
"typescript": "^5.9.0",
"vue-tsc": "^3.1.0"
}
@@ -877,6 +879,13 @@
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/lodash": {
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
@@ -892,6 +901,24 @@
"@types/lodash": "*"
}
},
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
@@ -1104,6 +1131,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
@@ -1238,6 +1271,25 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/linkify-it": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
+ "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
@@ -1270,6 +1322,51 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/markdown-it": {
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
+ "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.5.0",
+ "linkify-it": "^5.0.2",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -1360,6 +1457,15 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/rollup": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
@@ -1443,6 +1549,12 @@
"node": ">=14.17"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
diff --git a/ai_knowledge_base_v2/apps/admin-web/package.json b/ai_knowledge_base_v2/apps/admin-web/package.json
index 99c6a59..e005f8b 100644
--- a/ai_knowledge_base_v2/apps/admin-web/package.json
+++ b/ai_knowledge_base_v2/apps/admin-web/package.json
@@ -11,10 +11,12 @@
"dependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"element-plus": "^2.11.0",
+ "markdown-it": "^14.3.0",
"vite": "^7.1.0",
"vue": "^3.5.0"
},
"devDependencies": {
+ "@types/markdown-it": "^14.1.2",
"typescript": "^5.9.0",
"vue-tsc": "^3.1.0"
}
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue
index 1a092ab..caa9d63 100644
--- a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue
+++ b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue
@@ -1,11 +1,12 @@
+
+
+
+
+
+ 思考中
+
+
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 79c7e13..8d2acb9 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
@@ -2,6 +2,7 @@ import type {
AdminProfile,
AdminUser,
AgentDebugResult,
+ AgentDebugStreamComplete,
AgentGenerationConfig,
AgentRuntimeConfig,
AiLogRecord,
@@ -217,3 +218,48 @@ export const api = {
clearFeishuCache: () =>
request<{ cleared: number; message: string }>("/admin/feishu/cache/clear", { method: "POST", body: "{}" }),
};
+
+export async function streamDebugAgent(
+ payload: Record,
+ onChunk: (chunk: string) => void,
+ onComplete: (result: AgentDebugStreamComplete) => void,
+ signal?: AbortSignal,
+) {
+ const token = getToken();
+ const response = await fetch(`${API_BASE}/admin/agent/debug/stream`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ body: JSON.stringify(payload),
+ signal,
+ });
+ if (!response.ok || !response.body) {
+ const body = await readApiResponse(response);
+ throw new Error(body.detail || body.message || "Agent 调试连接失败");
+ }
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder("utf-8");
+ let buffer = "";
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ const events = buffer.split(/\r?\n\r?\n/);
+ buffer = events.pop() ?? "";
+ for (const event of events) {
+ const dataLines = event.split(/\r?\n/).filter((line) => line.startsWith("data:"));
+ if (!dataLines.length) continue;
+ const data = dataLines.map((line) => line.slice(5).trimStart()).join("\n").trim();
+ if (data === "[DONE]") return;
+ const parsed = JSON.parse(data) as AgentDebugStreamComplete & {
+ type?: string;
+ content?: string;
+ };
+ if (parsed.type === "error") throw new Error(parsed.message || "Agent 调试失败");
+ if (parsed.type === "content" && parsed.content) onChunk(parsed.content);
+ if (parsed.type === "complete") onComplete(parsed);
+ }
+ }
+}
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 c84aa33..6aca7e0 100644
--- a/ai_knowledge_base_v2/apps/admin-web/src/styles.css
+++ b/ai_knowledge_base_v2/apps/admin-web/src/styles.css
@@ -1380,6 +1380,115 @@ textarea {
line-height: 1.75;
}
+.agent-preview-markdown {
+ overflow-wrap: anywhere;
+ color: #263832;
+ font-size: 14px;
+ line-height: 1.75;
+}
+
+.agent-preview-markdown > :first-child { margin-top: 0; }
+.agent-preview-markdown > :last-child { margin-bottom: 0; }
+.agent-preview-markdown p { margin: 0 0 10px; }
+
+.agent-preview-markdown h1,
+.agent-preview-markdown h2,
+.agent-preview-markdown h3,
+.agent-preview-markdown h4 {
+ margin: 16px 0 8px;
+ color: #1f352e;
+ line-height: 1.45;
+}
+
+.agent-preview-markdown h1 { font-size: 19px; }
+.agent-preview-markdown h2 { font-size: 17px; }
+.agent-preview-markdown h3,
+.agent-preview-markdown h4 { font-size: 15px; }
+
+.agent-preview-markdown ul,
+.agent-preview-markdown ol {
+ margin: 8px 0 10px;
+ padding-left: 22px;
+}
+
+.agent-preview-markdown li + li { margin-top: 5px; }
+.agent-preview-markdown strong { color: #1f352e; font-weight: 700; }
+
+.agent-preview-markdown blockquote {
+ margin: 10px 0;
+ padding: 7px 10px;
+ border-left: 3px solid #8cc7b6;
+ background: #eef7f4;
+ color: #526b63;
+}
+
+.agent-preview-markdown code {
+ padding: 2px 5px;
+ border-radius: 5px;
+ background: #edf5f2;
+ color: #0f735d;
+ font-family: "SFMono-Regular", Consolas, monospace;
+ font-size: 0.92em;
+}
+
+.agent-preview-markdown pre {
+ max-width: 100%;
+ overflow-x: auto;
+ margin: 10px 0;
+ padding: 10px;
+ border-radius: 8px;
+ background: #13231e;
+ color: #f4fffb;
+}
+
+.agent-preview-markdown pre code {
+ padding: 0;
+ background: transparent;
+ color: inherit;
+}
+
+.agent-preview-markdown table {
+ display: block;
+ max-width: 100%;
+ overflow-x: auto;
+ border-collapse: collapse;
+ margin: 10px 0;
+}
+
+.agent-preview-markdown th,
+.agent-preview-markdown td {
+ padding: 6px 8px;
+ border: 1px solid #dbe7e3;
+ white-space: nowrap;
+}
+
+.agent-preview-markdown a { color: #0f735d; font-weight: 700; }
+
+.agent-preview-generation-state {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ min-height: 28px;
+ color: #667a73;
+ font-size: 13px;
+}
+
+.agent-preview-generation-state span {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: #0f735d;
+ animation: agent-generation-pulse 1.2s ease-in-out infinite;
+}
+
+.agent-preview-generation-state span:nth-child(2) { animation-delay: 0.14s; }
+.agent-preview-generation-state span:nth-child(3) { margin-right: 4px; animation-delay: 0.28s; }
+
+@keyframes agent-generation-pulse {
+ 0%, 70%, 100% { opacity: 0.25; transform: translateY(0); }
+ 35% { opacity: 1; transform: translateY(-2px); }
+}
+
.agent-preview-thinking {
margin-bottom: 10px;
padding: 9px 10px;
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 8fa4bbf..2c00ece 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
@@ -157,6 +157,15 @@ export interface AgentDebugResult {
retrievalLogId?: number;
}
+export interface AgentDebugStreamComplete {
+ message: string;
+ modelName?: string;
+ retrieveCount?: number;
+ knowledgeIds?: string;
+ retrievalTrace?: Record[];
+ retrievalLogId?: number;
+}
+
export interface KnowledgeVersion {
id: number;
knowledgeId: number;
diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py
index 695b4de..53b41f0 100644
--- a/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py
+++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py
@@ -1,6 +1,11 @@
from __future__ import annotations
+import json
+from collections.abc import AsyncIterator
+
from fastapi import APIRouter, Depends, HTTPException, Query, status
+from fastapi.encoders import jsonable_encoder
+from fastapi.responses import StreamingResponse
from sqlalchemy import func, select
from sqlalchemy.orm import Session
@@ -20,11 +25,10 @@ from app.schemas.admin import (
SystemConfigSaveRequest,
)
from app.services.admin_service import OperationLogService
+from app.services.agent_debug_service import AgentDebugService
from app.services.feishu_service import FeishuKnowledgeService
-from app.services.knowledge_agent_service import KnowledgeAgentService
from app.services.knowledge_service import KnowledgeScope
from app.services.model_service import ModelClientService
-from app.services.rag_service import PromptService, RagResult
from app.services.secret_service import MASKED_SECRET, SENSITIVE_CONFIG_KEYS, SecretService
router = APIRouter()
@@ -146,37 +150,11 @@ async def debug_agent(
if model is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
- rag_result = await KnowledgeAgentService.build_result(
- db,
- question=payload.question,
- version_overrides=payload.knowledgeVersions,
- preview_knowledge_ids=payload.knowledgeIds,
- )
- debug_messages = [
- {"role": "system", "content": payload.promptContent.strip()},
- *(rag_result.messages or []),
- ]
- rag_result = RagResult(
- question=rag_result.question,
- knowledge_scopes=rag_result.knowledge_scopes,
- chunks=rag_result.chunks,
- prompt=PromptService.render_messages(debug_messages),
- allow_general_knowledge=rag_result.allow_general_knowledge,
- retrieval_log_id=rag_result.retrieval_log_id,
- tool_trace=rag_result.tool_trace,
- messages=debug_messages,
- )
+ rag_result = await AgentDebugService.build_result(db, payload)
result = await ModelClientService.debug_model_async(
model,
rag_result,
- {
- "temperature": payload.temperature,
- "top_p": payload.topP,
- "top_k": payload.topK,
- "presence_penalty": payload.presencePenalty,
- "frequency_penalty": payload.frequencyPenalty,
- "max_token": payload.maxToken,
- },
+ AgentDebugService.overrides(payload),
)
result["retrievalTrace"] = rag_result.tool_trace or []
result["retrievalLogId"] = rag_result.retrieval_log_id
@@ -185,6 +163,42 @@ async def debug_agent(
return api_success(result)
+@router.post("/agent/debug/stream")
+def debug_agent_stream(
+ payload: AgentDebugRequest,
+ db: Session = Depends(get_db),
+ current_admin: Admin = Depends(get_current_admin),
+) -> StreamingResponse:
+ return StreamingResponse(
+ _debug_agent_stream(payload, db, current_admin),
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache, no-transform",
+ "Connection": "keep-alive",
+ "X-Accel-Buffering": "no",
+ },
+ )
+
+
+async def _debug_agent_stream(
+ payload: AgentDebugRequest,
+ db: Session,
+ current_admin: Admin,
+) -> AsyncIterator[str]:
+ async for event in AgentDebugService.stream(payload, db, current_admin):
+ yield _admin_sse_event(event)
+ yield _admin_sse_done()
+
+
+def _admin_sse_event(event: dict) -> str:
+ event = jsonable_encoder(event)
+ return f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
+
+
+def _admin_sse_done() -> str:
+ return "data: [DONE]\n\n"
+
+
@router.get("/agent/runtime-config")
def get_agent_runtime_config(
db: Session = Depends(get_db),
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py
new file mode 100644
index 0000000..eb1e71e
--- /dev/null
+++ b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py
@@ -0,0 +1,96 @@
+from __future__ import annotations
+
+import asyncio
+from collections.abc import AsyncIterator
+
+from sqlalchemy.orm import Session
+
+from app.models.admin import Admin
+from app.models.ai_config import ModelConfig
+from app.schemas.admin import AgentDebugRequest
+from app.services.admin_service import OperationLogService
+from app.services.knowledge_agent_service import KnowledgeAgentService
+from app.services.model_stream_service import ModelStreamService
+from app.services.rag_service import PromptService, RagResult
+
+
+class AgentDebugService:
+ @staticmethod
+ async def build_result(db: Session, payload: AgentDebugRequest) -> RagResult:
+ rag_result = await KnowledgeAgentService.build_result(
+ db,
+ question=payload.question,
+ version_overrides=payload.knowledgeVersions,
+ preview_knowledge_ids=payload.knowledgeIds,
+ )
+ debug_messages = [
+ {"role": "system", "content": payload.promptContent.strip()},
+ *(rag_result.messages or []),
+ ]
+ return RagResult(
+ question=rag_result.question,
+ knowledge_scopes=rag_result.knowledge_scopes,
+ chunks=rag_result.chunks,
+ prompt=PromptService.render_messages(debug_messages),
+ allow_general_knowledge=rag_result.allow_general_knowledge,
+ retrieval_log_id=rag_result.retrieval_log_id,
+ tool_trace=rag_result.tool_trace,
+ messages=debug_messages,
+ )
+
+ @staticmethod
+ def overrides(payload: AgentDebugRequest) -> dict:
+ return {
+ "temperature": payload.temperature,
+ "top_p": payload.topP,
+ "top_k": payload.topK,
+ "presence_penalty": payload.presencePenalty,
+ "frequency_penalty": payload.frequencyPenalty,
+ "max_token": payload.maxToken,
+ }
+
+ @classmethod
+ async def stream(
+ cls,
+ payload: AgentDebugRequest,
+ db: Session,
+ current_admin: Admin,
+ ) -> AsyncIterator[dict]:
+ model = db.get(ModelConfig, payload.modelId)
+ if model is None:
+ yield {"type": "error", "message": "模型不存在"}
+ return
+ try:
+ yield {"type": "status", "stage": "retrieving", "message": "思考中"}
+ rag_result = await cls.build_result(db, payload)
+ model_response = ModelStreamService.debug_stream_async(
+ model,
+ rag_result,
+ cls.overrides(payload),
+ )
+ async for chunk in model_response.chunks:
+ if chunk:
+ yield {"type": "content", "content": chunk}
+ OperationLogService.write(
+ db,
+ admin_id=current_admin.id,
+ module="agent",
+ action="debug_stream",
+ target_id=model.id,
+ )
+ db.commit()
+ yield {
+ "type": "complete",
+ "message": "Agent 调试完成",
+ "modelName": model_response.model_name,
+ "retrieveCount": len(rag_result.chunks),
+ "knowledgeIds": rag_result.knowledge_ids,
+ "retrievalTrace": rag_result.tool_trace or [],
+ "retrievalLogId": rag_result.retrieval_log_id,
+ }
+ except asyncio.CancelledError:
+ db.rollback()
+ raise
+ except Exception as exc:
+ db.rollback()
+ yield {"type": "error", "message": str(exc) or "Agent 调试失败"}
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py b/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py
index ea31419..927b412 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py
@@ -17,6 +17,7 @@ from app.services.model_service import (
_anthropic_headers,
_auth_headers,
_call_configured_model,
+ _copy_model_with_overrides,
_decimal_to_float,
_load_extra_params,
_mock_answer,
@@ -117,6 +118,22 @@ class ModelStreamService:
chunks=_stream_configured_model_async(model, rag_result),
)
+ @staticmethod
+ def debug_stream_async(
+ model: ModelConfig,
+ rag_result: RagResult,
+ overrides: dict[str, Any],
+ ) -> AsyncStreamingModelResponse:
+ debug_model = _copy_model_with_overrides(model, overrides)
+ if not (debug_model.api_url or debug_model.base_url) or not debug_model.api_key:
+ raise ExternalServiceError("模型 Base URL/API URL 或 API Key 未配置", provider="model")
+ return AsyncStreamingModelResponse(
+ model_id=model.id,
+ model_name=model.model_name,
+ input_token=_rough_token_count(rag_result.prompt),
+ chunks=_stream_configured_model_async(debug_model, rag_result),
+ )
+
def _get_enabled_model(db: Session) -> ModelConfig | None:
return db.scalar(
diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py b/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py
index 628da88..cca591b 100644
--- a/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py
+++ b/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py
@@ -1,17 +1,22 @@
import asyncio
from decimal import Decimal
+import json
from unittest.mock import patch
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
-from app.api.admin_settings import get_agent_runtime_config, save_agent_runtime_config
+from app.api.admin_settings import _debug_agent_stream, get_agent_runtime_config, save_agent_runtime_config
from app.models import Base
from app.models.admin import Admin
from app.models.ai_config import ModelConfig
-from app.schemas.admin import AgentRuntimeConfigSaveRequest
-from app.services.model_stream_service import _openai_stream_payload, _stream_configured_model_async
+from app.schemas.admin import AgentDebugRequest, AgentRuntimeConfigSaveRequest
+from app.services.model_stream_service import (
+ AsyncStreamingModelResponse,
+ _openai_stream_payload,
+ _stream_configured_model_async,
+)
from app.services.model_service import ModelClientService, _max_output_tokens
from app.services.rag_service import RagResult
@@ -128,6 +133,66 @@ def test_disabled_stream_returns_one_complete_chunk():
assert chunks == [answer]
+def test_agent_debug_stream_emits_status_content_and_trace():
+ async def chunks():
+ yield "内部思考"
+ yield "- **第一步**:停一下"
+
+ rag_result = RagResult(
+ question="怎么冷静",
+ knowledge_scopes=[],
+ chunks=[],
+ prompt="怎么冷静",
+ allow_general_knowledge=True,
+ retrieval_log_id=9,
+ tool_trace=[{"tool": "KnowledgeSearch", "status": "success"}],
+ messages=[{"role": "user", "content": "怎么冷静"}],
+ )
+ model_response = AsyncStreamingModelResponse(
+ model_id=1,
+ model_name="production-model",
+ input_token=10,
+ chunks=chunks(),
+ )
+
+ async def collect(payload, db, admin):
+ return [event async for event in _debug_agent_stream(payload, db, admin)]
+
+ with _database() as db:
+ admin = _admin()
+ model = _model()
+ db.add_all([admin, model])
+ db.commit()
+ payload = AgentDebugRequest(
+ promptContent="你是测试助手",
+ modelId=model.id,
+ knowledgeIds=[],
+ question="怎么冷静",
+ maxToken=8192,
+ )
+ with (
+ patch(
+ "app.services.agent_debug_service.KnowledgeAgentService.build_result",
+ return_value=rag_result,
+ ),
+ patch(
+ "app.services.agent_debug_service.ModelStreamService.debug_stream_async",
+ return_value=model_response,
+ ),
+ ):
+ events = asyncio.run(collect(payload, db, admin))
+
+ decoded = [
+ json.loads(event.removeprefix("data: "))
+ for event in events
+ if event != "data: [DONE]\n\n"
+ ]
+ assert [event["type"] for event in decoded] == ["status", "content", "content", "complete"]
+ assert decoded[1]["content"].startswith("")
+ assert decoded[2]["content"] == "- **第一步**:停一下"
+ assert decoded[3]["retrievalTrace"][0]["tool"] == "KnowledgeSearch"
+
+
def test_agent_debug_does_not_truncate_long_answer(monkeypatch):
monkeypatch.setattr(
"app.services.model_service._call_configured_model",