Separate agent tuning from model setup
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AdminProfile,
|
||||
AdminUser,
|
||||
AgentDebugResult,
|
||||
AiLogRecord,
|
||||
ApiResponse,
|
||||
ChatDetail,
|
||||
@@ -83,6 +84,8 @@ export const api = {
|
||||
savePrompt: (promptContent: string) =>
|
||||
request<{ promptContent: string }>("/admin/prompt", { method: "PUT", body: JSON.stringify({ promptContent }) }),
|
||||
resetPrompt: () => request<{ promptContent: string }>("/admin/prompt/reset", { method: "POST", body: "{}" }),
|
||||
debugAgent: (payload: Record<string, unknown>) =>
|
||||
request<AgentDebugResult>("/admin/agent/debug", { method: "POST", body: JSON.stringify(payload) }),
|
||||
models: () => request<ModelItem[]>("/admin/model/list"),
|
||||
createModel: (payload: Record<string, unknown>) =>
|
||||
request<ModelItem>("/admin/model", { method: "POST", body: JSON.stringify(payload) }),
|
||||
|
||||
@@ -241,18 +241,128 @@ textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-workbench {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(520px, 1fr) minmax(360px, 420px);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.agent-config-panel,
|
||||
.agent-preview-panel {
|
||||
padding: 18px;
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.agent-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 14px;
|
||||
}
|
||||
|
||||
.agent-config-panel .el-select,
|
||||
.agent-config-panel .el-input-number,
|
||||
.agent-config-panel .el-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-preview-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.agent-preview-head h3 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.agent-preview-head span {
|
||||
overflow: hidden;
|
||||
color: #667a73;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-preview-chat {
|
||||
height: 560px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.agent-preview-message {
|
||||
max-width: 92%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 8px;
|
||||
background: #f8fbfa;
|
||||
}
|
||||
|
||||
.agent-preview-message.user {
|
||||
align-self: flex-end;
|
||||
border-color: #cde7df;
|
||||
background: #e9f5f1;
|
||||
}
|
||||
|
||||
.agent-preview-message strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #0f735d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-preview-message pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.record-filter {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1.4fr) 140px 150px 360px 72px 72px 72px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.record-filter > .el-input {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.record-filter > .el-input-number {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.record-filter > .el-select {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.record-date-range {
|
||||
width: 580px !important;
|
||||
max-width: 100%;
|
||||
flex: 0 0 580px;
|
||||
}
|
||||
|
||||
.record-filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
@@ -65,6 +65,16 @@ export interface ModelItem {
|
||||
enabled: number;
|
||||
}
|
||||
|
||||
export interface AgentDebugResult {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
answer: string;
|
||||
prompt: string;
|
||||
modelName?: string;
|
||||
retrieveCount?: number;
|
||||
knowledgeIds?: string;
|
||||
}
|
||||
|
||||
export interface ChatRecord {
|
||||
id: number;
|
||||
userId: number;
|
||||
|
||||
@@ -9,9 +9,19 @@ from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.ai_config import ModelConfig, Prompt, SystemConfig
|
||||
from app.schemas.admin import EnableModelRequest, ModelSaveRequest, PromptSaveRequest, SystemConfigSaveRequest
|
||||
from app.models.knowledge import Knowledge
|
||||
from app.schemas.admin import (
|
||||
AgentDebugRequest,
|
||||
EnableModelRequest,
|
||||
ModelSaveRequest,
|
||||
PromptSaveRequest,
|
||||
SystemConfigSaveRequest,
|
||||
)
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.services.feishu_service import FeishuKnowledgeService
|
||||
from app.services.knowledge_service import KnowledgeScope
|
||||
from app.services.model_service import ModelClientService
|
||||
from app.services.rag_service import RagResult
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -48,6 +58,37 @@ def reset_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(g
|
||||
return api_success({"promptContent": prompt.prompt_content})
|
||||
|
||||
|
||||
@router.post("/agent/debug")
|
||||
def debug_agent(
|
||||
payload: AgentDebugRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
model = db.get(ModelConfig, payload.modelId)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
|
||||
|
||||
scopes = _agent_knowledge_scopes(db, payload.knowledgeIds)
|
||||
chunks = FeishuKnowledgeService.retrieve(payload.question, scopes)
|
||||
prompt = _build_agent_debug_prompt(payload.promptContent, payload.question, chunks)
|
||||
rag_result = RagResult(question=payload.question, knowledge_scopes=scopes, chunks=chunks, prompt=prompt)
|
||||
result = ModelClientService.debug_model(
|
||||
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,
|
||||
},
|
||||
)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="agent", action="debug", target_id=model.id)
|
||||
db.commit()
|
||||
return api_success(result)
|
||||
|
||||
|
||||
@router.get("/model/list")
|
||||
def list_models(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
models = db.scalars(select(ModelConfig).order_by(ModelConfig.id.desc())).all()
|
||||
@@ -235,6 +276,32 @@ def _model_dict(model: ModelConfig) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _agent_knowledge_scopes(db: Session, knowledge_ids: list[int]) -> list[KnowledgeScope]:
|
||||
query = select(Knowledge).where(Knowledge.status == 1)
|
||||
if knowledge_ids:
|
||||
query = query.where(Knowledge.id.in_(knowledge_ids))
|
||||
rows = db.scalars(query.order_by(Knowledge.id.asc())).all()
|
||||
return [
|
||||
KnowledgeScope(
|
||||
id=item.id,
|
||||
name=item.name,
|
||||
feishu_space_id=item.feishu_space_id,
|
||||
feishu_node_id=item.feishu_node_id,
|
||||
)
|
||||
for item in rows
|
||||
]
|
||||
|
||||
|
||||
def _build_agent_debug_prompt(prompt_content: str, question: str, chunks: list) -> str:
|
||||
context = "\n\n".join(
|
||||
f"[知识片段 {index}] 来源:{chunk.knowledge_name} / {chunk.title}\n{chunk.content}"
|
||||
for index, chunk in enumerate(chunks, start=1)
|
||||
)
|
||||
if not context:
|
||||
context = "未检索到相关知识片段。"
|
||||
return f"{prompt_content.strip()}\n\n{context}\n\n用户问题:{question.strip()}"
|
||||
|
||||
|
||||
def _config_dict(config: SystemConfig) -> dict:
|
||||
return {
|
||||
"id": config.id,
|
||||
|
||||
@@ -51,6 +51,19 @@ class PromptSaveRequest(BaseModel):
|
||||
promptContent: str = Field(min_length=1)
|
||||
|
||||
|
||||
class AgentDebugRequest(BaseModel):
|
||||
promptContent: str = Field(min_length=1)
|
||||
modelId: int = Field(gt=0)
|
||||
knowledgeIds: list[int] = Field(default_factory=list)
|
||||
question: str = Field(min_length=1, max_length=2000)
|
||||
temperature: float | None = Field(default=None, ge=0, le=2)
|
||||
topP: float | None = Field(default=None, ge=0, le=1)
|
||||
topK: int | None = Field(default=None, ge=1, le=1000)
|
||||
presencePenalty: float | None = Field(default=None, ge=-2, le=2)
|
||||
frequencyPenalty: float | None = Field(default=None, ge=-2, le=2)
|
||||
maxToken: int | None = Field(default=1024, ge=1, le=100000)
|
||||
|
||||
|
||||
class ModelSaveRequest(BaseModel):
|
||||
provider: str = Field(min_length=1, max_length=50)
|
||||
displayName: str | None = Field(default=None, max_length=100)
|
||||
|
||||
@@ -59,6 +59,23 @@ class ModelClientService:
|
||||
return {"ok": False, "message": str(exc), "answer": ""}
|
||||
return {"ok": True, "message": "模型配置可用", "answer": answer[:500]}
|
||||
|
||||
@staticmethod
|
||||
def debug_model(model: ModelConfig, rag_result: RagResult, overrides: dict[str, Any]) -> dict[str, Any]:
|
||||
debug_model = _copy_model_with_overrides(model, overrides)
|
||||
try:
|
||||
answer = _call_configured_model(debug_model, rag_result, allow_no_hit=True)
|
||||
except ExternalServiceError as exc:
|
||||
return {"ok": False, "message": str(exc), "answer": "", "prompt": rag_result.prompt}
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "Agent 调试完成",
|
||||
"answer": answer[:4000],
|
||||
"prompt": rag_result.prompt,
|
||||
"modelName": model.model_name,
|
||||
"retrieveCount": len(rag_result.chunks),
|
||||
"knowledgeIds": rag_result.knowledge_ids,
|
||||
}
|
||||
|
||||
|
||||
def _mock_answer(rag_result: RagResult) -> str:
|
||||
if not rag_result.is_hit:
|
||||
@@ -75,6 +92,35 @@ def _mock_answer(rag_result: RagResult) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _copy_model_with_overrides(model: ModelConfig, overrides: dict[str, Any]) -> ModelConfig:
|
||||
debug_model = ModelConfig(
|
||||
provider=model.provider,
|
||||
display_name=model.display_name,
|
||||
api_type=model.api_type,
|
||||
model_name=model.model_name,
|
||||
base_url=model.base_url,
|
||||
api_url=model.api_url,
|
||||
api_key=model.api_key,
|
||||
auth_type=model.auth_type,
|
||||
api_version=model.api_version,
|
||||
temperature=overrides.get("temperature"),
|
||||
top_p=overrides.get("top_p"),
|
||||
top_k=overrides.get("top_k"),
|
||||
presence_penalty=overrides.get("presence_penalty"),
|
||||
frequency_penalty=overrides.get("frequency_penalty"),
|
||||
max_token=overrides.get("max_token"),
|
||||
context_window=model.context_window,
|
||||
stream_enabled=model.stream_enabled,
|
||||
response_format=model.response_format,
|
||||
extra_params=model.extra_params,
|
||||
remark=model.remark,
|
||||
timeout_second=model.timeout_second,
|
||||
enabled=model.enabled,
|
||||
)
|
||||
debug_model.id = model.id
|
||||
return debug_model
|
||||
|
||||
|
||||
def _rough_token_count(text: str) -> int:
|
||||
return max(1, len(text.strip()) // 2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user