LangChain 实现 Skill 框架

53 阅读6分钟

Skill 基本概念

Skills 的概念由 Anthropic 提出,本质上是一种更高层次的模块化能力封装,用于扩展智能体的功能边界。每一个 Skill 都封装了指令、元数据以及可选的资源(如脚本、模板等),智能体在执行任务时,会根据上下文相关性自动选择并调用合适的 Skill,避免Prompt越来越长、工具调用越来越乱。

Skills 的组成部分:

  • SKILL.md
    • 头部元数据(YAML 格式):包含 name(名称)和 description(描述)字段。description 是 Agent 自动路由的“关键词”,务必写得精准、业务化。
    • 主体内容(Markdown 格式):关于如何使用该技能的说明和指引。只有在技能被触发后才会加载,避免污染全局上下文。
  • 脚本 (scripts/):可执行代码(Python / Bash 等),适用于需要确保可靠性或经常重复编写的任务,应保持“无状态+纯函数”设计,便于测试与复用。
  • 参考资料 (references/):领域文档、SOP、数据架构说明。按需注入上下文,指导 Agent 的推理路径。例如企业私有规范、合规要求、历史案例等。
  • 资源文件 (assets/):静态资源文件。如报告模板、UI 组件、配置文件等,用于最终输出。

Skills 的核心功能:

  1. 专业工作流:特定领域的多步骤操作流程
  2. 工具集成:使用特定文件格式或 API 的指导说明
  3. 领域专长:企业特有知识、数据架构、业务规则
  4. 资源包:处理复杂和重复任务所需的脚本、参考文档和相关资源

Skill | MCP | Tools

维度Tools(工具)Skills(技能)MCP(Model Context Protocol)
本质定位动作执行、工具调用专业工作流+知识封装标准化通信协议
核心职责调用 API、执行函数、操作外部系统加载领域 Prompt、编排多步骤流程、整合参考资料统一模型与外部数据/工具的连接标准
触发方式显式声明或函数调用(Function Calling)上下文语义匹配 → 自动发现 → 按需加载客户端/服务端通过 JSON-RPC 动态协商
上下文占用低(仅注入 schema 与结果)中/高(加载完整指引与参考文档)极低(仅定义接口,不承载业务逻辑)

三者的协作关系: MCP 提供标准化通道 → 接入各类 Tools 与数据源 → Skills 基于这些能力,结合领域知识编排工作流 → Agent 按需调用 Skill 完成复杂任务。

如何选择Skill、Tool还是MCP?

场景特征推荐方案示例
需要调用外部 API、数据库、第三方服务Tool查询天气、发送邮件、调用支付接口
涉及多步骤 SOP、合规审查、行业专有逻辑Skill合同风险审查、财报分析、工单分类派发
团队多人协作、能力需频繁迭代、知识沉淀Skill + MCP Server企业内部知识库问答、跨系统数据聚合分析

通过 LangChain 中间件实现 Skill 渐进式加载框架

整体框架

  1. 扫描 Skills 目录下所有的 Skill 的 name 和 description,封装到系统提示词;
  2. Agent 读取所有 Skill 的 name 和 description,选定 skill 执行 load_skill 工具,获取skill的详细内容。

实现方法

  1. 在 Skills 目录下创建一个新的 Skill ,目录名称为skill的名称,必须包含Skill.md,可以包含 scriptsreferences等目录。
---
name: food-calorie
description: 精准计算各类食物卡路里,支持按食材、重量、烹饪方式拆分核算总热量
metadata:
  author: nobody
  version: 2.0.0
---
  1. 根据 Skill 文件格式确定数据类型,部分 Skill 可能包含脚本文件:
class Skill(TypedDict):
    """文件夹型技能:支持 scripts / references / assets"""

    name: str  # 技能名 = 文件夹名
    description: str  # 简短描述(来自 SKILL.md 第一行)
    content: str  # SKILL.md 完整内容
    path: Path  # 技能根目录
    scripts: list[Path]  # scripts/ 下所有文件
    references: list[Path]  # references/ 下所有文件
    assets: list[Path]  # assets/ 下所有文件
  1. 从 skills 目录中读取所有 Skill 的基础信息:
def load_skills_from_dir(skills_root_dir: Path) -> list[Skill]:
    skills = []
    for skill_dir in sorted(skills_root_dir.iterdir()):
        if not skill_dir.is_dir():
            continue

        skill_md = skill_dir / "SKILL.md"
        if not skill_md.exists():
            continue

        text = skill_md.read_text(encoding="utf-8")

        # ======================
        # 修复点:从 YAML 头读取 description
        # ======================
        lines = text.splitlines()
        description = "No description"

        # 解析 --- 包裹的 YAML 头部
        if len(lines) > 2 and lines[0].strip() == "---":
            try:
                # 找到下一个 ---
                end_idx = lines[1:].index("---") + 1
                header_lines = lines[1:end_idx]

                # 读取 description 字段
                for line in header_lines:
                    if line.strip().startswith("description:"):
                        description = line.split(":", 1)[1].strip()
                        break
            except ValueError:
                pass

        skills.append(
            Skill(
                name=skill_dir.name,
                description=description,
                content=text,
                path=skill_dir,
                scripts=(
                    list((skill_dir / "scripts").glob("*"))
                    if (skill_dir / "scripts").exists()
                    else []
                ),
                references=(
                    list((skill_dir / "references").rglob("*"))
                    if (skill_dir / "references").exists()
                    else []
                ),
                assets=(
                    list((skill_dir / "assets").glob("*"))
                    if (skill_dir / "assets").exists()
                    else []
                ),
            )
        )
    return skills
    
SKILLS = load_skills_from_dir(Path(__file__).parent)
  1. 定义工具,供 Agent 加载指定的 skill
@tool
def load_skill(skill_name: str) -> str:
    """Load the full content of a skill into the agent's context."""
    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}"
  1. 通过 AgentMiddleware 中间件实现 Skill 渐进式信息披露:
class SkillMiddleware(AgentMiddleware):
    """自动把所有【文件夹技能】注入系统提示"""

    tools = [load_skill, list_skill_files, read_skill_file]

    def __init__(self):
        skills_list = []
        for skill in SKILLS:
            skills_list.append(f"- **{skill['name']}**: {skill['description']}")
        self.skills_prompt = "\n".join(skills_list)

    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 load_skill to get full instructions.\n"
            "Use list_skill_files to list files in a skill.\n"
            "Use read_skill_file to read any file in a skill."
        )

        new_content = list(request.system_message.content_blocks) + [
            {"type": "text", "text": skills_addendum}
        ]
        new_system_message = SystemMessage(content=new_content)
        modified_request = request.override(system_message=new_system_message)
        return handler(modified_request)
  1. 测试:
def test_skill_middleware():
    llm = ChatOpenAI(model="qwen-flash", temperature=0.3)
    agent = create_agent(
        model=llm,
        middleware=[SkillMiddleware()],
        checkpointer=InMemorySaver(),
        system_prompt="你是一位运动与营养学的专家",
    )
    config = {"configurable": {"thread_id": "1"}}

    result = agent.invoke(
        input={
            "messages": [
                {
                    "role": "user",
                    "content": "我今天吃了10碗米饭,而且还用青椒肉丝的汤汁浇在米饭上,摄入的卡路里是多少?",
                },
            ]
        },
        config=config,
    )

    # Print the conversation
    for message in result["messages"]:
        if hasattr(message, 'pretty_print'):
            message.pretty_print()
        else:
            print(f"{message.type}: {message.content}")

可以看到 Agent 加载了 food-calori skill:

================================== Ai Message ==================================
Tool Calls:
  load_skill (call_c790c732842245a98dc85c)
 Call ID: call_c790c732842245a98dc85c
  Args:
    skill_name: food-calorie
================================= Tool Message =================================
Name: load_skill

Loaded skill: food-calorie

---
name: food-calorie
description: 精准计算各类食物卡路里,支持按食材、重量、烹饪方式拆分核算总热量
metadata:
  author: DevTeam
  version: 2.0.0
---

# 食物卡路里计算专家指南
## 角色定位
你是专业营养学计算助手,擅长拆解混合食材、区分原生食材/加工烹饪方式、按国标营养热量标准,精准核算单种或多种食物总卡路里。
......
......

================================= Ai Message ==================================
Tool Calls:
  read_skill_file (call_76edf6d96c894868b889aa)
 Call ID: call_76edf6d96c894868b889aa
  Args:
    skill_name: food-calorie
    file_path: references/standard_calories.md
================================= Tool Message =================================
Name: read_skill_file

参考资料:

  1. LangChain 官方 Skills 文档
  2. Skills 框架教学文档