From 6c02662d3b0c1c55606908edf79613cdccb3b8bb Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Thu, 16 Jul 2026 16:07:11 +0800 Subject: [PATCH] feat(agent): persist production generation settings --- .../apps/admin-web/src/App.vue | 32 ++--- .../components/AgentGenerationParameters.vue | 49 +++++++ .../src/components/AgentManagementView.vue | 132 +++++++++++++----- .../apps/admin-web/src/services/api.ts | 5 + .../apps/admin-web/src/types/api.ts | 14 ++ .../apps/backend/app/api/admin_settings.py | 63 +++++++++ .../apps/backend/app/schemas/admin.py | 11 +- .../backend/app/services/model_service.py | 2 +- .../tests/test_agent_runtime_config.py | 101 ++++++++++++++ 9 files changed, 353 insertions(+), 56 deletions(-) create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/components/AgentGenerationParameters.vue create mode 100644 ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue index bc7cda6..a65b86d 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -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: "", diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentGenerationParameters.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentGenerationParameters.vue new file mode 100644 index 0000000..88153d5 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentGenerationParameters.vue @@ -0,0 +1,49 @@ + + + diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue index e0d4c17..5b0276d 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue @@ -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(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) {

Agent 管理

-

维护主提示词及历史版本,配置本次调试参数,并验证真实检索与回答效果。

+

维护主提示词、用户端正式生成参数和历史版本,并验证真实检索与回答效果。

当前版本 {{ prompt?.id ? `#${prompt.id}` : "系统默认" }} @@ -286,11 +349,34 @@ function errorMessage(error: unknown, fallback: string) {
+ +
+

用户端正式运行参数

保存后从用户端下一条消息开始生效。当前正式模型:{{ runtimeConfig?.modelName || '未启用模型' }}

+ 保存正式参数 +
+ + + +
+

正式生成参数

用户端正式对话使用
+ +
+
+
+

本次调试配置

配置只作用于后台预览,不会修改模型和知识库的正式状态。

- + @@ -301,32 +387,14 @@ function errorMessage(error: unknown, fallback: string) {

高级生成参数

仅影响本次后台调试
-
- - - - - - - - - - - - - - - - - - - - - - - - -
+
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts index 9b55f65..1276415 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts @@ -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(`/admin/prompt/history/${id}/restore`, { method: "POST", body: "{}" }), debugAgent: (payload: Record) => request("/admin/agent/debug", { method: "POST", body: JSON.stringify(payload) }), + agentRuntimeConfig: () => request("/admin/agent/runtime-config"), + saveAgentRuntimeConfig: (payload: AgentGenerationConfig) => + request("/admin/agent/runtime-config", { method: "PUT", body: JSON.stringify(payload) }), models: () => request("/admin/model/list"), createModel: (payload: Record) => request("/admin/model", { method: "POST", body: JSON.stringify(payload) }), diff --git a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts index d922ef3..16b0571 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts @@ -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; diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py index 85f6ead..d024a1d 100644 --- a/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py @@ -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, diff --git a/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py b/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py index 331078a..a7077eb 100644 --- a/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py +++ b/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py @@ -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): diff --git a/ai_knowledge_base_v2/apps/backend/app/services/model_service.py b/ai_knowledge_base_v2/apps/backend/app/services/model_service.py index 69fdffb..0049b3a 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/model_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/model_service.py @@ -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)) diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py b/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py new file mode 100644 index 0000000..d77fe7a --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py @@ -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