Langgragh 19. Skills 4. SkillToolset 式设计 —— 工具化按需加载的 Skills(含代码示例)

摘要 :本文介绍 SkillToolset 式设计 ------将 Skill 能力暴露为三个 Agent 工具(list_skillsget_skill_detailsload_skill_resource),实现「菜单式」按需加载。与 Langgraph 17. Skills 三级加载与 Token 优化 中图节点驱动 discover→select→load 不同,本模式由 Agent 自主决定 何时调用哪个工具,更贴近 ADK SkillToolset 的原始语义。案例介绍 :配套 demo 用 LangChain Tools + LangGraph 实现等价于 ADK SkillToolset 的 SkillToolset 类;构建带三工具的 ReAct Agent;并结合 Langgraph 18. Skill 四种形态 ------ Inline / File-based / External / Meta 的 Critic-Reviewer 模式,形成「技能调用 → 输出 → 评审 → 未达标则修订」的闭环,使案例具有足够的复杂度与参考价值。技术要点:Tools 的 schema 设计、LangGraph Agent+ToolNode 循环、系统提示中引导 Agent 先 list_skills 再 get_skill_details、L2 指令要求时调用 load_skill_resource;Critic 节点输出 score 与 critique,条件边决定是否回到 Agent 修订。

关键词:SkillToolset;list_skills;get_skill_details;load_skill_resource;菜单式加载;LangGraph;ReAct Agent;Critic-Reviewer

源代码链接:LangGraph 19. SkillToolset 式设计 示例源代码


1. 为什么需要 SkillToolset 式设计?

1.1 图驱动 vs 工具驱动

Langgraph 17. Skills 三级加载与 Token 优化 采用图节点驱动 :discover_node 扫 L1、select_node 用 LLM 选技能、load_l2_node 加载、execute_node 执行。流程由 LangGraph 固定编排,Agent 仅在 execute 中具备 load_skill_resource 工具。

与之相反,SkillToolset 式设计 将 L1/L2/L3 的获取全部交给 Agent 工具 :Agent 收到用户问题后,自主决定 先调用 list_skills 看菜单,再调用 get_skill_details 获取相关技能的 L2,若 L2 要求参考某文件,再调用 load_skill_resource。无需 discover、select、load_l2 等预定义节点,Agent 通过工具使用完成 Progressive Disclosure。

💡 理解要点 :SkillToolset 式 = 工具驱动的 Progressive Disclosure。Agent 拥有「查看菜单」「获取菜谱」「要配方卡」三个工具,何时用、用哪个由 Agent 在对话中自主决策。

1.2 三工具与 L1/L2/L3 的对应

参考 Lavi Nigam Part 2agentskills.io

工具 对应层级 职责
list_skills L1 返回所有技能的 name + description,供 Agent 浏览「菜单」
get_skill_details L2 按 name 返回该技能的完整 SKILL.md 正文(含 frontmatter)
load_skill_resource L3 按 skill_name + resource_path 返回 references/ 中文件内容

ADK 的 SkillToolsetprocess_llm_request 中自动注入 L1 列表;本文为简化实现,在系统提示中引导 Agent「先调用 list_skills 了解可用技能」,效果等价。

1.3 与 Langgraph 17. Skills 三级加载与 Token 优化Langgraph 18. Skill 四种形态 ------ Inline / File-based / External / Meta 的关系


2. 示例设定:「技能调用 + Critic 修订」闭环

2.1 案例背景

我们构建一个带 SkillToolset 的研究与写作 Agent

  • SkillToolset :提供 list_skillsget_skill_detailsload_skill_resource 三个工具;技能库含 blog-writer、seo-checklist、research-summarizer、code-review。
  • Agent :用户输入如「写一段关于 LangGraph 的博客开头并检查 SEO」时,Agent 先调用 list_skills,再 get_skill_details 获取 blog-writer、seo-checklist 的 L2,按 L2 指示调用 load_skill_resource 读取 references,最后生成博客草稿。
  • Critic:对 Agent 输出做质量评审,输出 0--100 分与修改建议;若分数低于阈值,将 critique 作为新的用户消息反馈给 Agent,触发修订轮;最多 N 轮。

2.2 方案设计

复制代码
START
  ↓
agent_node (LLM + list_skills, get_skill_details, load_skill_resource)
  ↓
条件边:最后一条消息有 tool_calls?
  ├─ 是 → tools_node (ToolNode) → 回到 agent_node
  └─ 否 → critic_node (评审输出)
              ↓
        条件边:critique_score >= 阈值?
          ├─ 是 → END
          └─ 否 → 将 critique 作为 HumanMessage 追加 → 回到 agent_node(修订轮)
  • Agent:系统提示中说明三工具用途,引导先 list_skills、再 get_skill_details、按需 load_skill_resource;ReAct 循环直至无 tool_calls。
  • Critic:对 Agent 最后一次文本回复评审,输出 JSON(score, critique);解析后写回 state。

2.3 典型流程示例

用户输入:「请写一段关于 LangGraph 的博客开头,并做 SEO 检查。」

  • Agent 调用 list_skills → 看到 blog-writer、seo-checklist、research-summarizer、code-review;
  • Agent 调用 get_skill_details("blog-writer")get_skill_details("seo-checklist")
  • L2 要求「使用 load_skill_resource 读取 references/xxx」→ Agent 调用 load_skill_resource
  • Agent 综合 L2+L3 生成博客草稿(无 tool_calls);
  • Critic 评审,输出 score=75、critique=「首段可更简洁」;
  • 若阈值 80,则将 critique 作为用户消息追加,Agent 进入修订轮,产出修订稿;
  • Critic 再次评审,score=85 ≥ 80 → END。

3. 状态与图结构:LangGraph 编排

3.1 状态定义

python 复制代码
class SkillToolsetState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]  # 对话与工具调用历史
    critique: str           # Critic 输出的评审意见
    critique_score: int     # 0--100
    iteration: int           # 当前轮次(1=首稿,2+=修订)
    max_iterations: int      # 最大修订轮数
    verbose: NotRequired[bool]

3.2 图结构

text 复制代码
START
  ↓
agent_node        # LLM + bind_tools(SkillToolset 三工具)
  ↓
条件边:last.tool_calls?
  ├─ 是 → tools_node → agent_node
  └─ 否 → critic_node
              ↓
        条件边:critique_score >= 阈值 或 iteration >= max_iterations?
          ├─ 是 → END
          └─ 否 → 追加 HumanMessage(critique) → agent_node

3.3 核心节点与模块

3.3.1 skill_toolset.py:SkillToolset 类与三工具

职责 :提供 list_skillsget_skill_detailsload_skill_resource 三个 LangChain Tool;内部使用 skill_loader 从 skills_library 读取。

设计要点

  • list_skills:无参,扫描 skills_library,解析每个 SKILL.md 的 frontmatter,返回 - name: description 的格式化文本,供 Agent 浏览「菜单」。
  • get_skill_details:参数 skill_name,调用 load_skill 返回完整 SKILL.md(含 frontmatter + 正文)。
  • load_skill_resource:参数 skill_nameresource_path,调用底层 load_skill_resource 读取 references/ 文件。

实现要点 :每个工具通过 @tool 装饰器定义,函数的 docstring 会作为 Tool 的 description 传给 LLM,影响 Agent 的调用决策;闭包捕获 skills_root,支持自定义技能库路径。

完整源代码demo_codes/skill_toolset.py):

python 复制代码
from pathlib import Path
from typing import Optional
from langchain_core.tools import StructuredTool, tool
from skill_loader import (
    DEFAULT_SKILLS_LIBRARY,
    discover_skills,
    load_skill,
    load_skill_resource as _load_skill_resource,
)


class SkillToolset:
    def __init__(self, skills_root: Optional[Path] = None):
        self.skills_root = Path(skills_root) if skills_root else DEFAULT_SKILLS_LIBRARY

    def get_tools(self) -> list:
        return [
            self._list_skills_tool(),
            self._get_skill_details_tool(),
            self._load_skill_resource_tool(),
        ]

    def _list_skills_tool(self) -> StructuredTool:
        skills_root = self.skills_root

        @tool
        def list_skills() -> str:
            """列出所有可用技能(L1 元数据)。先调用此工具了解有哪些技能,再按需获取详情。"""
            skills = discover_skills(skills_root)
            if not skills:
                return "当前技能库为空。"
            lines = [f"- {s.get('name', '')}: {s.get('description', '')}" for s in skills]
            return "\n".join(lines)

        return list_skills

    def _get_skill_details_tool(self) -> StructuredTool:
        skills_root = self.skills_root

        @tool
        def get_skill_details(skill_name: str) -> str:
            """获取指定技能的完整 SKILL.md 内容(L2)。技能名称需与 list_skills 返回的 name 一致。"""
            full, _ = load_skill(skill_name, skills_root)
            if not full:
                return f"未找到技能 '{skill_name}',请确认技能名称与 list_skills 返回的 name 一致。"
            return full

        return get_skill_details

    def _load_skill_resource_tool(self) -> StructuredTool:
        skills_root = self.skills_root

        @tool
        def load_skill_resource(skill_name: str, resource_path: str) -> str:
            """加载指定技能下的参考文件(L3)。resource_path 如 references/style-guide.md。"""
            content = _load_skill_resource(skill_name, resource_path, skills_root)
            if not content:
                return f"未找到 {skill_name} 下的资源 '{resource_path}'。"
            return content

        return load_skill_resource

3.3.2 agent_node:ReAct Agent

职责 :将系统提示与 messages 送入 llm.bind_tools(tools),返回 AIMessage。若 response.tool_calls 非空,由条件边进入 tools_node;否则进入 critic_node。

调用链state.messages → 若无 SystemMessage 则前置 SKILL_TOOLSET_AGENT_SYSTEMllm_with_tools.invoke(messages) → 返回 {messages: [response]}add_messages 归约器会将新 AIMessage 追加到 state。

verbose 日志state.verbose 为 True 时,输出本轮 tool_calls 的 name/args,或无 tool_calls 时的 content 摘要(截断 150 字)。

完整源代码skill_toolset_graph.py 中的 _agent_node):

python 复制代码
def _agent_node(state: SkillToolsetState) -> dict:
    messages = state.get("messages") or []
    verbose = state.get("verbose") or STEP_VERBOSE

    # 若无 SystemMessage 则前置系统提示
    if not messages or not isinstance(messages[0], SystemMessage):
        messages = [SystemMessage(content=SKILL_TOOLSET_AGENT_SYSTEM)] + list(messages)

    llm = _build_llm()
    toolset = SkillToolset()
    tools = toolset.get_tools()
    llm_with_tools = llm.bind_tools(tools)
    response = llm_with_tools.invoke(messages)

    if verbose:
        if hasattr(response, "tool_calls") and response.tool_calls:
            for tc in response.tool_calls:
                name = tc.get("name", "?") if isinstance(tc, dict) else getattr(tc, "name", "?")
                args = tc.get("args", {}) if isinstance(tc, dict) else getattr(tc, "args", {})
                logger.info("[Agent] 工具调用 %s(%s)", name, args)
        else:
            content_preview = _truncate(getattr(response, "content", "") or "", 150)
            logger.info("[Agent] 最终回复(无 tool_calls): %s", content_preview)

    return {"messages": [response]}

3.3.3 tools_node:ToolNode 执行

职责 :使用 LangGraph 预置的 ToolNode,根据 state 中最后一条 AIMessage 的 tool_calls,依次执行对应工具,将每个结果封装为 ToolMessage 追加到 messages

调用链ToolNode(tools).invoke(state) → 从 state.messages[-1].tool_calls 取待执行列表 → 调用 list_skills / get_skill_details / load_skill_resource → 返回 {messages: [ToolMessage, ...]}

verbose 日志 :输出每个 ToolMessage 的 name 与返回内容长度。

完整源代码

python 复制代码
def _tools_node(state: SkillToolsetState) -> dict:
    toolset = SkillToolset()
    tools = toolset.get_tools()
    tool_node = ToolNode(tools, name="tools")
    result = tool_node.invoke(state)

    verbose = state.get("verbose") or STEP_VERBOSE
    if verbose and result.get("messages"):
        for m in result["messages"]:
            name = getattr(m, "name", "")
            content = getattr(m, "content", "") or ""
            logger.info("[Tools] 工具 %s 返回长度: %s 字符", name, len(str(content)))

    return result

3.3.4 条件边 _should_continue_after_agent

职责 :根据 agent 输出决定下一步。若最后一条消息为 AIMessage 且含 tool_calls,则路由到 tools;否则路由到 critic

完整源代码

python 复制代码
def _should_continue_after_agent(state: SkillToolsetState) -> Literal["tools", "critic"]:
    messages = state.get("messages") or []
    if not messages:
        return "critic"
    last = messages[-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return "critic"

3.3.5 critic_node:质量评审

职责 :从 messages 中提取 (1) 第一条 HumanMessage.content 作为 user_task;(2) 最后一条 AIMessage.content 作为 agent_output。将二者填入 CRITIC_USER 模板,连同 CRITIC_SYSTEM 送入 LLM,要求输出 JSON(score, score_reason, critiques)。通过 _parse_critic_json 解析后写入 state 的 critiquecritique_score

调用链 :提取 user_task、agent_output → llm.invoke([SystemMessage, HumanMessage]) → 正则/JSON 解析 → 返回 {critique, critique_score}

verbose 日志:输出评审分数与 critique 摘要(截断 120 字)。

完整源代码

python 复制代码
def _critic_node(state: SkillToolsetState) -> dict:
    messages = state.get("messages") or []
    verbose = state.get("verbose") or STEP_VERBOSE

    user_task = ""
    for m in messages:
        if isinstance(m, HumanMessage):
            user_task = getattr(m, "content", "") or ""
            break

    agent_output = ""
    for m in reversed(messages):
        if isinstance(m, AIMessage):
            agent_output = getattr(m, "content", "") or ""
            break

    logger.info("[Critic] 评审 Agent 输出(%s 字)...", len(agent_output))
    if verbose:
        logger.info("[Critic] 用户任务: %s", _truncate(user_task, 60))

    llm = _build_llm()
    user_content = CRITIC_USER.format(
        user_task=user_task,
        agent_output=agent_output or "(空)",
    )
    raw = (
        llm.invoke([
            SystemMessage(content=CRITIC_SYSTEM),
            HumanMessage(content=user_content),
        ]).content
        or ""
    ).strip()
    score, critique_text = _parse_critic_json(raw)

    logger.info("[Critic] 分数: %s/100", score)
    if verbose:
        logger.info("[Critic] 评审摘要: %s", _truncate(critique_text, 120))

    return {"critique": critique_text, "critique_score": score}

3.3.6 条件边 _route_after_critic

职责 :根据 critique_scoreiteration 决定结束或继续修订。若 score >= CRITIC_SCORE_THRESHOLDiteration >= max_iterations,返回 "end";否则返回 "refine",触发修订轮。

完整源代码

python 复制代码
def _route_after_critic(state: SkillToolsetState) -> Literal["end", "refine"]:
    score = state.get("critique_score", 0)
    iteration = state.get("iteration", 0)
    max_iter = state.get("max_iterations") or DEFAULT_MAX_ITERATIONS

    if score >= CRITIC_SCORE_THRESHOLD:
        return "end"
    if iteration >= max_iter:
        return "end"
    return "refine"

3.3.7 refine 节点:注入修订指令

职责 :将 critique 包装为 HumanMessage 追加到 messages,并递增 iteration。下一轮 agent 将看到该消息,据此修订之前产出。

调用链state.critiqueHumanMessage(content="根据以下评审意见修订...") → 返回 {messages: [msg], iteration: iteration + 1}

完整源代码

python 复制代码
def _refine_agent(state: SkillToolsetState) -> dict:
    critique = (state.get("critique") or "").strip()
    iteration = state.get("iteration", 0)
    msg = HumanMessage(
        content=f"根据以下评审意见修订你的回答:\n\n{critique}\n\n请输出修订后的完整内容,不要加额外说明。"
    )
    return {"messages": [msg], "iteration": iteration + 1}

4. 运行方式

4.1 环境与依赖

  • Python 3.9+
  • langgraphlangchain-openailangchain-corepython-dotenvtiktoken

4.2 配置

demo_codes/ 下创建 .env

复制代码
OPENAI_API_KEY=sk-...
BASE_URL=        # 可选
MODEL=gpt-4o-mini

4.3 运行

shell 复制代码
cd demo_codes
python main.py
python main.py "写一段关于 LangGraph 的博客开头并检查 SEO"
python main.py -v "..."   # verbose 日志

Notebook :打开 main.ipynb,设置 VERBOSE=True,可观测 list_skills → get_skill_details → load_skill_resource 的完整调用链及 Critic 评审过程。

4.4 目录结构

复制代码
19_skills_4/
├── 19_skills_4.md
└── demo_codes/
    ├── skill_toolset.py       # SkillToolset 类,三工具
    ├── skill_loader.py        # L1/L2/L3 底层加载
    ├── skill_toolset_graph.py # LangGraph:agent + tools + critic
    ├── skills_library/        # blog-writer, seo-checklist, code-review, research-summarizer
    ├── prompt.py
    ├── config_parser.py
    ├── log_config.py
    ├── main.py, main.ipynb
    ├── README.md
    ├── requirements.txt
    └── .env.example

5. 小结与延伸

5.1 核心结论

  • SkillToolset 式 = 三工具驱动 L1/L2/L3 按需加载,Agent 自主决定调用顺序;
  • 与图驱动的区别:无需 discover/select/load_l2 节点,流程完全由 Agent 工具使用体现;
  • Critic 闭环:可复用 8_multiagent 的评审-修订模式,提升输出质量。

5.2 延伸方向

  1. L1 自动注入:在 system 或 process_llm_request 中预填 list_skills 结果,减少首轮 tool call;
  2. 与 18 整合:SkillToolset 支持 Inline/File/External 多形态;
  3. 生产化:对 get_skill_details、load_skill_resource 做缓存。

5.3 注意事项

  • 系统提示需明确引导 Agent 先 list_skills,否则可能跳过 L1 直接猜测;
  • Critic 阈值与 max_iterations 影响延迟与成本,需权衡。

参考资料

相关推荐
强风7942 小时前
OpenCV基础入门
人工智能·opencv·计算机视觉
人工智能培训2 小时前
如何衔接知识图谱与图神经网络
人工智能·神经网络·知识图谱
火星资讯2 小时前
Zenlayer Fabric Port 新加坡首发:城域免费,全球畅连
人工智能·科技
新缸中之脑2 小时前
20个Nano Banana 2创意工作流
人工智能
智驱力人工智能2 小时前
馆藏文物预防性保护依赖的图像分析技术 文物损害检测 文物破损检测 文物损害识别误报率优化方案 文物安全巡查AI系统案例 智慧文保AI监测
人工智能·算法·安全·yolo·边缘计算
tobias.b2 小时前
机器学习 超清晰通俗讲解 + 核心算法全解(深度+易懂版)
人工智能·算法·机器学习
code_pgf2 小时前
Jetson 上 OpenClaw + Ollama + llama.cpp 的联动配置模板部署大模型
服务器·数据库·人工智能·llama
北京耐用通信2 小时前
CC-Link IE转Modbus RTU选哪家?耐达讯自动化协议转换方案深度解析
人工智能·物联网·网络协议·自动化·信息与通信