在技​​能模式(Skills)中,专门化的能力被打包成可调用的“技能”,以增强Agent的行为。技能主要是由提示驱动的专业化功能,Agent可以按需调用这些功能。关键Skills的详细说明,请参阅Anthropic的官方文档“[Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)”。Skills模式具有如下的核心特征:

  • 提示驱动的专业化:技能主要由特定提示定义;
  • 渐进式披露:根据上下文或用户需求按序加载可用Skill。在此基础上更进一步,还包括根据Skill动态注册工具集;
  • 团队分布:不同团队可以独立开发和维护技能;
  • 轻量级构建:技能比完整的Sub-Agent更简单;
  • 引用感知:技能可以引用脚本、模板和其他资源。虽然每个技能只有一个提示,但该提示可以引用其他资源的位置,并提供Agent何时应使用这些资源的信息。当

这些资源变得相关时,Agent会知道这些文件的存在,并根据需要将它们读入内存以完成任务。这也遵循渐进式披露模式,并限制上下文窗口中的信息量。

当需要一个具备多种专业技能的单一Agent,且无需对技能之间施加特定约束,或者不同团队需要独立开发相应能力时,可以使用Skills模式。常见的例子包括编码助手(针对不同语言或任务的技能)、知识库(针对不同领域的技能)和创意助手(针对不同格式的技能)。我们已经尝试了三种模式(Sub-AgentRouterHandoffs)来开发我们的差旅助手Agent,现在我们继续将它改写成Skills模式。虽然“Deep Agents”提供了针对Skills的原生支持,但是为了让大家对Skills的实现原理有更清晰的认知,我决定使用一个更笨的解决方案来改写我们:使用自定义工具加载Skill。

1. 定义加载Skill的工具

一个Skill由元数据和主体内容组成。以名称和描述为核心的元数据会全程绑定到Agent上,所以我们要保证命名准确,描述精炼,以免占据过多的上下文窗口。主体内容相当于一份用于指导Agent工作的说明书,一般采用Markdown格式编写。为此我们定义了如下这个Skill类,并创建了用于购买机票和酒店预订的Skill,并将其保存到全局字典all_skills中(Key为Skill的名称)。

class Skill(TypedDict):
    name:str
    description:str
    content:str

buy_airplane_ticket_skill = """\
## 基本流程

- 确定当前是否注册了`buy_airplane_ticket`工具,如果没有注册该工具,拒绝执行并回复用户:抱歉,我无法购买机票,因为相关工具未注册。
- 调用`buy_airplane_ticket`工具购买机票,最后将工具返回的预订信息整理后返回给用户。

## 注意事项

- 购买机票是唯一的任务
- 调用`buy_airplane_ticket`工具是购买机票唯一的方式
- 只需要根据`buy_airplane_ticket`工具的Schema来分析购买机票的信息是否充分
- 可以完全自主决定航司、舱位等级和航班等信息,不需用户确认
"""

book_hotel_skill ="""\
## 基本流程

- 确定当前是否注册了`book_hotel`工具,如果没有注册该工具,拒绝执行并回复用户:抱歉,我无法预订酒店,因为相关工具未注册。
- 调用`book_hotel`工具预订酒店,最后将工具返回的预订信息整理后返回给用户。

## 注意事项

- 预订酒店是唯一的任务
- 调用`book_hotel`工具是预订酒店唯一的方式
- 只需要根据`book_hotel`工具的Schema来分析预订酒店的信息是否充分
- 可以完全自主决定酒店、价位和房型等信息,不需用户确认
"""

all_skills = [
    {
        "name":"buy-airplane-ticket",
        "description":"购买机票,只有在用户明确提出要求购买机票时才会使用",
        "content": buy_airplane_ticket_skill
    },
    {
        "name":"book-hotel",
        "description":"预订酒店,只有在用户明确提出要求预订酒店时才会使用",
        "content": book_hotel_skill
    },
]

为了跟踪当前加载的Skill,我们在状态类型State中添加了loaded_skills字段,并利用自定义的reducer函数添加加载的Skill名称。在推理过程中,LLM会根据推理任务和预先加载的Skill元数据决定所需的Skill,并作针对性的加载。如下这个load_skill函数就是我们为它准备的Skill加载工具。如果成功加载,返回的Command会通过修改loaded_skills通道将建在Skill名称添加到状态中。

class State(AgentState):
    loaded_skills:Annotated[set[str], lambda x,y: x.union(y)]

@tool
def load_skill(name:str, runtime:ToolRuntime) -> Command|str:
    """根据指定的技能名称加载技能详细内容"""
    for skill in all_skills:
        if skill["name"] == name:
            return Command(
                update={
                    "messages":[ToolMessage(content=skill["content"], tool_call_id=runtime.tool_call_id)],
                    "loaded_skills": {name},
                }
            )
        
    return f"抱歉,未找到名为{name}的技能。"

2. 定义Skill中间件

如下定义的SkillMiddleware中间件旨在完成两项任务:一、将所有可用Skill的元数据给格式化成员系统提示词的一部分;二、根据当前加载的Skill提供对应的工具集,这两项工作都是利用awrap_model_call方法针对模型调用的拦截实现的。

async def main():
    client = MultiServerMCPClient(
        connections= {
            "server": {
                "transport": "stdio",
                "command": "python",
                "args": ["server.py"]
            }
        })
    tools = {tool.name:tool for tool in await client.get_tools(server_name="server")}  
    skill_based_tools:dict[str, list[BaseTool]] = {
        "buy-airplane-ticket": [tools["buy_airplane_ticket"]],
        "book-hotel": [tools["book_hotel"]],
    }
    class SkillMiddleware(AgentMiddleware):
        tools = [load_skill]

        def __init__(self):
            skills = (f" - **{skill['name']}**: {skill['description']}" for skill in all_skills)
            self.skills_prompt = f"""\
    你拥有如下可用Skill:

    {'\n'.join(skills)}

    可以指定Skill名称调用`load_skill`工具来获取指定技能的详细信息。
    """
        async def awrap_model_call(
            self,
            request: ModelRequest,
            handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
        ) -> ModelResponse | AIMessage | ExtendedModelResponse:        
            system_message = request.system_message
            if system_message is None:
                system_message = SystemMessage(content=self.skills_prompt)
            else:
                cotents = system_message.content_blocks + [{"type": "text", "text": self.skills_prompt}]
                system_message = SystemMessage(content_blocks =cotents)
            
            tools = [load_skill]
            loaded_skills = request.state.get("loaded_skills", set())
            for skill in loaded_skills:
                tools.extend(skill_based_tools.get(skill, []))
            return await handler(request.override(system_message=system_message, tools=tools))# type: ignore

3. 创建和测试Agent

如果采用Skills模式,我们主要工作会放在Skill文档的撰写上面,程序员俨然变成一个文字工作者。所以我们直接可以根据提供的工具(load_skill和由MCP服务器提供的buy_airplane_ticketbook_hotel工具)、系统提示词和SkillMiddleware创建我们所需的Agent。最后我们依然采用与前面一样的测试用例(供了四种输入来模拟四种情况:同时包含酒店预订和机票购买需求、只包含酒店预订或者机票购买需求以及不涉及这两种需求)对构建的Agent进行测试:

async def main():
    ...        
    agent = create_agent(
        model=ChatOpenAI(model="gpt-5.2-chat"),
        state_schema=State,
        tools=[load_skill,*tools.values()],
        system_prompt="你是一个差旅助理,请严格按照注册的技能要求来分析和执行用户的请求。如果用户没有明确预订酒店或者购买机票的需求,直接回复:意图不明,无法执行",
        middleware=[SkillMiddleware()])
    
    inputs =[
            "我有明后两天(从上海)去成都的出差,请帮我预订酒店(明天入住,后天离开)和机票(往返,明天出发,后天返回)。",
            "我有明后两天(从上海)去成都的出差,请帮我预订酒店(明天入住,后天离开)。",
            "我有明后两天(从上海)去成都的出差,请帮我预订机票(往返,明天出发,后天返回)。",
            "随便说说!"
            ]
    for message in inputs:
        reuslt = await agent.ainvoke(input= {"messages": [HumanMessage(content=message)]}) # type: ignore
        print(f"用户输入:{message}")
        print(f"系统回复:{reuslt['messages'][-1].text}\n\n")

如下所示的是四段输出:

用户输入:我有明后两天(从上海)去成都的出差,请帮我预订酒店(明天入住,后天离开)和机票(往返,明天出发,后天返回)。
系统回复:好的,已为您完成全部预订,安排如下:


✈️ **机票预订**
- **去程**:上海 → 成都  
  出发日期:2026-04-18  
  航班号:MU5401  
- **返程**:成都 → 上海  
  出发日期:2026-04-19  
  航班号:MU5402  

🏨 **酒店预订**
- 城市:成都  
- 酒店:如家酒店  
- 入住时间:2026-04-18  
- 退房时间:2026-04-19  

如需调整航班时间、酒店档次或增加其他出行安排,请随时告诉我。祝您出差顺利!
用户输入:我有明后两天(从上海)去成都的出差,请帮我预订酒店(明天入住,后天离开)。
系统回复:✅ 酒店预订成功!

已为您安排好本次成都出差的住宿,具体信息如下:

- **酒店城市**:成都  
- **酒店名称**:如家酒店  
- **入住时间**:2026 年 4 月 18 日(明天)12:00  
- **退房时间**:2026 年 4 月 19 日(后天)12:00  

祝您出差顺利!如果后续还需要我帮您预订机票或调整行程,请随时告诉我。
用户输入:我有明后两天(从上海)去成都的出差,请帮我预订机票(往返,明天出发,后天返回)。
系统回复:✅ 机票已为您成功预订,行程如下:

### ✈️ 去程
- **航线**:上海 → 成都  
- **出发日期**:明天(2026-04-18)  
- **航班号**:MU5401  
- **起飞时间**:12:00  

### ✈️ 返程
- **航线**:成都 → 上海  
- **出发日期**:后天(2026-04-19)  
- **航班号**:MU5402  
- **起飞时间**:12:00  

如需 **预订酒店、调整航班时间或座位等级**,请随时告诉我,祝您出差顺利!
用户输入:随便说说!
系统回复:意图不明,无法执行

针对第一个请求,Agent内部的调用链如下所示。

Alternative Text

4. 完整的实现

如下提供完整的实现:

from typing import Annotated, Awaitable, Callable,TypedDict
from langchain.agents import create_agent, AgentState
from langchain_core.messages import AIMessage
from langchain_openai import ChatOpenAI
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents.middleware import AgentMiddleware, ExtendedModelResponse, ModelResponse, wrap_model_call, ModelRequest
from langchain.tools import ToolRuntime, tool,BaseTool
from langchain_core.messages import SystemMessage,HumanMessage,ToolMessage
from langgraph.types import Command
import asyncio
from dotenv import load_dotenv

load_dotenv()
class Skill(TypedDict):
    name:str
    description:str
    content:str

buy_airplane_ticket_skill = """\
## 基本流程

- 确定当前是否注册了`buy_airplane_ticket`工具,如果没有注册该工具,拒绝执行并回复用户:抱歉,我无法购买机票,因为相关工具未注册。
- 调用`buy_airplane_ticket`工具购买机票,最后将工具返回的预订信息整理后返回给用户。

## 注意事项

- 购买机票是唯一的任务
- 调用`buy_airplane_ticket`工具是购买机票唯一的方式
- 只需要根据`buy_airplane_ticket`工具的Schema来分析购买机票的信息是否充分
- 可以完全自主决定航司、舱位等级和航班等信息,不需用户确认
"""

book_hotel_skill ="""\
## 基本流程

- 确定当前是否注册了`book_hotel`工具,如果没有注册该工具,拒绝执行并回复用户:抱歉,我无法预订酒店,因为相关工具未注册。
- 调用`book_hotel`工具预订酒店,最后将工具返回的预订信息整理后返回给用户。

## 注意事项

- 预订酒店是唯一的任务
- 调用`book_hotel`工具是预订酒店唯一的方式
- 只需要根据`book_hotel`工具的Schema来分析预订酒店的信息是否充分
- 可以完全自主决定酒店、价位和房型等信息,不需用户确认
"""

all_skills = [
    {
        "name":"buy-airplane-ticket",
        "description":"购买机票,只有在用户明确提出要求购买机票时才会使用",
        "content": buy_airplane_ticket_skill
    },
    {
        "name":"book-hotel",
        "description":"预订酒店,只有在用户明确提出要求预订酒店时才会使用",
        "content": book_hotel_skill
    },
]

class State(AgentState):
    loaded_skills:Annotated[set[str], lambda x,y: x.union(y)]

@tool
def load_skill(name:str, runtime:ToolRuntime) -> Command|str:
    """根据指定的技能名称加载技能详细内容"""
    for skill in all_skills:
        if skill["name"] == name:
            return Command(
                update={
                    "messages":[ToolMessage(content=skill["content"], tool_call_id=runtime.tool_call_id)],
                    "loaded_skills": {name},
                }
            )
        
    return f"抱歉,未找到名为{name}的技能。"

async def main():
    client = MultiServerMCPClient(
        connections= {
            "server": {
                "transport": "stdio",
                "command": "python",
                "args": ["server.py"]
            }
        })
    tools = {tool.name:tool for tool in await client.get_tools(server_name="server")}  
    skill_based_tools:dict[str, list[BaseTool]] = {
        "buy-airplane-ticket": [tools["buy_airplane_ticket"]],
        "book-hotel": [tools["book_hotel"]],
    }
    class SkillMiddleware(AgentMiddleware):
        tools = [load_skill]

        def __init__(self):
            skills = (f" - **{skill['name']}**: {skill['description']}" for skill in all_skills)
            self.skills_prompt = f"""\
    你拥有如下可用技能:

    {'\n'.join(skills)}

    可以指定技能名称调用`load_skill`工具来获取指定技能的详细信息。
    """
        async def awrap_model_call(
            self,
            request: ModelRequest,
            handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
        ) -> ModelResponse | AIMessage | ExtendedModelResponse:        
            system_message = request.system_message
            if system_message is None:
                system_message = SystemMessage(content=self.skills_prompt)
            else:
                cotents = system_message.content_blocks + [{"type": "text", "text": self.skills_prompt}]
                system_message = SystemMessage(content_blocks =cotents)
            
            tools = [load_skill]
            loaded_skills = request.state.get("loaded_skills", set())
            for skill in loaded_skills:
                tools.extend(skill_based_tools.get(skill, []))
            return await handler(request.override(system_message=system_message, tools=tools))# type: ignore
        
    agent = create_agent(
        model=ChatOpenAI(model="gpt-5.2-chat"),
        state_schema=State,
        tools=[load_skill,*tools.values()],
        system_prompt="你是一个差旅助理,请严格按照注册的技能要求来分析和执行用户的请求。如果用户没有明确预订酒店或者购买机票的需求,直接回复:意图不明,无法执行",
        middleware=[SkillMiddleware()])
    
    inputs =[
            "我有明后两天(从上海)去成都的出差,请帮我预订酒店(明天入住,后天离开)和机票(往返,明天出发,后天返回)。",
            "我有明后两天(从上海)去成都的出差,请帮我预订酒店(明天入住,后天离开)。",
            "我有明后两天(从上海)去成都的出差,请帮我预订机票(往返,明天出发,后天返回)。",
            "随便说说!"
            ]
    for message in inputs:
        reuslt = await agent.ainvoke(input= {"messages": [HumanMessage(content=message)]}) # type: ignore
        print(f"用户输入:{message}")
        print(f"系统回复:{reuslt['messages'][-1].text}\n\n")

asyncio.run(main())
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐