用 Go 手撸 Agent 框架 #11:工具从哪里来——内置工具、MCP 与 Skills

第 10 篇的研究助手跑起来以后,顺着日志重新看了一遍工具调用:

text 复制代码
[Tool] 调用 search,参数:{"query":"Go 1.22 for loop variable change official"}
[Tool] search 完成(耗时 521ms,返回 1846 字节)
[Tool] 调用 read_webpage,参数:{"url":"https://go.dev/doc/go1.22"}
[Tool] read_webpage 完成(耗时 263ms,返回 8014 字节)

模型会决定调用哪个工具,但 searchread_webpage 还是我们自己写进 Go 项目的。

如果下一个任务要查 GitHub Issue、读数据库、操作浏览器,是不是还要继续写 GitHubToolDatabaseToolBrowserTool

可 Claude Code、Codex 这类通用 Agent 看起来能做很多事情。它们难道提前写好了成千上万个工具?

MCP、Skill 这些词我当然听过,也见过"安装 MCP"和"添加 Skill"这样的说法。可把它们放回我们刚写出来的 Agent.RunToolToolRegistry,我还说不清它们到底接在哪里。

这两个概念当然要在这一篇弄明白。只是光记住定义还不够,我还想知道它们到了代码里分别落在哪里。

所以这一次先把两个听过但还没真正用过的名词拆开查清:先把天气工具移到独立的 MCP Server,观察 Agent 怎样发现并调用外部能力;再学习 Skill 的定义、格式和调用入口,最后按规范写一份并实际运行。

一个需求不一定对应一个工具

我们前面写过的工具都很直白:

text 复制代码
查天气       -> get_weather
做计算       -> calculate
搜索互联网   -> search
读取网页     -> read_webpage

如果照着用户需求继续往下列,工具清单很快就会变成:

text 复制代码
查股票       -> get_stock_price
找 Issue     -> search_github_issue
查订单       -> query_order
读取文件     -> read_file
......

这里容易混淆两件事:用户想完成什么,以及 Agent 能执行什么动作。

用户想知道天气,不代表 Agent 必须拥有一个叫 get_weather 的工具。它也可以搜索网页、操作浏览器,或者通过 Shell 调用一个公开接口。天气是目标,搜索、浏览器和 Shell 是完成目标的不同手段。

这样再看通用 Agent,就容易理解一些了。它可以先提供少量覆盖面很广的基础工具,例如读写文件、搜索代码、执行命令和访问网页。一个 Shell 工具后面还能继续运行 gitgo testrgcurl,不需要把每条命令再包装成一种新工具。

但通用工具也不能替代所有专用工具。

创建订单、发起退款、修改数据库这类动作,更适合使用参数和行为都明确的专用工具。让 Agent 自由拼一段 Shell 命令,反而很难看清它到底会修改什么。

所以目前能得到的结论只是:

  • 少量通用工具可以覆盖很多开放任务。
  • 边界明确的业务动作仍然适合专用工具。

这还没有回答另一个问题:如果某个团队已经写好了 GitHub、数据库或者浏览器工具,我们自己的 Agent 怎么接进来?

先把熟悉的天气 Tool 搬到 MCP Server

如果第一次就接 GitHub、数据库之类的第三方 MCP,还要同时处理账号、认证和服务配置。即使最后跑通了,也不容易分清哪部分是 MCP,哪部分只是第三方服务自己的要求。

所以我先不接真正的第三方服务,而是回到前面已经用过很多次的天气查询。

第 8、9 篇里,天气 Tool 和 Agent 住在同一个 Go 程序里:

text 复制代码
Agent.Run
  -> ToolRegistry
  -> get_weather.Execute
  -> 返回天气

这一篇仍然解决天气查询,天气数据也还是本地 mock。只改变一件事:不再把 get_weather 直接放进 Agent,而是自己写一个最小天气 MCP Server,由它提供天气查询能力。

工具搬到另一个程序以后,Runtime 就不能再直接调用它的 Go 方法了。两个程序需要先约定:怎样连接、怎样公布工具、怎样描述参数,以及怎样返回执行结果。

这时 MCP 里的 Protocol 才有了具体含义。

MCP 是一套通信约定。提供能力的程序叫 MCP Server,连接它的一侧使用 MCP Client。按照 MCP 官方架构,Server 可以公布工具,Client 可以先通过 tools/list 发现工具,再通过 tools/call 调用工具。MCP Architecture

真正使用第三方 MCP 时,我们拿到的通常不是对方的源代码,而是一个 MCP 地址和可能需要的认证信息。把地址填进客户端以后,客户端才能发现对方提供的工具。

MCP 并不要求 Server 一定在远程。官方架构同时支持本地 stdio 和远程 Streamable HTTP。前一种适合由 Client 启动本地进程,后一种更接近我们配置第三方服务地址的使用体验。这一篇选择 Streamable HTTP,是为了先把"第三方怎样对外提供 MCP 服务"看清楚。

改造之后,调用关系变成:

text 复制代码
Agent.Run
  -> MCP Client
  -> 天气 MCP Server
  -> get_weather
  -> 返回天气

Agent 只知道天气 MCP 的地址,不再直接依赖天气工具的 Go 实现。以后换成 GitHub MCP 或数据库 MCP,关系也是相似的:GitHub MCP 提供查 Issue、建 PR 等能力,数据库 MCP 提供查表结构、执行查询等能力;Agent 通过 MCP 发现并调用它们。

这次自己写 Server,不是因为真实项目都应该重复实现第三方服务,而是为了把 Server 怎样公布工具、Client 怎样发现工具,以及 Runtime 怎样调用工具这三步放在眼前。等这条链路看清楚,再把地址换成真正的第三方 MCP,Agent 一侧的主要流程不会改变。

Claude Code 这类 Agent Host 通常会把安装或配置好的 MCP Server 记录在配置文件里。启动会话后,Host 根据配置连接这些 Server;真正处理任务时,模型再从已经发现的工具中选择需要调用的那个。

我们的 Runtime 还没有 MCP 配置文件,所以先用一个命令行参数代替。它只表示"本次运行连接这个 MCP Server",相当于一份只有一个 Server 的临时配置。

text 复制代码
11-tools-mcp-skills/
├── server/
│   ├── main.go                    # 通过 HTTP 对外提供 MCP 服务
│   └── main_test.go
├── client/
│   ├── main.go                    # 我们的 Agent 应用,同时包含 MCP Client
│   └── main_test.go
├── weather/
│   ├── weather.go                 # mock 天气数据
│   └── weather_test.go
└── skills/
    └── weather-report/
        └── SKILL.md               # 天气任务的步骤和约束

这里仍然有两个 main.go,但职责已经很清楚:

text 复制代码
server/main.go  -> 独立启动,在 http://127.0.0.1:8080/mcp 提供服务
client/main.go  -> 运行 Agent,只拿 MCP 地址连接 Server

它们不是两个互相调用的 Go 函数,Client 也不导入 weather 包。即使以后把 Server 部署到另一台机器,只要 MCP 地址和认证方式不变,Agent 一侧的接法仍然相同。

Server 提供的仍然只是天气能力

Server 注册了两个工具:

go 复制代码
mcp.AddTool(server, &mcp.Tool{
    Name:        "get_weather",
    Description: "查询城市的 mock 天气",
}, weather.Get)

mcp.AddTool(server, &mcp.Tool{
    Name:        "list_supported_cities",
    Description: "列出当前有 mock 天气数据的城市",
}, weather.ListSupportedCities)

注册工具以后,用 SDK 提供的 Streamable HTTP Handler 暴露 /mcp

go 复制代码
address := flag.String("addr", "127.0.0.1:8080", "HTTP 监听地址")
flag.Parse()

handler := mcp.NewStreamableHTTPHandler(
    func(*http.Request) *mcp.Server {
        return server
    },
    nil,
)

mux := http.NewServeMux()
mux.Handle("/mcp", handler)

httpServer := &http.Server{
    Addr:              *address,
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
}

if err := httpServer.ListenAndServe();
    err != nil && !errors.Is(err, http.ErrServerClosed) {
    log.Fatal(err)
}

所以当前 Server 确实有一个对外服务地址:

text 复制代码
http://127.0.0.1:8080/mcp

它不是我们另外设计的一套天气 REST API,这个地址直接承载 MCP 的初始化、工具发现和工具调用消息。将来换成真正的第三方 MCP,Client 仍然只连接对方给出的 MCP 地址。

127.0.0.1 只能由当前电脑访问,因此这个教学 Server 还不是公网第三方服务。真正的服务商会把同类 Handler 部署到服务器,通过 HTTPS 域名提供地址,并按需要增加 OAuth、API Key 等认证。传输位置变了,Client 侧的"连接、发现工具、调用工具"这条主线没有变。

天气数据写在 weather/weather.go

go 复制代码
report := map[string]string{
    "北京": "北京:晴,15°C",
    "上海": "上海:多云,19°C",
}

所以这不是实时天气。MCP 也没有凭空获得天气能力,它只是把 weather.Getweather.ListSupportedCities 按统一格式公布出去。

这点可以用一个问题检查:

如果把 weather.Get 删除,只保留 MCP Server,Agent 还能查天气吗?

不能。协议负责连接能力,不负责替我们实现业务能力。

第一次接上时,Runtime 还不认识 MCP Tool

Client 不启动 Server,也不读取 Server 的代码。它只使用命令行传入的地址建立 MCP 会话:

go 复制代码
session, err := client.Connect(ctx, &mcp.StreamableClientTransport{
    Endpoint: serverURL,
}, nil)

连接成功后,再询问 Server 有哪些工具:

go 复制代码
result, err := session.ListTools(ctx, nil)

Server 会返回工具名称、说明和 JSON Schema。问题是,我们自己的 Runtime 接收的是另一种类型:

go 复制代码
type Tool interface {
    Definition() ToolDefinition
    Execute(ctx context.Context, input json.RawMessage) (string, error)
    Permission() PermissionLevel
}

MCP 返回了工具,但 Agent.Run 还不能直接使用它们。真正缺少的是一层很薄的转换。

我给它起了一个朴素的名字:mcpTool

go 复制代码
type mcpTool struct {
    agent.BaseTool
    definition agent.ToolDefinition
    session    toolSession
    out        io.Writer
}

func (t *mcpTool) Definition() agent.ToolDefinition {
    return t.definition
}

发现工具时,把 MCP 的名称、说明和输入结构保存成 Runtime 已经认识的 ToolDefinition

go 复制代码
result, err := session.ListTools(ctx, nil)
if err != nil {
    return nil, fmt.Errorf("发现 MCP 工具: %w", err)
}

for _, tool := range result.Tools {
    schema, ok := tool.InputSchema.(map[string]any)
    if !ok {
        return nil, fmt.Errorf(
            "MCP 工具 %q 的输入结构不是 JSON 对象",
            tool.Name,
        )
    }

    tools = append(tools, &mcpTool{
        definition: agent.ToolDefinition{
            Name:        tool.Name,
            Description: tool.Description,
            InputSchema: schema,
        },
        session: session,
        out:     out,
    })
}

然后把这些工具注册进原来的 ToolRegistry

go 复制代码
registry := agent.NewToolRegistry()
for _, tool := range tools {
    if err := registry.Register(tool); err != nil {
        return fmt.Errorf("注册 MCP 工具: %w", err)
    }
}

到这里,模型看到的工具定义和本地 Tool 没有区别。Agent.Run 不需要知道它来自另一个进程。

模型选择以后,Execute 再转回 MCP 调用

模型如果选择 get_weather,Runtime 仍然会调用:

go 复制代码
tool.Execute(ctx, input)

只是这一次,Execute 内部不执行本地天气函数,而是调用 MCP Session:

go 复制代码
func (t *mcpTool) Execute(
    ctx context.Context,
    input json.RawMessage,
) (string, error) {
    var arguments map[string]any
    if err := json.Unmarshal(input, &arguments); err != nil {
        return "", fmt.Errorf(
            "解析 MCP 工具 %q 的参数: %w",
            t.definition.Name,
            err,
        )
    }

    result, err := t.session.CallTool(ctx, &mcp.CallToolParams{
        Name:      t.definition.Name,
        Arguments: arguments,
    })
    if err != nil {
        return "", fmt.Errorf(
            "调用 MCP 工具 %q: %w",
            t.definition.Name,
            err,
        )
    }

    // 把 MCP 返回的文本整理成 Runtime 使用的字符串。
    // 完整实现见配套代码。
    return output, nil
}

这层适配把两边接了起来:

text 复制代码
MCP Tool Definition
        ↓ 转换
agent.ToolDefinition
        ↓
模型选择工具
        ↓
agent.Tool.Execute
        ↓ 转换
MCP CallTool

现在再问:MCP 是 Agent 的思考能力吗?

不是。模型还是原来的模型,Agent.Run 还是第 9 篇的 loop。我们只是让这个 loop 多了一种工具来源。

Skill 又接在哪里

MCP 工具接进来以后,模型已经能够查询天气。接下来我遇到的是另一个问题:Skill 到底是什么?

我先去看 Agent Skills 官方概览,把这个概念本身弄清楚。官方给出的定位是:Agent Skills 是一种轻量、开放的格式,用来给 AI Agent 增加专门知识和工作流。一个 Skill 的核心是一个包含 SKILL.md 的目录;文件里有元数据和任务说明,也可以附带脚本、参考资料、模板等资源。官方还特别强调,Skill 是把流程知识和特定上下文打包起来,让 Agent 在需要时加载,而不是每次都把全部内容塞进上下文。

再看 Agent Skills Specification,我确认了最小格式:SKILL.md 前面的 namedescription 必须存在,正文再放任务步骤、示例和边界。看完这两份资料,我先把 Skill 理解成一份按约定组织起来、用于扩展 Agent 专门知识和工作流的说明。

理解这个定义之后,我再回头看第 10 篇。那篇研究助手的 system prompt 里,已经写过类似这样的规则:

text 复制代码
优先查官方资料
不要只相信搜索摘要
信息不足时更换关键词
最后列出来源

这些规则本身不是工具,只是在告诉 Agent 拿到工具以后应该先做什么、后做什么。对照刚才看到的定义,我才发现它们和 Skill 要承载的内容很接近。所以我想拿这几条规则试着整理一次,看看它们能不能变成一份真正可调用的 Skill 文件。

我准备按三步试试:

  1. 按规范写一份天气 Skill,把这次天气示例需要的步骤和回答要求放进去。
  2. 在提问时明确要求使用它,让 Agent 调用加载入口。
  3. 观察模型是否真的拿到了 Skill 内容,并把我们特意放进去的验证信息带回回答。

先按刚才看到的格式,写一份只服务于天气示例的 Skill:

markdown 复制代码
---
name: weather-report
description: 使用可用的天气工具回答示例中的城市天气问题。
---

# Weather report

回答天气问题时:

1. 先使用 `list_supported_cities` 确认这个示例能够查询哪些城市。
2. 再使用 `get_weather` 查询用户指定的城市。
3. 只根据工具返回的内容回答,不要补充工具没有提供的数据。
4. 明确告诉用户这里是本地 mock 数据,不是实时天气。
5. 每次最终回答结尾另起一行,原样输出:`查什么天气查天气,你又不出门游玩。`

最后这句话不是天气助手真正需要的业务规则,而是这次学习故意加的验证标记。它和天气工具没有关系:MCP Server 不会返回这句话,weather.Get 里也找不到它。如果模型最后原样输出,我就把它当作 Skill 内容确实进入上下文的一个验证信号。

接下来要验证 Skill 是否能按指令加载。为了分辨它到底有没有响应用户指令,我先增加一个 load_skill 入口:启动时只公开 Skill 的元数据,用户明确要求后再读取正文。这样才能看清正文到底是在什么时候进入上下文的:

text 复制代码
程序启动
  -> 扫描 Skill 的名称和简介
  -> 注册一个可以按名称读取 Skill 的 load_skill 工具
用户明确说"请使用 weather-report Skill"
  -> 模型调用 load_skill,加载它的完整说明
  -> 按说明调用 MCP 天气工具

运行时,Client 会扫描 skills 目录,取出每份 Skill 的 namedescription,先让模型知道"有哪些 Skill"。此时正文还没有进入模型上下文。

然后我又注册了一个本地工具 load_skill

go 复制代码
func (t *skillLoader) Definition() agent.ToolDefinition {
    return agent.ToolDefinition{
        Name: "load_skill",
        Description: "按名称加载一份 Skill 的完整工作说明......",
        InputSchema: map[string]any{
            "type": "object",
            "properties": map[string]any{
                "name": map[string]any{
                    "type": "string",
                    "enum": t.names,
                },
            },
            "required": []string{"name"},
        },
    }
}

模型调用 load_skill({"name":"weather-report"}) 后,Execute 才会把完整的 SKILL.md 作为工具结果返回:

go 复制代码
func (t *skillLoader) Execute(_ context.Context, input json.RawMessage) (string, error) {
    var params struct {
        Name string `json:"name"`
    }
    if err := json.Unmarshal(input, &params); err != nil {
        return "", fmt.Errorf("解析 load_skill 参数: %w", err)
    }
    record, ok := t.skills[params.Name]
    if !ok {
        return "", fmt.Errorf("未知 Skill: %s", params.Name)
    }
    content, err := os.ReadFile(record.path)
    if err != nil {
        return "", err
    }
    return string(content), nil
}

到这里,代码和文件都准备好了,但我还不知道模型能不能真的按用户指令触发 load_skill。先明确告诉它使用 weather-report,跑一遍看看日志里会不会出现"加载 Skill",以及文件里的要求有没有影响最后的回答。

这里也可以停一下:

如果删除两个 MCP 天气工具,只留下这份天气 Skill,Agent 还能返回上海天气吗?

不能。Skill 告诉它应该怎样查询,却没有提供执行查询的能力。

把整条链路跑起来

这一次需要模型参与,所以要配置 DeepSeek Key,但不需要第 10 篇的 Serper Key。

11-tools-mcp-skills/.env 中写入:

dotenv 复制代码
DEEPSEEK_API_KEY=你的_Key

Key 可以在 DeepSeek API Keys 创建。

打开第一个终端,启动 MCP Server:

bash 复制代码
cd 11-tools-mcp-skills
go run ./server

看到下面的日志,说明 MCP 地址已经可以访问:

text 复制代码
天气 MCP Server 已启动:http://127.0.0.1:8080/mcp

再打开第二个终端,运行 Agent,并把 MCP 地址交给它:

bash 复制代码
cd 11-tools-mcp-skills
go run ./client http://127.0.0.1:8080/mcp

这条命令需要拆开看:

text 复制代码
go run ./client
    启动我们的 Agent 应用

http://127.0.0.1:8080/mcp
    告诉 Agent 应用,本次运行要连接的 MCP Server 在哪里

它不是在下载 MCP Server。Server 已经由第一个终端启动了。

它也不是直接调用某个天气工具,更不是强制模型必须使用 MCP。Client 拿到地址以后,依次完成的是:

text 复制代码
读取 MCP URL
  -> 连接 Server
  -> ListTools 发现 get_weather 和 list_supported_cities
  -> 把工具定义放进本次 Runtime 的 ToolRegistry
  -> Agent.Run 把这些可用工具交给模型
  -> 模型根据用户问题和 Skill 决定调用哪个工具

和 Claude Code 相比,这里的差别是:

text 复制代码
我们的命令行参数:
http://127.0.0.1:8080/mcp

概念上相当于 Claude Code 配置中的:
weather -> http://127.0.0.1:8080/mcp

区别在于,Claude Code 会把多个 MCP Server 的配置保存下来,之后启动会话时再统一连接;我们的示例没有配置管理,只在这次进程启动时连接命令行传入的一个地址。

启动时我还要留意一件事:连接 MCP 的同时,Skill 会不会也被自动塞进模型上下文?程序的安排是先扫描名称和简介、注册 load_skill,再进入交互循环;只有用户明确要求使用它,才会读取 SKILL.md 正文。

所以启动阶段可以先看下面这些关键日志。此时 MCP 工具已经发现,Skill 只有元数据,模型还没有拿到 Skill 正文;中间的其他请求日志这里省略了:

text 复制代码
[Skill] 发现:weather-report - 使用可用的天气工具回答示例中的城市天气问题。(正文尚未交给模型)
[MCP] 连接 Server:http://127.0.0.1:8080/mcp
[MCP] -> ListTools:询问 Server 提供哪些工具
[MCP] <- 发现工具:get_weather - 查询城市的 mock 天气
[MCP] <- 发现工具:list_supported_cities - 列出当前有 mock 天气数据的城市

天气 Agent 已就绪(输入 quit 退出)

>

可以先故意不提 Skill,输入一个普通问题。下面是我这次运行的一种结果:

text 复制代码
> 上海天气怎么样?

[Agent] 选择工具:list_supported_cities
[Agent] 选择工具:get_weather

[Assistant] 上海今天是多云,19°C。

这次仍然能查到天气,但日志里没有 load_skill,回答末尾也没有那句验证用的废话。原因是天气能力来自 MCP Server,Skill 并不是查天气的必要条件。

再输入同一个类型的问题,并明确要求使用这份 Skill。下面是我这次加载成功后的一种结果;模型也可能把工具调用分成不同轮次,或者使用不同措辞:

text 复制代码
> 请使用 weather-report Skill 查询上海天气。

[Agent] 第 1 轮:请求模型(3 个可用工具)
[Agent] 第 1 轮:模型返回 tool_use
[Agent] 选择工具:load_skill,参数:{"name":"weather-report"}
[Skill] 已加载:weather-report(713 字节)
[Agent] 第 2 轮:请求模型(3 个可用工具)
[Agent] 第 2 轮:模型返回 tool_use
[Agent] 选择工具:list_supported_cities,参数:{}
[MCP] -> CallTool:list_supported_cities,参数:{}
[MCP] <- CallTool:支持的 mock 城市:北京、上海
[Agent] 第 3 轮:请求模型(3 个可用工具)
[Agent] 第 3 轮:模型返回 tool_use
[Agent] 选择工具:get_weather,参数:{"city": "上海"}
[MCP] -> CallTool:get_weather,参数:{"city": "上海"}
[MCP] <- CallTool:上海:多云,19°C
[Agent] 第 4 轮:请求模型(3 个可用工具)
[Agent] 第 4 轮:模型返回 end_turn

[Assistant] 上海当前的天气情况是:**多云**,气温 **19°C**。

这里是本地 mock 数据,并非实时天气预报。

查什么天气查天气,你又不出门游玩。

回答结束后,程序仍停在交互提示符,MCP Session 也继续复用。接着输入一个依赖上一轮上下文的问题:

text 复制代码
> 那北京呢?

[Agent] 第 1 轮:请求模型(3 个可用工具)
[Agent] 第 1 轮:模型返回 tool_use
[Agent] 选择工具:get_weather,参数:{"city": "北京"}
[MCP] -> CallTool:get_weather,参数:{"city": "北京"}
[MCP] <- CallTool:北京:晴,15°C
[Agent] 第 2 轮:请求模型(3 个可用工具)
[Agent] 第 2 轮:模型返回 end_turn

[Assistant] 北京当前的天气情况是:**晴**,气温 **15°C**。

这里同样是本地 mock 数据,并非实时天气。

查什么天气查天气,你又不出门游玩。

后面两次使用 Skill 的回答末尾都出现了那句与天气数据无关的话。它没有写在 MCP Server 和天气工具里,只存在于 SKILL.md;在这次运行中,可以据此判断 Skill 的内容确实进入了模型上下文,并影响了输出。

第二次也没有重新连接 Server,或者再次执行 ListTools。Client 复用了同一个 MCP Session,并把第一轮的对话历史传入新的 Agent.Run,所以模型知道"那北京呢"仍然在问天气。日志里的 Agent 轮次重新从 1 开始,是因为每次用户输入都会发起一次新的 Agent.Run

输入下面的命令才会退出交互循环并关闭 MCP Session:

text 复制代码
> quit

模型可能把两个工具放在同一轮,也可能分成两轮调用,最终措辞也可能不同。我运行两次后,拿日志逐项对照:

  1. Skill 被读取并交给模型。
  2. 工具来自 MCP Server 的 ListTools,不是写死在 Client 的工具清单里。
  3. 模型选择工具后,mcpTool.Execute 发出 CallTool
  4. MCP 返回的结果进入下一轮模型消息,最后才形成回答。
  5. 多次输入复用同一个 MCP Session,工具不会每轮重新发现。
  6. 对话历史和 MCP Session 是两套状态:前者帮助模型理解"那北京呢",后者负责继续调用 Server。

完整调用关系现在变成:

text 复制代码
Skill 元数据
   -> load_skill(只注册加载入口) ───────┐
用户指令"请使用 weather-report"         │
   -> 模型调用 load_skill                │
   -> SKILL.md 正文进入上下文            │
                                         ↓
MCP URL -> ListTools -> mcpTool -> ToolRegistry
                                         ↓
                                      Agent.Run
                                         ↓
                                   模型选择工具
                                         ↓
                                  mcpTool.Execute
                                         ↓
                              CallTool -> MCP URL
                                         ↓
                               MCP Server 执行业务代码

现在怎么区分 MCP 和 Skill

只看名词时,我容易把它们都理解成"给 Agent 装插件"。把代码跑通以后,区别落到了两个具体位置:

text 复制代码
MCP   -> mcpTool 适配 -> ToolRegistry
Skill -> 按需加载的任务说明,正文通过工具结果进入上下文

用这次天气示例描述:

  • MCP 让 Agent 接触到 get_weatherlist_supported_cities
  • Skill 告诉 Agent 先确认支持的城市,再查询目标城市,并说明结果是 mock 数据。
  • Runtime 负责把模型选择、工具执行和结果回填串成循环。

规范里还提到 MCP 的 Resource、Prompt,以及 Skill 目录里的脚本和参考资料。这次代码没有用到它们,先记住这些名字,等真正遇到需要时再展开。

回到最开始的问题

通用 Agent 不需要为用户可能提出的每个问题都准备一个同名工具。

它可以先拥有少量通用工具,再按实际环境连接专用能力。MCP 让外部程序按统一约定公布这些能力;本例则把第 10 篇研究助手里的任务规则整理成可按需加载的 Skill。

跑完这次实验,我现在能把两者放回代码里区分开:

MCP 负责把外部工具接进来,Skill 负责把做事方法交给模型,Agent loop 负责在两者之间完成一次次决策和执行。

最后检查一下自己是否真的理解了

  1. MCP Server 新增一个工具后,为什么 Agent 应用不需要再手写一条对应的业务调用分支?
  2. ListTools 已经返回工具,为什么还要写 mcpTool 这一层?
  3. 只有天气 Skill、没有天气工具时,模型为什么仍然查不到天气?
  4. 用户没有要求使用 Skill 时,为什么天气仍然可以查询?
  5. 这次接入以后,Agent.Run 为什么可以保持不变?

这次还有一个暂时没解决的边界:mcpTool 嵌入了 agent.BaseTool,所以当前发现的 MCP 工具会沿用 Runtime 的默认放行方式。天气查询只是读取 mock 数据,看起来没什么问题;可如果下一次接入的是写文件、执行命令或者删除数据的工具,还能继续自动执行吗?这个问题留到下一篇,从权限边界继续往下看。

参考资料

相关推荐
玉鸯1 小时前
Agent 开发之 Checkpoint:AI 工作流的状态恢复机制
agent
leeyi1 小时前
RAG 流水线设计:Eino 的 Loader → Transformer → Indexer → Retriever(第60篇-E46)
aigc·agent·ai编程
AI程序员1 小时前
万字长文详解 Agent 的评测机制:从任务、环境、轨迹到验证器、统计与持续回归
人工智能·agent
ch8562 小时前
别再只会 similaritySearch 了!RAG 在线阶段的 6 道鬼门关
agent
ch8562 小时前
别再调 prompt 了!RAG 的天花板,在文档进向量库那刻就焊死了
agent
ch8562 小时前
RAG 一上线就翻车?因为你缺这张全景地图
agent
love530love2 小时前
【排障实录】GPT Desktop (Codex) 开启 WSL 智能体模式后无法启动?手把手教你修复
人工智能·windows·gpt·agent
王中阳Go2 小时前
面试拷打实录:候选人聊Agent/RAG时的典型误区,我给了这些“避坑指南”
后端·面试·agent