乐于分享
好东西不私藏

从零搭一个能用的Agent:让AI学会用工具

从零搭一个能用的Agent:让AI学会用工具

从零搭一个能用的Agent:让AI学会用工具

AI 光会聊天不算 Agent。真正的 Agent,是你说”帮我查一下北京天气”,它真的去查了、查到了、告诉你了。这篇先用裸 API 搞懂原理,再用 CrewAI 框架快速实现。

前言

上一篇选完了框架,这篇正式动手。

但我不会一上来就甩框架代码。如果你不知道底层发生了什么,用框架就是”会开车但不知道发动机怎么转”——能跑,但出了问题不知道去哪修。

所以这篇的路线是:

1.先用裸 API 搭一个最小的 Tool Use Agent——理解核心机制

2.再用 CrewAI 实现同样的功能——感受框架帮你省了什么

3.对比两种方式——你就知道什么时候该用框架,什么时候裸写够了

全程可运行代码,跟着做就能跑。


一、Tool Use 到底是怎么回事

先搞清楚一件事:AI 不会直接执行任何操作。

很多人以为 Tool Use 是”AI 直接调 API”,其实不是。整个过程是这样的:

1.用户提问:”北京明天天气怎么样?”

2.AI 判断:这个问题我自己答不了,但我有一个 get_weather 工具可以用

3.AI 发出请求get_weather(location="北京")——注意,AI 只是它要用这个工具

4.你的代码执行:你写的代码真正去调天气 API,拿到结果

5.结果返回给 AI{"temp": 22, "condition": "晴"}

6.AI 生成回答:”北京明天 22 度,晴天,适合出门。”

AI 是”大脑”,你的代码是”手脚”。 AI 决定做什么,你的代码去执行。这也是 Tool Use 安全的原因——你完全控制哪些工具可用、怎么执行。


二、裸 API 实现:先懂发动机

我们用 Claude 的 API 来做最小实现。选 Claude 是因为它的 Tool Use 设计更清晰——用 stop_reason 明确告诉你”我需要调工具”,而不是让你猜。文末附 OpenAI 版本差异对照。

2.1 环境准备

pip install anthropic

设置 API Key:

# Windows
set ANTHROPIC_API_KEY=sk-ant-你的key

# Mac/Linux
export ANTHROPIC_API_KEY="sk-ant-你的key"

2.2 第一步:定义工具

工具定义就是一份 JSON “说明书”——告诉 AI 你有什么工具、需要什么参数:

tools = [
    {
        "name": "get_weather",
        "description": "获取指定城市的当前天气信息。当用户询问天气相关问题时使用。",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "城市名称,如'北京'、'上海'"
                }
            },
            "required": ["location"]
        }
    }
]

重点description 要写清楚”什么时候用这个工具”。写得越具体,AI 判断越准。

2.3 第二步:写执行函数

工具描述是给 AI 看的,执行函数是给你写的:

import json

def get_weather(location: str) -> str:
    """模拟天气查询(实际项目替换为真实 API)"""
    weather_data = {
        "北京": {"temp": 22, "condition": "晴", "humidity": 45},
        "上海": {"temp": 26, "condition": "多云", "humidity": 72},
        "深圳": {"temp": 30, "condition": "阵雨", "humidity": 85},
    }
    data = weather_data.get(location, {"temp": 20, "condition": "未知", "humidity": 50})
    return json.dumps(data, ensure_ascii=False)

2.4 第三步:完整的 Tool Use 循环

核心代码——AI 判断 → 工具调用 → 结果返回 → AI 回答:

from anthropic import Anthropic
import json

client = Anthropic()

# 工具注册表(名字 → 函数)
tool_registry = {
    "get_weather": get_weather,
}

def chat_with_tools(user_message: str):
    messages = [{"role": "user", "content": user_message}]

    while True:
        # 调用 Claude,带上工具列表
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system="你是一个有用的助手,可以查询天气信息。",
            tools=tools,
            messages=messages,
        )

        # Claude 明确告诉你:我需要调工具
        if response.stop_reason == "tool_use":
            # 把 AI 的回复加入对话历史
            messages.append({"role": "assistant", "content": response.content})

            # 找到 tool_use 块,执行对应函数
            for block in response.content:
                if block.type == "tool_use":
                    func = tool_registry.get(block.name)
                    result = func(**block.input) if func else '{"error": "未知工具"}'

                    # 把工具结果告诉 Claude
                    messages.append({
                        "role": "user",
                        "content": [{
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result
                        }]
                    })
            # 继续循环,让 Claude 处理结果
        else:
            # 不再需要工具,返回最终回答
            return response.content[0].text

# 测试
print(chat_with_tools("北京今天天气怎么样?"))
# → 北京今天晴朗,气温 22°C,湿度 45%,非常适合外出。

print(chat_with_tools("什么是量子力学?"))
# → 量子力学是物理学的一个分支...(没有调用工具)

这段代码大约 40 行。 记住这个数字,等下跟 CrewAI 对比。

Claude 的 Tool Use 有一个优雅的设计:stop_reason == "tool_use" 明确告诉你”我停下来是因为要用工具”。加上 while True 循环,Claude 可以连续调用多个工具——比如你问”北京和上海天气怎么样”,它会连续调两次 get_weather

把这个过程画成时序图,会更直观:

关键点:每次调用 API 时,messages 数组都要把之前的所有历史完整传回去。AI 没有真正的”记忆”,它的”上下文”就是你传给它的 messages。

2.5 完整项目结构

跟着上面跑通后,你的项目长这样——4 个文件就够了

my-first-agent/
├── .env # ANTHROPIC_API_KEY=sk-ant-xxx
├── requirements.txt # anthropic
├── tools.py # 工具函数(执行层)
└── agent.py # 主程序(描述层 + 编排层)

为什么是这个结构?

tools.py 单独放:加新工具只动这个文件,不污染主逻辑

tool_registry 字典分派:加工具就是字典 + 工具描述各加一项,主循环不动

主循环写一次能用while True + stop_reason 处理任意复杂的多轮工具调用

不要 agents/ 子目录、不要 __init__.py:项目还小,别过度组织

扩展时怎么加? 接真实天气 API 只改 tools.py 里函数体;加新工具就 tools.py 加函数 + agent.pytool_registrytools 各加一项。


三、CrewAI 实现:开自动挡

同样的功能——查天气的 Agent,用 CrewAI 怎么写?

3.1 安装

pip install crewai crewai-tools

3.2 定义工具

CrewAI 用装饰器定义工具,比写 JSON Schema 直观得多:

from crewai.tools import tool

@tool("查询天气")
def get_weather(location: str) -> str:
    """获取指定城市的当前天气信息。当用户询问天气时使用。

    Args:
        location: 城市名称,如'北京'、'上海'
    """
    weather_data = {
        "北京": {"temp": 22, "condition": "晴", "humidity": 45},
        "上海": {"temp": 26, "condition": "多云", "humidity": 72},
        "深圳": {"temp": 30, "condition": "阵雨", "humidity": 85},
    }
    data = weather_data.get(location, {"temp": 20, "condition": "未知", "humidity": 50})
    return str(data)

注意:同样的执行逻辑,但不需要手写 JSON Schema。CrewAI 从函数签名和 docstring 自动生成工具描述。

3.3 定义 Agent 和 Task

from crewai import Agent, Task, Crew

# 定义 Agent(角色)
weather_agent = Agent(
    role="天气助手",
    goal="准确回答用户的天气查询",
    backstory="你是一个专业的天气查询助手,能获取各城市的实时天气信息。",
    tools=[get_weather],
    verbose=True # 打印推理过程,方便调试
)

# 定义 Task(任务)
weather_task = Task(
    description="用户问:{query}",
    expected_output="用自然语言回答天气情况,包含温度、天气状况和建议",
    agent=weather_agent,
)

# 组建 Crew(团队)
crew = Crew(
    agents=[weather_agent],
    tasks=[weather_task],
    verbose=True
)

3.4 运行

result = crew.kickoff(inputs={"query": "北京今天天气怎么样?"})
print(result)

输出(verbose 模式会打印推理过程):

> Entering new CrewAgentExecutor chain...
I need to check the weather in Beijing.
Action: 查询天气
Action Input: {"location": "北京"}
Observation: {'temp': 22, 'condition': '晴', 'humidity': 45}
Thought: I now have the weather data for Beijing.
Final Answer: 北京今天天气晴朗,气温 22°C,湿度 45%,适合出门活动。

这段日志其实就是 Agent 的”内心独白”——Thought(思考)→ Action(行动)→ Observation(观察) 的循环,直到收敛到最终答案:

这个 Thought-Action-Observation 模式叫 ReAct(Reasoning + Acting),是 Agent 推理的经典范式。我们在第 3 篇《Agent 怎么想问题》里详细讲过。CrewAI 默认就是这个模式。

CrewAI 版核心代码大约 20 行。 裸 API 版 40 行。框架帮你省了一半。

3.5 完整项目结构

CrewAI 版结构比裸 API 还简单——3 个文件就够了

my-crewai-agent/
├── .env # ANTHROPIC_API_KEY=sk-ant-xxx
├── requirements.txt # crewai + crewai-tools
└── agent.py # 工具 + Agent + Task + Crew 全在一起

为什么比裸 API 还少一个文件? @tool 装饰器把”工具描述”和”工具实现”合并了——不需要单独维护 JSON Schema。

几个 CrewAI 特有的坑要注意:

依赖比较重:首次 pip install 要几分钟(几十个包)。建议用独立虚拟环境,跟裸 API 项目分开

默认走 OpenAI:要用 Claude 必须显式 LLM(model="claude-..."),否则会去找 OPENAI_API_KEY 而报错

verbose=True 是金矿:能看到完整的 Thought → Action → Observation 推理日志,调试必开

不要写 system prompt:CrewAI 用 role + goal + backstory 三件套替代,强行写 system 会冲突


四、对比:框架到底帮你省了什么?

两种方式实现同样的功能,区别在哪?

维度 裸 API CrewAI
工具定义 手写 JSON Schema(容易出错) 装饰器 + docstring(自动生成)
对话循环 自己写 if/while 判断 框架自动处理
工具分派 自己写 if func_name == "xxx" 框架自动根据工具名分派
错误处理 自己加 try-except 框架有内置重试机制
推理过程 看不到(除非手动打印) verbose=True 直接看
核心代码行数 ~40 行 ~20 行
多工具扩展 每加一个工具,要改分派逻辑 tools=[] 列表加就行

框架帮你省的不是”写代码的时间”,而是”出错的概率”。 JSON Schema 手写一个字段名拼错,调试半天。对话循环多一个 append 少一个 append,结果完全不同。框架把这些容易出错的部分标准化了。

那什么时候用裸 API? 当你的 Agent 逻辑极简(就一个工具、一轮对话),或者你需要极致性能(框架有额外开销),裸 API 反而更合适。


五、进阶:多工具 Agent

不管裸 API 还是 CrewAI,真正有用的 Agent 需要多个工具。我们加一个”记笔记”功能。

CrewAI 版(推荐)

from crewai.tools import tool

notes = []

@tool("记笔记")
def add_note(content: str) -> str:
    """当用户要求记录、保存、备忘某些信息时,调用此工具保存笔记。

    Args:
        content: 要记录的笔记内容
    """
    notes.append(content)
    return f"已记录,当前共 {len(notes)} 条笔记"

@tool("查看笔记")
def list_notes() -> str:
    """当用户要求查看、列出已有笔记时调用。"""
    if not notes:
        return "暂无笔记"
    return "\n".join(f"{i+1}. {n}" for i, n in enumerate(notes))

# Agent 直接挂上多个工具
assistant = Agent(
    role="私人助手",
    goal="帮用户查天气、记笔记,做一个贴心的助手",
    backstory="你是用户的私人AI助手,能查天气也能记笔记。",
    tools=[get_weather, add_note, list_notes],
    verbose=True
)

加工具就是往 tools=[] 列表里加——不用改分派逻辑、不用改对话循环。 这就是框架的价值。

AI 会自动判断用户意图:

“北京天气” → 调 get_weather

“帮我记一下明天开会” → 调 add_note

“我记了什么” → 调 list_notes

“1+1等于几” → 不调工具,直接答


六、三个常见坑

不管用哪种方式,这三个坑你大概率会踩:

6.1 工具描述太模糊,AI 不会用

症状:你问”帮我记个事儿”,AI 没调 add_note,而是回复”好的,你要记什么?”

解法:description 写清楚触发条件:

# 模糊 ❌
"""一个笔记工具"""

# 清晰 ✓
"""当用户要求记录、保存、备忘某些信息时,调用此工具保存笔记内容"""

6.2 工具执行出错,AI 编造结果

症状:天气 API 超时,AI 回复了一个编造的天气。

解法:把错误信息明确返回:

@tool("查询天气")
def get_weather(location: str) -> str:
    """..."""
    try:
        data = call_real_api(location)
        return str(data)
    except Exception as e:
        return f"查询失败:{str(e)}"

AI 收到错误信息,会如实告诉用户”天气查询暂时不可用”,而不是编数据。

6.3 工具太多,AI 选错了

症状:你注册了 20 个工具,AI 经常选错或同时调多个不相关的工具。

解法

工具数量建议控制在 10 个以内

工具名和描述要互相区分,避免”记笔记”和”保存笔记”这种歧义

复杂场景考虑分 Agent——每个 Agent 只挂 3-5 个相关工具


七、OpenAI 版本差异速查

如果你用 OpenAI 而不是 Claude,核心逻辑完全一样,只是 API 格式不同:

差异点 Claude(本文主线) OpenAI
安装 pip install anthropic pip install openai
工具参数名 input_schema parameters(外包一层 function
判断是否调工具 stop_reason == "tool_use" msg.tool_calls 非空
返回工具结果 type: "tool_result" role: "tool"
循环方式 while True(天然支持多轮工具调用) 通常两轮(请求→工具→回答)

但如果你用 CrewAI,这些差异完全不用管。 CrewAI 底层自动适配不同模型,你换个 model 名字就行。这是用框架的另一个好处——屏蔽底层差异。


总结

这篇我们用两种方式搭了一个能用工具的 Agent:

裸 API(理解原理):

Tool Use = AI 说”我要用什么工具” + 你的代码去执行

核心是对话循环:用户 → AI 判断 → 工具调用 → 结果返回 → AI 回答

~40 行代码,适合极简场景

CrewAI(框架实现):

工具用装饰器定义,Agent 用角色描述,一键启动

自动处理工具分派、对话循环、推理日志

~20 行代码,适合大多数场景

下一篇预告:给 Agent 装上记忆——用 LangGraph 实现状态管理和长期记忆,让 AI 越用越懂你。


写在最后

这是「Agent系列」的第七篇。从这篇开始,每一篇你都能跑出一个真正能用的东西。

如果你跟着做了,评论区分享你的 Agent 能做什么——看到真实用例我会回复。


往期推荐:

AI Agent到底是什么?别再跟聊天机器人搞混了

AI Agent的大脑长什么样:记忆、推理与工具

Agent怎么想问题:三种思考模式决定它有多聪明

AI Agent不是玩具了:2026年,谁在替你干活?

一个Agent不够用?教它召唤一个团队

Agent框架选型:别挑花了眼,三步选对你的第一个框架


关于作者 | 观渊智刃 & 土苞鱼

专注 AI 工具应用与效率提升,分享 AI 时代的工作方法论。


原创不易,如果觉得有帮助
欢迎点赞、在看、转发支持一下 ❤️