feat(agent): persist production generation settings
This commit is contained in:
@@ -103,7 +103,7 @@ const modelForm = reactive({
|
||||
topK: null as number | null,
|
||||
presencePenalty: null as number | null,
|
||||
frequencyPenalty: null as number | null,
|
||||
maxToken: null as number | null,
|
||||
maxToken: 8192 as number | null,
|
||||
contextWindow: null as number | null,
|
||||
streamEnabled: 1,
|
||||
responseFormat: "",
|
||||
@@ -471,18 +471,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -513,7 +501,7 @@ async function quickAddModel() {
|
||||
topK: null,
|
||||
presencePenalty: null,
|
||||
frequencyPenalty: null,
|
||||
maxToken: null,
|
||||
maxToken: 8192,
|
||||
contextWindow: null,
|
||||
streamEnabled: 1,
|
||||
responseFormat: null,
|
||||
@@ -594,13 +582,13 @@ function editModel(row: ModelItem) {
|
||||
apiKey: "",
|
||||
authType: row.authType,
|
||||
apiVersion: row.apiVersion ?? "",
|
||||
temperature: null,
|
||||
topP: null,
|
||||
topK: null,
|
||||
presencePenalty: null,
|
||||
frequencyPenalty: null,
|
||||
maxToken: null,
|
||||
contextWindow: null,
|
||||
temperature: row.temperature ?? null,
|
||||
topP: row.topP ?? null,
|
||||
topK: row.topK ?? null,
|
||||
presencePenalty: row.presencePenalty ?? null,
|
||||
frequencyPenalty: row.frequencyPenalty ?? null,
|
||||
maxToken: row.maxToken ?? 8192,
|
||||
contextWindow: row.contextWindow ?? null,
|
||||
streamEnabled: row.streamEnabled,
|
||||
responseFormat: row.responseFormat ?? "",
|
||||
extraParams: row.extraParams ?? "",
|
||||
@@ -626,7 +614,7 @@ function resetModelForm() {
|
||||
topK: null,
|
||||
presencePenalty: null,
|
||||
frequencyPenalty: null,
|
||||
maxToken: null,
|
||||
maxToken: 8192,
|
||||
contextWindow: null,
|
||||
streamEnabled: 1,
|
||||
responseFormat: "",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
temperature: number | null;
|
||||
topP: number | null;
|
||||
topK: number | null;
|
||||
presencePenalty: number | null;
|
||||
frequencyPenalty: number | null;
|
||||
maxToken: number | null;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
"update:temperature": [value: number | null];
|
||||
"update:topP": [value: number | null];
|
||||
"update:topK": [value: number | null];
|
||||
"update:presencePenalty": [value: number | null];
|
||||
"update:frequencyPenalty": [value: number | null];
|
||||
"update:maxToken": [value: number | null];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-config-grid">
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">回答随机性<el-tooltip placement="top" :show-after="200" content="取值 0–2。数值越小,回答越稳定、一致;数值越大,表达越多样,但也更容易发散。答疑场景建议使用较低数值。"><button type="button" class="agent-parameter-help" aria-label="回答随机性说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number :model-value="temperature" :disabled="disabled" :min="0" :max="2" :step="0.1" @update:model-value="$emit('update:temperature', $event)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">核采样概率<el-tooltip placement="top" :show-after="200" content="取值 0–1。模型只从累计概率达到该数值的候选词中选择。数值越小越专注,越大越多样。留空表示使用模型供应商默认值。"><button type="button" class="agent-parameter-help" aria-label="核采样概率说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number :model-value="topP" :disabled="disabled" :min="0" :max="1" :step="0.05" @update:model-value="$emit('update:topP', $event)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">候选词数量<el-tooltip placement="top" :show-after="200" content="取值 1–1000。每次生成只从概率最高的前 K 个候选词中选择。数值越小越保守,越大选择越多。留空表示使用供应商默认值,部分模型可能不支持。"><button type="button" class="agent-parameter-help" aria-label="候选词数量说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number :model-value="topK" :disabled="disabled" :min="1" :max="1000" @update:model-value="$emit('update:topK', $event)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">最大输出长度<el-tooltip placement="top" :show-after="200" content="限制模型本次最多生成的 Token 数,不等于字数。建议正式答疑使用 4096–8192;数值越大允许回答越长,也会增加耗时和费用,最终仍受模型自身上限约束。"><button type="button" class="agent-parameter-help" aria-label="最大输出长度说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number :model-value="maxToken" :disabled="disabled" :min="256" :max="100000" :step="256" @update:model-value="$emit('update:maxToken', $event)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">主题重复惩罚<el-tooltip placement="top" :show-after="200" content="取值 -2–2,0 表示不调整。正数会降低已出现内容再次出现的概率,鼓励引入新主题;负数会更容易重复已出现内容。数值过高可能导致回答跳跃。"><button type="button" class="agent-parameter-help" aria-label="主题重复惩罚说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number :model-value="presencePenalty" :disabled="disabled" :min="-2" :max="2" :step="0.1" @update:model-value="$emit('update:presencePenalty', $event)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">高频重复惩罚<el-tooltip placement="top" :show-after="200" content="取值 -2–2,0 表示不调整。正数会根据词语已出现的次数降低其再次出现的概率,用于减少反复表达;负数会增加重复倾向。数值过高可能影响语句连贯性。"><button type="button" class="agent-parameter-help" aria-label="高频重复惩罚说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number :model-value="frequencyPenalty" :disabled="disabled" :min="-2" :max="2" :step="0.1" @update:model-value="$emit('update:frequencyPenalty', $event)" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,7 +3,8 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
|
||||
import { api } from "../services/api";
|
||||
import type { AgentDebugResult, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api";
|
||||
import type { AgentDebugResult, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api";
|
||||
import AgentGenerationParameters from "./AgentGenerationParameters.vue";
|
||||
import AdminPagination from "./AdminPagination.vue";
|
||||
|
||||
const props = defineProps<{ previewKnowledgeId?: number | null }>();
|
||||
@@ -11,6 +12,7 @@ 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 activeConfigTab = ref("prompt");
|
||||
@@ -37,9 +39,18 @@ const agentForm = reactive({
|
||||
topK: null as number | null,
|
||||
presencePenalty: null as number | null,
|
||||
frequencyPenalty: null as number | null,
|
||||
maxToken: 1024 as number | null,
|
||||
maxToken: 8192 as number | null,
|
||||
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,
|
||||
});
|
||||
|
||||
const promptDirty = computed(() => promptContent.value !== savedPromptContent.value);
|
||||
const selectedModelName = computed(() => {
|
||||
@@ -63,13 +74,15 @@ watch(() => props.previewKnowledgeId, (knowledgeId) => {
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [promptResult, modelRows, knowledgeRows] = await Promise.all([
|
||||
api.prompt(), api.models(), api.knowledgeOptions(),
|
||||
const [promptResult, modelRows, knowledgeRows, formalConfig] = await Promise.all([
|
||||
api.prompt(), api.models(), api.knowledgeOptions(), api.agentRuntimeConfig(),
|
||||
]);
|
||||
applyPrompt(promptResult);
|
||||
models.value = modelRows;
|
||||
knowledge.value = knowledgeRows;
|
||||
agentForm.modelId = modelRows.find((item) => item.enabled === 1)?.id ?? modelRows[0]?.id;
|
||||
applyRuntimeConfig(formalConfig);
|
||||
applyDebugModelDefaults(agentForm.modelId);
|
||||
agentForm.knowledgeIds = knowledgeRows
|
||||
.filter((item) => item.status === 1 && item.lifecycleStatus === "active")
|
||||
.map((item) => item.id);
|
||||
@@ -82,6 +95,56 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
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];
|
||||
@@ -262,7 +325,7 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
<div class="page-head agent-page-head">
|
||||
<div>
|
||||
<h2>Agent 管理</h2>
|
||||
<p>维护主提示词及历史版本,配置本次调试参数,并验证真实检索与回答效果。</p>
|
||||
<p>维护主提示词、用户端正式生成参数和历史版本,并验证真实检索与回答效果。</p>
|
||||
</div>
|
||||
<div class="agent-current-summary">
|
||||
<el-tag type="success">当前版本 {{ prompt?.id ? `#${prompt.id}` : "系统默认" }}</el-tag>
|
||||
@@ -286,11 +349,34 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
</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"
|
||||
/>
|
||||
</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="选择已接入模型">
|
||||
<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>
|
||||
@@ -301,32 +387,14 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
</el-form-item>
|
||||
<section class="agent-advanced-settings">
|
||||
<div class="agent-subsection-title"><h4>高级生成参数</h4><span>仅影响本次后台调试</span></div>
|
||||
<div class="agent-config-grid">
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">回答随机性<el-tooltip placement="top" :show-after="200" content="取值 0–2。数值越小,回答越稳定、一致;数值越大,表达越多样,但也更容易发散。答疑场景建议使用较低数值。"><button type="button" class="agent-parameter-help" aria-label="回答随机性说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number v-model="agentForm.temperature" :min="0" :max="2" :step="0.1" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">核采样概率<el-tooltip placement="top" :show-after="200" content="取值 0–1。模型只从累计概率达到该数值的候选词中选择。数值越小越专注,越大越多样。留空表示使用模型供应商默认值。"><button type="button" class="agent-parameter-help" aria-label="核采样概率说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number v-model="agentForm.topP" :min="0" :max="1" :step="0.05" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">候选词数量<el-tooltip placement="top" :show-after="200" content="取值 1–1000。每次生成只从概率最高的前 K 个候选词中选择。数值越小越保守,越大选择越多。留空表示使用供应商默认值,部分模型可能不支持。"><button type="button" class="agent-parameter-help" aria-label="候选词数量说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number v-model="agentForm.topK" :min="1" :max="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">最大输出长度<el-tooltip placement="top" :show-after="200" content="限制模型本次最多生成的 Token 数,不等于字数,也不代表一定会生成到该长度。数值越大允许回答越长,同时可能增加耗时和费用,最终仍受模型上限约束。"><button type="button" class="agent-parameter-help" aria-label="最大输出长度说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number v-model="agentForm.maxToken" :min="1" :max="100000" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">主题重复惩罚<el-tooltip placement="top" :show-after="200" content="取值 -2–2,0 表示不调整。正数会降低已出现内容再次出现的概率,鼓励引入新主题;负数会更容易重复已出现内容。数值过高可能导致回答跳跃。"><button type="button" class="agent-parameter-help" aria-label="主题重复惩罚说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number v-model="agentForm.presencePenalty" :min="-2" :max="2" :step="0.1" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label><span class="agent-parameter-label">高频重复惩罚<el-tooltip placement="top" :show-after="200" content="取值 -2–2,0 表示不调整。正数会根据词语已出现的次数降低其再次出现的概率,用于减少反复表达;负数会增加重复倾向。数值过高可能影响语句连贯性。"><button type="button" class="agent-parameter-help" aria-label="高频重复惩罚说明">?</button></el-tooltip></span></template>
|
||||
<el-input-number v-model="agentForm.frequencyPenalty" :min="-2" :max="2" :step="0.1" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<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"
|
||||
/>
|
||||
</section>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -2,6 +2,8 @@ import type {
|
||||
AdminProfile,
|
||||
AdminUser,
|
||||
AgentDebugResult,
|
||||
AgentGenerationConfig,
|
||||
AgentRuntimeConfig,
|
||||
AiLogRecord,
|
||||
ApiResponse,
|
||||
ChatDetail,
|
||||
@@ -178,6 +180,9 @@ export const api = {
|
||||
restorePrompt: (id: number) => request<PromptDetail>(`/admin/prompt/history/${id}/restore`, { method: "POST", body: "{}" }),
|
||||
debugAgent: (payload: Record<string, unknown>) =>
|
||||
request<AgentDebugResult>("/admin/agent/debug", { method: "POST", body: JSON.stringify(payload) }),
|
||||
agentRuntimeConfig: () => request<AgentRuntimeConfig>("/admin/agent/runtime-config"),
|
||||
saveAgentRuntimeConfig: (payload: AgentGenerationConfig) =>
|
||||
request<AgentRuntimeConfig>("/admin/agent/runtime-config", { method: "PUT", 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) }),
|
||||
|
||||
@@ -133,6 +133,20 @@ export interface ModelItem {
|
||||
enabled: number;
|
||||
}
|
||||
|
||||
export interface AgentGenerationConfig {
|
||||
temperature: number | null;
|
||||
topP: number | null;
|
||||
topK: number | null;
|
||||
presencePenalty: number | null;
|
||||
frequencyPenalty: number | null;
|
||||
maxToken: number;
|
||||
}
|
||||
|
||||
export interface AgentRuntimeConfig extends AgentGenerationConfig {
|
||||
modelId: number | null;
|
||||
modelName: string | null;
|
||||
}
|
||||
|
||||
export interface AgentDebugResult {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.models.ai_config import ModelConfig, Prompt, SystemConfig
|
||||
from app.models.knowledge import Knowledge
|
||||
from app.schemas.admin import (
|
||||
AgentDebugRequest,
|
||||
AgentRuntimeConfigSaveRequest,
|
||||
EnableModelRequest,
|
||||
ModelSaveRequest,
|
||||
PromptSaveRequest,
|
||||
@@ -184,6 +185,46 @@ async def debug_agent(
|
||||
return api_success(result)
|
||||
|
||||
|
||||
@router.get("/agent/runtime-config")
|
||||
def get_agent_runtime_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
model = _enabled_model(db)
|
||||
return api_success(_agent_runtime_config_dict(model))
|
||||
|
||||
|
||||
@router.put("/agent/runtime-config")
|
||||
def save_agent_runtime_config(
|
||||
payload: AgentRuntimeConfigSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
model = _enabled_model(db)
|
||||
if model is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="当前没有已启用模型,请先在模型管理中启用一个模型",
|
||||
)
|
||||
model.temperature = payload.temperature
|
||||
model.top_p = payload.topP
|
||||
model.top_k = payload.topK
|
||||
model.presence_penalty = payload.presencePenalty
|
||||
model.frequency_penalty = payload.frequencyPenalty
|
||||
model.max_token = payload.maxToken
|
||||
db.add(model)
|
||||
OperationLogService.write(
|
||||
db,
|
||||
admin_id=current_admin.id,
|
||||
module="agent",
|
||||
action="save_runtime_config",
|
||||
target_id=model.id,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(model)
|
||||
return api_success(_agent_runtime_config_dict(model))
|
||||
|
||||
|
||||
@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()
|
||||
@@ -376,6 +417,28 @@ def _model_dict(model: ModelConfig) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _enabled_model(db: Session) -> ModelConfig | None:
|
||||
return db.scalar(
|
||||
select(ModelConfig)
|
||||
.where(ModelConfig.enabled == 1)
|
||||
.order_by(ModelConfig.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
|
||||
def _agent_runtime_config_dict(model: ModelConfig | None) -> dict:
|
||||
return {
|
||||
"modelId": model.id if model is not None else None,
|
||||
"modelName": (model.display_name or model.model_name) if model is not None else None,
|
||||
"temperature": float(model.temperature) if model is not None and model.temperature is not None else None,
|
||||
"topP": float(model.top_p) if model is not None and model.top_p is not None else None,
|
||||
"topK": model.top_k if model is not None else None,
|
||||
"presencePenalty": float(model.presence_penalty) if model is not None and model.presence_penalty is not None else None,
|
||||
"frequencyPenalty": float(model.frequency_penalty) if model is not None and model.frequency_penalty is not None else None,
|
||||
"maxToken": model.max_token if model is not None and model.max_token is not None else 8192,
|
||||
}
|
||||
|
||||
|
||||
def _default_prompt_detail() -> dict:
|
||||
return {
|
||||
"id": None,
|
||||
|
||||
@@ -89,7 +89,16 @@ class AgentDebugRequest(BaseModel):
|
||||
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)
|
||||
maxToken: int | None = Field(default=8192, ge=1, le=100000)
|
||||
|
||||
|
||||
class AgentRuntimeConfigSaveRequest(BaseModel):
|
||||
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 = Field(default=8192, ge=256, le=100000)
|
||||
|
||||
|
||||
class ModelSaveRequest(BaseModel):
|
||||
|
||||
@@ -494,7 +494,7 @@ def _agent_system_prompt() -> str:
|
||||
|
||||
|
||||
def _max_output_tokens(model: ModelConfig) -> int:
|
||||
configured = max(1, model.max_token or 1024)
|
||||
configured = max(1, model.max_token or 8192)
|
||||
if not model.context_window:
|
||||
return configured
|
||||
return min(configured, max(1, model.context_window // 3))
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.api.admin_settings import get_agent_runtime_config, save_agent_runtime_config
|
||||
from app.models import Base
|
||||
from app.models.admin import Admin
|
||||
from app.models.ai_config import ModelConfig
|
||||
from app.schemas.admin import AgentRuntimeConfigSaveRequest
|
||||
from app.services.model_stream_service import _openai_stream_payload
|
||||
from app.services.model_service import _max_output_tokens
|
||||
from app.services.rag_service import RagResult
|
||||
|
||||
|
||||
def _database() -> Session:
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
return Session(engine)
|
||||
|
||||
|
||||
def _admin() -> Admin:
|
||||
return Admin(id=1, username="admin", password="hash", name="系统管理员", status=1)
|
||||
|
||||
|
||||
def _model() -> ModelConfig:
|
||||
return ModelConfig(
|
||||
id=1,
|
||||
provider="openai-compatible",
|
||||
display_name="正式模型",
|
||||
api_type="openai_compatible",
|
||||
model_name="production-model",
|
||||
base_url="https://example.com/v1",
|
||||
api_url="",
|
||||
api_key="encrypted",
|
||||
auth_type="bearer",
|
||||
max_token=None,
|
||||
stream_enabled=1,
|
||||
timeout_second=30,
|
||||
enabled=1,
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_config_defaults_to_long_answer_safe_max_tokens():
|
||||
with _database() as db:
|
||||
admin = _admin()
|
||||
model = _model()
|
||||
db.add_all([admin, model])
|
||||
db.commit()
|
||||
|
||||
result = get_agent_runtime_config(db=db, current_admin=admin)["data"]
|
||||
|
||||
assert result["modelId"] == model.id
|
||||
assert result["modelName"] == "正式模型"
|
||||
assert result["maxToken"] == 8192
|
||||
assert _max_output_tokens(model) == 8192
|
||||
|
||||
|
||||
def test_saved_runtime_config_is_persisted_on_enabled_model():
|
||||
with _database() as db:
|
||||
admin = _admin()
|
||||
model = _model()
|
||||
db.add_all([admin, model])
|
||||
db.commit()
|
||||
|
||||
result = save_agent_runtime_config(
|
||||
AgentRuntimeConfigSaveRequest(
|
||||
temperature=0.3,
|
||||
topP=0.9,
|
||||
topK=40,
|
||||
presencePenalty=0.2,
|
||||
frequencyPenalty=0.4,
|
||||
maxToken=12000,
|
||||
),
|
||||
db=db,
|
||||
current_admin=admin,
|
||||
)["data"]
|
||||
|
||||
db.refresh(model)
|
||||
assert result["maxToken"] == 12000
|
||||
assert model.max_token == 12000
|
||||
assert model.temperature == Decimal("0.30")
|
||||
assert model.top_p == Decimal("0.90")
|
||||
assert model.top_k == 40
|
||||
assert model.presence_penalty == Decimal("0.20")
|
||||
assert model.frequency_penalty == Decimal("0.40")
|
||||
|
||||
payload = _openai_stream_payload(
|
||||
model,
|
||||
RagResult(question="测试", knowledge_scopes=[], chunks=[], prompt="测试", allow_general_knowledge=True),
|
||||
)
|
||||
assert payload["max_tokens"] == 12000
|
||||
assert payload["temperature"] == 0.3
|
||||
assert payload["top_p"] == 0.9
|
||||
assert payload["presence_penalty"] == 0.2
|
||||
assert payload["frequency_penalty"] == 0.4
|
||||
Reference in New Issue
Block a user