LangChain不支持AgentSkills?那就从0到1实现一个!

0 阅读14分钟

前言

今年以来笔者一直在进行智能体相关的开发工作。在近期的一个项目中遇到了一个非常棘手的问题:随着为大模型挂载的工具数量不断增加,出现了“工具越多,模型越笨”的现象。

想象一下当一个智能体(Agent)需要处理多达50个不同的工具——涵盖数据分析、PDF处理、图像识别、代码生成等多种能力——如果将这50个工具一次性全部注册给Agent,会导致什么后果?至少会引发以下三个明显问题:

  1. Token消耗激增:每次调用模型时,所有工具的描述信息都需要传入上下文,导致token使用量大幅上升。
  2. 决策质量下降:模型需要在大量工具中反复筛选和匹配,选择失误的概率显著增加。
  3. 响应延迟加剧:更长的上下文意味着更慢的推理速度,直接影响交互体验。

因此,如何提升大模型调用工具的准确性与效率,一直是智能体工程中的核心挑战。对此,Anthropic公司(Claude模型的创造者)提出了 Skills(技能)机制:一种渐进式的提示词加载策略。该机制并非一次性加载所有工具,而是仅在任务需要时才动态引入相关的提示词与工具脚本。这样不仅显著降低了token消耗,也大幅提升了工具调用的精准度。(如果对Agent Skills技术还不熟悉,建议先阅读笔者之前的文章:Agent Skills完全指南:核心概念丨设计模式丨实战代码)。

或许有小伙伴会问:“Skills机制似乎更多见于Claude Code、Trae这类代码智能体中,如果我想在自己的项目中实现类似逻辑,该怎么做呢?” 不必担心,本文将基于笔者的实际工作经验,详细介绍如何利用LangChain的中间件机制,从零开始构建一套属于自己的Agent Skills功能。

一、LangChain AgentSkill核心设计思想

本文的核心目标是将 Claude Skills 的渐进式加载思想应用于 LangChain 框架中,以解决大规模工具集场景下的调用准确性问题。设计主旨非常明确:让模型在每次调用时,仅“看到”与当前任务最相关的工具,而非全部工具。

举例来说,假设大家的智能体集成了数据分析、PDF处理、图像识别等数十种工具。当用户提问“帮我分析这份销售数据”时,系统应智能地仅呈现数据分析相关的工具;而当用户需求变为“请总结这篇PDF文档”时,模型则应在上下文中过滤掉无关工具,只保留PDF处理相关选项。

如何实现这一动态筛选机制?在笔者当前使用的 LangChain 1.0 框架中,一个自然而高效的实现途径是利用其中间件(Middleware)机制(中间件机制是LangChain 1.0 最核心的变更之一,大家如果不了解可以看笔者之前的文章:LangChain1.0速通指南(三)——LangChain1.0 create_agent api 高阶功能)。该机制是 LangChain 1.0 最核心的架构变更之一,它允许开发者在模型调用链的特定环节插入自定义逻辑,从而实现对输入、输出的拦截与处理。笔者初步的实现思路便是借助中间件,在请求到达模型之前,对可供选择的工具列表进行智能过滤。通过分析用户问题或对话上下文,系统可以只保留对当前任务有意义的工具函数,从而大幅缩减模型决策时的干扰项,降低上下文长度,最终实现调用准确性提升与资源消耗降低的双重优化。

这一设计思想在概念上非常直观,但在具体工程落地时,仍需在工具分类、路由策略、上下文感知等细节上精心设计。下文将详细拆解整个实现流程。

二、LangChain AgentSkill 实现核心思路

2.1 双层工具架构

要实现“按需加载工具”,首先要解决的核心问题是:如何设计一个机制,让大模型在推理时能够动态地看到相关功能的工具,同时过滤掉无关的工具?

笔者采用的方案是双层工具架构。其核心思想是将所有工具分为两个层级: “门面工具”“内容工具”

下面通过一个具体例子来直观地理解这一设计:

假设大家有四个具体的功能工具:

  1. calculate_statistics —— 计算统计数据
  2. generate_chart —— 生成图表
  3. pdf_to_csv —— 将PDF转换为CSV
  4. pdf_to_markdown —— 将PDF转换为Markdown

按照业务功能,它们可以分为两类:

  • 数据分析类工具calculate_statisticsgenerate_chart
  • PDF处理类工具pdf_to_csvpdf_to_markdown

在双层工具架构下,会额外引入两个特殊的“门面工具”——笔者称之为 Loader Tools(加载器工具) 。它们不直接处理用户任务,而是负责激活对应的Skill类别:

所有工具 (6个)
├── Loader Tools (2个)
│   ├── load_data_analysis_skill  (加载数据分析技能)
│   └── load_pdf_processing_skill (加载PDF处理技能)
└── Content Tools (4个)
    ├── calculate_statistics (数据分析类)
    ├── generate_chart       (数据分析类)
    ├── pdf_to_csv          (PDF处理类)
    └── pdf_to_markdown     (PDF处理类)

0.png

这样设计的好处在于:在初始状态下,模型只需“认知”两个Loader Tools(而不是全部六个工具),这极大地降低了模型的认知负担与上下文长度。只有当模型调用某个Loader Tool后,对应的Skill(即一组内容工具)才会被动态加载到后续上下文中,供模型选用。同时,Loader Tools本身会始终保留在工具列表中,以便模型在未来可以激活其他Skill。

2.2 状态驱动的动态过滤

引入Loader Tools后,下一个问题是:如何让模型在后续调用中“看到”被激活的特定工具集,而不是全部工具?笔者这里通过一个中心化的状态(State)中的变量skills_loaded来跟踪当前已加载的Skills,并基于此状态在每次模型调用前动态过滤工具列表。

具体工作流程如下:

  1. 用户提问:“请调用数据分析工具分析这份数据。”
  2. 模型在首轮推理中,识别出需要load_data_analysis_skill这个加载器工具,并调用它。
  3. 该Loader Tool的执行逻辑会更新LangChain智能体的运行时状态(State),在skills_loaded列表中添加”data_analysis”标识。
  4. 在下一轮模型调用开始前,我们的自定义中间件会读取skills_loaded状态,并只筛选出属于data_analysis类别的工具(如calculate_statisticsgenerate_chart),动态地替换掉原本的工具列表。
  5. 模型在缩小的、精确的工具范围内进行选择,准确率和效率得到提升。

2.3 中间件拦截机制

从上述流程可知,实现的关键在于在模型调用前拦截请求,并依据状态动态修改其可用的工具列表。这需要利用LangChain的中间件(Middleware)机制。

LangChain关于模型调用的中间件主要提供了两个钩子(Hook):

  • before_model:在模型调用执行,通常用于检查和修改状态或上下文。
  • wrap_model:在模型调用前后执行,它可以包装并拦截整个模型调用过程

1.png

对于本场景wrap_model是更合适的选择。因为该中间件需要完成的操作是:读取状态 -> 动态过滤工具 -> 将过滤后的新工具列表注入模型请求 -> 继续执行调用链。

wrap_model方法的核心参数和逻辑如下:

  • request:包含了本次模型调用的所有信息(如提示词、工具列表、状态等)。
  • handler:用于继续执行调用链的函数。

关键操作:使用request.override()方法创建一个新的请求对象,用过滤后的工具列表覆盖原请求中的工具。

一个简化的实现示例如下:

def wap_model_call(self, request, handler):
    # 步骤 1:从状态中读取已加载的 Skills
    skills_loaded = request.state.get("skills_loaded", [])
    
    # 步骤 2:根据状态过滤工具
    relevant_tools = self.registry.get_tools_for_skills(skills_loaded)
    
    # 步骤 3:覆盖请求中的工具列表(关键!)
    filtered_request = request.override(tools=relevant_tools)
    
    # 步骤 4:继续处理链
    return handler(filtered_request)

三、LangChain AgentSkill 实现核心架构与源码解析

理解了核心思路后,下一步就是要将其转化为可用代码,一个清晰、模块化的架构至关重要。为了方便大家阅读和实现,笔者已经将完整的代码开源(项目地址:github.com/TangBaron/L…)。国内访问GitHub不便的小伙伴,可以关注笔者的掘金账号与专栏,或关注同名微信公众号 大模型真好玩,私信 LangChain智能体开发 即可免费获取全部代码。

3.1 架构描述

在深入代码之前,笔者先绘制下面的架构图帮助大家了解实现的整体层次与数据流:

2.png

整个架构可分为三个核心层,它们协同工作以实现技能的动态加载:

  1. Skill定义层:开发者在此按照规范定义具体的技能(Skill)。每个Skill包含两个核心部分:instruction.md(用于描述该技能的适用场景和其包含的工具)和 skill.py 文件。skill.py文件中需要编写一个继承自BaseSkill的类,并实现get_loader_tool()(返回激活该技能的加载器工具)和get_tools()(返回该技能包含的所有功能工具)类,比如数据分析就需要定义DataAnalysisSkill
  2. Skill注册层:通过一个中心的SkillRegistry(技能注册表)类来统一管理所有已定义的Skill。它主要提供register()方法用于注册Skill,以及get_tools_for_skills()方法用于根据提供的skill获得应被激活的工具列表。
  3. 中间件层:这是连接LangChain框架与上述逻辑的桥梁。通过自定义一个继承自AgentMiddlewareSkillMiddleware类,并重写其wrap_model_call()方法。在此方法中,中间件会读取运行时状态,调用SkillRegistry来过滤工具,并动态修改发送给模型的请求。

3.2 关键实现细节

3.2.1 Skill定义层:BaseSkill基类

所有用户自定义的Skill都必须继承自BaseSkill抽象基类。它的核心职责是定义两类工具,源码位于项目 /core/base_skill.py

class BaseSkill(ABC):
    @abstractmethod
    def get_loader_tool(self) -> BaseTool:
        """返回 Loader Tool - 始终可见,负责激活 Skill"""
        pass

    @abstractmethod
    def get_tools(self) -> List[BaseTool]:
        """返回实际工具 - 仅激活后可见"""
        pass

关键点get_loader_tool() 返回的加载器工具,其核心作用不仅仅是作为一个工具函数,更重要的是在工具被调用时,需要更新Agent的运行时状态(state),将当前Skill的名称(如 "data_analysis")添加到 skills_loaded 列表中。这样,中间件才能在后续步骤中感知到这个Skill已被激活。

def get_loader_tool(self) -> BaseTool:
    @tool
    def skill_data_analysis(runtime) -> Command:
        return Command(
            update={
                "messages": [ToolMessage(content=instructions, ...)],
                "skills_loaded": ["data_analysis"]  # 关键:更新状态
            }
        )
    return skill_data_analysis

3.2.2 Skill注册层:SkillRegistry 查询逻辑

SkillRegistry 的核心源码位于 /core/registry.py。它的 get_tools_for_skills() 方法是整个动态过滤逻辑的核心,其设计有一个重要原则:始终包含所有Loader Tools。即使当前已经激活了data_analysis技能,返回的工具列表中依然包含load_pdf_processing_skill等所有其他加载器工具。这是为了确保Agent在后续对话中,如果用户需求改变(例如从“分析数据”切换到“处理PDF”),模型仍然有机会调用对应的Loader来激活新的技能,从而保持智能体功能的灵活性。

def get_tools_for_skills(self, skill_names: List[str]) -> List[BaseTool]:
    """
    返回:所有 Loader Tools + 已加载 Skills 的工具
    
    为什么包含所有 Loader Tools?
    - 因为模型可能需要激活新的 Skill
    """
    tools = self.get_all_loader_tools()  # 始终包含
    
    for name in skill_names:
        if name in self._skills:
            tools.extend(self._skills[name].get_tools())
    
    return tools

3.2.3 中间件层:SkillMiddleware 拦截逻辑

SkillMiddleware 的核心代码同样在 /middle/skill_middleware.py。它的 wrap_model_call 方法在每次模型被调用前执行,是实施动态过滤的“开关”。

def wrap_model_call(self, request, handler):
    # 步骤 1:从状态中读取已加载的 Skills
    skills_loaded = request.state.get("skills_loaded", [])
    
    # 步骤 2:根据状态过滤工具
    relevant_tools = self.registry.get_tools_for_skills(skills_loaded)
    
    # 步骤 3:覆盖请求中的工具列表(关键!)
    filtered_request = request.override(tools=relevant_tools)
    
    # 步骤 4:继续处理链
    return handler(filtered_request)

3.3 完整执行流程示例

下面笔者通过一个具体场景,将上述所有组件串联起来,加深理解:

场景:用户要求“计算这份数据的中位数和平均数”。

初始状态

  • skills_loaded = [] (空列表)
  • Agent 注册了 50 个工具(其中10个是Loader Tools,40个是实际的功能工具,只是为了之后筛选,工具函数的说明并不会进入上下文)

第 1 次模型调用

  1. SkillMiddleware 拦截请求,读取状态:skills_loaded = []
  2. 调用 SkillRegistry.get_tools_for_skills([]),返回结果:仅包含10个Loader Tools
  3. 模型接收到的请求中,tools 列表被替换为这10个Loader Tools。
  4. 模型分析用户问题,决策调用 skill_data_analysis 这个Loader Tool。

状态更新

  • Loader Tool 执行,将 "data_analysis" 加入状态,现在 skills_loaded = [“data_analysis”]

第 2 次模型调用

  1. SkillMiddleware 再次拦截,读取新状态:skills_loaded = [“data_analysis”]
  2. 调用 SkillRegistry.get_tools_for_skills([“data_analysis”]),返回:10个Loader Tools + 数据分析Skill下的所有功能工具(例如 calculate_statistics
  3. 模型在新工具列表(共14个)中看到 calculate_statistics 工具。
  4. 模型决策调用 calculate_statistics 工具,完成任务。

至此,整个动态加载、状态驱动、中间件拦截的流程就清晰完成了。通过这个架构成功地将一次性的从50个工具中选的“大海捞针”,变成了两次高效的“按图索骥”。

到现在为止整个的执行流程是不是非常清晰了,还有疑虑的点大家可以关注笔者的同名微信公众号 大模型真好玩,分享涉及的完整代码均可在公众号私信: LangChain智能体开发 免费获取,搭配大模型阅读食用效果更佳!大家也可以在源代码基础上轻松扩展包括权限控制、工具依赖管理等各种特性,实现大家私人定制的skill 功能,赶紧动手实践起来吧~

四、总结

本篇内容是笔者针对LangChain智能体工具过多导致性能下降的问题设计的一种仿Claude Skills的解决方案。其核心是通过双层工具架构(Loader Tools与Content Tools)、状态驱动与中间件拦截机制,实现工具的按需动态加载,从而显著降低Token消耗并提升模型调用工具的准确性与效率。

《深入浅出LangChain&LangGraph AI Agent 智能体开发》专栏内容源自笔者在实际学习和工作中对 LangChain 与 LangGraph 的深度使用经验,旨在帮助大家系统性地、高效地掌握 AI Agent 的开发方法,在各大技术平台获得了不少关注与支持。目前已更新38讲,正在更新LangGraph1.0速通指南,并随时补充笔者在实际工作中总结的拓展知识点。如果大家感兴趣,欢迎关注笔者的掘金账号与专栏,也可关注笔者的同名微信公众号 大模型真好玩,每期分享涉及的代码均可在公众号私信: LangChain智能体开发免费获取。

大模型时代的到来注定是颠覆世界的第四次工业革命,也希望大家可以紧跟AI时代的潮流,把握AI时代的风口。2026注定是大模型接续爆发的一年!为了让大家彻底搞懂大模型的作用原理,笔者也发布了《数据到模型到应用:大模型训练全流程实战指南》专栏,预计会有50期内容,将系统拆解从数据处理、模型训练到强化学习与智能体开发的全流程,并带大家从零实现模型,帮助大家掌握大模型训练的全技能,真正掌握塑造智能的能力!

需要注意的是:大模型训练对计算资源有一定要求,尤其是GPU显存。为降低学习门槛,笔者与国内主流云平台合作,大家可以通过打开网站: Lab4AI ,体验H100 GPU 6.5小时的算力。本系列所有实战教程均将在该平台上完成,帮助大家低成本上手实践。