智能体开发实战04|工具调用从零到一

前几篇我们做了三件事:搭了一个 Agent 骨架(第01篇),给它装了『操作系统』------重试、超时、步数限制(第02篇),然后帮它省了 80% 的 Token 费(第03篇)。

但你发现一个问题没?

这个 Agent 到现在还是个『嘴炮选手』------它能说会道,但一件事都干不了。

  • 想查一下实时天气?『抱歉我无法获取实时数据。』
  • 想让它写个文件?『建议您手动操作。』
  • 想从数据库查个东西?『我不具备数据库访问能力。』

这就是没有工具调用的 Agent ------ 只有大脑,没有手脚。

今天这篇,我们给它装上双手。

一、工具调用是什么

工具调用(Tool Calling),在 OpenAI 体系里叫 Function Calling,在 Anthropic 体系里叫 Tool Use。名字不同,本质一样:让大模型能够调用外部函数和 API。

没有工具调用的模型:

markdown 复制代码
用户:北京今天的天气怎么样?
模型:抱歉,我的训练数据截止到2025年,
      无法获取实时天气信息。建议您打开天气App查询。

有工具调用的模型:

css 复制代码
用户:北京今天的天气怎么样?
模型:[调用函数 get_weather(city="北京") → 返回 "晴 22-28°C"]
模型:北京今天晴,气温22到28度,
      适合外出,紫外线中等,建议防晒。

区别不在于模型『知道』什么------而在于模型能不能『行动』。工具调用就是给模型一个能力:它可以在生成回复之前,决定去调用某个函数,拿到结果后再组织回答。

1.1 工作流程

工具调用的完整流程分三步:

  1. 注册函数:定义好函数的名称、参数、用途,告诉模型『你有这些工具可用』
  2. 模型决策:模型分析用户意图,决定『要不要调用工具、调用哪个工具、传什么参数』
  3. 执行-返回:你(代码)去执行这个函数,把结果塞回上下文,模型看到结果后组织最终回复

注意:模型不帮你执行函数。模型只负责『决定该不该调用』和『给出参数建议』。真正执行函数的是你的代码------这是 Agent 架构的精髓:模型是决策者,代码是执行者。

二、从零实现┃Function Calling 实战

我们用 OpenAI 兼容的 API 来做演示。目前 DeepSeek、Qwen、Kimi 都支持 Function Calling,API 接口完全兼容。

2.1 第一步:定义你的第一个工具

工具定义是一个 JSON Schema,告诉模型『这个函数叫什么、做什么用、需要什么参数』:

ini 复制代码
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的实时天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如北京、上海、深圳"
                    },
                    "units": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位",
                        "default": "celsius"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

关键字段说明:

  • name:函数名,唯一标识符,模型用它来『叫』这个函数
  • description:简短描述,告诉模型什么时候用这个函数。很重要------模型的决策全靠这段描述
  • parameters:参数定义,用 JSON Schema 格式,模型按这个结构生成参数
  • required:必填参数列表,没列出来的都是可选

2.2 第二步:实际的函数实现

python 复制代码
# 这是真正执行的函数体
import requests

def get_weather(city: str, units: str = "celsius") -> str:
    """调用天气API获取实时天气"""
    api_key = "your_api_key"
    url = f"https://api.openweathermap.org/data/2.5/weather"
    params = {
        "q": city,
        "appid": api_key,
        "units": "metric" if units == "celsius" else "imperial"
    }
    resp = requests.get(url, params=params)
    data = resp.json()
    
    temp = data["main"]["temp"]
    desc = data["weather"][0]["description"]
    humidity = data["main"]["humidity"]
    
    return f"{city}天气:{desc},温度{temp}°{'C' if units == 'celsius' else 'F'},湿度{humidity}%"

2.3 第三步:完整的调用循环

这是最核心的部分------把模型决策和函数执行串联起来:

ini 复制代码
from openai import OpenAI

client = OpenAI(
    api_key="your-api-key",
    base_url="https://api.deepseek.com/v1"
)

# 工具定义的注册表------名字到函数的映射
function_registry = {
    "get_weather": get_weather
}

messages = [
    {"role": "system", "content": "你是一个智能助手,可以通过工具获取实时信息。"},
    {"role": "user", "content": "北京今天天气怎么样?适合去公园吗?"}
]

# 第一轮调用
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=tools,  # 传入工具定义
    tool_choice="auto"  # 让模型自动决定是否调用
)

# 检查模型是否想要调用工具
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
    # 模型决定调用了!
    for tool_call in tool_calls:
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)
        
        # 在注册表中找到函数并执行
        func = function_registry[func_name]
        result = func(**func_args)
        
        # 把结果塞回消息列表
        messages.append(response.choices[0].message)
        messages.append({
            "tool_call_id": tool_call.id,
            "role": "tool",
            "name": func_name,
            "content": str(result)
        })
    
    # 带着工具结果再调一次模型,让它组织最终回复
    final_response = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages
    )
    print(final_response.choices[0].message.content)
else:
    # 模型没调用工具,直接输出
    print(response.choices[0].message.content)

这个循环模式(模型的回复 → 检查工具调用 → 执行 → 返回结果 → 再调模型)是所有 Agent 框架的底层模式------OpenAI SDK、LangChain、AutoGen、Claude Tool Use 全都是这个逻辑。

三、高级模式┃多工具协作

一个工具不够用?那我们就给它一组工具。多工具协作是 Agent 真正的能力所在。

3.1 多工具注册

css 复制代码
tools = [    {        "type": "function",        "function": {            "name": "web_search",            "description": "搜索互联网获取最新信息",            "parameters": {                "type": "object",                "properties": {                    "query": {"type": "string", "description": "搜索关键词"}                },                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "read_webpage",
            "description": "读取指定URL的页面内容",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "网页URL"}
                },
                "required": ["url"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "save_to_file",
            "description": "将内容保存到本地文件",
            "parameters": {
                "type": "object",
                "properties": {
                    "filename": {"type": "string", "description": "文件名"},
                    "content": {"type": "string", "description": "文件内容"}
                },
                "required": ["filename", "content"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "run_code",
            "description": "执行Python代码并返回结果",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string", "description": "Python代码"}
                },
                "required": ["code"]
            }
        }
    }
]

# 注册表也相应扩展
function_registry = {
    "web_search": web_search_func,
    "read_webpage": read_webpage_func,
    "save_to_file": save_to_file_func,
    "run_code": run_code_func
}

有了这组工具,你的 Agent 就能做这样的事:

场景示例:一个科研助手

ini 复制代码
用户:帮我查一下最新的Agent框架对比,整理一下保存到文件。

第1步:模型调用 web_search(query="2025年 AI Agent框架对比")
第2步:查看搜索结果,发现几个关键文章
第3步:调用 read_webpage(url=...) 逐篇阅读
第4步:调用 save_to_file(filename="agent_frameworks_对比.md", content=...)

最终回复:已为您整理好5个主流Agent框架的对比,
          已保存到 agent_frameworks_对比.md

注意:模型不是一次性调用所有工具,而是一步一步推演------每步调用一个工具,看到结果后再决定下一步。这就是 Agent 的「思考链条「。

3.2 并行工具调用

很多场景下,多个工具调用没有依赖关系,可以同时执行。DeepSeek 和 OpenAI 都支持并行工具调用:

python 复制代码
# 模型可能在一个回复中请求多个工具调用
# 举例:对比三家公司的财报
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=tools
)

tool_calls = response.choices[0].message.tool_calls
# tool_calls 可能是数组,每个独立

import asyncio

async def execute_tool_calls(tool_calls, registry):
    """并行执行无依赖的工具调用"""
    async def execute_one(tc):
        func = registry[tc.function.name]
        args = json.loads(tc.function.arguments)
        result = await func(**args)
        return {
            "tool_call_id": tc.id,
            "role": "tool",
            "name": tc.function.name,
            "content": str(result)
        }
    
    # 全部并行执行
    results = await asyncio.gather(*[
        execute_one(tc) for tc in tool_calls
    ])
    return results

并行调用可以大幅缩短多步骤任务的执行时间。比如查询三个城市的天气,串行要6秒,并行只要2秒。

四、实战┃在 Agent 框架中集成工具系统

前几篇我们一直在搭 Agent 框架,现在是时候把工具系统集成进去了。先回顾一下我们已有的 Agent 结构:

python 复制代码
class Agent:
    def __init__(self, model="deepseek-chat"):
        self.model = model
        self.tools = []       # 工具定义列表
        self.registry = {}    # 函数注册表
        self.history = []     # 对话历史
        self.max_steps = 10   # 最大执行步数
    
    def register_tool(self, name: str, fn, schema: dict):
        """注册一个工具"""
        schema["name"] = name
        self.tools.append({
            "type": "function",
            "function": schema
        })
        self.registry[name] = fn
    
    async def run(self, user_input: str):
        messages = [
            {"role": "system", "content": self.system_prompt},
            *self.history[-10:],  # 仅保留最近10轮
            {"role": "user", "content": user_input}
        ]
        
        for step in range(self.max_steps):
            response = await self._call_llm(messages)
            msg = response.choices[0].message
            
            if not msg.tool_calls:
                # 模型没调用工具,直接输出
                self.history.append({"role": "user", "content": user_input})
                self.history.append({"role": "assistant", "content": msg.content})
                return msg.content
            
            # 模型调用了工具
            messages.append(msg)
            for tc in msg.tool_calls:
                fn = self.registry.get(tc.function.name)
                if not fn:
                    result = f"错误:未注册的函数 {tc.function.name}"
                else:
                    try:
                        args = json.loads(tc.function.arguments)
                        result = await fn(**args)
                    except Exception as e:
                        result = f"执行错误:{str(e)}"
                
                messages.append({
                    "tool_call_id": tc.id,
                    "role": "tool",
                    "name": tc.function.name,
                    "content": str(result)[:5000]  # 限制结果长度
                })
        
        return "已达到最大执行步数,任务未完成。"

关键设计要点:

  • 步数限制:防止工具调用死循环(比如搜索-看到结果-再搜索-再看到结果-永远没完)
  • 结果长度限制:工具返回的内容可能非常大,超出上下文窗口前果断截断
  • 异常处理:工具可能抛异常,不要让整个 Agent 崩溃
  • 历史管理:每轮对话后保留历史,但只保留最近 N 轮(接第03篇的上下文压缩)

4.1 实用工具库

下面这几个工具是 Agent 的「标配「,几乎每个 Agent 都用得上:

python 复制代码
tools_library = {
    # 1. 网络搜索
    "web_search": {
        "fn": lambda q: requests.get(
            f"https://api.duckduckgo.com/?q={q}&format=json"
        ).json(),
        "schema": {
            "description": "在互联网上搜索信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"}
                },
                "required": ["query"]
            }
        }
    },
    
    # 2. 网页读取
    "read_url": {
        "fn": lambda url: requests.get(url).text[:10000],
        "schema": {
            "description": "读取指定URL的文本内容",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string"}
                },
                "required": ["url"]
            }
        }
    },
    
    # 3. 文件操作
    "read_file": {
        "fn": lambda path: open(path, "r").read(),
        "schema": {
            "description": "读取本地文件内容",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "文件路径"}
                },
                "required": ["path"]
            }
        }
    },
    "write_file": {
        "fn": lambda path, content: (
            open(path, "w").write(content), f"已保存到 {path}"
        )[1],
        "schema": {
            "description": "将内容写入本地文件",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"}
                },
                "required": ["path", "content"]
            }
        }
    },
    
    # 4. 代码执行
    "python_exec": {
        "fn": exec_sandboxed_python,  # 安全沙箱
        "schema": {
            "description": "在安全的沙箱中执行Python代码",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string"}
                },
                "required": ["code"]
            }
        }
    }
}

安全提醒:代码执行工具(python_exec)一定要跑在沙箱里,永远不要在宿主环境直接 exec 模型生成的代码。可以用 Docker 容器、pyodide 沙箱或者子进程加限制。

五、踩坑指南┃工具调用的 10 个坑

实战中,工具调用的坑比想象的多。我把自己踩过的坑都列出来:

5.1 一个经典的死循环处理

python 复制代码
# 检测重复调用------防止死循环
class ToolCallTracker:
    def __init__(self, max_repeats=3):
        self.call_history = []
        self.max_repeats = max_repeats
    
    def track(self, name: str, args: dict) -> bool:
        """跟踪工具调用,检测是否陷入死循环"""
        self.call_history.append((name, str(args)))
        
        # 检查最近N次是否都是同一个工具调用
        recent = self.call_history[-self.max_repeats:]
        if len(recent) >= self.max_repeats:
            # 检查是否全是同一个调用
            first = recent[0]
            if all(c == first for c in recent):
                return False  # 死循环,需要干预
        return True
    
    def force_stop(self, messages, step_info):
        """强制打断并注入上下文"""
        messages.append({
            "role": "system",
            "content": f"你已连续重复调用工具 {self.call_history[-1][0]} {self.max_repeats} 次。"
                        f"请分析已获得的信息,直接回答用户问题,不要继续调用工具。"
        })
        return messages

六、对比┃各家工具调用的差异

不同厂商的工具调用接口有细微差别,但核心模式相同。这里总结关键差异:

建议的做法:统一用 OpenAI 兼容接口格式,这样切换模型只需要改 base_url 和 api_key,不需要改工具定义。

七、最佳实践总结

  1. 工具描述要精确:『搜索互联网获取最新信息』比『帮助用户搜索信息』好得多
  2. 参数尽量少:最少原则,能不传的参数就别定义
  3. 设置步数限制:永远加 max_steps,Agent 不是永动机
  4. 避免返回过大的结果:工具返回数据要精炼,不是全部 dump
  5. 加检测机制:检测循环调用、幻觉函数名、参数解析失败
  6. 用标准接口:OpenAI 兼容格式,方便切换模型
  7. 函数体要健壮:try/except 保底,不要让 Agent 因为工具异常而崩溃
  8. 安全第一:代码执行要沙箱,文件操作要限制路径

八、下期预告

  • 第05篇: Agent 记忆系统------从『转头就忘』到『过目不忘』
  • 第06篇: Multi-Agent 模式------把 Agent 变成团队
  • 第07篇: Skills 工程------如何给 Agent 批量安装技能

提示:工具调用是 Agent 从『聊天的』变成『干活的』最关键的一步。

没有工具的 Agent 就像一个超级聪明但手脚被绑住的人------他能理解你的每一个字,知道该怎么做,但就是做不了。而有了工具,你的 Agent 才能真正『动手』。

很多人在搭 Agent 的时候,花大把时间写 Prompt,却忽略了工具定义的质量。实际上,工具定义的精度直接决定了 Agent 的能力上限。工具是 Agent 的接口,接口质量 = 能力上限。

最后提醒一句:工具不是越多越好。5个精心设计的工具,强过50个随意定义的。

下一篇,我们来解决 Agent 的『失忆症』------记忆系统。


提示 :本文由 码农大坚果 出品,欢迎转发分享,转载请注明出处。

参考: OpenAI Function Calling 文档、DeepSeek API 文档、Claude Tool Use 指南、知识库 concepts/harness-paradigm | 整理 by 码农大坚果

相关推荐
guyoung2 小时前
BoxAgnts 运行时(4)——要能力安全,不要 Root 权限
agent·ai编程
码农大坚果2 小时前
智能体开发实战05|记忆系统实战
agent
DylanlZhao2 小时前
Superpowers 原理探析
agent·ai编程·claude
YDS8292 小时前
DeepSeek RAG&MCP + Agent智能体项目 —— 动态决策策略的接口对接
java·spring boot·ai·agent·spring ai·deepseek
yichudu3 小时前
autoResearch 官方项目复现笔记
agent
花椒技术3 小时前
AI 代码评审落地实践:GitLab 接入、项目规则与反馈闭环
后端·github·agent
阿里云云原生4 小时前
AI Agent 进入生产深水区:如何破解 Token 成本黑洞与排障难题?
人工智能·阿里云·agent·云监控
vivo互联网技术5 小时前
把输入框变成 AI 的“超级入口”(ProseMirror 全流程实战)
前端·agent