第2章 智能体使用工具的核心机制

本章你将学到

  • 工具(Tool)到底是什么,为什么描述比代码更重要
  • Function Calling的完整链路——模型如何决定调用工具,代码如何执行,结果如何返回
  • 手写一个最小工具调用循环,使用DeepSeek API,只用一个计算器工具跑通全流程
  • 如何将Harness五步法扩展到工具增强型Agent

本章你将产出:一个极简但完整的工具调用Agent脚本 minimal_agent.py,以及一个理解Function Calling全链路的清晰心智模型
全部章节:收录在专栏《AI应用工程化实战教程》之【智能体工具使用实战】


2.1 什么是工具(Tool)

在第1章结束的时候,你设想了三个函数:read_fileexecute_pythonwrite_file。你想让Agent在需要的时候自己调用它们。

但Agent是一个语言模型,它不“知道”这些函数的存在。它也不能直接执行任何Python代码。要让Agent使用工具,你必须做两件事:

第一,告诉它有哪些工具可用。 你需要用一段描述,让Agent理解每个工具的功能、适用场景、需要哪些参数、返回什么。这段描述会和用户消息一起发送给模型。

第二,当模型说“我想用这个工具”时,由你的代码真正执行它。 模型只负责“做决定”——决定调哪个工具、传什么参数。它不负责执行。执行是你写的Python代码的事。

所以一个工具,在代码层面由两部分组成:

组成部分 谁负责 内容
工具描述(传给模型) 你定义 函数名、用途说明、参数定义(名称、类型、是否必填)
工具实现(Python函数) 你实现 真正的业务逻辑——读文件、执行代码、调用外部API

举个例子。一个最简单的“计算器”工具,它的描述长这样:

calculator_tool = {
    "type": "function",
    "function": {
        "name": "calculator",
        "description": "执行一个数学表达式并返回计算结果。支持加减乘除、幂运算、括号。例如:'2+3*4'、'(15.8+2.3)*4'。",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "需要计算的数学表达式,如 '1+2*3'"
                }
            },
            "required": ["expression"]
        }
    }
}

对应的实现函数长这样:

def calculator(expression: str) -> str:
    """安全地计算数学表达式"""
    try:
        # 使用eval的安全替代方案,或使用简单的表达式解析
        # 这里为了教学,使用安全的数值计算
        result = eval(expression, {"__builtins__": {}}, {})
        return str(result)
    except Exception as e:
        return f"计算错误: {e}"

工具描述是写给AI看的,工具实现是写给Python解释器看的。描述的好坏,直接决定了AI能不能在正确的时机选对工具、传对参数。


2.2 工具描述规范

一个工具描述必须包含以下四个要素。缺了任何一个,模型都可能用错工具。

要素一:名称(name)

用英文,snake_case。这是模型在输出中说“我要调用这个工具”时引用的标识符。例如 read_fileexecute_pythonsend_email。命名要精准描述功能,不要起 tool1func_a 这种名字。

要素二:用途描述(description)

用自然语言写清楚这个工具是干什么的。这是整个描述中最关键的部分——模型就是靠这段话来判断“当前这个任务该不该用这个工具”。

差的描述:"执行代码"
好的描述:"在沙盒中执行一段Python代码,返回标准输出。适用于需要计算、数据处理、文件操作等任务。只支持pandas、numpy、math等安全模块。"

好的描述回答了三个问题:这个工具做什么?什么时候应该用它?它有什么限制?

要素三:参数定义(parameters)

使用JSON Schema格式。每个参数需要指定:

  • type:参数类型(string / number / integer / boolean)
  • description:参数的含义和约束,用自然语言写清楚
  • required:是否必填(在工具定义的最外层有一个 required 列表)

一个示例:

"parameters": {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "需要读取的文件路径,可以是相对路径或绝对路径"
        },
        "encoding": {
            "type": "string",
            "description": "文件编码,默认utf-8",
            "default": "utf-8"
        }
    },
    "required": ["path"]
}

要素四:返回值格式(在描述或函数注释中体现)

模型需要知道调用这个工具之后会拿到什么。你可以在 description 末尾加上:“返回文件内容的字符串”,或者“返回JSON格式的计算结果”。

如果工具可能执行失败,也要说明失败时返回什么。例如:“如果文件不存在,返回错误信息字符串。”


2.3 Function Calling的完整链路

现在我们来看一次完整的工具调用到底经历了哪些步骤。

假设用户说:“帮我算一下 (15.8 + 2.3) × 4 等于多少。”

第一步:用户消息 + 工具列表一起发给模型

你的代码把用户的问题和所有可用工具的JSON描述一起,发给DeepSeek的API。发给模型的messages大概是这样的(简化表示):

messages = [
    {"role": "system", "content": "你是一个助手,可以使用工具来完成任务。"},
    {"role": "user", "content": "帮我算一下 (15.8 + 2.3) × 4 等于多少。"}
]

tools = [calculator_tool]  # 工具列表

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=tools
)

第二步:模型返回“我要用工具”

模型读完你的问题,发现:“这个问题需要做数学计算,而我的工具列表里有一个 calculator 工具,正好能做这个。”于是它不再生成普通文本,而是返回一个 tool_call 指令。

response 中拿到的消息长这样(简化表示):

{
    "role": "assistant",
    "content": null,
    "tool_calls": [
        {
            "id": "call_abc123",
            "type": "function",
            "function": {
                "name": "calculator",
                "arguments": "{\"expression\": \"(15.8 + 2.3) * 4\"}"
            }
        }
    ]
}

注意:contentnull——当模型决定调用工具时,它不生成对话文字,而是生成一个工具调用指令。arguments 是一个JSON字符串,里面的参数完全符合你在工具描述里定义的格式。

第三步:你的代码真正执行工具

模型只是说“我想调 calculator,参数是这串表达式”。真正跑代码的是你。你的代码从 tool_calls 中提取函数名和参数,调用对应的Python函数:

import json

tool_call = response.choices[0].message.tool_calls[0]
func_name = tool_call.function.name      # "calculator"
arguments = json.loads(tool_call.function.arguments)  # {"expression": "(15.8 + 2.3) * 4"}

# 调用真正的函数
result = calculator(**arguments)  # 返回 "72.4"

第四步:把执行结果传回模型

工具执行完了,拿到了结果 "72.4"。但故事还没有结束——模型还不知道这个结果。你必须把执行结果包装成一条消息,追加到对话历史里,再发给模型:

messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": result  # "72.4"
})

第五步:模型根据工具结果生成最终回答

现在再次调用模型,它拿到了工具执行的结果 "72.4",整合后生成最终回答:

(15.8 + 2.3) × 4 = 72.4

这是整个链路的完整循环。用一张图表示:

用户提问
    │
    ▼
模型判断:需要工具吗?
    │
    ├── 不需要 ──→ 直接生成回答 ──→ 结束
    │
    └── 需要
         │
         ▼
    模型返回 tool_calls(工具名 + 参数)
         │
         ▼
    你的代码执行工具函数
         │
         ▼
    工具结果追加到对话历史
         │
         ▼
    再次调用模型,模型整合结果生成回答
         │
         ▼
        结束

这就是 Function Calling 循环。如果模型一次工具调用后觉得还需要再调另一个工具,它会继续返回新的 tool_calls,你的代码继续执行,直到模型觉得“够了”为止。


2.4 手写一个最小工具调用循环

现在我们把这个逻辑写成完整的Python脚本。目标是最小化——只用一个 calculator 工具,让Agent能够执行数学计算。这个脚本会是你后面所有工具增强型Agent的基础骨架。

2.4.1 项目准备

在你的 data-analyst-agent 项目目录下,确保 .env 文件中已经配置了DeepSeek的API Key:

DEEPSEEK_API_KEY=sk-你的密钥

确保已安装依赖:

pip install openai python-dotenv
2.4.2 完整代码:minimal_agent.py

在Trae的AI对话面板中输入以下指令,让Trae帮你生成代码(你也可以直接对照下面的代码手动创建):

在当前项目 data-analyst-agent 中创建一个 minimal_agent.py。
要求:
1. 使用 openai 库调用 DeepSeek API(base_url="https://api.deepseek.com")
2. 从 .env 文件中读取 DEEPSEEK_API_KEY
3. 定义一个 calculator 工具,描述清晰,参数为一个数学表达式字符串
4. 实现工具调用循环:
   - 将用户消息和工具列表发给模型
   - 如果模型返回 tool_calls,则执行对应函数,将结果以 tool 角色追加到 messages
   - 重复直到模型不再返回 tool_calls,打印最终回答
5. 在脚本底部写一个测试:计算 "(15.8 + 2.3) * 4"
6. 每一步在终端打印 log,显示模型返回了什么、工具执行了什么

如果你选择手动写,下面是一个可以参考的示例代码:

"""
minimal_agent.py —— 最小工具调用Agent演示
使用 DeepSeek API,带一个 calculator 工具
"""
import os
import json
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

# 初始化 DeepSeek 客户端(兼容 OpenAI 接口)
client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com"
)

# ============================================================
# 1. 定义工具
# ============================================================

calculator_tool = {
    "type": "function",
    "function": {
        "name": "calculator",
        "description": (
            "执行一个数学表达式并返回计算结果。"
            "支持加减乘除、幂运算、括号。"
            "例如:'2+3*4'、'(15.8+2.3)*4'。"
            "返回值为字符串格式的数字或错误信息。"
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "需要计算的数学表达式,如 '1+2*3'"
                }
            },
            "required": ["expression"]
        }
    }
}

tools = [calculator_tool]

# ============================================================
# 2. 工具实现函数
# ============================================================

def calculator(expression: str) -> str:
    """安全地计算数学表达式"""
    # 允许的命名空间(只允许基本的数学运算)
    allowed_names = {
        "abs": abs, "round": round,
        "min": min, "max": max,
        "pow": pow
    }
    try:
        # 使用 eval 的安全方式:空的内建,限制的命名空间
        result = eval(expression, {"__builtins__": {}}, allowed_names)
        return str(result)
    except Exception as e:
        return f"计算错误: {e}"

# 工具函数名到函数的映射
TOOL_MAP = {
    "calculator": calculator
}

# ============================================================
# 3. 工具调用循环
# ============================================================

def run_agent(user_query: str):
    """运行一次Agent对话,处理可能的工具调用"""

    # 初始消息
    messages = [
        {"role": "system", "content": "你是一个助手,可以使用工具来完成任务。如果用户的问题需要计算,请使用 calculator 工具。"},
        {"role": "user", "content": user_query}
    ]

    print("=" * 60)
    print(f"用户: {user_query}")
    print("=" * 60)

    # 循环:最多10轮,防止死循环
    for turn in range(10):
        print(f"\n--- 第 {turn+1} 轮调用模型 ---")

        # 调用API
        response = client.chat.completions.create(
            model="deepseek-chat",
            messages=messages,
            tools=tools
        )

        msg = response.choices[0].message

        # 如果模型没有要求调用工具,说明它已经生成了最终回答
        if not msg.tool_calls:
            print(f"模型回答: {msg.content}")
            return msg.content

        # 模型要求调用工具
        print(f"模型要求调用 {len(msg.tool_calls)} 个工具")

        # 把模型的响应加入对话历史
        messages.append(msg)

        # 逐一处理每个工具调用
        for tool_call in msg.tool_calls:
            func_name = tool_call.function.name
            arguments = json.loads(tool_call.function.arguments)

            print(f"  → 调用工具: {func_name}({arguments})")

            # 找到对应的函数并执行
            if func_name in TOOL_MAP:
                func = TOOL_MAP[func_name]
                result = func(**arguments)
            else:
                result = f"错误:未知工具 {func_name}"

            print(f"  → 工具返回: {result}")

            # 将工具执行结果追加到对话历史
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(result)
            })

    # 超过最大循环次数
    return "达到最大工具调用轮数,请简化问题。"

# ============================================================
# 4. 测试
# ============================================================

if __name__ == "__main__":
    # 测试1:简单计算
    run_agent("帮我算一下 (15.8 + 2.3) × 4 等于多少")

    print("\n" + "=" * 60)
    # 测试2:多步计算(模型可能需要判断用几次工具)
    run_agent("计算 1024 除以 32,再把结果乘以 7")
2.4.3 运行与观察

在终端中运行:

python minimal_agent.py

你会看到类似以下的输出:

============================================================
用户: 帮我算一下 (15.8 + 2.3) × 4 等于多少
============================================================

--- 第 1 轮调用模型 ---
模型要求调用 1 个工具
  → 调用工具: calculator({'expression': '(15.8 + 2.3) * 4'})
  → 工具返回: 72.4

--- 第 2 轮调用模型 ---
模型回答: (15.8 + 2.3) × 4 = 72.4

观察终端里的每一次API调用。你看到了什么?

  • 第一轮:模型没有输出任何文字,而是返回了一个 tool_calls。它判断“我需要先计算这个表达式”,于是调用了 calculator,传入表达式 "(15.8 + 2.3) * 4"
  • 你的代码执行了工具calculator 函数被调用,eval 执行了表达式,返回字符串 "72.4"。注意,这一步没有任何AI参与——是真正的Python解释器在算。
  • 第二轮:你把工具返回的结果 "72.4" 追加到对话历史,再次调用模型。这次模型拿到了结果,不再需要工具,直接生成了最终回答。

如果你运行第二个测试(1024 / 32 * 7),观察模型的行为。它可能一次性在 expression 里写 "1024 / 32 * 7",直接调一次工具完事;也可能先调一次 "1024 / 32",拿到结果后再调一次 "32 * 7"。两种方式都有可能,取决于模型自身的判断。这正是Function Calling灵活性的体现。


2.5 将Harness五步法扩展到工具增强型Agent

在本专栏的第一部**《AI智能体工程化实战》**中,Harness五步法是为纯推理型Agent设计的:规范定义、数据构建、Agent开发、自动化评测、迭代优化。评测的核心是“Agent的输出文本与黄金标准是否一致”。

现在Agent的行为变了——它不再只是“输出一段文本”,而是“选择工具→调用工具→整合结果→输出文本”。评测的复杂度增加了。你需要回答的新问题包括:

  • Agent选了正确的工具吗?
  • 传给工具的参数对吗?
  • 工具执行之后,Agent正确理解了返回结果吗?
  • 如果工具执行失败,Agent知道怎么处理吗?

因此,五步法在第二部需要做以下扩展:

步骤 第一部(纯推理Agent) 第二部(工具增强型Agent)
Step1 规范定义 定义输出内容的评测维度 新增:工具选择正确性、参数正确性、工具失败时的降级行为
Step2 数据构建 GT只包含最终输出 GT扩展为:{"final_output": "...", "expected_tool_calls": [...]}
Step3 Agent开发 生成Prompt和脚本 生成Prompt + 工具定义 + 工具实现 + 工具调用循环
Step4 自动化评测 评测Agent比对最终输出 评测Agent新增“工具调用审计”模块,比对各步工具调用
Step5 迭代优化 根据最终输出错误修改Prompt 根据工具选择错误、参数错误分别优化工具描述或Prompt

这个扩展版的五步法将在后续章节中逐步落地。在第6章,你会用Trae生成一套完整的工具调用评测体系。现在你只需要记住这个概念框架——当Agent的行为从“说”扩展到“做”时,你的评测也必须从“评说什么”扩展到“评做什么”。


2.6 本章小结

  • 工具(Tool) = 工具描述(给AI看) + 工具实现(给代码执行)。描述的质量决定了AI能不能选对工具。
  • 工具描述四要素:名称、用途描述、参数定义(JSON Schema)、返回值说明。缺一不可。
  • Function Calling完整链路:用户提问 → 模型返回tool_call → 你的代码执行工具 → 结果返回模型 → 模型生成回答。这是一轮循环,多轮循环直到模型不再要求工具。
  • 手写最小实现minimal_agent.py 演示了带一个calculator工具的完整循环。你确认了代码每一步都在做什么。
  • Harness五步法扩展:评测体系需要新增工具调用审计维度——不仅评最终输出,还要评中间每一步工具选择与参数。

下一章,我们将在Trae中搭建真正的数据分析环境,配置DeepSeek API Key,让Agent拥有第一个真正有用的工具——read_file


课后练习

  1. 运行 minimal_agent.py,尝试以下测试:
    • "计算 2024 年 6 月 9 日是星期几" —— Agent会怎么处理?它能用calculator解决吗?
    • "我的账单:午餐45元,晚餐68元,打车32元。帮我算总共花了多少,以及平均每项多少钱。" —— 观察Agent是否会把问题拆成两次计算。
  2. 修改 calculator 工具的描述,把 "支持加减乘除、幂运算、括号" 改成 "只能做加减法"。然后再次测试乘除运算。Agent的行为会有什么变化?为什么工具描述如此重要?
  3. (进阶)在 minimal_agent.py 中增加第二个工具 get_current_time,无参数,返回当前的时间字符串(用Python的 datetime.now())。修改工具列表和 TOOL_MAP,测试Agent能否在需要时选择合适的工具。
  4. (预备思考)如果你要给Agent添加一个 read_file 工具,它的工具描述应该怎么写?写出完整的四要素,我们将在第3章实现它。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐