From 186012e5e89801b992c4c7205ea9cfe2d3db27ef Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Fri, 6 Mar 2026 10:15:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(skills):=20=E4=BF=AE=E5=A4=8D=20skills=20?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E4=BB=A5=E5=8F=8A=E6=8C=89=E9=9C=80=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2=E9=97=AE=E9=A2=98=EF=BC=8C=E5=B9=B6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20reporter=20=E6=8A=80=E8=83=BD=EF=BC=8C=E5=B7=B2=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E6=9B=BF=E4=BB=A3=E6=95=B0=E6=8D=AE=E5=BA=93=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E6=99=BA=E8=83=BD=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agents/common/context.py | 2 +- .../common/middlewares/skills_middleware.py | 94 +++++++++++-------- src/agents/common/toolkits/mysql/tools.py | 6 -- src/agents/skills/reporter/SKILLS.md | 28 ++++++ 4 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 src/agents/skills/reporter/SKILLS.md diff --git a/src/agents/common/context.py b/src/agents/common/context.py index 13dd6ae7..b7b5c91f 100644 --- a/src/agents/common/context.py +++ b/src/agents/common/context.py @@ -93,7 +93,7 @@ class BaseContext: metadata={ "name": "Skills", "options": [], - "description": "可选技能列表(由超级管理员维护)。运行时仅挂载并只读暴露选中的 skills。", + "description": "可选技能列表(由超级管理员维护)。运行时仅挂载并只读暴露选中的 skills。技能依赖的工具和 MCP 服务器也会被自动挂载。", "type": "list", }, ) diff --git a/src/agents/common/middlewares/skills_middleware.py b/src/agents/common/middlewares/skills_middleware.py index 29872699..455f00db 100644 --- a/src/agents/common/middlewares/skills_middleware.py +++ b/src/agents/common/middlewares/skills_middleware.py @@ -13,6 +13,7 @@ from langchain.tools.tool_node import ToolCallRequest from langgraph.types import Command from sqlalchemy.ext.asyncio import AsyncSession +from src.agents.common.toolkits import get_all_tool_instances from src.repositories.skill_repository import SkillRepository from src.services.mcp_service import get_enabled_mcp_tools from src.services.skill_service import _normalize_string_list, is_valid_skill_slug @@ -171,6 +172,21 @@ class SkillsMiddleware(AgentMiddleware): self.skills_context_name = skills_context_name self.enable_skills_prompt = enable_skills_prompt self.skills_sources_for_prompt = skills_sources_for_prompt or ["/skills/"] + # 实例级缓存:避免每次模型调用都查数据库 + self._dependency_map_cache: dict[str, SkillDependencyNode] | None = None + self._prompt_metadata_cache: dict[str, SkillPromptMetadata] | None = None + + async def _get_dependency_map_cached(self) -> dict[str, SkillDependencyNode]: + """获取依赖映射(带缓存)""" + if self._dependency_map_cache is None: + self._dependency_map_cache = await get_dependency_map() + return self._dependency_map_cache + + async def _get_prompt_metadata_cached(self) -> dict[str, SkillPromptMetadata]: + """获取提示词元数据(带缓存)""" + if self._prompt_metadata_cache is None: + self._prompt_metadata_cache = await get_prompt_metadata() + return self._prompt_metadata_cache async def abefore_agent(self, state: SkillsState, runtime) -> dict[str, Any] | None: """在 agent 执行前注入 skills 提示词""" @@ -182,8 +198,8 @@ class SkillsMiddleware(AgentMiddleware): if getattr(runtime_context, "_skills_prompt_injected", False): return None - # 从数据库加载 skills 数据 - dependency_map = await get_dependency_map() + # 从数据库加载 skills 数据(使用缓存) + dependency_map = await self._get_dependency_map_cached() # 获取配置的 skills configured_skills = getattr(runtime_context, self.skills_context_name, None) or [] @@ -219,8 +235,8 @@ class SkillsMiddleware(AgentMiddleware): """包装模型调用,处理动态激活和依赖展开""" runtime_context = request.runtime.context - # 从数据库加载 skills 数据 - dependency_map = await get_dependency_map() + # 从缓存加载 skills 数据 + dependency_map = await self._get_dependency_map_cached() # 1. 获取配置的 skills configured_skills = getattr(runtime_context, self.skills_context_name, None) or [] @@ -239,40 +255,47 @@ class SkillsMiddleware(AgentMiddleware): # 4. 更新 runtime_context 中的 visible_skills setattr(runtime_context, "_visible_skills", visible_skills) - # 5. 构建依赖包 - deps_bundle = await self._build_dependency_bundle(visible_skills) + # 5. 构建依赖包(只从直接激活的 skills 获取依赖,不包含闭包展开的依赖) + deps_bundle = await self._build_dependency_bundle(activated) - # 6. 加载依赖的工具 - if deps_bundle["tools"] or deps_bundle["mcps"]: - enabled_tools = await self._get_tools_from_context( + # 6. 加载依赖的工具(普通工具 + MCP 工具) + enabled_tools = [] + + # 6.1 从 toolkits 获取普通工具 + if deps_bundle["tools"]: + all_tools = get_all_tool_instances() + required_tool_names = set(deps_bundle["tools"]) + enabled_tools = [t for t in all_tools if t.name in required_tool_names] + + # 6.2 获取 MCP 工具 + if deps_bundle["mcps"]: + mcp_tools = await self._get_mcp_tools_from_context( runtime_context, - extra_tool_names=deps_bundle["tools"], extra_mcps=deps_bundle["mcps"], ) + enabled_tools.extend(mcp_tools) - # 合并工具 - if enabled_tools: - existing_tools = list(request.tools or []) - enabled_tool_names = {t.name for t in enabled_tools} - merged_tools = [] - for t_bind in existing_tools: - if t_bind.name in enabled_tool_names: - merged_tools.append(t_bind) - if merged_tools: - request = request.override(tools=merged_tools) + # 合并工具:保留原有工具 + 追加依赖的新工具 + if enabled_tools: + existing_tool_names = {t.name for t in request.tools or []} + merged_tools = list(request.tools or []) + for t in enabled_tools: + if t.name not in existing_tool_names: + merged_tools.append(t) + request = request.override(tools=merged_tools) return await handler(request) - async def _build_dependency_bundle(self, visible_skills: list[str]) -> dict[str, list[str]]: - """根据 visible_skills 构建依赖包""" - dependency_map = await get_dependency_map() + async def _build_dependency_bundle(self, activated_skills: list[str]) -> dict[str, list[str]]: + """根据直接激活的 skills 构建依赖包(不包含闭包展开的依赖)""" + dependency_map = await self._get_dependency_map_cached() tools: list[str] = [] mcps: list[str] = [] seen_tools: set[str] = set() seen_mcps: set[str] = set() - for slug in visible_skills: + for slug in activated_skills: dep = dependency_map.get(slug, {}) for tool_name in dep.get("tools", []): if tool_name in seen_tools: @@ -285,11 +308,11 @@ class SkillsMiddleware(AgentMiddleware): seen_mcps.add(mcp_name) mcps.append(mcp_name) - return {"tools": tools, "mcps": mcps, "skills": visible_skills} + return {"tools": tools, "mcps": mcps, "skills": activated_skills} async def _collect_prompt_metadata(self, slugs: list[str]) -> list[SkillPromptMetadata]: """收集指定 slugs 的提示词元数据""" - prompt_metadata = await get_prompt_metadata() + prompt_metadata = await self._get_prompt_metadata_cached() result: list[SkillPromptMetadata] = [] seen: set[str] = set() @@ -310,28 +333,16 @@ class SkillsMiddleware(AgentMiddleware): return result - async def _get_tools_from_context( + async def _get_mcp_tools_from_context( self, context, *, - extra_tool_names: list[str] | None = None, extra_mcps: list[str] | None = None, ) -> list: - """从上下文配置中获取工具列表""" + """从上下文配置中获取 MCP 工具列表""" import asyncio - selected_tools = [] - - # 1. 工具(从 extra_tool_names 获取) - all_tool_names: list[str] = [] - for tool_name in extra_tool_names or []: - if isinstance(tool_name, str): - all_tool_names.append(tool_name) - - # 这里简化处理:假设工具已经在其他 middleware 中加载 - # SkillsMiddleware 主要负责 MCP 工具的加载 - - # 2. MCP 工具(并行加载) + # MCP 工具(并行加载) mcps = getattr(context, "mcps", None) or [] all_mcp_names: list[str] = [] for server_name in mcps: @@ -357,6 +368,7 @@ class SkillsMiddleware(AgentMiddleware): # 并行加载所有 MCP 工具 results = await asyncio.gather(*[load_mcp_tools(name) for name in unique_mcp_names]) + selected_tools = [] for tools in results: selected_tools.extend(tools) diff --git a/src/agents/common/toolkits/mysql/tools.py b/src/agents/common/toolkits/mysql/tools.py index ef191d14..a8e21125 100644 --- a/src/agents/common/toolkits/mysql/tools.py +++ b/src/agents/common/toolkits/mysql/tools.py @@ -46,18 +46,12 @@ def get_connection_manager() -> MySQLConnectionManager: return _connection_manager -class TableListModel(BaseModel): - """获取表名列表的参数模型""" - - pass - @tool( category="mysql", tags=["数据库", "查询"], display_name="列出MySQL表", name_or_callable="mysql_list_tables", - args_schema=TableListModel, ) def mysql_list_tables() -> str: """【查询表名及说明】获取数据库中的所有表名 diff --git a/src/agents/skills/reporter/SKILLS.md b/src/agents/skills/reporter/SKILLS.md new file mode 100644 index 00000000..4846cd83 --- /dev/null +++ b/src/agents/skills/reporter/SKILLS.md @@ -0,0 +1,28 @@ +--- +name: sql-reporter +description: "生成 SQL 查询报表并生成可视化图表。当用户需要查询数据库并以报表形式展示结果时使用此技能,包括:统计销售数据、分析用户行为、生成业务报表、查询业务指标等。" +--- + +# SQL 报表技能 + +根据用户的指令,使用数据库工具和图表绘制工具,构建 SQL 查询报告。 + +## 操作流程 + +1. 理解用户的指令,明确报表的需求和目标 +2. 使用 MySQL 工具生成正确的 SQL 查询 +3. 执行查询并获取结果 +4. 使用 Charts MCP 生成图表 +5. 将图表以 markdown 图片格式嵌入报表 + +## 关键约束 + +- 生成的 SQL 查询必须正确且高效,避免全表扫描 +- 图表生成工具的返回结果不会默认渲染,必须在最终报表中以 `![描述](图片URL)` 格式嵌入 +- 只返回报表相关的结论,不要返回原始 SQL 查询语句 + +## 允许的工具 + +- MySQL 工具:执行 SQL 查询 +- Charts MCP:生成可视化图表 +- 网络检索工具:必要时补充背景信息