From 8087ef4b00ed729685372b24cb093f73fe31d986 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Tue, 30 Dec 2025 19:01:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(chatbot):=20=E5=A2=9E=E5=BC=BA=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E5=B7=A5=E5=85=B7=E5=92=8C=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E9=9B=86=E6=88=90=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在智能体上下文中添加知识库字段,支持从知识库中选择工具 - 重构工具获取逻辑,支持根据上下文动态加载工具 - 优化前端知识库创建和加载逻辑,移入 store 管理 - 改进工具调用结果渲染,支持知识库工具识别 - 添加 clear-btn 样式改进 --- src/agents/chatbot/context.py | 17 ++- src/agents/chatbot/graph.py | 57 +++++---- src/agents/chatbot/tools.py | 2 + src/agents/common/context.py | 6 +- src/agents/common/tools.py | 10 +- web/src/components/AgentConfigSidebar.vue | 20 +++- .../ToolCallingResult/ToolCallRenderer.vue | 22 ++-- web/src/layouts/AppLayout.vue | 6 +- web/src/stores/database.js | 67 ++++++++++- web/src/views/DataBaseView.vue | 108 ++++++------------ 10 files changed, 187 insertions(+), 128 deletions(-) diff --git a/src/agents/chatbot/context.py b/src/agents/chatbot/context.py index 8892cde1..07d1e5bd 100644 --- a/src/agents/chatbot/context.py +++ b/src/agents/chatbot/context.py @@ -3,6 +3,7 @@ from typing import Annotated from src.agents.common import BaseContext, gen_tool_info from src.agents.common.mcp import MCP_SERVERS +from src.knowledge import knowledge_base from .tools import get_tools @@ -14,7 +15,17 @@ class Context(BaseContext): metadata={ "name": "工具", "options": lambda: gen_tool_info(get_tools()), # 这里的选择是所有的工具 - "description": "工具列表", + "description": "内置的部分工具,包含 common 工具和本智能体的特有工具(不含 MCP)。", + }, + ) + + knowledges: list[str] = field( + default_factory=list, + metadata={ + "name": "知识库", + "options": lambda: [k["name"] for k in knowledge_base.get_retrievers().values()], + "description": "知识库列表,可以在左侧知识库页面中创建知识库。", + "type": "list", # Explicitly mark as list type for frontend if needed }, ) @@ -23,6 +34,8 @@ class Context(BaseContext): metadata={ "name": "MCP服务器", "options": lambda: list(MCP_SERVERS.keys()), - "description": "MCP服务器列表", + "description": ( + "MCP服务器列表,建议使用支持 SSE 的 MCP 服务器," + "如果需要使用 uvx 或 npx 运行的服务器,也请在项目外部启动 MCP 服务器,并在项目中配置 MCP 服务器。"), }, ) diff --git a/src/agents/chatbot/graph.py b/src/agents/chatbot/graph.py index 080e81d7..58a61ced 100644 --- a/src/agents/chatbot/graph.py +++ b/src/agents/chatbot/graph.py @@ -2,14 +2,13 @@ from langchain.agents import create_agent from langchain.agents.middleware import ModelRetryMiddleware from src.agents.common import BaseAgent, load_chat_model -from src.agents.common.mcp import MCP_SERVERS +from src.agents.common.mcp import get_mcp_tools from src.agents.common.middlewares import ( - DynamicToolMiddleware, context_aware_prompt, context_based_model, inject_attachment_context, ) -from src.agents.common.subagents import calc_agent_tool +from src.agents.common.tools import get_kb_based_tools from .context import Context from .tools import get_tools @@ -18,7 +17,7 @@ from .tools import get_tools class ChatbotAgent(BaseAgent): name = "智能体助手" description = "基础的对话机器人,可以回答问题,默认不使用任何工具,可在配置中启用需要的工具。" - capabilities = ["file_upload"] # 支持文件上传功能 + capabilities = ["file_upload", "reload_graph"] # 支持文件上传功能和重载图 def __init__(self, **kwargs): super().__init__(**kwargs) @@ -26,34 +25,48 @@ class ChatbotAgent(BaseAgent): self.checkpointer = None self.context_schema = Context - def get_tools(self): - """返回基本工具""" - base_tools = get_tools() - base_tools.append(calc_agent_tool) - return base_tools + async def get_tools(self, tools: list[str] = None, mcps=None, knowledges=None): + + # 1. 基础工具 (从 context.tools 中筛选) + all_basic_tools = get_tools() + selected_tools = [] + + if tools: + # 创建工具映射表 + tools_map = {t.name: t for t in all_basic_tools} + for tool_name in tools: + if tool_name in tools_map: + selected_tools.append(tools_map[tool_name]) + + # 2. 知识库工具 + if knowledges: + kb_tools = get_kb_based_tools(db_names=knowledges) + selected_tools.extend(kb_tools) + + # 3. MCP 工具 + if mcps: + for server_name in mcps: + mcp_tools = await get_mcp_tools(server_name) + selected_tools.extend(mcp_tools) + + return selected_tools async def get_graph(self, **kwargs): """构建图""" if self.graph: return self.graph - # 创建动态工具中间件实例,并传入所有可用的 MCP 服务器列表 - dynamic_tool_middleware = DynamicToolMiddleware( - base_tools=self.get_tools(), mcp_servers=list(MCP_SERVERS.keys()) - ) + # 获取上下文配置 + context = self.context_schema.from_file(module_name=self.module_name) - # 预加载所有 MCP 工具并注册到 middleware.tools - await dynamic_tool_middleware.initialize_mcp_tools() - - # 使用 create_agent 创建智能体,并传入 middleware + # 使用 create_agent 创建智能体 graph = create_agent( - model=load_chat_model("siliconflow/Qwen/Qwen3-235B-A22B-Instruct-2507"), # 默认模型,会被 middleware 覆盖 - tools=get_tools(), # 注册基础工具 + model=load_chat_model(context.model), # 使用 context 中的模型配置 + tools=await self.get_tools(context.tools, context.mcps, context.knowledges), middleware=[ context_aware_prompt, # 动态系统提示词 - inject_attachment_context, # 附件上下文注入(LangChain 标准中间件) + inject_attachment_context, # 附件上下文注入 context_based_model, # 动态模型选择 - dynamic_tool_middleware, # 动态工具选择(支持 MCP 工具注册) ModelRetryMiddleware(), # 模型重试中间件 ], checkpointer=await self._get_checkpointer(), @@ -69,4 +82,4 @@ def main(): if __name__ == "__main__": main() - # asyncio.run(main()) + # asyncio.run(main()) \ No newline at end of file diff --git a/src/agents/chatbot/tools.py b/src/agents/chatbot/tools.py index b6eabce6..ead197cd 100644 --- a/src/agents/chatbot/tools.py +++ b/src/agents/chatbot/tools.py @@ -6,6 +6,7 @@ import requests from langchain.tools import tool from src.agents.common import get_buildin_tools +from src.agents.common.subagents import calc_agent_tool from src.storage.minio import aupload_file_to_minio from src.utils import logger @@ -51,4 +52,5 @@ def get_tools() -> list[Any]: """获取所有可运行的工具(给大模型使用)""" tools = get_buildin_tools() tools.append(text_to_img_demo) + tools.append(calc_agent_tool) return tools diff --git a/src/agents/common/context.py b/src/agents/common/context.py index d7d4e35e..dd9bc572 100644 --- a/src/agents/common/context.py +++ b/src/agents/common/context.py @@ -46,7 +46,11 @@ class BaseContext: model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field( default=sys_config.default_model, - metadata={"name": "智能体模型", "options": [], "description": "智能体的驱动模型"}, + metadata={ + "name": "智能体模型", + "options": [], + "description": "智能体的驱动模型,建议选择 Agent 能力较强的模型,不建议使用小参数模型。" + }, ) @classmethod diff --git a/src/agents/common/tools.py b/src/agents/common/tools.py index 05bd0b4a..1dfaae01 100644 --- a/src/agents/common/tools.py +++ b/src/agents/common/tools.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, Field from src import config, graph_base, knowledge_base from src.utils import logger -search = TavilySearch(max_results=10) +search = TavilySearch() search.metadata = {"name": "Tavily 网页搜索"} @@ -122,11 +122,12 @@ class KnowledgeRetrieverModel(BaseModel): ) -def get_kb_based_tools() -> list: +def get_kb_based_tools(db_names: list[str] | None = None) -> list: """获取所有知识库基于的工具""" # 获取所有知识库 kb_tools = [] retrievers = knowledge_base.get_retrievers() + db_ids = [kb_id for kb_id, kb in retrievers.items() if kb["name"] in db_names] or None def _create_retriever_wrapper(db_id: str, retriever_info: dict[str, Any]): """创建检索器包装函数的工厂函数,避免闭包变量捕获问题""" @@ -185,6 +186,9 @@ def get_kb_based_tools() -> list: return async_retriever_wrapper for db_id, retrieve_info in retrievers.items(): + if db_ids is not None and db_id not in db_ids: + continue + try: # 构建工具描述 description = ( @@ -227,8 +231,6 @@ def get_buildin_tools() -> list: tools = [] try: - # 获取所有知识库基于的工具 - tools.extend(get_kb_based_tools()) tools.extend(get_static_tools()) from src.agents.common.toolkits.mysql.tools import get_mysql_tools diff --git a/web/src/components/AgentConfigSidebar.vue b/web/src/components/AgentConfigSidebar.vue index 4e8bdd16..b15cc48c 100644 --- a/web/src/components/AgentConfigSidebar.vue +++ b/web/src/components/AgentConfigSidebar.vue @@ -152,6 +152,7 @@ @@ -819,12 +820,6 @@ const resetConfig = async () => { color: var(--gray-900); font-weight: 500; } - - .clear-btn { - padding: 0; - height: auto; - font-size: 12px; - } } .select-tools-btn { @@ -1133,6 +1128,19 @@ const resetConfig = async () => { } } + +.clear-btn { + padding: 0; + height: auto; + font-size: 12px; + font-weight: 600; + color: var(--main-700); + + &:hover { + color: var(--main-800); + } +} + // 响应式适配 @media (max-width: 768px) { .agent-config-sidebar.open { diff --git a/web/src/components/ToolCallingResult/ToolCallRenderer.vue b/web/src/components/ToolCallingResult/ToolCallRenderer.vue index 9ae38c6d..255e6804 100644 --- a/web/src/components/ToolCallingResult/ToolCallRenderer.vue +++ b/web/src/components/ToolCallingResult/ToolCallRenderer.vue @@ -35,6 +35,7 @@ import { computed, ref } from 'vue'; import BaseToolCall from './BaseToolCall.vue'; import { useAgentStore } from '@/stores/agent'; +import { useDatabaseStore } from '@/stores/database'; import WebSearchTool from './tools/WebSearchTool.vue'; import KnowledgeBaseTool from './tools/KnowledgeBaseTool.vue'; @@ -53,13 +54,18 @@ const props = defineProps({ }); const agentStore = useAgentStore(); +const databaseStore = useDatabaseStore(); const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || ''); const tool = computed(() => { const toolsList = agentStore?.availableTools ? Object.values(agentStore.availableTools) : []; - return toolsList.find(t => t.name === toolName.value) || null; + const tool = toolsList.find(t => t.name === toolName.value) + return tool || null; }); +const databases = computed(() => databaseStore.databases || []); + + const parseData = (content) => { if (typeof content === 'string') { try { @@ -90,17 +96,9 @@ const isTaskResult = computed(() => { }); const isKnowledgeBaseResult = computed(() => { - const currentTool = tool.value; - - if (currentTool && currentTool.metadata) { - const metadata = currentTool.metadata; - const hasKnowledgebaseTag = metadata.tag && metadata.tag.includes('knowledgebase'); - const isNotLightrag = metadata.kb_type !== 'lightrag'; - if (hasKnowledgebaseTag && isNotLightrag) { - // const data = parseData(props.toolCall.tool_call_result?.content); - // return Array.isArray(data) && data.length > 0; - return true - } + const databaseInfo = databases.value.find(db => db.name === toolName.value); + if (databaseInfo && databaseInfo.kb_type !== 'lightrag') { + return true } return false; }); diff --git a/web/src/layouts/AppLayout.vue b/web/src/layouts/AppLayout.vue index 14bd183b..a5c7b324 100644 --- a/web/src/layouts/AppLayout.vue +++ b/web/src/layouts/AppLayout.vue @@ -1,9 +1,7 @@