🛠️ Day 2 · Tool Use(工具调用)

预计时间:2-3 小时 · 难度:⭐⭐ · 目标:让 LLM 学会调用你写的函数,理解 Agent 的心脏 — tool_callstool_result 循环
📖 今日路线

为什么需要 Tool Use?

回想 Day 1 的对话机器人 —— 你问它"现在几点?",它只能瞎猜(因为 LLM 训练数据里没有当下时间)。

Tool Use 让 LLM 不再只是"文字生成器",而是能:

💡 本质:LLM 依然只会"生成文本",但当你告诉它"这些工具你可以用"时,它会生成一种结构化的调用请求(JSON 格式)。你的代码接住这个请求 → 执行 → 把结果告诉 LLM → LLM 继续。

核心概念:4 个角色的对话

Day 1 你用了 3 种 role:systemuserassistant。Day 2 引入第 4 种:tool

Role谁在说话内容
system你(开发者)身份设定、行为规范
user用户提问
assistantLLM文本回答 tool_calls(调用工具的请求)
tool你的代码工具执行结果(回传给 LLM)

一次完整的工具调用长这样:

轮次 1:
  [system]    你是助手,可以用 get_current_time 工具查时间。
  [user]      现在几点?
  [assistant] tool_calls: [{id: "call_1", name: "get_current_time", args: {}}]
              ↑ 注意这里没有 content!LLM 说"我要用工具"

  你的代码执行 get_current_time() → 返回 "2026-07-12 12:30:00"

轮次 2(同一次对话):
  [tool]      call_1 的结果:2026-07-12 12:30:00
  [assistant] 现在是 2026 年 7 月 12 日中午 12 点半。
              ↑ 现在有 content 了,是最终回答
⚠️ 关键理解:一次"用户提问"可能需要多轮 LLM 调用才能完成。第一轮 LLM 决定"要用工具",你执行完,第二轮 LLM 才给出最终答案。所以要写一个 while 循环,直到 LLM 不再要求调用工具为止。

✅ 任务清单


Step 1 · 环境准备

cd ~/Documents/practice/agents

# 建 day2 目录(复用 day1 的 .env 是常见做法,但我们各建各的更清晰)
mkdir -p day2
cd day2

# 复用之前的虚拟环境?也可以每天一个
python3 -m venv venv
source venv/bin/activate

# 依赖跟 Day 1 一样
pip install openai python-dotenv

把 Day 1 的 .env 拷过来(同一个 API Key 就行):

cp ../day1/.env .env

Step 2 · 理解 tools schema(重点!)

要让 LLM 知道"你可以用哪些工具",得用一种它认识的格式描述。这个格式叫 tools schema,本质是 JSON Schema。

示例:定义一个"查时间"工具

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_time",       # 工具名 → LLM 会用这个名字调用
            "description": "获取当前的日期和时间,返回 ISO 格式字符串。",
            # ↑ 这个描述极其重要!LLM 靠它判断"什么时候该用这个工具"
            "parameters": {
                "type": "object",
                "properties": {
                    "timezone": {
                        "type": "string",
                        "description": "时区,例如 'Asia/Shanghai'。默认为本地时区。"
                    }
                },
                "required": []   # 哪些参数必填
            }
        }
    }
]
💡 Java/Go 类比

三个 description 写作原则

  1. 写清楚"什么时候用":不是描述工具做什么,而是描述使用场景
  2. 参数示例:让 LLM 知道格式
  3. 返回值格式:LLM 需要知道怎么解读结果

Step 3 · Demo 1 · 看 LLM 怎么"决定用工具"

先写一个不执行的 demo —— 只观察 LLM 返回的 tool_calls 长什么样。这一步很关键,让你亲眼看到"LLM 是怎么表达'我想调用工具'的"。

day2/demo1_tool_basics.py(我已经建好,直接跑即可)。

预期观察

=== 问题 1:现在几点? ===

LLM 返回的 assistant 消息:
  content: None                       ← 注意:没有文字回答!
  tool_calls: [
    {
      id: "call_abc123",
      type: "function",
      function: {
        name: "get_current_time",
        arguments: '{"timezone": "Asia/Shanghai"}'
        ↑ 注意:arguments 是字符串(JSON string),不是 dict!
      }
    }
  ]

=== 问题 2:你好,介绍下自己 ===

LLM 返回的 assistant 消息:
  content: "你好!我是..."           ← 这次有文字回答
  tool_calls: None                    ← 没调工具(因为不需要)

3 个关键观察点

  1. 要用工具时,content = Nonetool_calls 有值
  2. 不需要工具时,正常回答文本
  3. argumentsJSON 字符串,不是 dict,用之前要 json.loads()

Step 4 · Demo 2 · 完整的 Agent Loop(今日重点)

现在写一个真正的 Agent:不但会调用工具,还能把结果回传给 LLM,让 LLM 继续对话。

核心 while 循环

while True:
    # 1. 调 LLM
    response = llm(messages, tools=tools)
    msg = response.choices[0].message

    # 2. 把 assistant 消息加进历史(无论有没有 tool_calls 都要加!)
    messages.append(msg)

    # 3. 如果 LLM 没要求调工具,任务完成
    if not msg.tool_calls:
        print(msg.content)
        break

    # 4. 有 tool_calls:逐个执行,把结果作为 role="tool" 消息加进历史
    for tool_call in msg.tool_calls:
        result = execute_tool(tool_call.function.name, tool_call.function.arguments)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,   # 必须!用来匹配是哪次调用的结果
            "content": str(result),
        })

    # 5. 回到循环开头,让 LLM 拿着 tool 结果继续
⚠️ 三个必错的点
  1. 忘了把 assistant 消息(含 tool_calls)加进 messages —— 下轮 LLM 会说"我不知道你在说啥"
  2. 忘了 tool_call_id —— LLM 不知道你回的是哪次调用
  3. arguments 忘了 json.loads() —— 类型错误

我们的两个工具

工具作用参数
get_current_time返回当前时间
read_file读取本地文件path: 文件路径

测试问题

# 问题 1:需要调用一个工具
你: 现在几点?

# 问题 2:需要调用另一个工具
你: 帮我看看 sample.txt 里写了什么?

# 问题 3:需要连续调用两个工具(真正的 Agent 感觉!)
你: 帮我读一下 sample.txt,然后告诉我现在几点。

# 问题 4:不需要工具
你: 你好,介绍下自己

🎯 Step 5 · 小挑战(选做)

给 Agent 加一个新工具:

list_files(directory):列出某个目录下的所有文件

然后问 Agent:"当前目录下有哪些文件?帮我读一下最小的那个。"

观察 Agent 会不会:

  1. 先调 list_files 拿到文件列表
  2. 思考"哪个最小"(可能它需要每个都读一下,或者猜)
  3. 决定读哪个
💡 这个挑战会让你看到 Agent "自主规划多步任务" 的能力。有时候它会做出让你惊讶的选择!

🕳️ 今日踩坑指南

症状解决
tool_calls 没加到 messages下轮 LLM 说"我没有调过工具"确保每次 messages.append(msg),包括有 tool_calls 的
arguments 直接当 dict 用TypeError 或参数错乱json.loads(tool_call.function.arguments)
tool_call_id 缺失API 400 报错role=tool 的消息必须带 tool_call_id
无限循环LLM 反复要求调同一个工具加最大轮次限制(比如 10 轮)
工具报错让 Agent 崩溃抛出 Python 异常工具内部 try/except,把错误消息作为结果返回给 LLM,让它自己判断
LLM 幻觉调用不存在的工具tool_call 里的 name 你没定义execute_tool 里判断,返回错误消息给 LLM

✅ 验收标准

收工前,你应该能:

  1. ✅ 说出 tools schema 里 3 个字段的作用(name / description / parameters)
  2. ✅ 画出一次完整对话的 messages 流:user → assistant(tool_calls) → tool → assistant(content)
  3. ✅ Demo 2 跑通,能连续调用多个工具
  4. ✅ 理解为什么要 while 循环而不是一次调用

📦 收工提交

cd ~/Documents/practice/agents

git status                            # 确认 .env 不在里面
git add day2/
git commit -m "Day 2: Tool Use 工具调用(观察 + 完整 Agent Loop)"
git push

🎉 明天预告:Day 3

Day 3 会引入 RAG(检索增强生成) —— 让 Agent 能"读"你的私人笔记。

你会学到: