Files
QuestionProject/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue

765 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ElMessage, ElMessageBox } from "element-plus";
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
import { api, streamDebugAgent } from "../services/api";
import type { AdminUser, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem, TopicSessionRecord } from "../types/api";
import AgentGenerationParameters from "./AgentGenerationParameters.vue";
import AgentBatchTestPanel from "./AgentBatchTestPanel.vue";
import AgentResponseDepthControl from "./AgentResponseDepthControl.vue";
import AdminPagination from "./AdminPagination.vue";
import StreamingMarkdownMessage from "./StreamingMarkdownMessage.vue";
import { formatDateTime } from "../utils/dateTime";
const props = withDefaults(defineProps<{ previewKnowledgeId?: number | null; canBatch?: boolean }>(), { canBatch: false });
const emit = defineEmits<{ consumedPreview: [] }>();
const loading = ref(false);
const promptSaving = ref(false);
const runtimeSaving = ref(false);
const historyLoading = ref(false);
const agentDebugging = ref(false);
const agentDebugAbortController = ref<AbortController | null>(null);
const agentPreviewChat = ref<HTMLElement | null>(null);
const activeConfigTab = ref("prompt");
const prompt = ref<PromptDetail | null>(null);
const promptContent = ref("");
const savedPromptContent = ref("");
const models = ref<ModelItem[]>([]);
const knowledge = ref<KnowledgeItem[]>([]);
const debugUsers = ref<AdminUser[]>([]);
const debugUserLoading = ref(false);
const debugTopics = ref<TopicSessionRecord[]>([]);
const debugTopicLoading = ref(false);
const history = ref<PromptHistoryItem[]>([]);
const historyPager = reactive({ page: 1, pageSize: 10, total: 0 });
const historyDetailOpen = ref(false);
const selectedHistory = ref<PromptDetail | null>(null);
const historyDetailLoading = ref(false);
const agentDebugTrace = ref<Record<string, any>[]>([]);
const lastDebugRoute = ref<{ modelName?: string; routeReason?: string } | null>(null);
const DEFAULT_AGENT_PREVIEW_MESSAGE = "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。";
const agentPreviewMessages = ref<{
role: "user" | "assistant" | "system";
content: string;
reasoning?: string;
showReasoning?: boolean;
streaming?: boolean;
}[]>([
{ role: "assistant", content: DEFAULT_AGENT_PREVIEW_MESSAGE },
]);
const agentForm = reactive({
modelId: undefined as number | undefined,
userId: undefined as number | undefined,
topicSessionId: undefined as number | undefined,
knowledgeIds: [] as number[],
temperature: 0.2 as number | null,
topP: null as number | null,
topK: null as number | null,
presencePenalty: null as number | null,
frequencyPenalty: null as number | null,
maxToken: 8192 as number | null,
streamEnabled: 1,
reasoningVisible: 0,
responseDepth: 35,
question: "",
});
const runtimeConfig = ref<AgentRuntimeConfig | null>(null);
const runtimeForm = reactive({
temperature: null as number | null,
topP: null as number | null,
topK: null as number | null,
presencePenalty: null as number | null,
frequencyPenalty: null as number | null,
maxToken: 8192 as number | null,
streamEnabled: 1,
reasoningVisible: 0,
responseDepth: 35,
});
const promptDirty = computed(() => promptContent.value !== savedPromptContent.value);
const selectedModelName = computed(() => {
const model = models.value.find((item) => item.id === agentForm.modelId);
return model?.displayName || model?.modelName || "未选择模型";
});
const selectedKnowledgeSummary = computed(() => {
if (!agentForm.knowledgeIds.length) return "默认使用全部正式开放知识库";
if (agentForm.knowledgeIds.length === 1) {
return knowledge.value.find((item) => item.id === agentForm.knowledgeIds[0])?.name || "1 个知识库";
}
return `${agentForm.knowledgeIds.length} 个知识库`;
});
const selectedDebugUserLabel = computed(() => {
if (!agentForm.userId) return "未模拟学员";
const user = debugUsers.value.find((item) => item.id === agentForm.userId);
return user ? `${user.name || user.nickname || user.phone} · #${user.id}` : `学员 #${agentForm.userId}`;
});
const selectedDebugTopicLabel = computed(() => {
if (!agentForm.topicSessionId) return "未指定主题";
const topic = debugTopics.value.find((item) => item.id === agentForm.topicSessionId);
return topic ? topic.title : `主题 #${agentForm.topicSessionId}`;
});
const batchConfig = computed(() => ({
promptContent: promptContent.value,
modelId: agentForm.modelId,
knowledgeIds: [...agentForm.knowledgeIds],
temperature: agentForm.temperature,
topP: agentForm.topP,
topK: agentForm.topK,
presencePenalty: agentForm.presencePenalty,
frequencyPenalty: agentForm.frequencyPenalty,
maxToken: agentForm.maxToken,
streamEnabled: agentForm.streamEnabled,
responseDepth: agentForm.responseDepth,
}));
onMounted(load);
watch(() => props.previewKnowledgeId, (knowledgeId) => {
if (knowledgeId && knowledge.value.length) applyPreviewKnowledge(knowledgeId);
});
async function load() {
loading.value = true;
try {
const [promptResult, modelRows, knowledgeRows, formalConfig] = await Promise.all([
api.prompt(), api.agentModelOptions(), api.agentKnowledgeOptions(), api.agentRuntimeConfig(),
]);
applyPrompt(promptResult);
models.value = modelRows;
knowledge.value = knowledgeRows;
agentForm.modelId = modelRows.find((item) => item.isDefault === 1)?.id
?? modelRows.find((item) => item.enabled === 1)?.id
?? modelRows[0]?.id;
applyRuntimeConfig(formalConfig);
applyDebugModelDefaults(agentForm.modelId);
// 空选择由后端使用和用户端一致的“正式开放知识库”规则。
// 只有管理员显式勾选时,才进入指定知识库的预览模式。
agentForm.knowledgeIds = [];
if (props.previewKnowledgeId) applyPreviewKnowledge(props.previewKnowledgeId);
void searchDebugUsers("");
await loadHistory();
} catch (error) {
ElMessage.error(errorMessage(error, "Agent 配置加载失败"));
} finally {
loading.value = false;
}
}
async function searchDebugUsers(keyword: string) {
debugUserLoading.value = true;
try {
const result = await api.users({ keyword, page: 1, pageSize: 20 });
debugUsers.value = result.items;
} catch {
debugUsers.value = [];
} finally {
debugUserLoading.value = false;
}
}
async function handleDebugUserChange(userId?: number) {
agentForm.topicSessionId = undefined;
debugTopics.value = [];
clearAgentPreview("模拟学员已切换,可以选择其中一个主题继续调试。");
if (!userId) return;
debugTopicLoading.value = true;
try {
await loadDebugTopics(userId);
} catch (error) {
ElMessage.error(errorMessage(error, "学员主题加载失败"));
} finally {
debugTopicLoading.value = false;
}
}
async function loadDebugTopics(userId: number, keyword = "") {
const selected = debugTopics.value.find((item) => item.id === agentForm.topicSessionId);
const result = await api.userTopics(userId, { keyword, page: 1, pageSize: 100 });
debugTopics.value = selected && !result.items.some((item) => item.id === selected.id)
? [selected, ...result.items]
: result.items;
}
async function searchDebugTopics(keyword: string) {
if (!agentForm.userId) return;
debugTopicLoading.value = true;
try {
await loadDebugTopics(agentForm.userId, keyword);
} catch (error) {
ElMessage.error(errorMessage(error, "学员主题搜索失败"));
} finally {
debugTopicLoading.value = false;
}
}
function handleDebugTopicChange() {
clearAgentPreview(
agentForm.topicSessionId
? "模拟主题已切换,下一条调试会加载该主题的摘要和最近对话。"
: "已恢复通用学员模拟,不加载具体主题对话。",
);
}
function debugTopicOptionLabel(topic: TopicSessionRecord) {
const statusLabel = topic.status === "active" ? "进行中" : topic.status === "completed" ? "已完成" : topic.status;
const summaryLabel = topic.summaryAvailable ? "有摘要" : "无摘要";
return `${topic.title} · ${statusLabel} · ${topic.messageCount} 条消息 · ${summaryLabel}`;
}
function applyRuntimeConfig(value: AgentRuntimeConfig) {
runtimeConfig.value = value;
Object.assign(runtimeForm, {
temperature: value.temperature,
topP: value.topP,
topK: value.topK,
presencePenalty: value.presencePenalty,
frequencyPenalty: value.frequencyPenalty,
maxToken: value.maxToken,
streamEnabled: value.streamEnabled,
reasoningVisible: value.reasoningVisible,
responseDepth: value.responseDepth ?? 35,
});
}
function applyDebugModelDefaults(modelId?: number) {
const model = models.value.find((item) => item.id === modelId);
if (!model) return;
Object.assign(agentForm, {
temperature: model.temperature ?? null,
topP: model.topP ?? null,
topK: model.topK ?? null,
presencePenalty: model.presencePenalty ?? null,
frequencyPenalty: model.frequencyPenalty ?? null,
maxToken: model.maxToken ?? (model.enabled === 1 ? runtimeConfig.value?.maxToken : null) ?? 8192,
streamEnabled: model.streamEnabled ?? 1,
reasoningVisible: runtimeConfig.value?.reasoningVisible ?? 0,
responseDepth: runtimeConfig.value?.responseDepth ?? 35,
});
}
async function saveRuntimeConfig() {
if (!runtimeConfig.value?.modelId) return ElMessage.error("请先在模型管理中启用一个正式模型");
if (!runtimeForm.maxToken) return ElMessage.warning("最大输出长度不能为空");
runtimeSaving.value = true;
try {
const saved = await api.saveAgentRuntimeConfig({
temperature: runtimeForm.temperature,
topP: runtimeForm.topP,
topK: runtimeForm.topK,
presencePenalty: runtimeForm.presencePenalty,
frequencyPenalty: runtimeForm.frequencyPenalty,
maxToken: runtimeForm.maxToken,
streamEnabled: runtimeForm.streamEnabled,
reasoningVisible: runtimeForm.reasoningVisible,
responseDepth: runtimeForm.responseDepth,
});
applyRuntimeConfig(saved);
const model = models.value.find((item) => item.id === saved.modelId);
if (model) Object.assign(model, runtimeForm);
if (agentForm.modelId === saved.modelId) applyDebugModelDefaults(saved.modelId ?? undefined);
ElMessage.success("正式运行参数已保存,将从用户端下一条消息开始生效");
} catch (error) {
ElMessage.error(errorMessage(error, "正式运行参数保存失败"));
} finally {
runtimeSaving.value = false;
}
}
function applyPreviewKnowledge(knowledgeId: number) {
if (knowledge.value.some((item) => item.id === knowledgeId)) {
agentForm.knowledgeIds = [knowledgeId];
activeConfigTab.value = "debug";
}
emit("consumedPreview");
}
function applyPrompt(value: PromptDetail) {
prompt.value = value;
promptContent.value = value.promptContent;
savedPromptContent.value = value.promptContent;
}
async function savePrompt() {
if (!promptContent.value.trim()) return ElMessage.warning("主提示词不能为空");
promptSaving.value = true;
try {
applyPrompt(await api.savePrompt(promptContent.value.trim()));
ElMessage.success("主提示词已保存为新版本");
await loadHistory(1);
} catch (error) {
ElMessage.error(errorMessage(error, "主提示词保存失败"));
} finally {
promptSaving.value = false;
}
}
async function resetPrompt() {
try {
await ElMessageBox.confirm(
"恢复默认提示词会新增一个版本,现有历史不会被删除。确认继续?",
"恢复默认提示词",
{ type: "warning", confirmButtonText: "确认恢复", cancelButtonText: "取消" },
);
} catch {
return;
}
promptSaving.value = true;
try {
applyPrompt(await api.resetPrompt());
ElMessage.success("已恢复默认提示词并保存为新版本");
await loadHistory(1);
} catch (error) {
ElMessage.error(errorMessage(error, "恢复默认提示词失败"));
} finally {
promptSaving.value = false;
}
}
async function loadHistory(page = historyPager.page, pageSize = historyPager.pageSize) {
historyLoading.value = true;
try {
const result = await api.promptHistory({ page, pageSize });
history.value = result.items;
Object.assign(historyPager, { page: result.page, pageSize: result.pageSize, total: result.total });
} catch (error) {
ElMessage.error(errorMessage(error, "提示词历史加载失败"));
} finally {
historyLoading.value = false;
}
}
async function openHistory(item: PromptHistoryItem) {
historyDetailOpen.value = true;
historyDetailLoading.value = true;
selectedHistory.value = null;
try {
selectedHistory.value = await api.promptHistoryDetail(item.id);
} catch (error) {
ElMessage.error(errorMessage(error, "提示词版本加载失败"));
historyDetailOpen.value = false;
} finally {
historyDetailLoading.value = false;
}
}
async function restoreHistory(id: number) {
try {
await ElMessageBox.confirm(
`确认把版本 #${id} 恢复为当前主提示词?系统会新增一个恢复版本,不会覆盖历史。`,
"恢复历史版本",
{ type: "warning", confirmButtonText: "确认恢复", cancelButtonText: "取消" },
);
} catch {
return;
}
promptSaving.value = true;
try {
applyPrompt(await api.restorePrompt(id));
historyDetailOpen.value = false;
activeConfigTab.value = "prompt";
ElMessage.success(`版本 #${id} 已恢复为当前主提示词`);
await loadHistory(1);
} catch (error) {
ElMessage.error(errorMessage(error, "历史版本恢复失败"));
} finally {
promptSaving.value = false;
}
}
async function restoreSelectedHistory() {
if (selectedHistory.value?.id) await restoreHistory(selectedHistory.value.id);
}
async function debugAgent() {
if (agentDebugging.value) return;
if (!agentForm.modelId) return ElMessage.error("请选择调试模型");
if (!agentForm.question.trim()) return ElMessage.error("请输入调试问题");
if (!promptContent.value.trim()) return ElMessage.error("主提示词不能为空");
const question = agentForm.question.trim();
const conversationHistory = agentPreviewMessages.value
.slice(1)
.filter((message) => ["user", "assistant"].includes(message.role) && message.content.trim() && !message.streaming)
.map((message) => ({ role: message.role, content: message.content.trim() }));
agentPreviewMessages.value.push({ role: "user", content: question });
agentPreviewMessages.value.push({ role: "assistant", content: "", streaming: true });
const assistantIndex = agentPreviewMessages.value.length - 1;
const currentAssistant = () => agentPreviewMessages.value[assistantIndex];
agentForm.question = "";
agentDebugging.value = true;
agentDebugTrace.value = [];
lastDebugRoute.value = null;
agentDebugAbortController.value = new AbortController();
scrollAgentPreview();
try {
await streamDebugAgent(
{
promptContent: promptContent.value,
modelId: agentForm.modelId,
userId: agentForm.userId || null,
topicSessionId: agentForm.topicSessionId || null,
knowledgeIds: agentForm.knowledgeIds,
question,
history: conversationHistory,
temperature: agentForm.temperature,
topP: agentForm.topP,
topK: agentForm.topK,
presencePenalty: agentForm.presencePenalty,
frequencyPenalty: agentForm.frequencyPenalty,
maxToken: agentForm.maxToken,
streamEnabled: agentForm.streamEnabled,
reasoningVisible: agentForm.reasoningVisible,
responseDepth: agentForm.responseDepth,
},
(chunk) => {
currentAssistant().content += chunk;
scrollAgentPreview();
},
(chunk) => {
currentAssistant().reasoning = (currentAssistant().reasoning || "") + chunk;
scrollAgentPreview();
},
(status) => {
currentAssistant().showReasoning = status.reasoningVisible;
scrollAgentPreview();
},
(result) => {
agentDebugTrace.value = result.retrievalTrace || [];
lastDebugRoute.value = { modelName: result.modelName, routeReason: result.routeReason };
ElMessage.success(`${result.message || "Agent 调试完成"} · ${result.modelName || selectedModelName.value}`);
},
agentDebugAbortController.value.signal,
);
if (!currentAssistant().content.trim()) currentAssistant().content = "模型未返回正式回答。";
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
currentAssistant().content ||= "已停止生成";
} else {
const message = errorMessage(error, "Agent 调试失败");
currentAssistant().content ||= message;
ElMessage.error(message);
}
} finally {
currentAssistant().streaming = false;
agentDebugging.value = false;
agentDebugAbortController.value = null;
scrollAgentPreview();
}
}
function stopDebugAgent() {
agentDebugAbortController.value?.abort();
}
function scrollAgentPreview() {
void nextTick(() => {
const element = agentPreviewChat.value;
if (element) element.scrollTop = element.scrollHeight;
});
}
function clearAgentPreview(message?: string) {
const notice = typeof message === "string" && message.trim()
? message
: "预览已清空,可以继续发起新的 Agent 调试。";
agentPreviewMessages.value = [{ role: "assistant", content: notice }];
agentDebugTrace.value = [];
lastDebugRoute.value = null;
agentForm.question = "";
scrollAgentPreview();
}
function changeTypeLabel(value?: string) {
return ({ save: "手动保存", reset: "恢复默认", restore: "历史恢复", default: "系统默认" } as Record<string, string>)[value || ""] || "保存";
}
function changeTypeTag(value?: string) {
if (value === "reset") return "warning";
if (value === "restore") return "success";
return "info";
}
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
</script>
<template>
<div class="page-head agent-page-head">
<div>
<h2>Agent 管理</h2>
<p>维护主提示词用户端正式生成参数和历史版本并验证真实检索与回答效果</p>
</div>
<div class="agent-current-summary">
<el-tag type="success">当前版本 {{ prompt?.id ? `#${prompt.id}` : "系统默认" }}</el-tag>
<span>{{ prompt?.updatedByName || "系统默认" }} · {{ formatDateTime(prompt?.updatedAt, "尚未保存") }}</span>
</div>
</div>
<section v-loading="loading" class="agent-workbench agent-workbench-v2" :class="{ 'agent-batch-mode': activeConfigTab === 'batch' }">
<div class="agent-config-panel agent-control-panel">
<el-tabs v-model="activeConfigTab" class="agent-config-tabs">
<el-tab-pane label="主提示词" name="prompt">
<div class="agent-section-intro">
<div><h3>当前主提示词</h3><p>保存恢复默认或恢复历史版本时都会新增版本历史记录不会被覆盖</p></div>
<el-tag v-if="promptDirty" type="warning">有未保存修改</el-tag>
</div>
<el-input v-model="promptContent" class="agent-prompt-editor" type="textarea" :rows="18" resize="vertical" placeholder="请输入 Agent 主提示词" />
<div class="agent-editor-meta"><span>{{ promptContent.length }} 字符</span><span>调试会立即使用编辑框中的内容</span></div>
<div class="actions agent-primary-actions">
<el-button type="primary" :loading="promptSaving" :disabled="!promptDirty" @click="savePrompt">保存为新版本</el-button>
<el-button :loading="promptSaving" @click="resetPrompt">恢复默认提示词</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="正式运行配置" name="runtime">
<div class="agent-section-intro">
<div><h3>用户端正式运行参数</h3><p>保存后从用户端下一条消息开始生效当前正式模型{{ runtimeConfig?.modelName || '未启用模型' }}</p></div>
<el-button type="primary" :loading="runtimeSaving" :disabled="!runtimeConfig?.modelId" @click="saveRuntimeConfig">保存正式参数</el-button>
</div>
<el-alert v-if="!runtimeConfig?.modelId" title="当前没有已启用模型,请先到模型管理中启用一个正式模型。" type="warning" :closable="false" show-icon />
<el-alert v-else title="最大输出长度默认提升为 8192 Token设置过低会导致长回答在完成前被模型截断。实际可用上限仍受模型供应商限制。" type="info" :closable="false" show-icon />
<el-form label-position="top" class="agent-runtime-form">
<section class="agent-advanced-settings">
<div class="agent-subsection-title"><h4>正式生成参数</h4><span>用户端正式对话使用</span></div>
<AgentGenerationParameters
v-model:temperature="runtimeForm.temperature"
v-model:top-p="runtimeForm.topP"
v-model:top-k="runtimeForm.topK"
v-model:presence-penalty="runtimeForm.presencePenalty"
v-model:frequency-penalty="runtimeForm.frequencyPenalty"
v-model:max-token="runtimeForm.maxToken"
:disabled="!runtimeConfig?.modelId"
/>
<AgentResponseDepthControl v-model="runtimeForm.responseDepth" :disabled="!runtimeConfig?.modelId" />
<el-form-item class="agent-stream-setting">
<div class="agent-stream-setting-copy">
<strong>用户端流式输出</strong>
<span>开启后回答正文会边生成边展示关闭后等待模型完成再一次性显示完整回答</span>
</div>
<el-switch
v-model="runtimeForm.streamEnabled"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
inline-prompt
:disabled="!runtimeConfig?.modelId"
/>
</el-form-item>
<el-form-item class="agent-stream-setting">
<div class="agent-stream-setting-copy">
<strong>展示思考过程</strong>
<span>开启后用户端和后台调试预览可展开查看模型返回的思考内容关闭时后端不会下发该内容</span>
</div>
<el-switch
v-model="runtimeForm.reasoningVisible"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
inline-prompt
:disabled="!runtimeConfig?.modelId"
/>
</el-form-item>
</section>
</el-form>
</el-tab-pane>
<el-tab-pane label="调试配置" name="debug">
<div class="agent-section-intro"><div><h3>本次调试配置</h3><p>配置只作用于后台预览不会修改模型和知识库的正式状态</p></div></div>
<el-form label-position="top">
<el-form-item label="调试模型">
<el-select v-model="agentForm.modelId" placeholder="选择已接入模型" @change="applyDebugModelDefaults">
<el-option v-for="model in models" :key="model.id" :label="model.displayName || model.modelName" :value="model.id" />
</el-select>
</el-form-item>
<div class="agent-debug-context-grid">
<el-form-item label="模拟学员(可选)">
<el-select
v-model="agentForm.userId"
clearable
filterable
remote
:remote-method="searchDebugUsers"
:loading="debugUserLoading"
:disabled="agentDebugging"
placeholder="不选择则不注入学员权益和近期实修回顾"
@change="handleDebugUserChange"
>
<el-option
v-for="user in debugUsers"
:key="user.id"
:label="`${user.name || user.nickname || user.phone} · ${user.phone} · #${user.id}`"
:value="user.id"
/>
</el-select>
<div class="agent-form-help">加载该学员的权益和近期实修回顾只用于保持近期对话连续性不会消耗额度或改动正式会话</div>
</el-form-item>
<el-form-item label="模拟主题(可选)">
<el-select
v-model="agentForm.topicSessionId"
clearable
filterable
remote
:remote-method="searchDebugTopics"
:loading="debugTopicLoading"
:disabled="!agentForm.userId || agentDebugging"
placeholder="选择学员后可加载具体主题"
@change="handleDebugTopicChange"
>
<el-option
v-for="topic in debugTopics"
:key="topic.id"
:label="debugTopicOptionLabel(topic)"
:value="topic.id"
/>
</el-select>
<div class="agent-form-help">选择后会加载该主题摘要和最近对话用于验证追问效果</div>
</el-form-item>
</div>
<el-form-item label="调试知识库">
<el-select v-model="agentForm.knowledgeIds" class="debug-knowledge-select" multiple filterable collapse-tags collapse-tags-tooltip :max-collapse-tags="2" popper-class="debug-knowledge-popper" placeholder="默认使用全部已开放知识库">
<el-option v-for="item in knowledge" :key="item.id" :label="`${item.name}${item.status === 0 ? ' · 已关闭(仅本次预览)' : ''}`" :value="item.id" />
</el-select>
<div class="agent-form-help">不选时与用户端一致只使用已开放已确认且来源正常的正式版本显式选择则是本次预览范围</div>
</el-form-item>
<section class="agent-advanced-settings">
<div class="agent-subsection-title"><h4>高级生成参数</h4><span>仅影响本次后台调试</span></div>
<el-alert
v-if="agentForm.temperature == null"
class="agent-debug-randomness-alert"
title="当前未指定回答随机性,将使用模型供应商默认值;做同题稳定性对比时建议设为 0。"
type="warning"
:closable="false"
show-icon
/>
<AgentGenerationParameters
v-model:temperature="agentForm.temperature"
v-model:top-p="agentForm.topP"
v-model:top-k="agentForm.topK"
v-model:presence-penalty="agentForm.presencePenalty"
v-model:frequency-penalty="agentForm.frequencyPenalty"
v-model:max-token="agentForm.maxToken"
/>
<AgentResponseDepthControl v-model="agentForm.responseDepth" />
<el-form-item class="agent-stream-setting">
<div class="agent-stream-setting-copy">
<strong>流式输出</strong>
<span>开启后后台预览会边生成边展示关闭后等待模型完成再一次性显示</span>
</div>
<el-switch
v-model="agentForm.streamEnabled"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
inline-prompt
/>
</el-form-item>
<el-form-item class="agent-stream-setting">
<div class="agent-stream-setting-copy">
<strong>展示思考过程</strong>
<span>开启后本次后台预览可展开查看模型思考内容不会修改用户端正式配置</span>
</div>
<el-switch
v-model="agentForm.reasoningVisible"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
inline-prompt
/>
</el-form-item>
</section>
</el-form>
</el-tab-pane>
<el-tab-pane v-if="props.canBatch" label="批量测试" name="batch" lazy>
<AgentBatchTestPanel
:config="batchConfig"
:model-name="selectedModelName"
:knowledge-summary="selectedKnowledgeSummary"
/>
</el-tab-pane>
<el-tab-pane label="历史版本" name="history">
<div class="agent-section-intro"><div><h3>主提示词历史</h3><p>查看完整内容或恢复任意版本恢复操作会作为一个新版本记录</p></div></div>
<div v-loading="historyLoading" class="prompt-history-list">
<el-empty v-if="history.length === 0" description="还没有已保存的提示词版本" :image-size="64" />
<article v-for="item in history" :key="item.id" class="prompt-history-item" :class="{ current: item.isCurrent }">
<div class="prompt-history-head">
<strong>版本 #{{ item.id }}</strong>
<el-tag v-if="item.isCurrent" type="success" size="small">当前</el-tag>
<el-tag :type="changeTypeTag(item.changeType)" size="small">{{ changeTypeLabel(item.changeType) }}</el-tag>
<span>{{ item.updatedByName }} · {{ formatDateTime(item.updatedAt, "尚未保存") }}</span>
</div>
<p>{{ item.preview || "空内容" }}</p>
<div class="prompt-history-foot">
<span>{{ item.charCount }} 字符<span v-if="item.sourcePromptId"> · 来源 #{{ item.sourcePromptId }}</span></span>
<div><el-button link type="primary" @click="openHistory(item)">查看完整内容</el-button><el-button v-if="!item.isCurrent" link type="primary" @click="restoreHistory(item.id)">恢复此版本</el-button></div>
</div>
</article>
</div>
<AdminPagination :page="historyPager.page" :page-size="historyPager.pageSize" :total="historyPager.total" @change="loadHistory" />
</el-tab-pane>
</el-tabs>
</div>
<aside class="agent-preview-panel">
<div class="agent-preview-head">
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }} · {{ selectedDebugUserLabel }} · {{ selectedDebugTopicLabel }}</small></div>
<div class="agent-preview-head-actions"><span :title="lastDebugRoute?.routeReason || ''">{{ lastDebugRoute?.modelName || selectedModelName }}</span><el-button link :disabled="agentDebugging" @click="clearAgentPreview()">清空</el-button></div>
</div>
<div ref="agentPreviewChat" class="agent-preview-chat">
<article v-for="(message, index) in agentPreviewMessages" :key="`${message.role}-${index}`" class="agent-preview-message-row" :class="message.role">
<div class="agent-preview-avatar">{{ message.role === "user" ? "你" : "AI" }}</div>
<div class="agent-preview-bubble">
<strong>{{ message.role === "user" ? "你" : "Agent" }}</strong>
<template v-if="message.role === 'assistant'">
<StreamingMarkdownMessage
:content="message.content"
:reasoning="message.reasoning"
:show-reasoning="message.showReasoning"
:streaming="message.streaming"
/>
</template>
<pre v-else>{{ message.content }}</pre>
</div>
</article>
</div>
<div class="agent-preview-composer">
<div class="agent-preview-input">
<el-input v-model="agentForm.question" type="textarea" :rows="2" resize="none" :disabled="agentDebugging" placeholder="向 Agent 发送测试问题Enter 发送Shift+Enter 换行" @keydown.enter.exact.prevent="debugAgent" />
<small>连续发送会携带上方历史同题对比请先清空并将回答随机性设为 0</small>
</div>
<el-button :type="agentDebugging ? 'danger' : 'primary'" @click="agentDebugging ? stopDebugAgent() : debugAgent()">{{ agentDebugging ? '停止' : '发送' }}</el-button>
</div>
</aside>
<aside class="agent-trace-panel agent-trace-wide">
<div class="agent-preview-head"><div><h3>检索追踪</h3><small>按调用顺序查看请求响应耗时与错误</small></div><span>本轮 {{ agentDebugTrace.length }} Tool 调用</span></div>
<div class="agent-trace-list">
<el-empty v-if="agentDebugTrace.length === 0" description="发送问题后查看目录、检索与章节回读" :image-size="64" />
<details v-for="(step, index) in agentDebugTrace" :key="index" class="tool-trace-node">
<summary><strong>{{ index + 1 }}. {{ step.tool }}</strong><el-tag size="small" :type="step.status === 'failed' ? 'danger' : 'success'">{{ step.status || '完成' }}</el-tag><span>{{ step.durationMs ?? 0 }} ms</span></summary>
<h5>请求</h5><pre>{{ JSON.stringify(step.request ?? {}, null, 2) }}</pre>
<h5>响应</h5><pre>{{ step.response == null ? '返回为空' : JSON.stringify(step.response, null, 2) }}</pre>
<el-alert v-if="step.error" :title="step.error" type="error" :closable="false" />
</details>
</div>
</aside>
</section>
<el-dialog v-model="historyDetailOpen" width="min(820px, 92vw)" title="主提示词历史版本" destroy-on-close>
<div v-loading="historyDetailLoading" class="prompt-history-detail">
<template v-if="selectedHistory">
<div class="prompt-history-detail-meta"><el-tag :type="changeTypeTag(selectedHistory.changeType)">{{ changeTypeLabel(selectedHistory.changeType) }}</el-tag><strong>版本 #{{ selectedHistory.id }}</strong><span>{{ selectedHistory.updatedByName }} · {{ formatDateTime(selectedHistory.updatedAt, "尚未保存") }} · {{ selectedHistory.charCount }} 字符</span></div>
<pre>{{ selectedHistory.promptContent }}</pre>
</template>
</div>
<template #footer><el-button @click="historyDetailOpen = false">关闭</el-button><el-button v-if="selectedHistory?.id !== prompt?.id" type="primary" :loading="promptSaving" @click="restoreSelectedHistory">恢复为当前主提示词</el-button></template>
</el-dialog>
</template>