欢迎光临
我们一直在努力

【万字】带你实现一个Agent(上),从Tools、MCP到Skills实战指南

本文详细介绍如何构建旅游Agent应用,从开发环境配置、API密钥设置入手,深入讲解工具调用(Function Calling)机制原理与实现,解析MCP协议解决工具复用问题,以及Skills概念提升Agent稳定性。通过完整代码示例展示如何使用DeepSeek模型实现具备景点搜索、天气查询、路线规划等功能的智能Agent,为开发者提供从基础到高级的Agent开发全流程指南。


盘点25年最火的两个事件,我会选择DeepSeek发布和Manus发布,市场今年是一看到这类产品就很兴奋,包括最近爆火的Agent变形产品:OpenClaw(Clawdbot→Moltbot)。

今天的话,我们更加务实一些,直接手把手的教大家如何做一个Agent,由此会让大家更加理解Agent的难点到底在哪:

unsetunset一、开发环境配置unsetunset

在开发中,不同项目需要不同版本的 Python(例如 3.10、3.12)。为了方便管理多版本 Python,我们使用 pyenv。

官方地址:https://github.com/pyenv/pyenv

macOS 使用 Homebrew 安装:

brew update
brew install pyenv

或者通过脚本安装:

curl https://pyenv.run | bash

然后就是Linux和Windows 安装了:

curl -fsSL https://pyenv.run | bash

Windows 使用 pyenv-win:
1. 打开 PowerShell(管理员权限)。
2. 执行安装命令:
Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"

安装完成后,运行以下命令查看版本:

pyenv –version

如果显示版本号,则安装成功。以下是一些常见命令:

# 查看可安装的Python版本
pyenv install –list | grep 3.12
# 安装Python 3.12.0
pyenv install 3.12.0
# 查看已安装的版本
pyenv versions
# 设置全局Python版本
pyenv global 3.12.0
# 为当前目录设置Python版本
pyenv local 3.12.0

Python 包管理工具

uv 是一个轻量级的 Python 包管理工具,用于管理虚拟环境和依赖包,类似 pip + venv 的组合。使用 uv 可以方便地为不同项目创建独立的虚拟环境,并管理依赖版本。

官方地址:https://github.com/astral-sh/uv

确保已经安装好 Python(推荐通过 pyenv 管理不同版本):

pyenv install 3.12.0
pyenv global 3.12.0 # 或者在项目目录使用 pyenv local 3.12.0

macOS / Linux 通过官方脚本安装,或者使用 pip 安装::

curl -fsSL https://uv.dev/install.sh | bash
pip install uv-cli

多数同学可能是Windows,这里安装有两个步骤:

  • 打开 PowerShell(推荐管理员权限);
  • 执行命令:
  • pip install uv-cli

    安装完成后,运行以下命令检查版本,如果显示版本号,则说明安装成功:

    uv –version

    常用命令如下:

    # 初始化项目(自动创建虚拟环境)
    uv init
    # 查看当前项目使用的 Python 版本
    uv run python –version
    # 安装依赖包
    uv add requests flask
    uv add openai
    # 卸载依赖包
    uv remove requests
    # 锁定当前环境依赖(生成 uv.lock)
    uv lock
    # 根据锁文件安装依赖
    uv sync

    pyCharm开发工具

    最后是开发工具的安装,官网下载地址 https://www.jetbrains.com/pycharm/download/

    首先我们后面会用到很多第三方服务,这里需要一些Key:

    一、访问 DeepSeek开发者平台 注册登录,创建一个API Key用于大模型调用:

    二、访问高德开放平台官网创建应用并选择Web服务类型,获取的Key将用于查询酒店、景点、路径规划和天气信息,个人开发者有一定的免费调用额度,完全满足日常使用需求:

    三、将获取到的key配置到.env 文件中,或者配置到系统环境变量中:

    echo"DEEPSEEK_API_KEY=youkey" > .env
    echo"AMAP_API_KEY=youkey" >> .env # 注意这里是两个大于号

    这里可以简单测试下:

    # deepseek
    def deepseekDemo():
    print("deepseekDemo,hello")
    client = OpenAI(
    api_key=os.environ.get('DEEPSEEK_API_KEY'),
    base_url="https://api.deepseek.com")

    response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
    {"role": "system", "content": "You are a helpful assistant"},
    {"role": "user", "content": "Hello"},
    ],
    stream=False
    )
    print(response.choices[0].message.content)

    # 高德
    def amap_demo():
    key = os.getenv("AMAP_API_KEY")
    response = httpx.Client().get(
    "https://restapi.amap.com/v3/weather/weatherInfo?key=" + key + "&city=110101&extensions=all")
    print(response.json())
    print(json.dumps(response.json(), indent=2,ensure_ascii=False))

    基础环境OK了,我们就来看Agent的能力基石Function Calling:

    unsetunset二、工具调用unsetunset

    大模型本身只能做文本理解和生成,无法直接访问数据或执行外部逻辑,例如查询天气、搜索景点、计算路线等。

    Tools(函数调用)机制的作用,是由应用侧提供一组可调用的函数,在模型推理过程中,由模型决定是否需要调用这些函数、以及调用哪一个、使用什么参数。

    模型只负责决策,应用程序负责真正执行函数并返回结果:

    Tools 的调用流程

    Tools 的调用并不是一次完成的,而是一个多轮交互过程:

    1.发起第一次模型调用

    应用程序首先向大模型发起一个包含用户问题与模型可调用工具清单的请求。

    2.接收模型的工具调用指令(工具名称与入参)

    若模型判断需要调用外部工具,会返回一个JSON格式的指令,用于告知应用程序需要执行的函数与入参;

    若模型判断无需调用工具,会返回自然语言格式的回复。

    3.在应用端运行工具

    应用程序接收到工具指令后,需要运行工具,获得工具输出结果。

    4.发起第二次模型调用

    获取到工具输出结果后,需添加至模型的上下文(messages),再次发起模型调用。

    5.接收来自模型的最终响应

    模型将工具输出结果和用户问题信息整合,生成自然语言格式的回复。

    为什么要使用 Tools

    在早期没有标准化工具调用能力时,Agent主要依赖提示词驱动。

    开发者需要提前实现所有功能函数,再通过复杂提示词告诉模型有哪些函数、什么时候用、参数怎么传。

    模型通常以自然语言或半结构化文本的形式输出“行动计划”,应用程序还需要解析这些文本,判断要调用哪个函数并手动执行,再把结果拼回上下文。

    这种方式在工程上存在明显问题:

  • 强耦合,函数设计、调用规则、解析逻辑和提示词紧密绑定,一旦业务变化,就需要同时修改代码和提示词;
  • 稳定性不足,模型输出的是自然语言,哪怕格式略有变化,解析逻辑就可能失效,线上风险很高;
  • 提示词复杂且难维护,为了约束模型行为,提示词往往变得又长又重,阅读和维护成本持续上升;
  • 如果不想在提示词里面做上述动作,那么就要在微调侧做投入,只不过当大模型原生支持 Tools(Function Calling)后,依旧是最优解:

    工具调用能力被以标准化,包含明确的名称、功能描述和参数结构。模型不再“描述自己想做什么”,而是直接返回结构化的工具调用请求,应用或框架只需按约定执行对应函数并回传结果。

    总结来说,提示词更适合描述目标和约束,而 Tools 更适合承载真实可执行的能力。当Agent开始具备实际行动能力时,使用 Tools 是唯一选择。

    典型案例:旅行Agent

    旅行规划是最经典的Tools案例,非常适合作为 Tools 的示例场景。

    一个可用的旅行Agent,至少需要具备景点搜索、天气查询、酒店筛选、行程生成和路线规划等能力,这些能力都无法仅依赖模型内部知识完成,必须通过外部接口获取或计算。

    在本示例中,我们先将旅行规划所需的能力拆解为多个独立工具,例如查询目的地景点信息、获取实时天气、根据预算筛选酒店、规划公共交通路线等。每个工具只负责一类明确的能力,返回结构化数据,不参与业务推理。

    所有工具函数统一定义在 tools 目录中,与地图、地点、路线相关的能力通过高德开放平台 API 实现。工具本身只负责数据获取和计算,如何组合这些结果、如何生成最终的旅行方案,由模型在多轮推理中完成。

    当工具准备好之后,还需要一套配套机制:将工具信息转换成模型可识别的 tools 参数;在模型返回工具调用指令时,执行对应的工具函数;并将工具执行结果回传给模型。这部分逻辑构成了旅行Agent与 Tools 之间的桥梁,下面将通过代码进行具体说明。

    程序执行的时序图如下(其中的一种可能,具体的调用需要大模型决策执行) :

    实现思路

    为了构建这个旅行智能体,我们采用了模块化的代码设计,将“能力实现”与“智能决策”解耦。核心包含三个主要部分:

    一、标准化的工具定义

    我们没有为模型专门写一套复杂的配置文件,而是直接利用 Python 原生的语法特性来定义工具。在 TravelTools 类中,每一个方法(如 geocode, search_poi)都遵循以下规范:

  • **类型提示:**明确声明每个参数和返回值的类型,这不仅规范了代码,也为生成 Schema 提供了类型依据。
  • **文档字符串:**这是最关键的部分。我们用自然语言详细描述了“这个函数是做什么的”、“每个参数的具体含义”以及“返回的数据结构”。
  • **异步设计:**涉及网络请求的操作均采用 async/await,确保高并发下的性能。
  • class TravelTools:
    """
    旅游规划助手工具类
    """
    def __init__(self, amap_api_key: Optional[str] = None, request_delay: float = 0.2):
    self.amap_api_key = amap_api_key or os.getenv("AMAP_API_KEY")
    # …
    async def estimate_travel_cost(self, city: str, days: int, hotel_level: str = "舒适",
    attractions: Optional[List[str]] = None) -> Dict[str, Any]:
    """
    估算旅游费用(不含往返交通)
    使用场景:
    – 制定旅游预算
    – 比较不同档次的旅游费用
    – 规划旅游支出
    Args:
    city: 旅游城市名称,如'西安'、'北京'、'上海'
    days: 旅游天数(含当天),如3表示2晚3天
    hotel_level: 住宿档次,可选值:
    – '经济': 150元/晚(快捷酒店)
    – '舒适': 300元/晚(三星级酒店,默认)
    – '豪华': 500元/晚(四星级及以上)
    attractions: 计划游览的景点列表(可选),用于估算门票费用
    如 ['兵马俑', '华清宫', '大雁塔']
    Returns:
    费用估算字典,包含:
    – city: 城市名称
    – days: 旅游天数
    – breakdown: 费用明细(住宿、餐饮、交通、门票)
    – total: 总费用
    – tips: 温馨提示
    """
    # … 具体实现逻辑 …
    pass

    这种方式让开发者只需专注于编写标准的 Python 函数,而无需手动编写繁琐的 JSON Schema。

    二、自动化注册与 Schema 生成

    为了连接 Python 代码和大模型,我们实现了一个 ToolRegistry 工具类。它的核心职责是“翻译”和“管理”:

  • 自省与生成:利用 Python 的反射机制,自动读取工具函数的签名和文档,将其动态转换为大模型能够理解的 OpenAI Function Calling 格式(JSON Schema)。
  • 统一执行:提供了一个 execute_tool 方法。当模型发出调用指令时,Registry 负责解析参数,找到对应的 Python 函数执行,并将结果序列化为 JSON 格式返回。
  • @dataclass
    class Tool:
    """工具定义数据模型"""
    name: str # 工具名称/函数名
    description: str # 工具描述(用于大模型理解)
    parameters: Dict[str, Any] # 参数JSON Schema
    function: Callable # 实际的执行函数

    def to_dict(self) -> dict:
    """转换为OpenAI Function Calling格式"""
    return {
    "type": "function",
    "function": {
    "name": self.name,
    "description": self.description,
    "parameters": self.parameters
    }
    }

    class ToolRegistry:
    # …

    def register_from_class(self, cls, …):
    """从类中自动注册异步方法为工具"""
    # 利用 Python 的反射机制(Inspect模块)
    # 自动读取工具函数的签名和文档
    # …
    pass

    async def execute_tool(self, tool_call: ToolCall) -> ToolResult:
    """执行单个工具调用"""
    # …
    pass

    三、ReAct循环

    Agent的运行逻辑是一个经典的 While 循环,模拟了“观察-思考-行动”的过程:

  • **Prompt 构造:**将系统指令、用户问题以及所有可用的工具描述(由 Registry 生成)一同发送给模型。
  • 模型决策:
    • 模型判断是否需要调用工具。如果需要,它会返回一个 tool_calls 结构,包含函数名和参数。
    • 如果不需要,它会直接生成最终回复。
  • **工具执行:**应用程序捕获 tool_calls,通过 Registry 执行具体的函数(如查询天气、搜索路线),获取真实数据。
  • **结果回填:**将工具执行的结果封装为 tool 角色的消息,追加到对话历史中。
  • **递归思考:**带着最新的工具结果,再次请求模型。模型会根据新的信息决定是继续调用下一个工具,还是基于现有信息回答用户问题。
  • # … 初始化 messages …
    while count < 15:
    count = count + 1
    # 发送请求给大模型
    result = await llm_client_with_tools(messages, tool_registry.get_tools())
    assistant_message = result.choices[0].message
    # 检查是否有工具调用
    if assistant_message.tool_calls:
    # 1. 添加 assistant 消息到历史
    messages.append({
    "role": "assistant",
    "content": assistant_message.content,
    "tool_calls": […]
    })
    # 2. 执行所有工具调用
    for tool_call in assistant_message.tool_calls:
    # 执行工具
    tool_result = await tool_registry.execute_tool(…)

    # 3. 添加工具结果到历史
    messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": tool_result.content
    })
    # 继续下一轮循环,将工具结果带给模型
    continue
    else:
    # 没有工具调用,输出最终回复
    print(assistant_message.content)
    break

    通过这三层架构,我们将一个复杂的Agent拆解为:写好函数 -> 自动注册 -> 循环交互 的清晰流程,极大地降低了开发维护成本。

    最后的效果如下:

    在这个基础之下,我们再聊一聊上半年的热门词汇:MCP。

    unsetunset三、MCPunsetunset

    MCP(Model Context Protocol)是一种用于规范大模型与外部能力交互方式的协议。

    它关注的不是某一个具体工具,而是如何以统一、标准的方式,把外部系统的能力、数据和上下文暴露给模型使用。

    如果说 Tools 解决的是“模型如何调用一个函数”,那么 MCP 解决的是“模型如何与一个长期存在、可复用的能力服务交互”。

    在 MCP 体系中,大模型并不直接面对零散的函数,而是通过协议连接到一个 MCP Server。这个 Server 可以对外提供多种能力,例如工具调用、资源读取、上下文查询等,模型通过 MCP Client 与之通信。

    为什么会出现MCP

    随着Agent应用越来越复杂,仅靠应用内定义的 Tools 会暴露出几个问题:

  • **复用困难:**能力通常绑定在单个项目中,跨项目或跨 Agent 使用时需要重复实现。
  • **生命周期不匹配:**Tools 通常随一次调用存在,而很多能力本身是长期运行的服务,例如数据库访问或搜索引擎。
  • **边界和治理难:**随着可调用能力增多,权限、审计和隔离难以统一管理。
  • 事实上在MCP之前,我们也会用各种工程手法去实现,其中一些经典实现已经接近了MCP,只不过自从官方提出来后,我们也就懒得折腾了;

    MCP提供了一套标准化协议,把能力从单个应用中抽离,形成独立、可复用、可治理的服务层。通过 MCP:

  • 能力可以集中管理,统一维护;
  • 多个模型或应用可以共享同一套能力;
  • 明确能力边界,减少重复实现和耦合;
  • 至于要说他与Tools之间的关系,可以说一个是Agent的能力基石、另一个是Tools过多后的工程化管理实践结果:

  • Tools:模型调用具体能力的表达方式,通常是函数级别的实现;
  • MCP:模型访问、管理和调用这些能力的协议和服务层;
  • 在实践中,Tools 往往作为 MCP Server 内部能力的实现存在,但不再直接依赖某个应用。模型通过 MCP Client 调用能力时,只需关注“如何使用”,而不必关心实现细节,从而实现稳定、可复用且可扩展的Agent能力。

    MCP的典型场景如下:

  • 多个 Agent 共享的通用能力(搜索、数据库、业务系统接口);
  • 生命周期较长、需要持续维护的服务;
  • 需要统一权限、日志和审计的能力模块;
  • 跨语言、跨项目复用的工具集合;
  • 接下来,我们来实际用用:

    MCP Server 与 MCP Client

    MCP 包含两个角色:

    • **MCP Server:**对外提供能力和上下文的服务端;
    • **MCP Client:**运行在应用或 Agent 中,负责与 Server 通信;

    Server 负责能力的定义、执行和管理,Client 负责将模型的请求转换为 MCP 协议请求,并将返回结果交给模型:

    MCP案例

    依旧以之前的旅游Agent为例,MCP Server 端代码示例:

    from typing import Dict, List, Optional, Any, Annotated
    from pydantic import Field
    from fastmcp import FastMCP
    # 导入TravelTools类
    from code.Function_Calling.tools import TravelTools
    mcp = FastMCP(name="旅游规划助手")
    # 初始化TravelTools实例
    travel_tools = TravelTools()
    @mcp.tool("get_current_weather", description="获取当前天气信息")
    async def get_current_weather(
    city: Annotated[str, Field(description="城市名称,如'西安'")],
    province: Annotated[str, Field(description="省份名称,如'陕西'")]
    ) -> Dict[str, Any]:
    try:
    weather = await travel_tools.get_weather(city, province)
    return weather.to_dict()
    except Exception as e:
    return {"error": str(e)}

    @mcp.tool("geocode", description="地理编码:将地址转换为经纬度坐标")
    async def geocode(
    address: Annotated[str, Field(description="地址或地点名称,如'兵马俑'、'大雁塔'、'西安市钟楼'")],
    city: Annotated[str, Field(description="城市名称(可选),用于限定搜索范围,如'西安'、'北京'")] = ""
    ) -> Dict[str, Any]:
    try:
    location = await travel_tools.geocode(address, city)
    return location.to_dict()
    except Exception as e:
    return {"error": str(e)}

    @mcp.tool("search_poi", description="搜索兴趣点(POI) – 关键词搜索")
    async def search_poi(
    keywords: Annotated[str, Field(description="搜索关键词,如'兵马俑'、'回民街'、'火锅'、'快捷酒店'")],
    city: Annotated[str, Field(description="城市名称,如'西安'、'北京'、'上海'")],
    types: Annotated[str, Field(description="POI类型筛选(可选),可选值:'景点'、'美食'、'酒店'、'交通'")] = "",
    page_size: Annotated[int, Field(description="返回结果数量,默认10,最大25")] = 10
    ) -> List[Dict[str, Any]]:
    try:
    pois = await travel_tools.search_poi(keywords, city, types, page_size)
    return [poi.to_dict() for poi in pois]
    except Exception as e:
    return [{"error": str(e)}]

    @mcp.tool("search_nearby", description="搜索周边POI – 基于位置的周边搜索")
    async def search_nearby(
    location: Annotated[str, Field(description="中心点坐标,格式为'经度,纬度',如'109.273528,34.384926'")],
    keywords: Annotated[str, Field(description="搜索关键词(可选),如'餐厅'、'便利店'、'ATM'")] = "",
    types: Annotated[str, Field(description="POI类型筛选(可选),可选值:'景点'、'美食'、'酒店'")] = "",
    radius: Annotated[int, Field(description="搜索半径,单位:米,默认1000米(1公里)")] = 1000,
    page_size: Annotated[int, Field(description="返回结果数量,默认5,最大25")] = 5
    ) -> List[Dict[str, Any]]:
    try:
    pois = await travel_tools.search_nearby(location, keywords, types, radius, page_size)
    return [poi.to_dict() for poi in pois]
    except Exception as e:
    return [{"error": str(e)}]

    @mcp.tool("route_planning", description="路线规划 – 多种出行方式的路线规划")
    async def route_planning(
    origin: Annotated[str, Field(description="起点坐标,格式为'经度,纬度'")],
    destination: Annotated[str, Field(description="终点坐标,格式为'经度,纬度'")],
    mode: Annotated[
    str, Field(description="出行方式,可选值:'walking'(步行)、'driving'(驾车)、'transit'(公交/地铁)")] = "transit",
    city: Annotated[str, Field(description="城市名称,公交路线规划时必填,如'西安'、'北京'")] = "",
    origin_name: Annotated[str, Field(description="起点名称(可选),用于公交方案显示")] = "",
    destination_name: Annotated[str, Field(description="终点名称(可选),用于公交方案显示")] = ""
    ) -> Dict[str, Any]:
    try:
    route = await travel_tools.route_planning(origin, destination, mode, city, origin_name, destination_name)
    return route
    except Exception as e:
    return {"error": str(e)}

    @mcp.tool("get_weather_forecast", description="查询城市天气预报(未来3-4天)")
    async def get_weather_forecast(
    city: Annotated[str, Field(description="城市名称或城市编码(adcode),如'西安'、'110000'")],
    province: Annotated[str, Field(description="省份名称,如'陕西'")],
    days: Annotated[int, Field(description="预报天数,默认4天(包含当天)")] = 4
    ) -> Dict[str, Any]:
    try:
    forecast = await travel_tools.get_weather_forecast(city, province, days)
    return forecast
    except Exception as e:
    return {"error": str(e)}

    @mcp.tool("estimate_travel_cost", description="估算旅游费用(不含往返交通)")
    def estimate_travel_cost(
    city: Annotated[str, Field(description="旅游城市名称,如'西安'、'北京'、'上海'")],
    days: Annotated[int, Field(description="旅游天数(含当天),如3表示2晚3天'")],
    hotel_level: Annotated[
    str, Field(description="住宿档次,可选值:'经济'(150元/晚)、'舒适'(300元/晚)、'豪华'(500元/晚)")] = "舒适",
    attractions: Annotated[
    Optional[List[str]], Field(description="计划游览的景点列表(可选),用于估算门票费用")] = None
    ) -> Dict[str, Any]:
    try:
    cost = travel_tools.estimate_travel_cost(city, days, hotel_level, attractions)
    return cost
    except Exception as e:
    return {"error": str(e)}

    @mcp.tool("get_attraction_info", description="获取景点详细信息")
    async def get_attraction_info(
    attraction_name: Annotated[str, Field(description="景点名称,如'兵马俑'、'华清宫'、'大雁塔'")],
    city: Annotated[str, Field(description="城市名称,如'西安'、'北京'")]
    ) -> Dict[str, Any]:
    try:
    info = await travel_tools.get_attraction_info(attraction_name, city)
    return info
    except Exception as e:
    return {"error": str(e)}

    if __name__ == '__main__':
    mcp.run(transport="http", port=8001)

    MCP Client 端代码示例:

    import asyncio
    import json
    import os
    from openai import OpenAI
    from fastmcp import Client
    from code.Working_with_LLMs.llm_client import llm_client_with_tools

    # 系统提示词
    SYSTEM_PROMPT = """你是一个专业的旅游规划助手。当用户提出旅游规划需求时,请:
    1. 理解需求:确认目的地、天数、预算、出行人数、特殊偏好
    2. 使用工具搜索:
    – 使用 search_poi 搜索热门景点
    – 使用 get_attraction_info 获取景点详情
    – 使用 estimate_travel_cost 估算费用
    – 使用 route_planning 规划路线
    3. 输出格式:
    按天输出详细行程,每天包含:
    – 上午/下午/晚上的景点安排
    – 餐饮推荐
    – 景点间交通方式和时间
    – 当日预估花费
    4. 费用把控:根据用户预算合理分配费用
    5. 贴心提醒:提供穿着建议、必带物品、注意事项

    请确保使用工具获取最新信息,并给出具体的行程安排。"""

    async def chat_with_mcp(user_input: str):
    client = OpenAI(api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com")

    # 连接到你的服务器
    mcpClient = Client("http://127.0.0.1:8001/mcp") # 如果是HTTP服务器,这里放URL
    await mcpClient.__aenter__()
    # 列出可用工具
    mcp_tools = await mcpClient.list_tools()
    print("可用工具:", mcp_tools)
    llm_tools = []
    for tool in mcp_tools:
    llm_tools.append({
    "type": "function",
    "function": {
    "name": tool.name,
    "description": tool.description,
    "parameters": tool.inputSchema,
    },
    })
    # 加载工具
    # 第一次调用,让模型自己判断是否使用工具
    messages = [{"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": user_input}]

    count = 0
    while count < 15:
    count = count + 1

    result = await llm_client_with_tools(messages, llm_tools)
    print(result)
    assistant_message = result.choices[0].message
    content = assistant_message.content or ""
    # 检查是否有工具调用
    if assistant_message.tool_calls:
    # 构建tool_calls记录
    tool_calls_data = [
    {
    "id": tc.id,
    "type": tc.type,
    "function": {
    "name": tc.function.name,
    "arguments": tc.function.arguments
    }
    } for tc in assistant_message.tool_calls
    ]

    # 添加到消息历史
    messages.append({
    "role": "assistant",
    "content": content,
    "tool_calls": tool_calls_data
    })

    # 执行所有工具调用
    for tool_call in assistant_message.tool_calls:
    tool_name = tool_call.function.name
    print("正在执行工具:", tool_name)
    try:
    arguments = json.loads(tool_call.function.arguments)
    print("工具参数:", arguments)
    except json.JSONDecodeError:
    arguments = {}

    # 执行工具
    tool_result = await mcpClient.call_tool(tool_name, arguments)
    print("工具结果:", tool_result)
    # 添加工具结果
    messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": json.dumps(tool_result.structured_content, ensure_ascii=False)
    })

    continue
    else:
    count = 15
    print(content)
    if __name__ == '__main__':
    asyncio.run(chat_with_mcp("西安2日游"))

    unsetunset四、Skillsunsetunset

    虽然MCP解决了多Agent对Tools的调用解耦,但Agent的老大难问题依旧没被解决,核心就两点:

  • Tools多了,调用不准;
  • 在执行过程中,无论模型怎么循环依旧不稳定的问题;
  • 在这个基础下,我们依旧有很多的工程手段去做优化:

  • 首先是按需加载Tools,先做用户输入的问题识别,再加载Tools;
  • 其次是在提示词里面写类似Workflow的代码;
  • 只不过这个工程优化点与MCP一样,很快被官方以标准化的方式做掉了:Anthropic 官方文档给出了 Skills 的解决方案,他包含三个层级,从抽象到具体:

  • **元数据:**Skill 的名称、描述、标签等信息;
  • **指令:**Skill 具体的指令;
  • **资源:**Skill 附带的相关资源(比如文件、可执行代码等);
  • 虽然Skills首先由Claude推出,但现在几乎成为了事实上的标准:

    深度解析

    Claude Skills 设计遵循了一个非常重要的原则:Progressive Disclosure,渐进式批露;

    分阶段、按需加载信息,而不是在任务开始时就将所有内容全部塞入上下文窗口中。整个加载过程分为三个层次,对应上面的三要素:

    第一层:元数据(始终加载)

    Claude 在启动时会扫描所有已安装的 Skills,并加载这些元数据,将其纳入系统提示(System Prompt)中。作用:

  • 让 Claude 知道“自己拥有哪些技能”;
  • 用于后续的意图匹配和技能触发判断;
  • 不包含具体执行逻辑,占用上下文极小;

  • name: douyin-summary
    description: 抖音视频总结助手。当用户提供抖音(douyin.com 或 v.douyin.com)视频链接并请求总结、获取文案或了解视频内容时,使用此技能。通过调用 Coze API 工作流获取视频的转录文本或文案,然后为用户提供智能化的内容总结。

    第二层:核心指令(触发时加载)

    当用户的请求与某个 skill 的描述相匹配时,Claude 会通过 bash 从文件系统中读取对应的 SKILL.md 文件,并将其完整内容加载进当前对话上下文:

  • 为 Claude 提供清晰、可复用的任务执行逻辑
  • 将“反复解释的 Prompt”固化为稳定的能力指令
  • # 抖音视频总结助手
    此技能用于获取和总结抖音视频的内容。
    ## 工作流程
    当用户提供抖音链接时:
    1. **识别抖音链接**: 检测用户输入中的 douyin.com 或 v.douyin.com 链接
    2. **调用脚本获取内容**: 使用 `scripts/fetch_douyin.py` 获取视频转录/文案
    python3 ./scripts/fetch_douyin.py <url>
    3. **总结内容**: 基于获取的文本内容,提取核心观点、关键信息或有趣之处
    4. **友好输出**: 以简洁易懂的方式呈现给用户

    第三层:代码与资源(按需加载)

    一个复杂的 skill 可能包含多个文件,形成一个完整的知识库。skill 可以将这些资源与指令一起打包,实现完整的任务闭环。

    通过 元数据 → 指令 → 代码与资源 这三层结构,一个 skill 不仅能被 Claude 正确识别和触发,还能真正完成从“理解需求”到“执行任务”的完整闭环:

    └── skill-name/ # 技能根目录
    ├── meta.json # [路由层] 告诉模型这个技能是干嘛的
    ├── skill.md # [逻辑层] 包含了 System Prompt 和 SOP
    └── scripts/ # [执行层] 实际干活的 Python/Bash 脚本

    接下来给个具体的案例:

    Skills的安装与使用

    在Claude Code中,你可以通过以下两种方式使用Skills:

    方法一:使用官方技能市场(推荐)

    # 添加官方技能库
    /plugin marketplace add anthropics/skills
    # 浏览可用技能
    /plugin list
    # 安装文档处理技能
    /plugin install document-skills@anthropic-agent-skills

    安装完成后,你可以直接询问Claude有哪些技能:

    方法二:手动创建自定义技能

    如果你想让自己定义一个skills,比如“自动总结抖音视频”,你可以自己编写一个 Skill。 原理非常简单:

  • 在用户根目录下的 ~/.claude/skills/ 中创建一个新文件夹,比如 douyin-summary。
  • 编写指令:在文件夹内创建一个 SKILL.md,告诉 Claude 这个技能是干嘛的、怎么用。
  • 准备工具:(可选) 放入 Python 脚本或其他辅助文件。
  • 目录结构看起来是这样的:

    1.创建技能目录:

    mkdir -p ~/.claude/skills/douyin-summary

    1.编写SKILL.md:


    name: douyin-summary
    description: 抖音视频总结助手。当用户提供抖音视频链接时,自动调用此技能获取文案并总结。

    # 抖音视频总结助手
    ## 工作流程
    1. 识别用户输入中的douyin.com链接
    2. 调用scripts/fetch_douyin.py获取视频文案
    3. 提取核心观点并结构化输出

    3.实际使用:

    配置好后,可以查看技能是否安装成功。老版本的Claude code 需要重启才能看到新安装的技能,新版本已经不需要重启了:

    使用Claude Skills提取抖音视频内容的效果如下图所示:

    核心理念

    **Skills 是一种模块化、可复用的能力单元。**它不仅仅包含“能做什么”(Function Call),还包含“怎么做”(Prompt/SOP)以及“用什么做”(Code/Resources),其核心为:

  • **按需加载:**只有用到时才加载相关上下文,节省 Token。
  • **结构化定义:**把 Prompt、代码、配置像乐高积木一样封装在一起。
  • **关注流程:**让模型关注任务流程,而不仅仅是底层 API 调用。
  • 在 Claude 的定义中,skill 表现为一个标准的文件系统目录,包含了描述、指令和执行脚本:

    Skills与Function Calling

    很多同学容易混淆这两个概念。我们可以通过一个对比表来看清它们的本质区别:

    特性Function CallingSkills
    定位 原子能力 任务模块
    构成 仅包含函数定义(JSON Schema) 包含 Prompt + 代码 + 流程定义
    关注点 我可以调用这个 API 我知道如何完成这项工作
    上下文 无状态,通常一次性调用 有状态,通过 Prompt 引导多步推理
    复用性 代码级复用 业务逻辑级复用

    一句话总结:Tools 是能力的低层接口(API);Skills 是任务的高级模板(SOP + Tools)。

    一个 skill 内部通常会调用一个或多个 Tools 来完成具体工作,但它更强调流程编排和知识注入。

    优势说明

    Skills 最初是由 Anthropic 在 Claude 生态中形式化的概念,但它所代表的**“能力即文件”**(Capability as Files)的设计思想,已经超越了特定平台的限制,成为构建复杂 Agent 的一种通用最佳实践。

    所以,各个基模平台都在跟进,这里的原因是:

    一、动态上下文管理

    传统的 Agent 开发往往将所有能力的 Prompt 一次性写入 System Prompt,这会导致:

  • **上下文窗口浪费:**无关的指令占用了大量 Token。
  • **注意力分散:**模型容易在大量指令中迷失,导致遵循指令能力下降。
  • Skills 模式的核心在于按需加载:只有当模型判定需要处理特定任务(如“查询天气”)时,系统才会读取对应的 skill.md 并注入上下文。

    这种渐进式披露机制让 Agent 能以较小的 Context 承载近乎无限的能力库。

    只不过这里的说法依旧有些夸张,当Tools基数达到一个量时候,又会有skill过多的问题,这时候可能会产生基于Skills的“Skills技术”。

    二、标准化的能力封装

    skill是三位一体(Meta + Prompt + Code)的结构,让 Agent 的能力变得像代码库一样,可以被:

  • 版本控制(Git 管理);
  • 独立测试(单独运行 Skill);
  • 社区共享(直接 Copy 文件夹即可使用);
  • 三、模型无关性

    Skills 本质上只是一种文件结构约定。

    只要我们编写代码去解析这些文件,并根据 meta.json 的描述与大模型交互,就可以让 DeepSeek、OpenAI 或任何具备 Tool Calling 能力的模型 拥有“加载 Skills”的能力。

    这也是说其他模型都在跟进这种工程优化能力的原因,其实他们不跟进也没关系,我们自己做实现就好了,我们这里说干就干:

    在DeepSeek上实现Skills引擎

    首先,我们需要读取本地文件夹,把 meta.json 转换成大模型能看懂的 Tool Definition:

    def load_skills_from_meta():
    """扫描目录,从 meta.json 加载技能元数据"""
    # 示例:假设我们读取到了 simple_weather_skill
    with open("./simple_weather_skill/meta.json", "r", encoding="utf-8") as f:
    meta = json.load(f)

    # 构造兼容 OpenAI/DeepSeek 格式的工具定义
    return [{
    "type": "function",
    "function": {
    "name": meta["name"],
    "description": meta["description"],
    "parameters": {
    "type": "object",
    "properties": {},
    "required": []
    },
    },
    }]

    当用户提问时,我们把加载好的 skills 列表发给 DeepSeek,利用它的 Function Calling 能力来判断"需要使用哪个 Skill",这也是最核心的意图识别:

    # 1. 准备工具列表
    skills = load_skills_from_meta()

    # 2. 发送请求给 DeepSeek,让其选择
    messages = [{"role": "user", "content": user_input}]
    result = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=skills # 关键点:把 Skill 描述当作 Tools 传进去
    )

    然后就是上下文注入了,如果模型决定调用某个 skill(例如 weather_skill),我们不执行它,而是读取该 skill 的 skill.md,把它追加到 System Prompt 中:

    # 检查模型是否想调用 Skill
    if result.choices[0].message.tool_calls:
    skill_name = result.choices[0].message.tool_calls[0].function.name
    print(f"模型命中技能: {skill_name}")
    # 读取对应的 Prompt 模板
    with open(f"./{skill_name}/skill.md", "r") as f:
    skill_prompt = f.read()
    # 【核心魔法】:构建一个新的对话上下文,使其立即拥有该技能的知识
    skill_messages = [
    {"role": "system", "content": skill_prompt}, # 注入技能 Prompt
    {"role": "user", "content": user_input} # 重放用户问题
    ]
    # 此时,DeepSeek 已经变身为“天气专家”,准备好执行具体脚本了

    最后是执行,在注入了 skill.md 的新会话中,模型会根据 Prompt 的指示,去调用 scripts/ 下真正的 Python 脚本(通常通过 execute_script 这样一个通用工具来实现),最终返回结果:

    def execute_script(script_path, args=None):
    """一个通用的脚本执行器工具"""
    cmd = ["python", script_path] + (args or [])
    res = subprocess.run(cmd, capture_output=True, text=True)
    return res.stdout

    通过这套机制,我们就在 DeepSeek 上完美模拟了 Claude 的 Skills 流程:路由 -> 加载 Prompt -> 执行脚本。

    unsetunset结语unsetunset

    好了,今天文章的信息量已经足够大了,这里就不赘述了,希望文章对大家有用,后续内容我们会在记忆系统、ReAct框架与一个完整案例做展开,敬请期待…

    如何学习大模型 AI ?

    由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。

    但是具体到个人,只能说是:

    “最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

    这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

    我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

    我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

    第一阶段(10天):初阶应用

    该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。

    • 大模型 AI 能干什么?
    • 大模型是怎样获得「智能」的?
    • 用好 AI 的核心心法
    • 大模型应用业务架构
    • 大模型应用技术架构
    • 代码示例:向 GPT-3.5 灌入新知识
    • 提示工程的意义和核心思想
    • Prompt 典型构成
    • 指令调优方法论
    • 思维链和思维树
    • Prompt 攻击和防范

    第二阶段(30天):高阶应用

    该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。

    • 为什么要做 RAG
    • 搭建一个简单的 ChatPDF
    • 检索的基础概念
    • 什么是向量表示(Embeddings)
    • 向量数据库与向量检索
    • 基于向量检索的 RAG
    • 搭建 RAG 系统的扩展知识
    • 混合检索与 RAG-Fusion 简介
    • 向量模型本地部署

    第三阶段(30天):模型训练

    恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。

    到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?

    • 为什么要做 RAG
    • 什么是模型
    • 什么是模型训练
    • 求解器 & 损失函数简介
    • 小实验2:手写一个简单的神经网络并训练它
    • 什么是训练/预训练/微调/轻量化微调
    • Transformer结构简介
    • 轻量化微调
    • 实验数据集的构建

    第四阶段(20天):商业闭环

    对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。

    • 硬件选型
    • 带你了解全球大模型
    • 使用国产大模型服务
    • 搭建 OpenAI 代理
    • 热身:基于阿里云 PAI 部署 Stable Diffusion
    • 在本地计算机运行大模型
    • 大模型的私有化部署
    • 基于 vLLM 部署大模型
    • 案例:如何优雅地在阿里云私有部署开源大模型
    • 部署一套开源 LLM 项目
    • 内容安全
    • 互联网信息服务算法备案

    学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。

    如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

    这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

    战。天道酬勤,你越努力,就会成为越优秀的自己。

    如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

    这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【万字】带你实现一个Agent(上),从Tools、MCP到Skills实战指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址