From 3babd9be54ac88f4fb82910a928d22c434bc5447 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Fri, 20 Mar 2026 01:42:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(chatbot):=20=E6=B7=BB=E5=8A=A0=E5=AD=90?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E6=94=AF=E6=8C=81=E5=92=8C=E6=91=98?= =?UTF-8?q?=E8=A6=81=E4=B8=AD=E9=97=B4=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yuxi/agents/buildin/chatbot/graph.py | 17 ++++++- backend/package/yuxi/agents/common/context.py | 33 ++++++++++--- web/src/components/AgentConfigSidebar.vue | 48 +++++++++++++++++-- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/backend/package/yuxi/agents/buildin/chatbot/graph.py b/backend/package/yuxi/agents/buildin/chatbot/graph.py index d3bbaeb6..8e785d67 100644 --- a/backend/package/yuxi/agents/buildin/chatbot/graph.py +++ b/backend/package/yuxi/agents/buildin/chatbot/graph.py @@ -11,6 +11,7 @@ from yuxi.agents.common import BaseAgent, load_chat_model from yuxi.agents.common.backends import create_agent_composite_backend from yuxi.agents.common.middlewares import ( RuntimeConfigMiddleware, + SummaryOffloadMiddleware, save_attachments_to_fs, ) from yuxi.agents.common.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware @@ -38,10 +39,10 @@ class ChatbotAgent(BaseAgent): await get_tools_from_all_servers() ) # 因为异步加载,无法放在 RuntimeConfigMiddleware 的 __init__ 中 + # subagents subagents = await get_subagents_from_names(context.subagents) - subagents_model = load_chat_model(context.subagents_model) subagents_middleware = SubAgentMiddleware( - default_model=subagents_model, + default_model=load_chat_model(context.subagents_model), subagents=subagents, general_purpose_agent=True, default_middleware=[ @@ -50,6 +51,17 @@ class ChatbotAgent(BaseAgent): ], ) + # summary middleware + # 主 Agent 上下文优化:90k tokens 触发压缩(128k context window 的 70%) + summary_middleware = SummaryOffloadMiddleware( + model=load_chat_model(fully_specified_name=context.model), + trigger=("tokens", getattr(context, "summary_threshold", 100) * 1024), + trim_tokens_to_summarize=4000, + summary_offload_threshold=500, + max_retention_ratio=0.5, + ) + + # all middlewares middlewares = [ save_attachments_to_fs, # 附件注入提示词 FilesystemMiddleware(backend=_create_fs_backend), # 文件系统后端 @@ -57,6 +69,7 @@ class ChatbotAgent(BaseAgent): RuntimeConfigMiddleware(extra_tools=all_mcp_tools), # 运行时配置应用(模型/工具/MCP/提示词) SkillsMiddleware(), # Skills 中间件(提示词注入、依赖展开、动态激活) subagents_middleware, + summary_middleware, ModelRetryMiddleware(), # 模型重试中间件 TodoListMiddleware(), PatchToolCallsMiddleware(), diff --git a/backend/package/yuxi/agents/common/context.py b/backend/package/yuxi/agents/common/context.py index 98d4e64c..28aeab5a 100644 --- a/backend/package/yuxi/agents/common/context.py +++ b/backend/package/yuxi/agents/common/context.py @@ -34,12 +34,6 @@ class BaseContext: metadata={"name": "用户ID", "configurable": False, "description": "用来唯一标识一个用户"}, ) - # 不需要了,使用 user_id 判断 - # department_id: int | None = field( - # default=None, - # metadata={"name": "部门ID", "configurable": False, "description": "用来唯一标识一个部门"}, - # ) - system_prompt: Annotated[str, {"__template_metadata__": {"kind": "prompt"}}] = field( default="You are a helpful assistant.", metadata={"name": "系统提示词", "description": "用来描述智能体的角色和行为"}, @@ -94,6 +88,33 @@ class BaseContext: }, ) + subagents_model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field( + default=sys_config.default_model, + metadata={ + "name": "子智能体的默认模型", + "description": "子智能体的默认模型,会被子智能体的配置覆盖。", + }, + ) + + subagents: Annotated[list[str], {"__template_metadata__": {"kind": "subagents"}}] = field( + default_factory=list, + metadata={ + "name": "子智能体", + "options": [], + "description": "可选子智能体列表。为空表示不启用任何 SubAgent。但依然会启用一个 general-purpose 的子智能体", + "type": "list", + }, + ) + + summary_threshold: int = field( + default=100, + metadata={ + "name": "上下文摘要触发阈值 (KB)", + "description": "当上下文大小超过该值时,启用摘要功能以优化上下文使用。单位为 KB,默认值为 100KB。", + "type": "number", + }, + ) + @classmethod def get_configurable_items(cls): """实现一个可配置的参数列表,在 UI 上配置时使用""" diff --git a/web/src/components/AgentConfigSidebar.vue b/web/src/components/AgentConfigSidebar.vue index acd0e88c..b604fceb 100644 --- a/web/src/components/AgentConfigSidebar.vue +++ b/web/src/components/AgentConfigSidebar.vue @@ -431,6 +431,7 @@ import { useAgentStore } from '@/stores/agent' import { useUserStore } from '@/stores/user' import { useDatabaseStore } from '@/stores/database' import { skillApi } from '@/apis/skill_api' +import { subagentApi } from '@/apis/subagent_api' import { toolApi } from '@/apis/tool_api' import { storeToRefs } from 'pinia' @@ -461,6 +462,7 @@ watch( if (val) { databaseStore.loadDatabases().catch(() => {}) loadLiveSkillOptions().catch(() => {}) + loadSubagentOptions().catch(() => {}) loadToolOptions().catch(() => {}) if (selectedAgentId.value) { try { @@ -496,6 +498,7 @@ const currentSystemPromptKey = ref(null) const systemPromptDraft = ref('') const activeTab = ref('basic') const liveSkillOptions = ref([]) +const liveSubagentOptions = ref([]) const toolOptionsFromApi = ref([]) const isEmptyConfig = computed(() => { @@ -518,7 +521,8 @@ const hasOtherConfigs = computed(() => { value.template_metadata?.kind === 'mcps' || value.template_metadata?.kind === 'knowledges' || value.template_metadata?.kind === 'tools' || - value.template_metadata?.kind === 'skills' + value.template_metadata?.kind === 'skills' || + value.template_metadata?.kind === 'subagents' return !isBasic && !isTools }) @@ -580,9 +584,30 @@ const loadToolOptions = async (force = false) => { } } +const loadSubagentOptions = async (force = false) => { + if (!userStore.isAdmin) { + liveSubagentOptions.value = [] + return + } + if (!force && liveSubagentOptions.value.length > 0) { + return + } + try { + const result = await subagentApi.getSubAgents() + const rows = result?.data || [] + liveSubagentOptions.value = rows.map((item) => ({ + id: item.name, + name: item.name, + description: item.description || '' + })) + } catch (error) { + console.warn('加载 Subagents 列表失败:', error) + } +} + // 判断是否为需要跳转的配置类型 const isToolsKind = (kind) => { - return ['knowledges', 'tools', 'mcps', 'skills'].includes(kind) + return ['knowledges', 'tools', 'mcps', 'skills', 'subagents'].includes(kind) } // 强制刷新对应配置项的选项列表 @@ -601,6 +626,10 @@ const refreshConfigOptions = async (key, kind) => { await loadLiveSkillOptions(true) message.success('Skills 列表已刷新') break + case 'subagents': + await loadSubagentOptions(true) + message.success('Subagents 列表已刷新') + break case 'mcps': // MCP 没有前端 store,提示用户刷新页面 message.info('请在 MCP 管理页面刷新') @@ -631,6 +660,9 @@ const navigateToConfigPage = (kind) => { case 'skills': router.push({ path: '/extensions', query: { tab: 'skills' } }) break + case 'subagents': + router.push({ path: '/extensions', query: { tab: 'subagents' } }) + break } }, 100) } @@ -651,13 +683,16 @@ const getConfigOptions = (value) => { if (value?.template_metadata?.kind === 'skills') { return liveSkillOptions.value.length > 0 ? liveSkillOptions.value : value?.options || [] } + if (value?.template_metadata?.kind === 'subagents') { + return liveSubagentOptions.value.length > 0 ? liveSubagentOptions.value : value?.options || [] + } return value?.options || [] } const isListConfig = (key, value) => { const isTools = value?.template_metadata?.kind === 'tools' const isList = value?.type === 'list' - return isTools || isList || key === 'skills' + return isTools || isList || key === 'skills' || key === 'subagents' } const getOptionValue = (option) => { @@ -723,7 +758,9 @@ const shouldShowConfig = (key, value) => { value.template_metadata?.kind === 'knowledges' || value.template_metadata?.kind === 'tools' || value.template_metadata?.kind === 'skills' || - key === 'skills' + value.template_metadata?.kind === 'subagents' || + key === 'skills' || + key === 'subagents' if (activeTab.value === 'basic') { // 基础:System Prompt, LLM Model @@ -825,6 +862,9 @@ const openSelectionModal = async (key) => { if (configurableItems.value[key]?.template_metadata?.kind === 'skills') { await loadLiveSkillOptions() } + if (configurableItems.value[key]?.template_metadata?.kind === 'subagents') { + await loadSubagentOptions() + } const currentValues = agentConfig.value[key] || [] tempSelectedValues.value = [...currentValues] selectionModalOpen.value = true