前言
今年以来笔者一直在进行智能体相关的开发工作。在近期的一个项目中遇到了一个非常棘手的问题:随着为大模型挂载的工具数量不断增加,出现了“工具越多,模型越笨”的现象。
想象一下当一个智能体(Agent)需要处理多达50个不同的工具——涵盖数据分析、PDF处理、图像识别、代码生成等多种能力——如果将这50个工具一次性全部注册给Agent,会导致什么后果?至少会引发以下三个明显问题:
- Token消耗激增:每次调用模型时,所有工具的描述信息都需要传入上下文,导致token使用量大幅上升。
- 决策质量下降:模型需要在大量工具中反复筛选和匹配,选择失误的概率显著增加。
- 响应延迟加剧:更长的上下文意味着更慢的推理速度,直接影响交互体验。
因此,如何提升大模型调用工具的准确性与效率,一直是智能体工程中的核心挑战。对此,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 双层工具架构
要实现“按需加载工具”,首先要解决的核心问题是:如何设计一个机制,让大模型在推理时能够动态地看到相关功能的工具,同时过滤掉无关的工具?
笔者采用的方案是双层工具架构。其核心思想是将所有工具分为两个层级: “门面工具” 和 “内容工具” 。
下面通过一个具体例子来直观地理解这一设计:
假设大家有四个具体的功能工具:
calculate_statistics—— 计算统计数据generate_chart—— 生成图表pdf_to_csv—— 将PDF转换为CSVpdf_to_markdown—— 将PDF转换为Markdown
按照业务功能,它们可以分为两类:
- 数据分析类工具:
calculate_statistics、generate_chart - PDF处理类工具:
pdf_to_csv、pdf_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处理类)
这样设计的好处在于:在初始状态下,模型只需“认知”两个Loader Tools(而不是全部六个工具),这极大地降低了模型的认知负担与上下文长度。只有当模型调用某个Loader Tool后,对应的Skill(即一组内容工具)才会被动态加载到后续上下文中,供模型选用。同时,Loader Tools本身会始终保留在工具列表中,以便模型在未来可以激活其他Skill。
2.2 状态驱动的动态过滤
引入Loader Tools后,下一个问题是:如何让模型在后续调用中“看到”被激活的特定工具集,而不是全部工具?笔者这里通过一个中心化的状态(State)中的变量skills_loaded来跟踪当前已加载的Skills,并基于此状态在每次模型调用前动态过滤工具列表。
具体工作流程如下:
- 用户提问:
“请调用数据分析工具分析这份数据。” - 模型在首轮推理中,识别出需要
load_data_analysis_skill这个加载器工具,并调用它。 - 该Loader Tool的执行逻辑会更新LangChain智能体的运行时状态(State),在
skills_loaded列表中添加”data_analysis”标识。 - 在下一轮模型调用开始前,我们的自定义中间件会读取
skills_loaded状态,并只筛选出属于data_analysis类别的工具(如calculate_statistics、generate_chart),动态地替换掉原本的工具列表。 - 模型在缩小的、精确的工具范围内进行选择,准确率和效率得到提升。
2.3 中间件拦截机制
从上述流程可知,实现的关键在于在模型调用前拦截请求,并依据状态动态修改其可用的工具列表。这需要利用LangChain的中间件(Middleware)机制。
LangChain关于模型调用的中间件主要提供了两个钩子(Hook):
before_model:在模型调用前执行,通常用于检查和修改状态或上下文。wrap_model:在模型调用前后执行,它可以包装并拦截整个模型调用过程。
对于本场景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 架构描述
在深入代码之前,笔者先绘制下面的架构图帮助大家了解实现的整体层次与数据流:
整个架构可分为三个核心层,它们协同工作以实现技能的动态加载:
- Skill定义层:开发者在此按照规范定义具体的技能(Skill)。每个Skill包含两个核心部分:
instruction.md(用于描述该技能的适用场景和其包含的工具)和skill.py文件。skill.py文件中需要编写一个继承自BaseSkill的类,并实现get_loader_tool()(返回激活该技能的加载器工具)和get_tools()(返回该技能包含的所有功能工具)类,比如数据分析就需要定义DataAnalysisSkill。 - Skill注册层:通过一个中心的
SkillRegistry(技能注册表)类来统一管理所有已定义的Skill。它主要提供register()方法用于注册Skill,以及get_tools_for_skills()方法用于根据提供的skill获得应被激活的工具列表。 - 中间件层:这是连接LangChain框架与上述逻辑的桥梁。通过自定义一个继承自
AgentMiddleware的SkillMiddleware类,并重写其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 次模型调用:
SkillMiddleware拦截请求,读取状态:skills_loaded = []。- 调用
SkillRegistry.get_tools_for_skills([]),返回结果:仅包含10个Loader Tools。 - 模型接收到的请求中,
tools列表被替换为这10个Loader Tools。 - 模型分析用户问题,决策调用
skill_data_analysis这个Loader Tool。
状态更新:
- Loader Tool 执行,将
"data_analysis"加入状态,现在skills_loaded = [“data_analysis”]。
第 2 次模型调用:
SkillMiddleware再次拦截,读取新状态:skills_loaded = [“data_analysis”]。- 调用
SkillRegistry.get_tools_for_skills([“data_analysis”]),返回:10个Loader Tools + 数据分析Skill下的所有功能工具(例如calculate_statistics) 。 - 模型在新工具列表(共14个)中看到
calculate_statistics工具。 - 模型决策调用
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小时的算力。本系列所有实战教程均将在该平台上完成,帮助大家低成本上手实践。