Langchain 调用 Agent Skills

随着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)做复杂重复任务,大家公认的三大痛点:

  1. 每次都要重复写超长提示词 ,频繁复制粘贴,经常超出限制。
  2. 输出风格和质量不稳定(今天严谨明天水), 靠运气+反复改提示 。
  3. 做同样的事要反复教(写周报、做竞品分析、改代码风格),像带个失忆实习生。

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)}"
相关推荐
java资料站4 小时前
第01章:LangChain使用概述
langchain
老王熬夜敲代码5 小时前
test_node流程详解
langchain
qq_54702617913 小时前
LangChain 中间件(Middleware)
中间件·langchain
Cha0DD14 小时前
【由浅入深探究langchain】第二十集-SQL Agent+Human-in-the-loop
人工智能·python·ai·langchain
Cha0DD14 小时前
【由浅入深探究langchain】第十九集-官方的SQL Agent示例
人工智能·python·ai·langchain
Barkamin1 天前
LangChain简单介绍
langchain
百年੭ ᐕ)੭*⁾⁾1 天前
Chroma简单上手
人工智能·语言模型·langchain·chroma·rag
Roselind_Yi1 天前
【吴恩达2026 Agentic AI】面试向+项目实战(含面试题+项目案例)-2
人工智能·python·机器学习·面试·职场和发展·langchain·agent
JaydenAI1 天前
[RAG在LangChain中的实现-04]常用的向量存储和基于向量存储的检索器
python·langchain·ai编程