Separate agent tuning from model setup

This commit is contained in:
2026-07-07 13:31:20 +08:00
parent dada4aafcd
commit da262f4596
10 changed files with 518 additions and 76 deletions

View File

@@ -6,6 +6,7 @@ import { api, clearToken, getToken, saveToken } from "./services/api";
import type {
AdminProfile,
AdminUser,
AgentDebugResult,
AiLogRecord,
ChatDetail,
ChatRecord,
@@ -33,6 +34,10 @@ const chatDetail = ref<ChatDetail | null>(null);
const chatDetailOpen = ref(false);
const recordTab = ref("chats");
const editingModelId = ref<number | null>(null);
const agentDebugging = ref(false);
const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; content: string }[]>([
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
]);
const knowledgeForm = reactive({
name: "",
@@ -52,12 +57,12 @@ const modelForm = reactive({
apiKey: "",
authType: "bearer",
apiVersion: "",
temperature: 0.2 as number | null,
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: 1024,
maxToken: null as number | null,
contextWindow: null as number | null,
streamEnabled: 1,
responseFormat: "",
@@ -80,10 +85,10 @@ const quickModelProviders = [
baseUrl: "https://api.deepseek.com",
authType: "bearer",
apiVersion: "",
temperature: 0.2,
temperature: null,
topP: null,
maxToken: 1024,
contextWindow: 128000,
maxToken: null,
contextWindow: null,
models: [
{ label: "deepseek-v4-pro", value: "deepseek-v4-pro" },
{ label: "deepseek-v4-flash", value: "deepseek-v4-flash" },
@@ -98,8 +103,8 @@ const quickModelProviders = [
apiVersion: "",
temperature: null,
topP: null,
maxToken: 1024,
contextWindow: 1000000,
maxToken: null,
contextWindow: null,
models: [
{ label: "MiniMax-M3", value: "MiniMax-M3" },
{ label: "MiniMax-M2.7", value: "MiniMax-M2.7" },
@@ -161,6 +166,18 @@ const chatFilters = reactive({
dateRange: [] as string[],
});
const agentForm = reactive({
modelId: 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: 1024 as number | null,
question: "",
});
onMounted(async () => {
if (!getToken()) return;
try {
@@ -201,7 +218,15 @@ async function loadCurrentMenu() {
if (activeMenu.value === "dashboard") stats.value = await api.dashboard();
if (activeMenu.value === "users") users.value = await api.users(userKeyword.value);
if (activeMenu.value === "knowledge") knowledge.value = await api.knowledge();
if (activeMenu.value === "prompt") promptContent.value = (await api.prompt()).promptContent;
if (activeMenu.value === "prompt") {
const [prompt, modelRows, knowledgeRows] = await Promise.all([api.prompt(), api.models(), api.knowledge()]);
promptContent.value = prompt.promptContent;
models.value = modelRows;
knowledge.value = knowledgeRows;
if (!agentForm.modelId && modelRows.length) {
agentForm.modelId = modelRows.find((model) => model.enabled === 1)?.id ?? modelRows[0].id;
}
}
if (activeMenu.value === "models") models.value = await api.models();
if (activeMenu.value === "configs") configs.value = await api.configs();
if (activeMenu.value === "records") {
@@ -260,6 +285,18 @@ function buildModelPayload() {
payload[field] = null;
}
});
Object.assign(payload, {
temperature: null,
topP: null,
topK: null,
presencePenalty: null,
frequencyPenalty: null,
maxToken: null,
contextWindow: null,
streamEnabled: 1,
responseFormat: null,
extraParams: null,
});
return payload;
}
@@ -281,13 +318,13 @@ async function quickAddModel() {
apiKey: quickModelForm.apiKey,
authType: provider.authType,
apiVersion: provider.apiVersion,
temperature: provider.temperature,
topP: provider.topP,
temperature: null,
topP: null,
topK: null,
presencePenalty: null,
frequencyPenalty: null,
maxToken: provider.maxToken,
contextWindow: provider.contextWindow,
maxToken: null,
contextWindow: null,
streamEnabled: 1,
responseFormat: null,
extraParams: null,
@@ -332,7 +369,7 @@ function applyModelPreset(label: string) {
{
apiVersion: "",
authType: "bearer",
temperature: 0.2,
temperature: null,
topP: null,
topK: null,
presencePenalty: null,
@@ -356,13 +393,13 @@ function editModel(row: ModelItem) {
apiKey: "",
authType: row.authType,
apiVersion: row.apiVersion ?? "",
temperature: row.temperature ?? null,
topP: row.topP ?? null,
topK: row.topK ?? null,
presencePenalty: row.presencePenalty ?? null,
frequencyPenalty: row.frequencyPenalty ?? null,
maxToken: row.maxToken ?? 1024,
contextWindow: row.contextWindow ?? null,
temperature: null,
topP: null,
topK: null,
presencePenalty: null,
frequencyPenalty: null,
maxToken: null,
contextWindow: null,
streamEnabled: row.streamEnabled,
responseFormat: row.responseFormat ?? "",
extraParams: row.extraParams ?? "",
@@ -383,12 +420,12 @@ function resetModelForm() {
apiKey: "",
authType: "bearer",
apiVersion: "",
temperature: 0.2,
temperature: null,
topP: null,
topK: null,
presencePenalty: null,
frequencyPenalty: null,
maxToken: 1024,
maxToken: null,
contextWindow: null,
streamEnabled: 1,
responseFormat: "",
@@ -427,6 +464,52 @@ async function saveConfig() {
await loadCurrentMenu();
}
async function debugAgent() {
if (!agentForm.modelId) {
ElMessage.error("请选择调试模型");
return;
}
if (!agentForm.question.trim()) {
ElMessage.error("请输入调试问题");
return;
}
agentPreviewMessages.value.push({ role: "user", content: agentForm.question.trim() });
agentDebugging.value = true;
try {
const result: AgentDebugResult = await api.debugAgent({
promptContent: promptContent.value,
modelId: agentForm.modelId,
knowledgeIds: agentForm.knowledgeIds,
question: agentForm.question.trim(),
temperature: agentForm.temperature,
topP: agentForm.topP,
topK: agentForm.topK,
presencePenalty: agentForm.presencePenalty,
frequencyPenalty: agentForm.frequencyPenalty,
maxToken: agentForm.maxToken,
});
if (!result.ok) {
ElMessage.error(result.message);
agentPreviewMessages.value.push({ role: "assistant", content: result.message });
return;
}
agentPreviewMessages.value.push({ role: "assistant", content: result.answer });
ElMessage.success(result.message);
} catch (error) {
const message = error instanceof Error ? error.message : "Agent 调试失败";
ElMessage.error(message);
agentPreviewMessages.value.push({ role: "assistant", content: message });
} finally {
agentDebugging.value = false;
}
}
function clearAgentPreview() {
agentPreviewMessages.value = [
{ role: "assistant", content: "预览已清空,可以继续发起新的 Agent 调试。" },
];
}
async function searchChats() {
chats.value = await api.chats(buildChatQuery());
}
@@ -461,7 +544,7 @@ function buildChatQuery() {
<section class="login-box">
<div class="brand">AI</div>
<h1>AI 知识库后台</h1>
<p>用于维护用户知识库Prompt模型和审计记录</p>
<p>用于维护用户知识库Agent模型和审计记录</p>
<el-form label-position="top" @submit.prevent="login">
<el-form-item label="管理员账号">
<el-input v-model="loginForm.username" />
@@ -480,7 +563,7 @@ function buildChatQuery() {
<button :class="{ active: activeMenu === 'dashboard' }" @click="switchMenu('dashboard')">Dashboard</button>
<button :class="{ active: activeMenu === 'users' }" @click="switchMenu('users')">用户管理</button>
<button :class="{ active: activeMenu === 'knowledge' }" @click="switchMenu('knowledge')">知识库管理</button>
<button :class="{ active: activeMenu === 'prompt' }" @click="switchMenu('prompt')">Prompt 管理</button>
<button :class="{ active: activeMenu === 'prompt' }" @click="switchMenu('prompt')">Agent 管理</button>
<button :class="{ active: activeMenu === 'models' }" @click="switchMenu('models')">模型管理</button>
<button :class="{ active: activeMenu === 'configs' }" @click="switchMenu('configs')">系统配置</button>
<button :class="{ active: activeMenu === 'records' }" @click="switchMenu('records')">记录审计</button>
@@ -549,11 +632,83 @@ function buildChatQuery() {
<template v-if="activeMenu === 'prompt'">
<div class="page-head inline">
<div><h2>Prompt 管理</h2><p>保存后会影响后续问答 Prompt 组装</p></div>
<el-button @click="resetPrompt">恢复默认</el-button>
<div><h2>Agent 管理</h2><p>配置提示词模型知识库和调试参数并在右侧实时预览对话效果</p></div>
<el-button @click="resetPrompt">恢复默认提示词</el-button>
</div>
<el-input v-model="promptContent" type="textarea" :rows="16" />
<div class="actions"><el-button type="primary" @click="savePrompt">保存 Prompt</el-button></div>
<section class="agent-workbench">
<div class="agent-config-panel">
<el-form label-position="top">
<el-form-item label="Agent 提示词">
<el-input v-model="promptContent" type="textarea" :rows="10" />
</el-form-item>
<div class="agent-config-grid">
<el-form-item label="调试模型">
<el-select v-model="agentForm.modelId" placeholder="选择已接入模型">
<el-option
v-for="model in models"
:key="model.id"
:label="model.displayName || model.modelName"
:value="model.id"
/>
</el-select>
</el-form-item>
<el-form-item label="调试知识库">
<el-select v-model="agentForm.knowledgeIds" multiple collapse-tags placeholder="默认使用全部启用知识库">
<el-option v-for="item in knowledge" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="Temperature">
<el-input-number v-model="agentForm.temperature" :min="0" :max="2" :step="0.1" />
</el-form-item>
<el-form-item label="Top P">
<el-input-number v-model="agentForm.topP" :min="0" :max="1" :step="0.05" />
</el-form-item>
<el-form-item label="Top K">
<el-input-number v-model="agentForm.topK" :min="1" :max="1000" />
</el-form-item>
<el-form-item label="Max Tokens">
<el-input-number v-model="agentForm.maxToken" :min="1" :max="100000" />
</el-form-item>
<el-form-item label="Presence Penalty">
<el-input-number v-model="agentForm.presencePenalty" :min="-2" :max="2" :step="0.1" />
</el-form-item>
<el-form-item label="Frequency Penalty">
<el-input-number v-model="agentForm.frequencyPenalty" :min="-2" :max="2" :step="0.1" />
</el-form-item>
</div>
<el-form-item label="调试问题">
<el-input
v-model="agentForm.question"
type="textarea"
:rows="3"
placeholder="输入一个真实用户可能会问的问题"
/>
</el-form-item>
<div class="actions">
<el-button type="primary" @click="savePrompt">保存 Agent 提示词</el-button>
<el-button type="primary" :loading="agentDebugging" @click="debugAgent">发送调试</el-button>
<el-button @click="clearAgentPreview">清空预览</el-button>
</div>
</el-form>
</div>
<aside class="agent-preview-panel">
<div class="agent-preview-head">
<h3>调试预览</h3>
<span>{{ models.find((model) => model.id === agentForm.modelId)?.modelName || "未选择模型" }}</span>
</div>
<div class="agent-preview-chat">
<article
v-for="(message, index) in agentPreviewMessages"
:key="`${message.role}-${index}`"
class="agent-preview-message"
:class="message.role"
>
<strong>{{ message.role === "user" ? "用户" : "Agent" }}</strong>
<pre>{{ message.content }}</pre>
</article>
</div>
</aside>
</section>
</template>
<template v-if="activeMenu === 'models'">
@@ -639,48 +794,10 @@ function buildChatQuery() {
<el-form-item label="API Version">
<el-input v-model="modelForm.apiVersion" placeholder="Anthropic 默认 2023-06-01" />
</el-form-item>
<el-form-item label="Temperature">
<el-input-number v-model="modelForm.temperature" :min="0" :max="2" :step="0.1" />
</el-form-item>
<el-form-item label="Top P">
<el-input-number v-model="modelForm.topP" :min="0" :max="1" :step="0.05" />
</el-form-item>
<el-form-item label="Top K">
<el-input-number v-model="modelForm.topK" :min="1" :max="1000" />
</el-form-item>
<el-form-item label="Max Tokens">
<el-input-number v-model="modelForm.maxToken" :min="1" :max="100000" />
</el-form-item>
<el-form-item label="上下文窗口">
<el-input-number v-model="modelForm.contextWindow" :min="1" :max="2000000" />
</el-form-item>
<el-form-item label="Presence Penalty">
<el-input-number v-model="modelForm.presencePenalty" :min="-2" :max="2" :step="0.1" />
</el-form-item>
<el-form-item label="Frequency Penalty">
<el-input-number v-model="modelForm.frequencyPenalty" :min="-2" :max="2" :step="0.1" />
</el-form-item>
<el-form-item label="流式输出">
<el-switch v-model="modelForm.streamEnabled" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="响应格式">
<el-select v-model="modelForm.responseFormat" clearable>
<el-option label="默认文本" value="" />
<el-option label="JSON Object" value="json_object" />
</el-select>
</el-form-item>
<el-form-item label="超时秒数">
<el-input-number v-model="modelForm.timeoutSecond" :min="1" :max="300" />
</el-form-item>
</div>
<el-form-item label="高级 JSON 参数">
<el-input
v-model="modelForm.extraParams"
type="textarea"
:rows="4"
placeholder='例如:{"stop":["###"],"seed":42}'
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="modelForm.remark" placeholder="用途、额度、注意事项等" />
</el-form-item>
@@ -695,9 +812,7 @@ function buildChatQuery() {
<el-table-column prop="apiType" label="协议" width="170" />
<el-table-column prop="modelName" label="模型" />
<el-table-column prop="baseUrl" label="Base URL" min-width="220" show-overflow-tooltip />
<el-table-column prop="temperature" label="温度" width="80" />
<el-table-column prop="topP" label="Top P" width="80" />
<el-table-column prop="maxToken" label="Max" width="90" />
<el-table-column prop="authType" label="鉴权" width="110" />
<el-table-column prop="enabled" label="启用" width="90" />
<el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }">
@@ -740,14 +855,18 @@ function buildChatQuery() {
</el-select>
<el-date-picker
v-model="chatFilters.dateRange"
class="record-date-range"
type="datetimerange"
unlink-panels
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DDTHH:mm:ss"
/>
<el-button type="primary" @click="searchChats">搜索</el-button>
<el-button @click="resetChatFilters">重置</el-button>
<el-button @click="exportChats">导出</el-button>
<div class="record-filter-actions">
<el-button type="primary" @click="searchChats">搜索</el-button>
<el-button @click="resetChatFilters">重置</el-button>
<el-button @click="exportChats">导出</el-button>
</div>
</div>
<el-table :data="chats" stripe>
<el-table-column prop="id" label="会话ID" width="90" />