Agent Skills 是个啥?
你可以把 Agent Skills 理解为“给 AI 看的可执行入职手册”。它的核心思路很简单:用一个包含 SKILL.md 文件的文件夹,把一套完整的操作流程、脚本、模板和参考资料打包起来,形成一个可复用、可版本管理、并且能按需加载的“技能包”。

一个标准的 Skill 文件夹结构通常是这样的:
a-specific-skill/
├── SKILL.md # 核心文件(必需):定义技能的元数据、执行流程与最佳实践。
├── scripts/ # 可选:存放可执行的 Python、Shell 脚本,用于执行确定性强的计算或操作。
├── references/ # 可选:存放需要按需引用的参考资料,如 API 文档、数据模板、知识库文章。
└── assets/ # 可选:存放任务输出所需的静态资源,如 PPT 模板、图片素材等。
Skills 为啥突然火起来了?
回想一下之前用 Claude、GPT 或 Gemini 处理复杂重复任务时的场景,是不是总绕不开这几个痛点?
- 提示词又臭又长:每次都要从头写一遍超长指令,频繁复制粘贴不说,还动不动就超出上下文限制。
- 输出质量像开盲盒:今天严谨专业,明天敷衍了事,风格和质量全凭运气,得靠反复修改提示词来“校准”。
- 像带了个失忆的实习生:同样的任务,比如写周报、做竞品分析、改代码风格,每次都得重新教一遍,无法形成可沉淀的经验。
Skills 的出现,本质上是一次思路的升级:从“提示词工程”转向了“流程工程”。它让普通用户也能将个人习惯、团队方法乃至企业 SOP(标准作业程序)沉淀下来,变成可复用、可分享、甚至可交易的标准化能力资产。这彻底改变了大模型的使用范式。
背后的行业趋势也很明显:
- 过去:大家追求训练一个“全能型”大模型,希望它什么都会,结果往往是样样都懂一点,样样都不精。
- 现在:思路转变为“基础大模型 + 按需加载专项 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 Calling 这类原生的工具型 Agent,情况就稍微复杂一点。因为 LangChain 本身并没有内置“技能目录扫描”的功能,需要我们自己动手实现:
- 遍历指定的技能文件夹;
- 解析每个技能目录下的
SKILL.md等描述文件; - 将技能逻辑封装成标准的 Tool(工具),然后通过自定义的
load_skill等方法加载,供 Agent 调用。
下面我们重点聊聊第二种方式的实现细节。第一种方式在 Deep Agents 的官方文档里有很清晰的案例,这里就不赘述了。
技能准备
假设你已经准备好了两个技能文件夹,一个用于 SQL 优化,一个用于前端页面设计。
skills/
├── sql-optimization/
│ └── SKILL.md
├── frontend-design/
│ └── SKILL.md
└── ...想加啥技能就新建个目录
每个 SKILL.md 文件通常包含两部分:文件头部的 frontmatter(用于定义技能名称、描述等元数据)和正文部分的技能详细说明。例如:
---
name: sales_analytics
description: 用于销售数据分析的技能,包含数据库 schema 和常见查询示例。
---
# sales_analytics
## Overview
...
扫描并解析所有 SKILL.md
第一步,我们需要一个函数来扫描技能目录,把所有技能加载到内存中。在同级目录下创建一个 load_skills.py 文件,写入以下代码:
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 获得执行该技能所需的全套指令、策略和操作规范。
@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", "tra vel_booking")
"""
for skill in SKILLS:
if skill["name"] == skill_name:
return f"Loaded skill: {skill_name}nn{skill['content']}"
a vailable = ", ".join(s["name"] for s in SKILLS)
return f"Skill '{skill_name}' not found. A vailable skills: {a vailable}"
把技能描述注入 System Prompt
这里有个关键技巧:我们不需要在系统提示词里塞进所有技能的完整内容,那样太占 Token 了。正确的做法是,只注入技能的“元信息”——也就是名称和简短描述。完整的技能指令,则通过上面定义的 load_skill 工具在需要时动态加载。
我们可以通过实现一个自定义的 SkillMiddleware 中间件来自动完成这个“元信息注入”和上下文管理的流程。
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"nn## A vailable Skillsnn{self.skills_prompt}nn"
"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
万事俱备,现在可以创建一个集成了技能的 Agent 了。
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 内容(以及你放在技能目录里的其他资源)读入上下文。
运行日志
运行上面的代码,你会看到类似下面的交互过程:
================================ 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
HA VING 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'`) |
| **聚合** | 按客户汇总下单金额和订单数 |
| **筛选** | `HA VING SUM(o.total_amount) > 1000` 过滤金额超过 1000 的客户 |
如果你能提供具体的表结构或数据库类型,我可以给你更精确的查询!
至此,一个最小可用的 Skills 调用单元就完成了。
读取技能相关参考文件与执行脚本调用
当然,一个完整的技能体系可能不止一个 SKILL.md 文件。如果还想支持读取技能目录下的参考文档,或者执行内置的脚本,只需要再额外扩展两个工具即可:
view_skill_file:查看指定技能目录下的任意参考文件内容。run_skill_script:执行技能目录下的 Python 或 Shell 脚本。
最后,把这两个新工具也集成到之前的 SkillMiddleware 中间件里,Agent 就能自动调用它们了。
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 tra versal 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)}"
