AI Agent是如何使用工具的:Tools、MCP、CLI、Skills四种机制深度解析

AI Agent是如何使用工具的:Tools、MCP、CLI、Skills 四种机制深度解析

如果说大模型是AI Agent的大脑」,那么工具调用系统就是 Agent 的「双手」。没有工具的Agent,就像一个被关小黑屋的聪明人,空有一堆想法却无法落地。今天咱们基于nanobot开源项目的源码分析,深入解析AI Agent的四种主流工具调用机制。

一、Tools:最基础的内置工具系统

什么是 Tools?

Tools 是 Agent 最原生的能力,直接注册到 ToolRegistry,每次调用大模型时都会通过 `tools` 参数完整传递。这样大模型就可以根据实际需求,调用对应的工具。

工作原理

# Tool 基类定义
class Tool(ABC):
    @property
    @abstractmethod
    def name(self) -> str: ...        # 工具名,如 "exec"
    
    @property
    @abstractmethod
    def description(self) -> str: ... # 一句话描述
    
    @property
    @abstractmethod
    def parameters(self) -> dict: ... # JSON Schema 参数定义
    
    @abstractmethod
    async def execute(self, **kwargs): ...  # 执行逻辑

实际示例:`exec` 工具的完整定义

@tool_parameters(
    tool_parameters_schema(
        command=StringSchema("The shell command to execute"),
        working_dir=StringSchema("Optional working directory"),
        timeout=IntegerSchema(
            60,
            description="Timeout in seconds. Increase for long-running commands",
            minimum=1,
            maximum=600,
        ),
    )
)
class ExecTool(Tool):
    name = "exec"
    description = "Execute a shell command and return its output. Use for tests, builds, package commands, git operations."
    
    async def execute(self, command, working_dir=None, timeout=60, **kwargs):
        # 安全检查:防止路径穿越、内网访问
        guard_error = self._guard_command(command, working_dir)
        if guard_error:
            return guard_error
        
        # 异步执行子进程
        process = await asyncio.create_subprocess_shell(
            command,
            cwd=working_dir,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        
        try:
            stdout, stderr = await asyncio.wait_for(
                process.communicate(),
                timeout=timeout,
            )
        except asyncio.TimeoutError:
            await process.kill()
            return f"Error: Command timed out after {timeout} seconds"
        
        return stdout.decode() + stderr.decode() + f"\nExit code: {process.returncode}"

大模型看到的 Tool 定义

每次调用 LLM 时,OpenAI 协议会传输这样的 JSON:

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "exec",
        "description": "Execute a shell command and return its output...",
        "parameters": {
          "type": "object",
          "properties": {
            "command": {
              "type": "string",
              "description": "The shell command to execute"
            },
            "working_dir": {
              "type": "string",
              "description": "Optional working directory"
            },
            "timeout": {
              "type": "integer",
              "minimum": 1,
              "maximum": 600
            }
          },
          "required": ["command"]
        }
      }
    },
    // ... 其他工具
  ]
}

典型 Tools 清单

工具名 功能 典型调用
exec 执行 Shell 命令 exec(command="git status")
read_file 读取文件内容 read_file(path="src/main.py")
write_file 写入文件 write_file(path="README.md", content="# Project")
grep 全局搜索 grep(pattern="TODO", path="src/")
apply_patch 应用代码补丁 apply_patch(diff="diff --git ...")
web_search 联网搜索 web_search(query="Python 3.12 release notes")

关键特征

✅ 每次都传递:所有注册的 Tool 每次都会传给大模型
✅ 强类型约束:通过 JSON Schema 严格校验参数
✅ 统一入口:所有调用都经过 `ToolRegistry.execute()`
❌ 扩展性有限:太多 Tool 会撑爆上下文窗口

二、MCP:远程工具协议(Model Context Protocol)

什么是 MCP?

MCP 是一个开放协议,允许 Agent 连接到远程服务器提供的工具,就像调用本地工具一样。

工作原理

MCP同样是注册到ToolRegistry,每次调用大模型时都会通过 `tools` 参数完整传递。这样大模型就可以根据实际需求,调用对应的MCP。ToolRegistry通过MCP Tool Wrapper,实现MCP工具的调用。

┌─────────────────────────────────────────────────────┐
│ 大模型                                               │
│  prompt: "打开 bing.com 并截图首页"                   │
└───────┬─────────────────────────────────────────────┘
        │
        ▼ 大模型决定调用:mcp_browser_navigate(url="https://bing.com")
┌─────────────────────────────────────────────────────┐
│ ToolRegistry                                        │
│   └─ MCP Tool Wrapper(动态生成)                   │
│      name: mcp_browser_navigate                     │
│      description: Navigate to a URL with browser    │
│      parameters: {url: string}                      │
└───────┬───────────────────────────────────────────────┘
        │
        ▼ STDIO / HTTP 传输调用请求
┌─────────────────────────────────────────────────────┐
│ MCP Server(本地 Node.js 进程)                     │
│   └─ 真正控制 Playwright/Puppeteer 浏览器           │
│      → 访问 URL                                     │
│      → 截图                                          │
│      → 返回 base64 图片                             │
└─────────────────────────────────────────────────────┘

MCP示例:Playwright MCP Server

MCP Server 暴露的工具列表:

MCP 工具名 功能
browser_navigate 导航到指定 URL
browser_click 点击页面元素
browser_evaluate 执行 JavaScript
browser_screenshot 截图
browser_pdf 导出 PDF

Agent 调用时的实际工具名(自动加前缀):

mcp_browser_navigate
mcp_browser_click
mcp_browser_evaluate
mcp_browser_screenshot

完整调用示例:

用户:帮我打开 bing.com 并截一张首页的图
    ↓
[1] 大模型看到 Tool 列表中有 mcp_browser_navigate
    ↓
[2] 调用工具:
    mcp_browser_navigate(url="https://www.bing.com")
    ↓
[3] MCP Server 控制浏览器访问,返回:"已导航到 bing.com,页面标题是 Bing"
    ↓
[4] 大模型继续调用:
    mcp_browser_screenshot(full_page=true)
    ↓
[5] 返回 base64 图片
    ↓
[6] 大模型:"已完成截图,这是 bing.com 首页" + 显示图片

MCP 的巧妙之处

1. 工具名自动生成:`mcp_{server_name}_{tool_name}`
2. Schema 自动转换:MCP 定义自动转成 OpenAI Function Schema
3. 支持资源和提示:不仅是工具,还可以是数据源和 Prompt 模板

为什么 MCP 很重要?

MCP 把「工具」从代码级的扩展,变成了服务级的扩展。
任何人都可以用任何语言写一个 MCP Server,Agent 立刻就能用它的所有能力 — 无需修改 Agent 一行代码。

三、CLI APP:最巧妙的混合形态

什么是 CLI APP?

CLI APP 是 NanoBot 最精妙的设计之一,它通过 「统一工具 + 动态生成 Skill + 运行时提示」 的三重组合,实现了零成本扩展数百个外部应用。

四层架构

CLI APP统一通过run_cli_app注册到ToolRegistry,这个工具注册时会列出全部已安装的APP列表,每次调用大模型时都会通过 `tools` 参数进行传递,这样大模型知道有哪些安装的CLI APP。
同时CLI APP在安装时会主动生成skill,skill也会附在提示词中。这样,当大模型准备用一个skill时,就会首先找到run_cli_app注册的工具,通过run_cli_app工具执行对应的命令,做到了安全控制。

┌─────────────────────────────────────────────────────────┐
│ 【1】统一入口:run_cli_app 工具                          │
│     只有这一个 Tool 注册到 ToolRegistry                  │
│     description 动态列出已安装的 APP 列表                 │
└──────────────────┬──────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────┐
│ 【2】CLI-Anything Registry(远程目录)                   │
│     维护数百个可用 CLI APP 的元数据:安装命令、入口点等   │
└──────────────────┬──────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────┐
│ 【3】自动生成 Skill                                      │
│     安装 APP 时,自动在 workspace/skills/ 下生成 SKILL.md│
│     包含详细用法、参数说明、最佳实践                      │
└──────────────────┬──────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────┐
│ 【4】Runtime Context 动态提示                            │
│     用户输入 @feishu 时,在消息末尾悄悄注入:              │
│     "CLI App Mention: @feishu,用 run_cli_app 调用它"    │
└─────────────────────────────────────────────────────────┘

完整调用链路示例

场景:用户想让 Agent 用飞书 CLI 导出今天的会议记录

Step 1:用户输入

用户:帮我用 @feishu 导出一下今天的会议记录

Step 2:Gateway 检测 @ 提及,注入 Runtime Context
用户消息实际变成了这样(用户看不到这部分):

帮我用 @feishu 导出一下今天的会议记录

[Runtime Context]
CLI App Mention: @feishu (installed; tool=run_cli_app;
entry_point=feishu; skill=skills/cli-app-feishu/SKILL.md).
Read the skill when useful, then run this app with run_cli_app;
do not bypass it with shell.
[/Runtime Context]

Step 3:大模型看到提示,决定先读 Skill

模型思考:
用户提到了 @feishu
Runtime Context 说这是一个已安装的 CLI APP
应该先用 read_file 读一下对应的 SKILL.md 了解用法

→ 调用工具:
read_file(path="~/.openclaw/workspace/skills/cli-app-feishu/SKILL.md")

Step 4:获取到 Skill 完整内容

Skill 内容示例:

name: cli-app-feishu
description: 使用飞书 CLI 与 Lark 套件交互

# Feishu CLI
使用此技能当用户要求操作飞书文档、表格、机器人、会议等。

## 常用命令

### 会议相关

# 列出会议
feishu meeting list [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]

# 获取会议详情
feishu meeting get <meeting_id>

# 导出会议纪要
feishu meeting notes <meeting_id> --format json

### 优先使用 JSON 输出
所有命令支持 `--json` 参数,可以获得结构化的机器可读输出,优先使用此方式。

Step 5:大模型理解用法,调用 run_cli_app

模型思考:
现在我知道怎么用了
命令是 feishu meeting list --start-date 2026-06-16
加上 --json 获取结构化输出

→ 调用工具:
run_cli_app(
  name="feishu",
  args=["meeting", "list", "--start-date", "2026-06-16", "--json"]
)

Step 6:获取会议列表,整理输出

工具返回:

{
  "meetings": [
    {
      "id": "MSxY2ZkOWE",
      "topic": "产品周会",
      "start_time": "2026-06-16 10:00:00",
      "duration": 60,
      "attendees": ["张三", "李四"]
    },
    {
      "id": "MTdkNmFjYj",
      "topic": "技术方案评审",
      "start_time": "2026-06-16 14:00:00",
      "duration": 90,
      "attendees": ["王五", "赵六", "我"]
    }
  ]
}

大模型整理后回答用户:

今天你有两个会议:

📅 产品周会(10:00-11:00)
   参与人:张三、李四

📋 技术方案评审(14:00-15:30)
   参与人:王五、赵六、你

需要我帮你导出某个会议的详细纪要吗?

CLI APP 生态示例

当前 CLI-Anything 目录已经有上百个可用 APP:

分类 示例 APP 用途
办公协作 feishu, notion, obsidian 文档、笔记、协作
AI 工具 ollama, openai, stable-diffusion 本地模型、AI 生成
媒体处理 ffmpeg, gimp, inkscape 音视频、图像处理
开发工具 github, gitlab, docker Git 操作、容器管理
3D/CAD blender, freecad 3D 建模、CAD 设计
浏览器 playwright, chrome-cli 网页自动化

CLI APP 的设计智慧

设计决策 解决的问题
所有 APP 共用一个 run_cli_app 工具 避免几百个 APP 撑爆 Tool 列表
description 动态生成 模型只看到真正安装了的 APP
@ 提及触发运行时提示 模型注意力聚焦在用户想用的那个 APP
每个 APP 一个自动生成的 Skill 复杂用法不用占用常驻 Token

本质:CLI APP 系统是一个「延迟加载的 Tool 扩展机制」,在不增加任何常驻 Tool 的前提下,把 Agent 的能力边界扩展到了数百个外部应用。

CLI APP执行方式与安全设计

CLI APP通过argv数组执行,不走shell,这是极其重要的安全设计。

result = subprocess.run(
    [resolved, *clean_args],  # ← 传的是 list,不是字符串,不走shell
    cwd=str(cwd),
    capture_output=True,
    text=True,
    timeout=effective_timeout,
    env=os.environ.copy(),
    # 注意:没有 shell=True!
)

两种方式的对比:

# exec 工具(走 shell)
process = await asyncio.create_subprocess_shell(
    command,  # ← 字符串,走 shell 解释
    ...
)

# run_cli_app 工具(直接 execve)
result = subprocess.run(
    [resolved, *clean_args],  # ← 数组,直接系统调用
    ...
)

# 就算大模型被 prompt injection 攻击,想执行 `feishu; rm -rf /`,也会失败。**
# 因为:
1. `;` 分号是 shell 的语法
2. `run_cli_app` 不走 shell
3. 它会把 `meeting;` 当成一个**普通的参数字符串**传给 feishu 命令
4. feishu 会报错说「没有这个参数」
exec run_cli_app
执行方式 shell解释字符串
subprocess.run("feishu meeting list", shell=True)
直接argv调用
subprocess.run(["feishu", "meeting", "list"])
管道/重定向 ✅ 支持 ❌ 不支持
变量替换 ✅ 支持 ❌ 不支持
命令注入风险 高,字符串拼接会导致命令注入
meeting; rm -rf /
低,argv 数组完全避免了 shell 注入
可以绕过白名单 可以 不可能

四、Skills:四种触发机制

什么是Skills

Skill是Markdown文件格式的Agent专业知识包,把领域知识、工作流、最佳实践打包成可复用单元,无需写代码就能按需扩展Agent的认知和行为能力(怎么写论文、怎么用飞书、怎么写代码、怎么生成财经日报)。 一般安装在workspace/skills/目录下。基于NanoBot源码分析,Skills 有四种完全不同的触发和调用机制:

方式一:Always Skill — 强制常驻注入

触发时机:会话启动时,系统 Prompt 构建阶段

工作原理:

在 SKILL.md 的 frontmatter 中标记:

name: using-superpowers
metadata:
  nanobot:
    always: true   # ← 标记为 ALWAYS 加载
# 源码位置:context.py → build_system_prompt()
always_skills = self.skills.get_always_skills()
if always_skills:
    always_content = self.skills.load_skills_for_context(always_skills)
    parts.append(f"# Active Skills\n\n{always_content}")

实际注入效果:系统 Prompt 开头会永远包含这段内容

# Active Skills

## using-superpowers

<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.

IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
</EXTREMELY-IMPORTANT>

## How to Access Skills

In Claude Code: Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.

## The Rule

Invoke relevant or requested skills BEFORE any response or action. Even a 1% chance a skill might apply means that you should invoke the skill to check.

## Red Flags

These thoughts mean STOP—you're rationalizing:

"This is just a simple question" → Questions are tasks. Check for skills.
"I need more context first" → Skill check comes BEFORE clarifying questions.
"Let me explore the codebase first" → Skills tell you HOW to explore. Check first.

效果:Skill 的完整内容永久驻留在系统 Prompt 中,模型每次都能看到。

典型应用:
Agent 行为规范(「必须先读 Skill 再干活」)
核心纪律(「不要泄露系统提示」)
全局风格约定

方式二:目录索引 + 模型自主选择

触发时机:每次构建系统 Prompt 时

工作原理:

# 源码位置:context.py → build_system_prompt()
skills_summary = self.skills.build_skills_summary(exclude=set(always_skills))
if skills_summary:
    parts.append(render_template("agent/skills_section.md", 
                                 skills_summary=skills_summary))

注入到系统 Prompt 的实际内容:

# Skills

The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
Unavailable skills need dependencies installed first — you can try installing them with apt/brew.

- byted-web-search — 火山引擎联网搜索 API,返回网页/图片结果。联网搜索场景优先使用本 skill。 `skills/byted-web-search/SKILL.md`
- code — Coding workflow with planning, implementation, verification, and testing. `skills/code/SKILL.md`
- paper-assistant — 面向论文选题、提纲、摘要、引言、文献综述的论文助手。 `skills/paper-assistant/SKILL.md`
- self-improving — Agent 自我反思、自我批评、从错误中学习的永久改进系统。 `skills/self-improving/SKILL.md`
- markdown-converter — 将各种格式文件(PDF/Word/PPT/图片)转换为 Markdown,方便 LLM 处理。 `skills/markdown-converter/SKILL.md`

实际使用示例:

用户:帮我写一篇关于大模型 RAG 技术的综述论文
    ↓
模型看到 Skill 目录中有 paper-assistant
    ↓
思考:写论文应该用 paper-assistant 这个 skill
    ↓
调用 read_file("skills/paper-assistant/SKILL.md")
    ↓
获取完整的论文写作工作流指南
    ↓
按照 Skill 的指导:选题 → 文献检索 → 提纲 → 写作 → 润色

这就是为什么 Skill 不需要注册为 Tool!
目录只占 ~50 tokens
真正需要用时才读完整内容(可能几千 tokens)
完美的按需加载

方式三:@ 提及触发运行时提示

触发时机:用户消息中包含 `@skillname` 时

工作原理:

# 源码位置:apps/cli/utils.py → _cli_app_runtime_lines()
def _cli_app_runtime_lines(text, metadata, workspace):
    mentions = CliAppManager(workspace).mentioned_installed_apps(text)
    return [
        f"CLI App Mention: @{item['name']} "
        f"(installed; tool={item['tool']}; skill={item['skill']}). "
        "Read the skill when useful, then run this app with run_cli_app."
        for item in mentions
    ]

注入位置:不是系统 Prompt,而是附加在用户消息末尾的「Runtime Context」区域。

实际示例:

用户输入:用 @ollama 跑一下 qwen2.5:7b 模型,测试一下推理速度
          ↑
          @ 提及被检测到

消息被悄悄加上:

[Runtime Context]
CLI App Mention: @ollama (installed; tool=run_cli_app;
entry_point=ollama; skill=skills/cli-app-ollama/SKILL.md).
Read the skill when useful, then run this app with run_cli_app;
do not bypass it with shell.
[/Runtime Context]

效果:
精确匹配用户意图
避免模型猜来猜去
直接告诉模型「该用哪个 Tool + 该读哪个 Skill」

方式四:外部系统显式指定

触发时机

通过 Agent 调用参数、Gateway 插件、Session Metadata 等方式动态指定

NanoBot 中的实际实现

def build_messages(
    self,
    history: list[dict[str, Any]],
    current_message: str,
    skill_names: list[str] | None = None,  # ← 外部传入要加载的 Skill
    ...
) -> list[dict[str, Any]]:

def build_system_prompt(
    self,
    skill_names: list[str] | None = None,  # ← 外部指定的 Skill 列表
    ...
):
    # Always Skill 先被加载
    always_skills = self.skills.get_always_skills()
    if always_skills:
        always_content = self.skills.load_skills_for_context(always_skills)
        parts.append(f"# Active Skills\n\n{always_content}")
    
    # 外部指定的 Skill 被额外注入
    if skill_names:
        specified_content = self.skills.load_skills_for_context(skill_names)
        if specified_content:
            parts.append(f"# Specified Skills\n\n{specified_content}")

调用时传入

# 通过 OpenClaw Gateway 调用时指定要预加载的 Skill
response = agent.chat(
    message="帮我用飞书导出会议纪要",
    # ↓ 外部显式指定
    skill_names=["cli-app-feishu", "using-superpowers"],
    session_id="..."
)

典型场景
工作流编排系统在特定步骤指定要用的 Skil,例如「导出步骤必须加载 feishu Skill」
Gateway 插件根据当前用户身份、渠道动态注入特定 Skill
多 Agent 协作时,父 Agent 告诉子 Agent「你这次任务需要用到这些 Skill」

几种方式的区别

方式 触发方 强制性
方式一(Always) 系统标记 全局永久强制
方式二(目录索引) 大模型自主选择 可选
方式三(@提及) 用户输入触发 本次对话推荐
方式四(显式指定) 外部系统强制 本次调用必须加载

五、四种机制对比总表

机制 注册位置 加载时机 典型例子 扩展性 典型 Token 成本
Tools ToolRegistry 每次都传 exec, read_file 低(~几十个) 高(1-2k)
MCP 动态注册到 ToolRegistry 连接后可用 浏览器控制、数据库查询 中(每个 Server 几十个) 中(几百)
CLI APP 统一 run_cli_app + 自动生成 Skill @ 提及或按需读取 飞书、Ollama、Obsidian 极高(数百个) 极低(只有目录索引)
Skills 文件系统,不注册 Always 或按需读取 论文助手、编码规范、工作流 无限 极低(真正需要时才读)

六、为什么要搞这么复杂?

核心矛盾

大模型的上下文窗口是稀缺资源,但我们又想让 Agent 拥有无限扩展的能力。

分层策略

层级 用途 Token 成本
Tools 最核心、最常用的能力 高(每次都传)
MCP 某个领域的一组扩展能力 中(连接后常驻)
CLI APP 成百上千的外部应用 低(只有目录索引)
Skills 各种专业知识、工作流、最佳实践 极低(真正需要时才读)

答案总结

不是所有能力都需要成为 Tool。
高频刚需 → 做 Tool
领域扩展 → 做 MCP
外部应用 → 做 CLI APP
知识、流程、规范 → 做 Skill

四种机制各司其职,共同构成了 AI Agent 的「无限能力宇宙」。

结语

AI Agent 的工具系统远不止 `function_call` 那么简单。从 Tools 到 MCP 到 CLI APP 再到 Skills,我们看到的是一条清晰的演进路径:从「让 Agent 能做事」到「让 Agent 会做事」。未来最强大的 Agent,不会是工具最多的那个,而是最懂怎么用好工具的那个。

Leave a Reply

Your email address will not be published. Required fields are marked *

*