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 { import type {
AdminProfile, AdminProfile,
AdminUser, AdminUser,
AgentDebugResult,
AiLogRecord, AiLogRecord,
ChatDetail, ChatDetail,
ChatRecord, ChatRecord,
@@ -33,6 +34,10 @@ const chatDetail = ref<ChatDetail | null>(null);
const chatDetailOpen = ref(false); const chatDetailOpen = ref(false);
const recordTab = ref("chats"); const recordTab = ref("chats");
const editingModelId = ref<number | null>(null); 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({ const knowledgeForm = reactive({
name: "", name: "",
@@ -52,12 +57,12 @@ const modelForm = reactive({
apiKey: "", apiKey: "",
authType: "bearer", authType: "bearer",
apiVersion: "", apiVersion: "",
temperature: 0.2 as number | null, temperature: null as number | null,
topP: null as number | null, topP: null as number | null,
topK: null as number | null, topK: null as number | null,
presencePenalty: null as number | null, presencePenalty: null as number | null,
frequencyPenalty: null as number | null, frequencyPenalty: null as number | null,
maxToken: 1024, maxToken: null as number | null,
contextWindow: null as number | null, contextWindow: null as number | null,
streamEnabled: 1, streamEnabled: 1,
responseFormat: "", responseFormat: "",
@@ -80,10 +85,10 @@ const quickModelProviders = [
baseUrl: "https://api.deepseek.com", baseUrl: "https://api.deepseek.com",
authType: "bearer", authType: "bearer",
apiVersion: "", apiVersion: "",
temperature: 0.2, temperature: null,
topP: null, topP: null,
maxToken: 1024, maxToken: null,
contextWindow: 128000, contextWindow: null,
models: [ models: [
{ label: "deepseek-v4-pro", value: "deepseek-v4-pro" }, { label: "deepseek-v4-pro", value: "deepseek-v4-pro" },
{ label: "deepseek-v4-flash", value: "deepseek-v4-flash" }, { label: "deepseek-v4-flash", value: "deepseek-v4-flash" },
@@ -98,8 +103,8 @@ const quickModelProviders = [
apiVersion: "", apiVersion: "",
temperature: null, temperature: null,
topP: null, topP: null,
maxToken: 1024, maxToken: null,
contextWindow: 1000000, contextWindow: null,
models: [ models: [
{ label: "MiniMax-M3", value: "MiniMax-M3" }, { label: "MiniMax-M3", value: "MiniMax-M3" },
{ label: "MiniMax-M2.7", value: "MiniMax-M2.7" }, { label: "MiniMax-M2.7", value: "MiniMax-M2.7" },
@@ -161,6 +166,18 @@ const chatFilters = reactive({
dateRange: [] as string[], 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 () => { onMounted(async () => {
if (!getToken()) return; if (!getToken()) return;
try { try {
@@ -201,7 +218,15 @@ async function loadCurrentMenu() {
if (activeMenu.value === "dashboard") stats.value = await api.dashboard(); if (activeMenu.value === "dashboard") stats.value = await api.dashboard();
if (activeMenu.value === "users") users.value = await api.users(userKeyword.value); if (activeMenu.value === "users") users.value = await api.users(userKeyword.value);
if (activeMenu.value === "knowledge") knowledge.value = await api.knowledge(); 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 === "models") models.value = await api.models();
if (activeMenu.value === "configs") configs.value = await api.configs(); if (activeMenu.value === "configs") configs.value = await api.configs();
if (activeMenu.value === "records") { if (activeMenu.value === "records") {
@@ -260,6 +285,18 @@ function buildModelPayload() {
payload[field] = null; 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; return payload;
} }
@@ -281,13 +318,13 @@ async function quickAddModel() {
apiKey: quickModelForm.apiKey, apiKey: quickModelForm.apiKey,
authType: provider.authType, authType: provider.authType,
apiVersion: provider.apiVersion, apiVersion: provider.apiVersion,
temperature: provider.temperature, temperature: null,
topP: provider.topP, topP: null,
topK: null, topK: null,
presencePenalty: null, presencePenalty: null,
frequencyPenalty: null, frequencyPenalty: null,
maxToken: provider.maxToken, maxToken: null,
contextWindow: provider.contextWindow, contextWindow: null,
streamEnabled: 1, streamEnabled: 1,
responseFormat: null, responseFormat: null,
extraParams: null, extraParams: null,
@@ -332,7 +369,7 @@ function applyModelPreset(label: string) {
{ {
apiVersion: "", apiVersion: "",
authType: "bearer", authType: "bearer",
temperature: 0.2, temperature: null,
topP: null, topP: null,
topK: null, topK: null,
presencePenalty: null, presencePenalty: null,
@@ -356,13 +393,13 @@ function editModel(row: ModelItem) {
apiKey: "", apiKey: "",
authType: row.authType, authType: row.authType,
apiVersion: row.apiVersion ?? "", apiVersion: row.apiVersion ?? "",
temperature: row.temperature ?? null, temperature: null,
topP: row.topP ?? null, topP: null,
topK: row.topK ?? null, topK: null,
presencePenalty: row.presencePenalty ?? null, presencePenalty: null,
frequencyPenalty: row.frequencyPenalty ?? null, frequencyPenalty: null,
maxToken: row.maxToken ?? 1024, maxToken: null,
contextWindow: row.contextWindow ?? null, contextWindow: null,
streamEnabled: row.streamEnabled, streamEnabled: row.streamEnabled,
responseFormat: row.responseFormat ?? "", responseFormat: row.responseFormat ?? "",
extraParams: row.extraParams ?? "", extraParams: row.extraParams ?? "",
@@ -383,12 +420,12 @@ function resetModelForm() {
apiKey: "", apiKey: "",
authType: "bearer", authType: "bearer",
apiVersion: "", apiVersion: "",
temperature: 0.2, temperature: null,
topP: null, topP: null,
topK: null, topK: null,
presencePenalty: null, presencePenalty: null,
frequencyPenalty: null, frequencyPenalty: null,
maxToken: 1024, maxToken: null,
contextWindow: null, contextWindow: null,
streamEnabled: 1, streamEnabled: 1,
responseFormat: "", responseFormat: "",
@@ -427,6 +464,52 @@ async function saveConfig() {
await loadCurrentMenu(); 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() { async function searchChats() {
chats.value = await api.chats(buildChatQuery()); chats.value = await api.chats(buildChatQuery());
} }
@@ -461,7 +544,7 @@ function buildChatQuery() {
<section class="login-box"> <section class="login-box">
<div class="brand">AI</div> <div class="brand">AI</div>
<h1>AI 知识库后台</h1> <h1>AI 知识库后台</h1>
<p>用于维护用户知识库Prompt模型和审计记录</p> <p>用于维护用户知识库Agent模型和审计记录</p>
<el-form label-position="top" @submit.prevent="login"> <el-form label-position="top" @submit.prevent="login">
<el-form-item label="管理员账号"> <el-form-item label="管理员账号">
<el-input v-model="loginForm.username" /> <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 === 'dashboard' }" @click="switchMenu('dashboard')">Dashboard</button>
<button :class="{ active: activeMenu === 'users' }" @click="switchMenu('users')">用户管理</button> <button :class="{ active: activeMenu === 'users' }" @click="switchMenu('users')">用户管理</button>
<button :class="{ active: activeMenu === 'knowledge' }" @click="switchMenu('knowledge')">知识库管理</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 === 'models' }" @click="switchMenu('models')">模型管理</button>
<button :class="{ active: activeMenu === 'configs' }" @click="switchMenu('configs')">系统配置</button> <button :class="{ active: activeMenu === 'configs' }" @click="switchMenu('configs')">系统配置</button>
<button :class="{ active: activeMenu === 'records' }" @click="switchMenu('records')">记录审计</button> <button :class="{ active: activeMenu === 'records' }" @click="switchMenu('records')">记录审计</button>
@@ -549,11 +632,83 @@ function buildChatQuery() {
<template v-if="activeMenu === 'prompt'"> <template v-if="activeMenu === 'prompt'">
<div class="page-head inline"> <div class="page-head inline">
<div><h2>Prompt 管理</h2><p>保存后会影响后续问答 Prompt 组装</p></div> <div><h2>Agent 管理</h2><p>配置提示词模型知识库和调试参数并在右侧实时预览对话效果</p></div>
<el-button @click="resetPrompt">恢复默认</el-button> <el-button @click="resetPrompt">恢复默认提示词</el-button>
</div> </div>
<el-input v-model="promptContent" type="textarea" :rows="16" /> <section class="agent-workbench">
<div class="actions"><el-button type="primary" @click="savePrompt">保存 Prompt</el-button></div> <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>
<template v-if="activeMenu === 'models'"> <template v-if="activeMenu === 'models'">
@@ -639,48 +794,10 @@ function buildChatQuery() {
<el-form-item label="API Version"> <el-form-item label="API Version">
<el-input v-model="modelForm.apiVersion" placeholder="Anthropic 默认 2023-06-01" /> <el-input v-model="modelForm.apiVersion" placeholder="Anthropic 默认 2023-06-01" />
</el-form-item> </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-form-item label="超时秒数">
<el-input-number v-model="modelForm.timeoutSecond" :min="1" :max="300" /> <el-input-number v-model="modelForm.timeoutSecond" :min="1" :max="300" />
</el-form-item> </el-form-item>
</div> </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-form-item label="备注">
<el-input v-model="modelForm.remark" placeholder="用途、额度、注意事项等" /> <el-input v-model="modelForm.remark" placeholder="用途、额度、注意事项等" />
</el-form-item> </el-form-item>
@@ -695,9 +812,7 @@ function buildChatQuery() {
<el-table-column prop="apiType" label="协议" width="170" /> <el-table-column prop="apiType" label="协议" width="170" />
<el-table-column prop="modelName" label="模型" /> <el-table-column prop="modelName" label="模型" />
<el-table-column prop="baseUrl" label="Base URL" min-width="220" show-overflow-tooltip /> <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="authType" label="鉴权" width="110" />
<el-table-column prop="topP" label="Top P" width="80" />
<el-table-column prop="maxToken" label="Max" width="90" />
<el-table-column prop="enabled" label="启用" width="90" /> <el-table-column prop="enabled" label="启用" width="90" />
<el-table-column label="操作" width="210" fixed="right"> <el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
@@ -740,15 +855,19 @@ function buildChatQuery() {
</el-select> </el-select>
<el-date-picker <el-date-picker
v-model="chatFilters.dateRange" v-model="chatFilters.dateRange"
class="record-date-range"
type="datetimerange" type="datetimerange"
unlink-panels
start-placeholder="开始时间" start-placeholder="开始时间"
end-placeholder="结束时间" end-placeholder="结束时间"
value-format="YYYY-MM-DDTHH:mm:ss" value-format="YYYY-MM-DDTHH:mm:ss"
/> />
<div class="record-filter-actions">
<el-button type="primary" @click="searchChats">搜索</el-button> <el-button type="primary" @click="searchChats">搜索</el-button>
<el-button @click="resetChatFilters">重置</el-button> <el-button @click="resetChatFilters">重置</el-button>
<el-button @click="exportChats">导出</el-button> <el-button @click="exportChats">导出</el-button>
</div> </div>
</div>
<el-table :data="chats" stripe> <el-table :data="chats" stripe>
<el-table-column prop="id" label="会话ID" width="90" /> <el-table-column prop="id" label="会话ID" width="90" />
<el-table-column prop="userPhone" label="手机号" width="140" /> <el-table-column prop="userPhone" label="手机号" width="140" />

View File

@@ -1,6 +1,7 @@
import type { import type {
AdminProfile, AdminProfile,
AdminUser, AdminUser,
AgentDebugResult,
AiLogRecord, AiLogRecord,
ApiResponse, ApiResponse,
ChatDetail, ChatDetail,
@@ -83,6 +84,8 @@ export const api = {
savePrompt: (promptContent: string) => savePrompt: (promptContent: string) =>
request<{ promptContent: string }>("/admin/prompt", { method: "PUT", body: JSON.stringify({ promptContent }) }), request<{ promptContent: string }>("/admin/prompt", { method: "PUT", body: JSON.stringify({ promptContent }) }),
resetPrompt: () => request<{ promptContent: string }>("/admin/prompt/reset", { method: "POST", body: "{}" }), 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"), models: () => request<ModelItem[]>("/admin/model/list"),
createModel: (payload: Record<string, unknown>) => createModel: (payload: Record<string, unknown>) =>
request<ModelItem>("/admin/model", { method: "POST", body: JSON.stringify(payload) }), request<ModelItem>("/admin/model", { method: "POST", body: JSON.stringify(payload) }),

View File

@@ -241,18 +241,128 @@ textarea {
width: 100%; 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 { .actions {
margin-top: 12px; margin-top: 12px;
} }
.record-filter { .record-filter {
display: grid; display: flex;
grid-template-columns: minmax(260px, 1.4fr) 140px 150px 360px 72px 72px 72px; flex-wrap: wrap;
gap: 10px; gap: 10px;
align-items: center; align-items: flex-start;
margin-bottom: 14px; 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 { .chat-summary {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));

View File

@@ -65,6 +65,16 @@ export interface ModelItem {
enabled: number; enabled: number;
} }
export interface AgentDebugResult {
ok: boolean;
message: string;
answer: string;
prompt: string;
modelName?: string;
retrieveCount?: number;
knowledgeIds?: string;
}
export interface ChatRecord { export interface ChatRecord {
id: number; id: number;
userId: number; userId: number;

View File

@@ -9,9 +9,19 @@ from app.core.dependencies import get_current_admin
from app.core.responses import api_success from app.core.responses import api_success
from app.models.admin import Admin from app.models.admin import Admin
from app.models.ai_config import ModelConfig, Prompt, SystemConfig 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.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.model_service import ModelClientService
from app.services.rag_service import RagResult
router = APIRouter() 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}) 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") @router.get("/model/list")
def list_models(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict: 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() 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: def _config_dict(config: SystemConfig) -> dict:
return { return {
"id": config.id, "id": config.id,

View File

@@ -51,6 +51,19 @@ class PromptSaveRequest(BaseModel):
promptContent: str = Field(min_length=1) 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): class ModelSaveRequest(BaseModel):
provider: str = Field(min_length=1, max_length=50) provider: str = Field(min_length=1, max_length=50)
displayName: str | None = Field(default=None, max_length=100) displayName: str | None = Field(default=None, max_length=100)

View File

@@ -59,6 +59,23 @@ class ModelClientService:
return {"ok": False, "message": str(exc), "answer": ""} return {"ok": False, "message": str(exc), "answer": ""}
return {"ok": True, "message": "模型配置可用", "answer": answer[:500]} 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: def _mock_answer(rag_result: RagResult) -> str:
if not rag_result.is_hit: 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: def _rough_token_count(text: str) -> int:
return max(1, len(text.strip()) // 2) return max(1, len(text.strip()) // 2)

View File

@@ -41,6 +41,7 @@
| D-018 | 模型高级参数用 JSON 对象透传。 | 默认采用 | 用较低成本兼容供应商差异,避免数据库字段无限膨胀。 | | D-018 | 模型高级参数用 JSON 对象透传。 | 默认采用 | 用较低成本兼容供应商差异,避免数据库字段无限膨胀。 |
| D-019 | 管理后台 Element Plus 改为按需注册,并拆分表格和基础 vendor chunk。 | 默认采用 | 解决后台构建单 chunk 过大的提示,降低首屏主入口体积,同时不引入额外构建插件。 | | D-019 | 管理后台 Element Plus 改为按需注册,并拆分表格和基础 vendor chunk。 | 默认采用 | 解决后台构建单 chunk 过大的提示,降低首屏主入口体积,同时不引入额外构建插件。 |
| D-020 | MiniMax 快速添加默认使用 Anthropic Messages 兼容协议。 | 默认采用 | MiniMax 文本生成指南推荐 Anthropic-compatible 路径;后端按 `auth_type` 兼容 Bearer 和 x-api-key 两种鉴权。 | | D-020 | MiniMax 快速添加默认使用 Anthropic Messages 兼容协议。 | 默认采用 | MiniMax 文本生成指南推荐 Anthropic-compatible 路径;后端按 `auth_type` 兼容 Bearer 和 x-api-key 两种鉴权。 |
| D-021 | 模型管理只负责 LLM 接入Agent 管理负责提示词、知识库、模型选择和推理参数调试。 | 默认采用 | 避免模型连接配置与 Agent 运行策略混在一起,方便后续做 Agent 版本、调试和发布流程。 |
## 待确认决策 ## 待确认决策

View File

@@ -0,0 +1,72 @@
# Agent 管理与模型接入边界调整记录
日期2026-07-07
## 背景
模型管理页面此前同时承担了“接入大模型”和“配置调用参数”的职责,导致 API Key 后面出现 Temperature、Top P、Max Tokens、Penalty、高级 JSON 参数等内容。实际产品语义上,这些参数更像 Agent 调试和运行策略,不应该放在模型接入页面。
同时,原来的 Prompt 管理只是一块提示词文本编辑区,无法选择模型、知识库和调试参数,也没有预览聊天窗口,不利于调试 Agent 效果。
## 本次调整
### 模型管理
- 保留模型接入相关字段:
- 快速添加 DeepSeek / MiniMax
- 协议模板
- 供应商标识
- 显示名称
- 协议类型
- 模型名
- Base URL
- API URL
- 鉴权方式
- API Key
- API Version
- 超时秒数
- 备注
- 移除模型管理页面中的 Agent 参数:
- Temperature
- Top P
- Top K
- Max Tokens
- Presence Penalty
- Frequency Penalty
- 上下文窗口
- 流式输出
- 响应格式
- 高级 JSON 参数
### Agent 管理
- 将侧边栏“Prompt 管理”改为“Agent 管理”。
- Agent 管理页新增两栏布局:
- 左侧:提示词、模型、知识库、调试参数、调试问题。
- 右侧:预览聊天窗口。
- 支持在 Agent 管理页选择:
- 已接入模型。
- 已启用知识库。
- Temperature、Top P、Top K、Max Tokens、Presence Penalty、Frequency Penalty。
- 新增管理员调试接口 `POST /api/admin/agent/debug`
- 调试接口不会保存参数到模型配置,只对本次请求临时生效。
### 记录审计
- 修复聊天记录筛选区时间范围控件挤压问题。
- 筛选区改为可换行工具栏。
- 时间范围控件固定宽度,搜索、重置、导出按钮保持同一行。
## 当前边界
- Agent 调试参数当前只用于调试请求,尚未落库为正式 Agent 配置版本。
- 目前仍只有一个全局提示词存储表 `sys_prompt`,后续如需要多 Agent、多版本、灰度发布需要新增 Agent 配置表。
- 调试接口会调用已选模型;如果本地没有配置真实模型,无法完成真实模型调试。
- 知识库选择会参与检索范围;当前开发环境仍可能使用 mock 检索结果。
## 后续建议
1. 新增正式 `sys_agent` / `sys_agent_version` 表,把提示词、模型、知识库和参数形成可发布配置。
2. Agent 管理页增加“保存草稿 / 发布版本 / 回滚版本”。
3. Agent 调试记录单独落日志,便于追踪调试问题和参数变化。
4. 模型管理后续只维护连接可用性、密钥、协议和启停状态。

View File

@@ -35,4 +35,5 @@
| `2026-07-07-admin-chat-records-completion.md` | 管理后台聊天记录补齐记录。 | | `2026-07-07-admin-chat-records-completion.md` | 管理后台聊天记录补齐记录。 |
| `2026-07-07-model-management-completion.md` | 管理后台模型管理补全记录。 | | `2026-07-07-model-management-completion.md` | 管理后台模型管理补全记录。 |
| `2026-07-07-admin-web-build-optimization.md` | 管理后台构建体积优化记录。 | | `2026-07-07-admin-web-build-optimization.md` | 管理后台构建体积优化记录。 |
| `2026-07-07-agent-management-adjustment.md` | Agent 管理与模型接入边界调整记录。 |
| `../test_records/2026-07-06-phase6-verification.md` | 阶段六验证记录。 | | `../test_records/2026-07-06-phase6-verification.md` | 阶段六验证记录。 |