LangChain技能封装:从工具调用到智能体编排的实战指南
2026/8/8 3:13:49 网站建设 项目流程

1. 从“工具调用”到“技能编排”:为什么我们需要Agent Skills?

如果你最近在折腾LangChain或者AI Agent开发,大概率会频繁听到一个词:Skills。它听起来比单纯的“工具”(Tools)要酷一些,但很多人可能只是把它当作一个花哨的同义词来用。在我过去一年多的Agent项目实战中,我逐渐意识到,从“工具”思维切换到“技能”思维,是构建一个真正智能、可靠且易于维护的Agent系统的关键分水岭。

简单来说,一个“工具”就像一把螺丝刀,它有一个明确的接口(拧螺丝),你告诉Agent“用这把螺丝刀”,它就去拧。而一个“技能”则更像“组装一台电脑”,它内部可能调用了螺丝刀、硅脂涂抹器、防静电手环等多种工具,并且遵循一套逻辑(先装CPU,再涂硅脂,然后装散热器)。Skills的本质,是对一个或多个底层工具(Tool)的封装、编排与逻辑增强,使其能够完成一个更高级、更符合人类语义的任务单元。

为什么这个区别如此重要?因为当我们直接让大模型(LLM)去调用零散的工具时,经常会遇到几个头疼的问题:工具选择困难(LLM面对十几个功能相近的工具时容易选错)、复杂流程割裂(一个用户查询需要连续调用多个工具,LLM的上下文可能丢失中间状态)、错误处理缺失(工具调用失败后,LLM往往不知道如何优雅地重试或降级)。而Skills正是为了解决这些问题而生。它允许我们将领域知识、业务流程和异常处理逻辑固化下来,让LLM从一个“微观操作工”升级为“宏观调度员”,直接调用封装好的高级技能,从而大幅提升Agent的可靠性、执行效率和可维护性。

2. LangChain中Skills的三种实现范式与核心API

在LangChain的生态里,实现一个Skill并没有唯一的“标准答案”,这取决于你的场景复杂度。根据我的经验,可以归纳为三种主流的实现范式,它们各有优劣,适用于不同的阶段。

2.1 范式一:基于Tool类的直接封装(入门级)

这是最直接的方式,适合将单个功能封装成技能。LangChain的BaseTool类是所有工具的基类,创建一个Skill本质上就是继承它并实现_run方法。

from langchain.tools import BaseTool from typing import Optional, Type from pydantic import BaseModel, Field class WeatherQueryInput(BaseModel): """查询天气的输入参数""" city_name: str = Field(description="城市名称,例如:北京、上海") class GetWeatherSkill(BaseTool): name = "get_weather" description = "根据城市名称查询实时天气情况。" args_schema: Type[BaseModel] = WeatherQueryInput def _run(self, city_name: str) -> str: # 这里是技能的核心逻辑 # 1. 可能调用一个第三方天气API # 2. 对API返回的原始数据进行清洗和格式化 # 3. 加入业务逻辑,比如根据温度给出穿衣建议 try: # 模拟API调用 temperature = 22 condition = "晴" advice = "天气舒适,建议穿单衣。" return f"{city_name}的天气是{condition},气温{temperature}摄氏度。{advice}" except Exception as e: # 技能内部的错误处理 return f"查询{city_name}天气时出错:{str(e)},请检查城市名称或稍后重试。" async def _arun(self, city_name: str): """异步版本""" # 实现异步调用逻辑 pass # 使用技能 weather_skill = GetWeatherSkill()

核心要点与避坑

  • args_schema是灵魂:务必使用Pydantic模型明确定义输入参数。这不仅是类型检查,更重要的是为LLM提供了清晰、结构化的“使用说明书”。LLM会根据这个schema来生成调用你的技能所需的参数。
  • description要精准:描述必须清晰说明技能做什么以及何时使用。例如,“查询天气”不如“根据中文城市名查询实时温度、天气状况并给出简要穿衣建议”来得有效。
  • 错误处理内置化:在_run内部处理好异常,并返回对用户友好的错误信息,而不是抛出异常让Agent不知所措。

2.2 范式二:基于Tool+Runnable的序列化技能(进阶级)

当你的技能需要按顺序执行多个步骤,或者需要在步骤间传递和加工数据时,简单的_run方法就显得力不从心了。这时,可以利用LangChain的Runnable协议来构建一个技能流水线

from langchain.tools import BaseTool from langchain.schema.runnable import RunnablePassthrough, RunnableLambda from langchain.schema import StrOutputParser from typing import Dict class DataAnalysisSkill(BaseTool): name = "analyze_sales_data" description = "分析上传的销售数据CSV文件,生成月度总结报告。" # 注意,这里我们可能不定义固定的args_schema,因为输入可能是一个文件路径或数据字典。 def _run(self, file_path: str) -> str: # 我们不直接把逻辑写在这里,而是调用一个构建好的执行链 chain = self._build_analysis_chain() return chain.invoke({"file_path": file_path}) def _build_analysis_chain(self): """构建一个可运行的技能执行链""" # 步骤1:加载数据 load_data = RunnableLambda(lambda x: self._load_csv(x["file_path"])) # 步骤2:清洗数据(例如处理空值) clean_data = RunnableLambda(lambda df: self._clean_data(df)) # 步骤3:核心分析(计算月度销售额、Top产品等) run_analysis = RunnableLambda(lambda df: self._perform_analysis(df)) # 步骤4:格式化报告 format_report = RunnableLambda(lambda result: self._format_to_markdown(result)) # 组装链 chain = ( RunnablePassthrough.assign(data=load_data) # 传递原始输入并添加`data`字段 | RunnablePassthrough.assign(cleaned_data=clean_data) # 传递并添加`cleaned_data` | RunnablePassthrough.assign(analysis_result=run_analysis) | format_report | StrOutputParser() ) return chain def _load_csv(self, file_path): # 模拟函数 return f"Loaded DataFrame from {file_path}" def _clean_data(self, df): # 模拟函数 return f"Cleaned {df}" def _perform_analysis(self, df): # 模拟函数 return {"monthly_sales": 100000, "top_product": "Product_A"} def _format_to_markdown(self, result): return f"# 销售分析报告\n- 月度总销售额:{result['monthly_sales']}\n- 畅销产品:{result['top_product']}" # 使用方式不变,但内部是强大的流水线 analysis_skill = DataAnalysisSkill()

为什么选择这种范式?

  • 可观测性与调试Runnable链的每个步骤都是独立的,你可以轻松插入日志、监控点,或者单独测试某个步骤。
  • 灵活组合:你可以复用其他Runnable组件(如其他Tool、LLM调用、条件判断等),像搭积木一样构建复杂技能。
  • 与LangGraph无缝集成Runnable是LangGraph节点的天然组成部分,当你需要将技能嵌入到有状态、可循环的Agent工作流时,这种范式迁移成本最低。

2.3 范式三:基于自定义Agent的宏观技能(专家级)

对于一些极其复杂、决策路径长的任务(例如“为我规划一个为期三天的北京旅游行程”),将其作为一个单独的Skill来开发可能更合适。这个Skill本身内部就运行着一个微型的、专门的Agent。

from langchain.agents import AgentExecutor, create_react_agent from langchain.tools import Tool from langchain_community.chat_models import ChatOpenAI from langchain.prompts import PromptTemplate class TravelPlanningSkill(BaseTool): name = "plan_travel_itinerary" description = "为一个城市规划多日旅游行程,会综合考虑景点、餐饮、交通和预算。" def __init__(self): super().__init__() # 1. 为这个技能专门初始化一个LLM self.llm = ChatOpenAI(model="gpt-4", temperature=0) # 2. 定义这个微型Agent所需的子工具 self.sub_tools = [ Tool(name="search_attractions", func=self._search_attractions, description="搜索某个城市的景点信息"), Tool(name="check_restaurant", func=self._check_restaurant, description="查询餐厅评价和人均消费"), Tool(name="estimate_transit", func=self._estimate_transit, description="估算两点间的交通时间和方式"), ] # 3. 创建专属的Agent提示词 prompt = PromptTemplate.from_template( """你是一个专业的旅行规划师。请为用户规划在{city}为期{days}天的行程。 用户要求:{user_request} 请使用工具来获取必要信息,并生成一份详细到小时、包含景点、餐饮、交通和预估费用的行程单。 思考过程:{agent_scratchpad}""" ) # 4. 创建并初始化一个Agent执行器 self.agent = create_react_agent(llm=self.llm, tools=self.sub_tools, prompt=prompt) self.agent_executor = AgentExecutor(agent=self.agent, tools=self.sub_tools, verbose=True, handle_parsing_errors=True) def _run(self, city: str, days: int, user_request: str = "") -> str: # 运行这个微型Agent inputs = {"city": city, "days": days, "user_request": user_request} result = self.agent_executor.invoke(inputs) return result["output"] # 子工具的实现(模拟) def _search_attractions(self, query): return f"景点信息:{query}" def _check_restaurant(self, query): return f"餐厅信息:{query}" def _estimate_transit(self, query): return f"交通信息:{query}"

适用场景与挑战

  • 场景:任务本身具有高度的探索性和不确定性,需要多次调用不同工具并基于中间结果进行决策。将这部分复杂性封装在一个Skill内部,对外提供简洁的接口。
  • 挑战:技能内部的Agent也会消耗Token,增加延迟和成本。需要精心设计提示词和工具集,防止内部Agent陷入死循环或做出低效决策。

3. 技能(Skills)与智能体(Agent)的协同:四种集成模式解析

创建了Skills之后,如何让主Agent智能地调用它们?这里我总结了四种常见的集成模式,它们对应着不同的控制粒度与灵活性需求。

3.1 模式一:直接注入工具列表(最常用)

这是最经典的模式。将所有的Skills实例化后,直接作为tools参数列表提供给create_react_agent或类似的Agent创建函数。

from langchain.agents import create_react_agent, AgentExecutor from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI # 假设我们已经有了几个技能实例 weather_skill = GetWeatherSkill() analysis_skill = DataAnalysisSkill() travel_skill = TravelPlanningSkill() all_skills = [weather_skill, analysis_skill, travel_skill] llm = ChatOpenAI(model="gpt-4", temperature=0) prompt = PromptTemplate.from_template("...你的提示词...") agent = create_react_agent(llm=llm, tools=all_skills, prompt=prompt) agent_executor = AgentExecutor(agent=agent, tools=all_skills, verbose=True) # 现在,Agent可以根据用户问题,自主选择调用哪个技能 result = agent_executor.invoke({"input": "帮我看看北京明天天气怎么样?"})

经验之谈

  • 技能描述(description)是导航图:Agent完全依赖每个Skill的namedescription来决定调用谁。因此,描述的独特性至关重要。避免出现“处理数据”、“获取信息”这种模糊描述。
  • 数量控制:通常,一个Agent同时管理的Skills数量不宜超过10-15个。过多会导致LLM选择困难,性能下降。可以考虑按功能域拆分出多个专门的Agent。

3.2 模式二:基于路由(Router)的技能分发

当技能数量众多或功能有重叠时,可以引入一个“路由Agent”作为调度层。用户请求先到达路由Agent,由它决定将任务分发给哪个专用的“技能Agent”。

用户 -> 路由Agent -> (判断) -> 天气Skill Agent -> 执行 -> 结果返回给用户 -> (判断) -> 旅行Skill Agent -> 执行 -> (判断) -> 数据分析Skill Agent -> 执行

在LangChain中,这可以通过MultiRouteChain或利用LLMRouterChain配合ConversationChain来实现。更现代、更强大的方式是使用LangGraph来显式地构建这种有状态的工作流。在LangGraph中,你可以定义一个Router节点,根据LLM的判断,将状态(State)指向不同的技能处理分支。

优势:架构清晰,职责分离,每个技能Agent可以有自己的优化提示词和工具集,易于维护和扩展。

3.3 模式三:技能的动态加载与注册

在某些场景下,我们可能不希望一次性加载所有技能,而是根据上下文、用户身份或会话阶段动态地添加或移除技能。这需要维护一个中央的技能注册表。

class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill: BaseTool): if skill.name in self._skills: raise ValueError(f"Skill '{skill.name}' already registered.") self._skills[skill.name] = skill def get_skill(self, name: str) -> Optional[BaseTool]: return self._skills.get(name) def get_skills_for_context(self, user_context: Dict) -> List[BaseTool]: # 基于用户上下文(如角色、权限、当前任务)过滤技能 available_skills = [] for skill in self._skills.values(): if self._is_skill_relevant(skill, user_context): available_skills.append(skill) return available_skills def _is_skill_relevant(self, skill, context): # 实现你的业务逻辑,例如检查用户权限标签是否匹配技能所需标签 return True # 使用 registry = SkillRegistry() registry.register(weather_skill) registry.register(analysis_skill) # 当新会话开始时,根据用户信息获取可用技能 user_ctx = {"role": "analyst", "project": "sales"} available_tools = registry.get_skills_for_context(user_ctx) # 然后用 available_tools 初始化Agent

这种模式在构建企业级、多租户的Agent平台时非常有用。

3.4 模式四:技能作为LangGraph中的功能节点

这是目前构建复杂、稳定Agent系统最推荐的方式。在LangGraph中,每个Skill可以被建模为一个独立的节点(Node),而Agent(或LLM)则作为调度中心

from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator # 1. 定义状态结构 class AgentState(TypedDict): messages: Annotated[list, operator.add] # 对话历史 next: str # 下一步该执行哪个节点 skill_result: str # 技能执行结果 # 2. 定义技能节点函数 def call_weather_skill(state: AgentState): # 从state中解析出参数,例如从最新一条用户消息中提取城市 last_message = state[“messages”][-1] city = extract_city(last_message.content) # 假设的解析函数 result = weather_skill.run(city) return {"skill_result": result, "next": "process_result"} def call_travel_skill(state: AgentState): # ...类似逻辑 pass # 3. 定义路由节点(LLM决策) def router(state: AgentState): llm_decision = llm.invoke(f"根据对话历史决定下一步:{state['messages']}。可选动作:weather, travel, end。") if "weather" in llm_decision.content.lower(): return "weather_node" elif "travel" in llm_decision.content.lower(): return "travel_node" else: return "end" # 4. 构建图 workflow = StateGraph(AgentState) workflow.add_node("router", router) workflow.add_node("weather_node", call_weather_skill) workflow.add_node("travel_node", call_travel_skill) workflow.add_node("process_result", process_result_node) # 处理技能结果并生成回复的节点 workflow.set_entry_point("router") workflow.add_conditional_edges("router", router) # 根据router的输出决定下一个节点 workflow.add_edge("weather_node", "process_result") workflow.add_edge("travel_node", "process_result") workflow.add_edge("process_result", END) app = workflow.compile()

这种模式的优势是革命性的

  • 显式控制流:整个Agent的决策和执行流程一目了然,不再是LLM内部的“黑箱”。
  • 持久化状态State可以持久化,轻松实现长对话、复杂任务的中断与恢复。
  • 错误处理与重试:可以轻松地在图中添加错误处理节点,当技能调用失败时,路由到重试或降级逻辑。
  • 并行与异步:可以设计让多个技能节点并行执行,极大提升效率。

4. 实战避坑指南:提升Skill可靠性的五个关键细节

在真实项目中,让Skills稳定工作远比跑通一个Demo复杂。下面是我从多个踩坑项目中总结出的核心经验。

4.1 细节一:为技能设计“防御性”的输入输出Schema

Pydantic Schema不仅是类型声明,更是与LLM沟通的契约。一个健壮的Schema能极大减少调用错误。

反面教材

class PoorInput(BaseModel): query: str

问题query太模糊,LLM可能传入“北京明天天气”,也可能传入“帮我查下天气”。技能内部需要做复杂的字符串解析,极易出错。

最佳实践

class RobustWeatherInput(BaseModel): location: str = Field(description="具体的城市或区县名称,例如:北京市海淀区") date: Optional[str] = Field(default="今天", description="查询日期,格式为‘今天’、‘明天’或‘YYYY-MM-DD’") need_detail: bool = Field(default=False, description="是否需要湿度、风速等详细信息") @validator('date') def validate_date(cls, v): # 添加自定义验证逻辑,确保日期格式有效或转换为标准格式 if v not in ["今天", "明天"]: try: datetime.strptime(v, "%Y-%m-%d") except ValueError: raise ValueError("日期格式错误,请使用‘今天’、‘明天’或‘YYYY-MM-DD’格式") return v

这样做的好处

  1. 字段语义清晰:LLM能准确理解每个参数要填什么。
  2. 提供默认值:降低LLM的思考负担。
  3. 内置验证:在参数传入技能逻辑前就拦截非法值,返回清晰的错误信息给LLM,让它能重新调整输入。

4.2 细节二:实现技能的“优雅降级”与重试机制

网络调用、第三方API不稳定是常态。技能内部必须有容错设计。

def _run_with_retry(self, api_func, *args, max_retries=2, **kwargs): """带重试的调用封装""" last_exception = None for attempt in range(max_retries + 1): try: return api_func(*args, **kwargs) except (TimeoutError, ConnectionError) as e: last_exception = e if attempt < max_retries: time.sleep(1 * (attempt + 1)) # 指数退避 continue # 所有重试都失败,执行降级逻辑 return self._fallback_logic(*args, **kwargs) def _fallback_logic(self, city): """降级逻辑:返回缓存数据、估算数据或友好的错误提示""" # 例如:从本地缓存文件读取最近一次的成功数据 # 或者:返回一个基于历史数据的估算值,并明确告知用户“当前服务不稳定,以下是预估数据” return f"暂时无法获取{city}的实时天气。根据历史数据,此时节平均气温约为15-25度。"

在LangGraph中,你甚至可以将这个重试和降级逻辑建模为图中的一个独立节点,形成调用技能 -> 失败? -> 重试节点/降级节点的清晰流程。

4.3 细节三:为技能添加可观测性(Observability)

技能上线后,你需要知道它被调用的频率、成功率、耗时。简单的打印日志是不够的。

import time from functools import wraps import statsd # 或使用OpenTelemetry def observe_skill(func): """一个简单的技能执行观测装饰器""" @wraps(func) def wrapper(self, *args, **kwargs): skill_name = self.name start_time = time.time() try: result = func(self, *args, **kwargs) duration = (time.time() - start_time) * 1000 # 毫秒 # 发送指标到监控系统 statsd_client.timing(f"skill.{skill_name}.duration", duration) statsd_client.incr(f"skill.{skill_name}.success") return result except Exception as e: statsd_client.incr(f"skill.{skill_name}.error") # 可以记录详细的错误信息和参数(注意脱敏) logger.error(f"Skill {skill_name} failed with args {args}, error: {e}") raise # 或者返回降级结果 return wrapper # 在技能的_run方法上使用装饰器 class MySkill(BaseTool): @observe_skill def _run(self, ...): ...

这样,你就能在Grafana等看板上清晰地看到每个技能的健康状况,快速定位瓶颈。

4.4 细节四:管理技能间的依赖与冲突

当多个技能需要共享资源(如数据库连接、API客户端)或存在执行顺序依赖时,需要妥善管理。

  • 共享依赖注入:不要在技能内部硬编码创建资源客户端。应该在初始化技能时,通过构造函数注入。
    class DBAnalysisSkill(BaseTool): def __init__(self, db_client): super().__init__() self.db_client = db_client # 从外部传入共享的数据库客户端 ...
  • 技能执行副作用:如果一个技能会修改某个共享状态(如向一个公共任务队列添加作业),必须在描述中清晰说明,并考虑在LangGraph中用状态管理来协调,避免并发冲突。

4.5 细节五:技能的版本化与热更新

对于线上服务,技能的迭代更新是常态。你需要考虑:

  1. 版本标识:在技能类中添加version属性。
  2. 向后兼容:更新输入输出Schema时,尽量保证旧版调用方式仍能工作一段时间。
  3. 热更新策略:通过技能注册表(Skill Registry)动态替换技能实例,而无需重启整个Agent服务。新的用户会话将使用新技能,而进行中的会话可能继续使用旧技能实例直至结束。

5. 从Skills到智能工作流:LangGraph的降维打击

最后,我想强调,当你熟练掌握了Skills的构建后,LangGraph是你将能力提升到下一个层次的必然选择。它让你从“思考如何让LLM调用工具”转变为“设计一个智能的工作流”。

在LangGraph的范式下:

  • Skill就是一个Node:每个技能被封装成一个纯净的函数节点,只负责计算。
  • LLM也是一个Node:专门负责决策(路由)和生成自然语言回复。
  • 状态(State)是共享内存:所有节点通过修改和读取State来协作。
  • 边(Edges)是控制流:清晰地定义了工作流的走向,包括条件分支、循环和并行。

以前用LangChain Agent难以实现的复杂逻辑,比如“先执行A技能,如果结果满足条件X则执行B,否则执行C,最后汇总结果生成报告”,在LangGraph中可以通过构图直观地实现。这大大降低了心智负担,也让整个系统的可调试性和可维护性得到了质的飞跃。

我个人的实践路径是:先从封装好单个Tool开始,然后用Runnable链组合成复杂Skill,最后将这些Skill作为节点,用LangGraph编织成强大的智能工作流。这个过程中,对Skill的良好抽象和设计,是最终构建出稳定、高效Agent系统的基石。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询