From c2655361b545b8a0b87cf2268984e789a661b226 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Mon, 20 Jul 2026 11:36:18 +0800 Subject: [PATCH] feat: add agent response depth control --- .../src/components/AgentManagementView.vue | 9 +++ .../components/AgentResponseDepthControl.vue | 43 ++++++++++++ .../apps/admin-web/src/main.ts | 3 + .../apps/admin-web/src/styles.css | 34 ++++++++++ .../apps/admin-web/src/types/api.ts | 1 + .../apps/backend/app/api/admin_settings.py | 9 ++- .../apps/backend/app/schemas/admin.py | 2 + .../app/services/agent_debug_service.py | 1 + .../app/services/knowledge_agent_service.py | 2 + .../apps/backend/app/services/rag_service.py | 5 +- .../app/services/response_style_service.py | 68 +++++++++++++++++++ .../tests/test_agent_runtime_config.py | 20 +++++- 12 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/components/AgentResponseDepthControl.vue create mode 100644 ai_knowledge_base_v2/apps/backend/app/services/response_style_service.py 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 1a7bd6a..766eeeb 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 @@ -5,6 +5,7 @@ import { computed, nextTick, onMounted, reactive, ref, watch } from "vue"; import { api, streamDebugAgent } from "../services/api"; import type { AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api"; import AgentGenerationParameters from "./AgentGenerationParameters.vue"; +import AgentResponseDepthControl from "./AgentResponseDepthControl.vue"; import AdminPagination from "./AdminPagination.vue"; import StreamingMarkdownMessage from "./StreamingMarkdownMessage.vue"; @@ -51,6 +52,7 @@ const agentForm = reactive({ maxToken: 8192 as number | null, streamEnabled: 1, reasoningVisible: 0, + responseDepth: 35, question: "", }); const runtimeConfig = ref(null); @@ -63,6 +65,7 @@ const runtimeForm = reactive({ maxToken: 8192 as number | null, streamEnabled: 1, reasoningVisible: 0, + responseDepth: 35, }); const promptDirty = computed(() => promptContent.value !== savedPromptContent.value); @@ -119,6 +122,7 @@ function applyRuntimeConfig(value: AgentRuntimeConfig) { maxToken: value.maxToken, streamEnabled: value.streamEnabled, reasoningVisible: value.reasoningVisible, + responseDepth: value.responseDepth ?? 35, }); } @@ -134,6 +138,7 @@ function applyDebugModelDefaults(modelId?: number) { maxToken: model.maxToken ?? (model.enabled === 1 ? runtimeConfig.value?.maxToken : null) ?? 8192, streamEnabled: model.streamEnabled ?? 1, reasoningVisible: runtimeConfig.value?.reasoningVisible ?? 0, + responseDepth: runtimeConfig.value?.responseDepth ?? 35, }); } @@ -151,6 +156,7 @@ async function saveRuntimeConfig() { maxToken: runtimeForm.maxToken, streamEnabled: runtimeForm.streamEnabled, reasoningVisible: runtimeForm.reasoningVisible, + responseDepth: runtimeForm.responseDepth, }); applyRuntimeConfig(saved); const model = models.value.find((item) => item.id === saved.modelId); @@ -304,6 +310,7 @@ async function debugAgent() { maxToken: agentForm.maxToken, streamEnabled: agentForm.streamEnabled, reasoningVisible: agentForm.reasoningVisible, + responseDepth: agentForm.responseDepth, }, (chunk) => { currentAssistant().content += chunk; @@ -423,6 +430,7 @@ function errorMessage(error: unknown, fallback: string) { v-model:max-token="runtimeForm.maxToken" :disabled="!runtimeConfig?.modelId" /> +
用户端流式输出 @@ -480,6 +488,7 @@ function errorMessage(error: unknown, fallback: string) { v-model:frequency-penalty="agentForm.frequencyPenalty" v-model:max-token="agentForm.maxToken" /> +
流式输出 diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentResponseDepthControl.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentResponseDepthControl.vue new file mode 100644 index 0000000..7911973 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentResponseDepthControl.vue @@ -0,0 +1,43 @@ + + + diff --git a/ai_knowledge_base_v2/apps/admin-web/src/main.ts b/ai_knowledge_base_v2/apps/admin-web/src/main.ts index 1399661..feeb849 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/main.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/main.ts @@ -19,6 +19,7 @@ import { ElPagination, ElProgress, ElSelect, + ElSlider, ElSwitch, ElTabPane, ElTable, @@ -51,6 +52,7 @@ import "element-plus/theme-chalk/el-progress.css"; import "element-plus/theme-chalk/el-popper.css"; import "element-plus/theme-chalk/el-scrollbar.css"; import "element-plus/theme-chalk/el-select.css"; +import "element-plus/theme-chalk/el-slider.css"; import "element-plus/theme-chalk/el-switch.css"; import "element-plus/theme-chalk/el-table.css"; import "element-plus/theme-chalk/el-tabs.css"; @@ -86,6 +88,7 @@ app.config.globalProperties.$ELEMENT = { locale: zhCn }; ElPagination, ElProgress, ElSelect, + ElSlider, ElSwitch, ElTabPane, ElTable, diff --git a/ai_knowledge_base_v2/apps/admin-web/src/styles.css b/ai_knowledge_base_v2/apps/admin-web/src/styles.css index a0bac47..c9f7ce5 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/styles.css +++ b/ai_knowledge_base_v2/apps/admin-web/src/styles.css @@ -1049,12 +1049,46 @@ textarea { line-height: 1.6; } +.agent-response-depth { + margin: 16px 0 0; + padding: 14px 16px 18px; + border: 1px solid #dce7e3; + border-radius: 8px; + background: #ffffff; +} + +.agent-response-depth > .el-form-item__content { + display: grid; + gap: 14px; +} + +.agent-response-depth-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + width: 100%; +} + +.agent-response-depth .el-slider { + --el-slider-main-bg-color: #0f735d; + --el-slider-runway-bg-color: #dce7e3; + width: calc(100% - 12px); + margin: 2px 6px 0; +} + @media (max-width: 680px) { .agent-stream-setting > .el-form-item__content { align-items: flex-start; flex-direction: column; gap: 12px; } + + .agent-response-depth-head { + align-items: flex-start; + flex-direction: column; + gap: 10px; + } } .agent-parameter-label { 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 673b209..d0d5fed 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 @@ -139,6 +139,7 @@ export interface AgentGenerationConfig { maxToken: number; streamEnabled: number; reasoningVisible: number; + responseDepth: number; } export interface AgentRuntimeConfig extends AgentGenerationConfig { 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 c22805c..5c05ba8 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 @@ -30,6 +30,7 @@ from app.services.feishu_service import FeishuKnowledgeService from app.services.knowledge_service import KnowledgeScope from app.services.model_service import ModelClientService from app.services.reasoning_policy_service import ReasoningPolicyService +from app.services.response_style_service import ResponseStyleService from app.services.secret_service import MASKED_SECRET, SENSITIVE_CONFIG_KEYS, SecretService router = APIRouter() @@ -206,7 +207,7 @@ def get_agent_runtime_config( current_admin: Admin = Depends(get_current_admin), ) -> dict: model = _enabled_model(db) - return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db))) + return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db), ResponseStyleService.get_depth(db))) @router.put("/agent/runtime-config") @@ -230,6 +231,7 @@ def save_agent_runtime_config( model.stream_enabled = payload.streamEnabled db.add(model) ReasoningPolicyService.set_visible(db, payload.reasoningVisible == 1, current_admin.id) + ResponseStyleService.set_depth(db, payload.responseDepth, current_admin.id) OperationLogService.write( db, admin_id=current_admin.id, @@ -239,7 +241,7 @@ def save_agent_runtime_config( ) db.commit() db.refresh(model) - return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db))) + return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db), ResponseStyleService.get_depth(db))) @router.get("/model/list") @@ -443,7 +445,7 @@ def _enabled_model(db: Session) -> ModelConfig | None: ) -def _agent_runtime_config_dict(model: ModelConfig | None, reasoning_visible: bool = False) -> dict: +def _agent_runtime_config_dict(model: ModelConfig | None, reasoning_visible: bool = False, response_depth: int = 35) -> 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, @@ -455,6 +457,7 @@ def _agent_runtime_config_dict(model: ModelConfig | None, reasoning_visible: boo "maxToken": model.max_token if model is not None and model.max_token is not None else 8192, "streamEnabled": model.stream_enabled if model is not None else 1, "reasoningVisible": 1 if reasoning_visible else 0, + "responseDepth": response_depth, } 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 a402dc9..9397c86 100644 --- a/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py +++ b/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py @@ -98,6 +98,7 @@ class AgentDebugRequest(BaseModel): maxToken: int | None = Field(default=8192, ge=1, le=100000) streamEnabled: int = Field(default=1, ge=0, le=1) reasoningVisible: int = Field(default=0, ge=0, le=1) + responseDepth: int = Field(default=35, ge=0, le=100) class AgentRuntimeConfigSaveRequest(BaseModel): @@ -109,6 +110,7 @@ class AgentRuntimeConfigSaveRequest(BaseModel): maxToken: int = Field(default=8192, ge=256, le=100000) streamEnabled: int = Field(default=1, ge=0, le=1) reasoningVisible: int = Field(default=0, ge=0, le=1) + responseDepth: int = Field(default=35, ge=0, le=100) class ModelSaveRequest(BaseModel): diff --git a/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py index 8ea3dbd..31c2cb1 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py @@ -30,6 +30,7 @@ class AgentDebugService: version_overrides=payload.knowledgeVersions, preview_knowledge_ids=payload.knowledgeIds, prompt_override=payload.promptContent, + response_depth=payload.responseDepth, ) return RagResult( question=rag_result.question, diff --git a/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py b/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py index 3dd5dc5..8e954b7 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py @@ -84,6 +84,7 @@ class KnowledgeAgentService: preview_knowledge_ids: list[int] | None = None, context_trace: list[dict] | None = None, prompt_override: str | None = None, + response_depth: int | None = None, ) -> RagResult: started = perf_counter() catalog = cls.get_knowledge_catalog( @@ -207,6 +208,7 @@ class KnowledgeAgentService: session_summary, summary_up_to_message_id, prompt_override=prompt_override, + response_depth=response_depth, ) return RagResult( question=question, diff --git a/ai_knowledge_base_v2/apps/backend/app/services/rag_service.py b/ai_knowledge_base_v2/apps/backend/app/services/rag_service.py index 1d4cadb..6875e30 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/rag_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/rag_service.py @@ -11,6 +11,7 @@ from app.models.user import User from app.services.chat_context_service import ChatContextService from app.services.feishu_service import FeishuKnowledgeService from app.services.knowledge_service import KnowledgeAccessService, KnowledgeScope +from app.services.response_style_service import ResponseStyleService NO_HIT_ANSWER = "当前知识库中未检索到相关内容,请联系管理员补充相关知识。" @@ -119,6 +120,7 @@ class PromptService: session_summary: str | None = None, summary_up_to_message_id: int | None = None, prompt_override: str | None = None, + response_depth: int | None = None, ) -> list[dict[str, str]]: prompt = prompt_override.strip() if prompt_override and prompt_override.strip() else cls._load_active_prompt(db) @@ -135,7 +137,8 @@ class PromptService: + "[不可关闭的最低安全规则 v1]\n" + "现实危险、自伤伤人风险应优先建议立即寻求线下专业帮助;医疗、法律、财务问题不得给出替代专业意见的结论;" + "不得伪造老师观点或课程内容;不得输出整篇课程文章、大段连续原文,也不得通过多轮拼接还原完整资料。" - + cls._knowledge_type_rules(chunks), + + cls._knowledge_type_rules(chunks) + + ResponseStyleService.build_instruction(db, response_depth), } ] diff --git a/ai_knowledge_base_v2/apps/backend/app/services/response_style_service.py b/ai_knowledge_base_v2/apps/backend/app/services/response_style_service.py new file mode 100644 index 0000000..c504e87 --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/services/response_style_service.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.models.ai_config import SystemConfig + + +RESPONSE_DEPTH_KEY = "agent_response_depth" +DEFAULT_RESPONSE_DEPTH = 35 +MIN_RESPONSE_DEPTH = 0 +MAX_RESPONSE_DEPTH = 100 + + +class ResponseStyleService: + @staticmethod + def get_depth(db: Session) -> int: + config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == RESPONSE_DEPTH_KEY)) + if config is None: + return DEFAULT_RESPONSE_DEPTH + try: + return _clamp_depth(int(config.config_value)) + except (TypeError, ValueError): + return DEFAULT_RESPONSE_DEPTH + + @staticmethod + def set_depth(db: Session, depth: int, admin_id: int | None) -> SystemConfig: + config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == RESPONSE_DEPTH_KEY)) + if config is None: + config = SystemConfig(config_key=RESPONSE_DEPTH_KEY, config_value=str(DEFAULT_RESPONSE_DEPTH)) + config.config_value = str(_clamp_depth(depth)) + config.description = "Agent 回答深度,0 为精简,100 为深入" + config.updated_by = admin_id + db.add(config) + return config + + @classmethod + def build_instruction(cls, db: Session, depth: int | None = None) -> str: + value = _clamp_depth(cls.get_depth(db) if depth is None else depth) + if value <= 33: + mode = "精简" + rules = ( + "回答要短,通常控制在1到3段或3条以内;不展开心理分析、不复盘学员长篇经历," + "优先给出一句方向确认,再引导回到当下、身体感受和情绪觉察。建议只给当下最小一步," + "避免连续列出多个深入方案。" + ) + elif value <= 66: + mode = "平衡" + rules = ( + "回答保持适中,可以先确认方向,再补充必要原因和1到3个可执行提醒;" + "不做过度拆解,不把分析过程完整罗列出来。" + ) + else: + mode = "深入" + rules = ( + "可以更完整地解释原因、步骤和注意事项,但仍要避免变成咨询式长篇分析;" + "优先围绕回到当下、回到自身、觉察情绪和身体感受、如是释放来组织内容。" + ) + return ( + "\n\n[回答深度控制]\n" + f"当前回答深度:{value}/100({mode})。{rules}" + "无论深度如何,都不要暴露内部分析过程;不要替学员做复杂人格判断;" + "涉及功课反馈时,先判断方向是否偏离,再给最少必要的下一步。" + ) + + +def _clamp_depth(value: int) -> int: + return max(MIN_RESPONSE_DEPTH, min(MAX_RESPONSE_DEPTH, value)) 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 index 991b58c..ac678ec 100644 --- 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 @@ -20,7 +20,7 @@ from app.services.model_stream_service import ( _stream_configured_model_async, ) from app.services.model_service import ModelClientService, _copy_model_with_overrides, _max_output_tokens -from app.services.rag_service import RagResult +from app.services.rag_service import PromptService, RagResult def _database() -> Session: @@ -73,6 +73,7 @@ def test_runtime_config_defaults_to_long_answer_safe_max_tokens(): assert result["maxToken"] == 8192 assert result["streamEnabled"] == 1 assert result["reasoningVisible"] == 0 + assert result["responseDepth"] == 35 assert _max_output_tokens(model) == 8192 @@ -97,6 +98,7 @@ def test_saved_runtime_config_is_persisted_on_enabled_model(): maxToken=12000, streamEnabled=0, reasoningVisible=1, + responseDepth=20, ), db=db, current_admin=admin, @@ -113,7 +115,9 @@ def test_saved_runtime_config_is_persisted_on_enabled_model(): assert model.stream_enabled == 0 assert result["streamEnabled"] == 0 assert result["reasoningVisible"] == 1 + assert result["responseDepth"] == 20 assert db.query(SystemConfig).filter_by(config_key="show_model_reasoning").one().config_value == "true" + assert db.query(SystemConfig).filter_by(config_key="agent_response_depth").one().config_value == "20" payload = _openai_stream_payload( model, @@ -191,10 +195,24 @@ def test_debug_preview_passes_conversation_history_and_replaces_saved_prompt(): kwargs = build_result.await_args.kwargs assert [item.content for item in kwargs["history"]] == ["最开始的问题", "第一次回答"] assert kwargs["prompt_override"] == "调试主提示词" + assert kwargs["response_depth"] == 35 assert result.messages == rag_result.messages assert result.prompt == "调试提示词渲染结果" +def test_prompt_includes_response_depth_instruction(): + with _database() as db: + db.add(SystemConfig(config_key="agent_response_depth", config_value="20")) + db.commit() + + messages = PromptService.build_messages(db, "做功课没反应怎么办?", []) + + system = messages[0]["content"] + assert "回答深度:20/100(精简)" in system + assert "不展开心理分析" in system + assert "回到当下、身体感受和情绪觉察" in system + + @pytest.mark.parametrize("reasoning_visible", [True, False]) def test_agent_debug_stream_respects_reasoning_visibility(reasoning_visible): async def chunks():