摘要 :本文介绍 SkillToolset 式设计 ------将 Skill 能力暴露为三个 Agent 工具(list_skills、get_skill_details、load_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 2 与 agentskills.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 的 SkillToolset 在 process_llm_request 中自动注入 L1 列表;本文为简化实现,在系统提示中引导 Agent「先调用 list_skills 了解可用技能」,效果等价。
1.3 与 Langgraph 17. Skills 三级加载与 Token 优化、Langgraph 18. Skill 四种形态 ------ Inline / File-based / External / Meta 的关系
- Langgraph 17. Skills 三级加载与 Token 优化:图节点 discover→select→load_l2→execute,L3 在 execute 中由 Agent 调用 load_skill_resource。
- Langgraph 18. Skill 四种形态 ------ Inline / File-based / External / Meta:四种形态(Inline/File/External/Meta)+ 条件路由(任务执行 vs 技能创建)。
- SkillToolset 式设计:不设 discover/select/load_l2 节点,仅一个 Agent 节点 + 三工具;Agent 通过工具调用完成 L1→L2→L3 的按需加载,并直接产出回复。本模式与 ADK SkillToolset 的「工具即菜单」理念一致。
2. 示例设定:「技能调用 + Critic 修订」闭环
2.1 案例背景
我们构建一个带 SkillToolset 的研究与写作 Agent:
- SkillToolset :提供
list_skills、get_skill_details、load_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_skills、get_skill_details、load_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_name、resource_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_SYSTEM → llm_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 的 critique、critique_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_score 与 iteration 决定结束或继续修订。若 score >= CRITIC_SCORE_THRESHOLD 或 iteration >= 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.critique → HumanMessage(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+
langgraph、langchain-openai、langchain-core、python-dotenv、tiktoken
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 延伸方向
- L1 自动注入:在 system 或 process_llm_request 中预填 list_skills 结果,减少首轮 tool call;
- 与 18 整合:SkillToolset 支持 Inline/File/External 多形态;
- 生产化:对 get_skill_details、load_skill_resource 做缓存。
5.3 注意事项
- 系统提示需明确引导 Agent 先 list_skills,否则可能跳过 L1 直接猜测;
- Critic 阈值与 max_iterations 影响延迟与成本,需权衡。
参考资料: