fix(skills): 修复 skills 加载以及按需暴露问题,并新增 reporter 技能,已可以替代数据库报表智能体

This commit is contained in:
Wenjie Zhang 2026-03-06 10:15:58 +08:00
parent 96ce4dbe8a
commit 186012e5e8
4 changed files with 82 additions and 48 deletions

View File

@ -93,7 +93,7 @@ class BaseContext:
metadata={
"name": "Skills",
"options": [],
"description": "可选技能列表(由超级管理员维护)。运行时仅挂载并只读暴露选中的 skills。",
"description": "可选技能列表(由超级管理员维护)。运行时仅挂载并只读暴露选中的 skills。技能依赖的工具和 MCP 服务器也会被自动挂载。",
"type": "list",
},
)

View File

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

View File

@ -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:
"""【查询表名及说明】获取数据库中的所有表名

View File

@ -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生成可视化图表
- 网络检索工具:必要时补充背景信息