前面几课的 Claude 只能"说话"。但做研究需要它能搜网页、写文件、跑代码。这节课教你怎么给 Claude"装手装脚"。

第5课:给AI装工具 —— Tools 和 MCP

本课目标

前面几课的 Claude 只能"说话"。但做研究需要它能搜网页、写文件、跑代码。这节课教你怎么给 Claude"装手装脚"。


什么是 Tool(工具)?

打个比方:

  • Claude 本身 = 一个超聪明的大脑
  • Tool = 大脑的手脚和感官

没有工具的 Claude 只能靠自己的知识回答问题。有了工具,它可以:

工具名 能干嘛 研究平台里谁用
WebSearch 搜索互联网 研究员
WebFetch 打开网页读内容 研究员
Write 创建/写入文件 研究员、分析师、报告员
Read 读取文件内容 分析师、报告员
Glob 搜索文件(按名字模式) 分析师、报告员
Bash 执行命令行命令 分析师(跑 Python 画图)
Task 派活给子 Agent 组长

给 Claude 装上内置工具

code
"""
05_builtin_tools.py
让 Claude 使用内置工具
"""
import anyio
from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions,
    AssistantMessage, TextBlock, ToolUseBlock
)


async def main():
    options = ClaudeAgentOptions(
        system_prompt="你是一个研究助手,会用工具搜索最新信息。",
        allowed_tools=["WebSearch", "Read", "Write"],  # 允许用这些工具
        permission_mode="acceptEdits",  # 自动批准文件编辑
        max_turns=3,
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("搜索一下 2025 年全球AI芯片市场规模,把结果保存到 ai_chips.md")

        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(f"[文字] {block.text}")
                    elif isinstance(block, ToolUseBlock):
                        print(f"[工具调用] {block.name}({block.input})")


anyio.run(main)

关键参数:

  • allowed_tools — 白名单,只有列在这里的工具 Claude 才能用
  • permission_mode — 权限模式:
  • "default" — 每次用工具都要你手动批准(安全但麻烦)
  • "acceptEdits" — 自动批准文件编辑(方便)
  • "bypassPermissions" — 跳过所有权限检查(研究平台用这个)

运行后你会看到 Claude 主动去搜索网页,然后把结果写到文件里。这就是工具的力量。


观察工具调用过程

Claude 调用工具时,消息流是这样的:

code
1. AssistantMessage (ToolUseBlock: WebSearch "AI芯片市场规模 2025")
   ↑ Claude 决定搜索

2. UserMessage (ToolResultBlock: 搜索结果...)
   ↑ 工具执行完成,结果返回给 Claude

3. AssistantMessage (ToolUseBlock: Write "ai_chips.md")
   ↑ Claude 决定把结果写入文件

4. UserMessage (ToolResultBlock: 写入成功)
   ↑ 文件写入完成

5. AssistantMessage (TextBlock: "我已经搜索并保存了...")
   ↑ Claude 汇报完成

每一次工具调用就是一个"轮次"(turn),这就是为什么 max_turns 要设大一点,不然 Claude 用一两个工具就到上限了。


自定义工具(MCP Server)

内置工具不够用?你可以自己写工具给 Claude 用!

比如,做一个查询数据库的工具:

code
"""
05_custom_tool.py
自定义工具示例
"""
import anyio
from claude_agent_sdk import (
    tool, create_sdk_mcp_server,
    ClaudeSDKClient, ClaudeAgentOptions,
    AssistantMessage, TextBlock
)


# 用 @tool 装饰器定义一个工具
@tool(
    name="lookup_stock",
    description="查询股票的最新价格和涨跌幅",
    input_schema={"ticker": str}  # 参数:股票代码
)
async def lookup_stock(args):
    """这里正常应该调真实API,这里用假数据演示"""
    ticker = args["ticker"]

    # 模拟数据库查询
    fake_data = {
        "TSLA": {"price": 248.50, "change": "+3.2%"},
        "NVDA": {"price": 892.30, "change": "+1.8%"},
        "AAPL": {"price": 195.60, "change": "-0.5%"},
    }

    data = fake_data.get(ticker.upper(), {"price": "未知", "change": "未知"})

    return {
        "content": [
            {"type": "text", "text": f"{ticker}: 价格 ${data['price']},涨跌 {data['change']}"}
        ]
    }


async def main():
    # 创建 MCP 服务器,把工具注册进去
    server = create_sdk_mcp_server(
        name="stock-tools",
        version="1.0.0",
        tools=[lookup_stock]
    )

    options = ClaudeAgentOptions(
        system_prompt="你是一个股票分析助手。用 lookup_stock 工具查询股价。",
        mcp_servers={"stocks": server},          # 注册自定义工具
        allowed_tools=["mcp__stocks__lookup_stock"],  # 允许使用
        max_turns=3,
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("帮我查一下特斯拉和英伟达的股价")

        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)


anyio.run(main)

自定义工具的套路:

  1. @tool(name, description, input_schema) 装饰一个 async 函数
  2. 函数接收 args 字典,返回固定格式的结果
  3. create_sdk_mcp_server() 打包成服务
  4. 通过 mcp_servers 参数注册到 client
  5. allowed_tools 里用 mcp__服务名__工具名 格式允许

mcp__服务名__工具名 是怎么拼出来的?

回头看代码里的两个地方:

code
# 第一处:注册 MCP 服务器时的 key → 就是"服务名"
mcp_servers={"stocks": server}
#             ^^^^^^
#             这个 key 就是服务名

# 第二处:@tool 装饰器里的 name → 就是"工具名"
@tool(name="lookup_stock", ...)
#          ^^^^^^^^^^^^
#          这个 name 就是工具名

所以拼起来就是:mcp__stocks__lookup_stock

code
mcp  __  stocks  __  lookup_stock
 ↑        ↑              ↑
固定前缀  服务名           工具名
         (mcp_servers    (@tool 的
          的 key)         name 参数)

注意是双下划线 __,不是单下划线。

如果你有多个服务、每个服务有多个工具,命名就像这样:

code
mcp_servers={
    "stocks": stock_server,    # 股票服务
    "weather": weather_server, # 天气服务
}

allowed_tools=[
    "mcp__stocks__lookup_stock",     # 股票服务 → 查股价工具
    "mcp__stocks__get_history",      # 股票服务 → 查历史工具
    "mcp__weather__get_forecast",    # 天气服务 → 查天气工具
]

这样设计的好处是:服务名起到命名空间的作用。即使两个服务都有叫 search 的工具,通过服务名也能区分开——mcp__stocks__searchmcp__weather__search 是两个不同的工具。

这就是 MCP(Model Context Protocol)的核心思想:给 AI 一个标准化的"接口"来调用外部功能。你的数据库、API、内部系统,都可以通过这种方式暴露给 Claude。


在研究平台里,工具是怎么分配的?

每个角色拿到的工具不一样,很像公司里不同岗位有不同的权限:

bash
# 组长:只能派活,不能自己干活
lead_agent_tools = ["Task"]

# 研究员:搜索 + 写笔记
researcher_tools = ["WebSearch", "Write"]

# 数据分析师:读文件 + 跑代码 + 写结果
data_analyst_tools = ["Glob", "Read", "Bash", "Write"]

# 报告撰写人:读所有材料 + 跑代码生成PDF + 用技能指导
report_writer_tools = ["Skill", "Write", "Glob", "Read", "Bash"]

为什么要这样分?最小权限原则——每个角色只拿到它需要的工具,防止越权操作。研究员不需要跑代码,所以不给 Bash;组长不需要搜索,所以不给 WebSearch。


本课小结

  1. 工具让 Claude 从"只会说"变成"能动手"
  2. allowed_tools 控制能用哪些工具
  3. permission_mode 控制是否需要人工审批
  4. 自定义工具 通过 @tool + create_sdk_mcp_server() 创建
  5. 不同角色分配不同工具,这是研究平台的设计哲学

课后练习

  1. 让 Claude 用 Bash 工具执行 ls 命令,看看它能不能列出当前目录
  2. 写一个自定义工具 get_weather,返回假的天气数据,让 Claude 调用它
  3. 试试只给 Claude Read 工具(不给 Write),然后让它写文件,看看会怎样

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录