feat(chatbot): 添加子智能体支持和摘要中间件

This commit is contained in:
Wenjie Zhang 2026-03-20 01:42:38 +08:00
parent 403608fb7d
commit 3babd9be54
3 changed files with 86 additions and 12 deletions

View File

@ -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(),

View File

@ -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 上配置时使用"""

View File

@ -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