随着Agent Skills的爆火,基于Skills拆解与组合的交互范式优势被不断放大,越来越多的大模型应用系统开始深度集成标准化 Skills,本文将通过实现一个可运行的案例,详细展示在 LangChain 框架中如何设计、注册并高效调用各类 Skills,为构建更强大的智能体应用提供实践参考。
Agent Skills 是个啥?
Agent Skills 可以理解为"给 AI 看的可执行入职手册":用一个包含 SKILL.md 的文件夹, 把流程、脚本、模板、参考资料打包成可复用、可版本化、可按需加载的"技能"。
一个 Skill 通常是一个遵循特定规范的文件夹,其标准结构如下:
bash
a-specific-skill/
├── SKILL.md # 核心文件(必需):定义技能的元数据、执行流程与最佳实践。
├── scripts/ # 可选:存放可执行的 Python、Shell 脚本,用于执行确定性强的计算或操作。
├── references/ # 可选:存放需要按需引用的参考资料,如 API 文档、数据模板、知识库文章。
└── assets/ # 可选:存放任务输出所需的静态资源,如 PPT 模板、图片素材等。
Skills 为啥突然火起来了?
之前用Claude(或GPT、Gemini)做复杂重复任务,大家公认的三大痛点:
- 每次都要重复写超长提示词 ,频繁复制粘贴,经常超出限制。
- 输出风格和质量不稳定(今天严谨明天水), 靠运气+反复改提示 。
- 做同样的事要反复教(写周报、做竞品分析、改代码风格),像带个失忆实习生。
Skills 的出现,本质是把 "提示词工程" 升级为 "流程工程"。普通人也能将个人使用习惯、团队工作方法、企业 SOP 沉淀为可复用、可分享、可交易的标准化能力资产,彻底改变了大模型的使用方式。
这也标志着行业思路的转变:
- 以前:追求训练一个全能大模型 → 看似什么都会,实则样样不精;
- 现在:基础大模型 + 按需加载专项 Skills → 专注、高效、专业,随用随配。
与此同时,Skills 还带来一系列关键优势:
-
更省 Token:无需把全部知识塞进 Prompt,只需渐进式加载技能元数据,单条技能仅占用约 100 Token;
-
更专业:每个 Skill 都由领域场景打磨而成,能力更聚焦、结果更可控;
-
易维护:更新能力只需修改技能文件,无需重新训练模型;
-
高灵活:支持动态组合、按需加载,可根据任务自由搭配技能集。
把 Skills 塞进 LangChain
在 LangChain 中,加载 Agent Skills 主要有两种常见方式
- 如果你使用 Deep Agents(
langchain-deepagents) 可在创建 Agent 时直接指定skills=["/path/to/skills/"],框架会自动扫描目录结构,识别并加载每个子目录下的SKILL.md技能描述文件。 - 如果你使用 原生 LangChain Agent (如 ReAct、Function Call 等工具型 Agent)LangChain 本身并未内置
技能目录扫描的能力,需要自行实现:- 遍历技能文件夹;
- 解析每个技能对应的
SKILL.md等描述文件; - 将技能逻辑统一封装为 Tool,并通过
load_skill等方式加载后供 Agent 调用。
本文主要集中在第二种,第一种 Deep Agents 可以直接去官网查看,案例也比较清晰简单。
技能准备
假设你已经有两个skills(一个用于写sql优化,一个用于写前端页面)
js
skills/
├── sql-optimization/
│ └── SKILL.md
├── frontend-design/
│ └── SKILL.md
└── ...想加啥技能就新建个目录
每个 SKILL.md 里有 frontmatter(name、description 等)和技能说明,例如:
yaml
---
name: sales_analytics
description: 用于销售数据分析的技能,包含数据库 schema 和常见查询示例。
---
# sales_analytics
## Overview
...
扫描并解析所有 SKILL.md
用一个函数把所有技能加载到内存(Python dict),同级目录下新增load_skills.py,存放扫描skills函数。
python
from pathlib import Path
from typing import List, TypedDict
import yaml
class SkillDict(TypedDict):
"""A skill that can be progressively disclosed to the agent."""
name: str
description: str
content: str
def load_skills_from_dir(skills_dir: str) -> List[SkillDict]:
"""扫描目录,解析每个 SKILL.md,返回技能列表"""
skills = []
base_dir = Path(__file__).parent
skills_path = base_dir / skills_dir
for skill_dir in skills_path.iterdir():
if not skill_dir.is_dir():
continue
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
continue
content = skill_file.read_text(encoding="utf-8")
# 解析 frontmatter(假定格式正确)
# 简单起见用 yaml 解析,也可以用 python-frontmatter 库
parts = content.split("---", maxsplit=2)
if len(parts) >= 3:
frontmatter_str = parts[1].strip()
body = parts[2].strip()
meta = yaml.safe_load(frontmatter_str)
name = meta.get("name", skill_dir.name)
description = meta.get("description", "")
else:
# 没有 frontmatter,就用目录名和整个内容
name = skill_dir.name
description = f"Skill for {name}"
body = content
skills.append(
{
"name": name,
"description": description,
"content": body, # 或者包含整个文件内容
"dir": str(skill_dir),
}
)
return skills
# 全局技能列表
SKILLS = load_skills_from_dir("skills")
写一个 load_skill 工具
创建skills_tools.py文件,用于定义load_skill工具。该工具的作用是根据技能名称,全量加载并返回对应 SKILL.md 文件的完整内容,让 Agent 获取指定技能的全套执行指令、策略规则与操作规范。
python
@tool
def load_skill(skill_name: str) -> str:
"""Load a complete skill into the agent's context.
Use this tool when you need detailed information about handling a specific
type of request. It provides complete instructions, strategy rules, and
operational guidance within the skill's domain.
Args:
skill_name: The name of the skill to load
(e.g., "expense_reporting", "travel_booking")
"""
for skill in SKILLS:
if skill["name"] == skill_name:
return f"Loaded skill: {skill_name}\n\n{skill['content']}"
available = ", ".join(s["name"] for s in SKILLS)
return f"Skill '{skill_name}' not found. Available skills: {available}"
把技能描述注入 System Prompt
无需在系统提示词中塞入全部技能的完整内容,仅注入技能名称 + 精简描述即可,完整技能指令通过load_skills工具动态按需加载。可通过实现一个自定义 SkillMiddleware 中间件自动完成技能元信息注入与上下文管理。
python
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
class SkillMiddleware(AgentMiddleware):
tools = [load_skill, view_skill_file, run_skill_script]
def __init__(self, skills_list: List[SkillDict]):
lines = []
# 遍历技能列表,生成技能元信息
# 每个技能元信息包含技能名称和描述
for s in skills_list:
lines.append(f"- {s['name']}: {s['description']}")
self.skills_prompt = "\n".join(lines)
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
# 组合技能元信息与系统提示词
skills_addendum = (
f"\n\n## Available Skills\n\n{self.skills_prompt}\n\n"
"Use the load_skill tool when you need detailed information "
"about handling a specific type of request."
)
# 获取当前系统提示词内容
current_content = getattr(request.system_message, "content", "") or ""
# 合并当前系统提示词与技能元信息
new_system_message = SystemMessage(content=current_content + skills_addendum)
# 重写请求,包含新的系统提示词
new_request = request.override(system_message=new_system_message)
# 调用模型处理新的请求
return handler(new_request)
创建带有技能的 Agent
python
def main():
model = ChatOpenAI(model="minimax/minimax-m2.5", temperature=0.7, streaming=True)
agent = create_agent(
model,
system_prompt="你是一个助手,可以根据用户问题加载不同技能来完成任务。",
middleware=[SkillMiddleware(SKILLS)],
)
thread_id = str(uuid.uuid4())
result = agent.invoke(
{
"messages": [
HumanMessage(
content="帮我写一个最近一个月下单金额超过 1000 的客户 SQL", # noqa: E501
),
]
},
config={"configurable": {"thread_id": thread_id}},
)
for message in result["messages"]:
if hasattr(message, "pretty_print"):
message.pretty_print()
else:
print(f"{message.type}: {message.content}")
if __name__ == "__main__":
main()
这样,Agent 在需要详细技能内容时,会自动调用 load_skill("sql-optimization"),把完整的 SKILL.md(以及你愿意加的其他资源)读进来。
运行日志
js
================================ Human Message =================================
帮我写一个最近一个月下单金额超过 1000 的客户 SQL
================================== Ai Message ==================================
Tool Calls:
load_skill (call_3a8973b46576482ca734cdfd)
Call ID: call_3a8973b46576482ca734cdfd
Args:
skill_name: sql-optimization
================================= Tool Message =================================
Name: load_skill
Loaded skill: sql-optimization
# SQL Performance Optimization Assistant
Expert SQL performance optimization for ${selection} (or entire project if no selection). Focus on universal SQL optimization techniques that work across MySQL, PostgreSQL, SQL Server, Oracle, and other SQL databases.
...
================================== Ai Message ==================================
# 查询最近一个月下单金额超过 1000 的客户
根据常见的电商数据库结构,以下是几种数据库的写法:
## 通用 SQL(MySQL/PostgreSQL/SQL Server 通用)
SELECT
c.id AS customer_id,
c.name AS customer_name,
c.email,
SUM(o.total_amount) AS total_order_amount,
COUNT(o.id) AS order_count
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
WHERE o.created_at >= DATE_SUB(CURRENT_DATE, INTERVAL 1 MONTH)
-- PostgreSQL 使用: WHERE o.created_at >= CURRENT_DATE - INTERVAL '1 MONTH'
-- SQL Server 使用: WHERE o.created_at >= DATEADD(MONTH, -1, GETDATE())
AND o.status != 'cancelled' -- 排除已取消订单
GROUP BY c.id, c.name, c.email
HAVING SUM(o.total_amount) > 1000
ORDER BY total_order_amount DESC;
## 建议的索引
-- 订单表索引
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_customer_date ON orders(customer_id, created_at, total_amount);
## 说明
| 要素 | 说明 |
|------|------|
| **时间范围** | 最近 30 天(`DATE_SUB(CURRENT_DATE, INTERVAL 1 MONTH)`) |
| **过滤条件** | 排除已取消订单(`status != 'cancelled'`) |
| **聚合** | 按客户汇总下单金额和订单数 |
| **筛选** | `HAVING SUM(o.total_amount) > 1000` 过滤金额超过 1000 的客户 |
如果你能提供具体的表结构或数据库类型,我可以给你更精确的查询!
至此已经完成了一个最小可调用skills单元的例子。
读取技能相关参考文件与执行脚本调用
如需支持读取 SKILL.md 中关联的参考文档、以及执行技能内置脚本,只需额外新增两个工具即可完整扩展能力:
view_skill_file:查看指定技能的参考文件内容run_skill_script:执行技能对应的脚本逻辑
最后将这两个工具统一集成到中间件(SkillMiddleware)中,Agent 即可自动调用。
python
import subprocess
from pathlib import Path
from typing import Optional
from langchain.tools import tool
from load_skills import SKILLS
SKILLS_DIR = Path("./skills")
def validate_path(base_dir: Path, target_path: str) -> Path:
"""
Validate path safety to prevent path traversal attacks.
Ensures the target path is within the base_dir.
"""
abs_base = base_dir.resolve()
abs_target = (abs_base / target_path).resolve()
if not str(abs_target).startswith(str(abs_base)):
raise ValueError(
f"Invalid path access: {target_path} is outside the allowed directory"
)
return abs_target
@tool
def view_skill_file(skill_name: str, file_name: str) -> str:
"""View a reference file within a skill's directory (e.g., docs, data files, configs).
Args:
skill_name: Name of the skill.
file_name: Name of the file to view within the skill's directory.
"""
try:
skill_dir = SKILLS_DIR / skill_name
safe_path = validate_path(skill_dir, file_name)
if not safe_path.exists():
return f"Error: File '{file_name}' not found in skill '{skill_name}'."
if safe_path.stat().st_size > 1 * 1024 * 1024:
return (
f"Error: File too large ({safe_path.name}), use a more specific path."
)
return safe_path.read_text(encoding="utf-8")
except ValueError as e:
return f"Security error: {str(e)}"
except Exception as e:
return f"Error reading file: {str(e)}"
@tool
def run_skill_script(
skill_name: str, script_name: str, arguments: Optional[str] = ""
) -> str:
"""Execute a script within a skill's directory (supports .py and .sh).
Args:
skill_name: Name of the skill.
script_name: Name of the script file to execute.
arguments: Optional string of arguments to pass to the script.
"""
try:
skill_dir = SKILLS_DIR / skill_name
safe_script_path = validate_path(skill_dir, script_name)
if not safe_script_path.exists():
return f"Error: Script '{script_name}' not found."
command = []
if safe_script_path.suffix == ".py":
command = ["python", str(safe_script_path)]
elif safe_script_path.suffix == ".sh":
command = ["bash", str(safe_script_path)]
else:
return f"Error: Unsupported script type '{safe_script_path.suffix}'. Supported: .py, .sh"
if arguments:
import shlex
command.extend(shlex.split(arguments))
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=60,
cwd=str(skill_dir),
)
if result.returncode == 0:
return f"Script executed successfully:\n{result.stdout}"
else:
return f"Script failed (exit code {result.returncode}):\n{result.stderr}"
except subprocess.TimeoutExpired:
return "Error: Script execution timed out (60s limit)."
except ValueError as e:
return f"Security error: {str(e)}"
except Exception as e:
return f"Error executing script: {str(e)}"