iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
AI Engineering

從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理系列 第 21 篇

讓 AI 動手:Tool Use 的原理與第一次實作

  • 分享至 

  • xImage
  •  

今天是整個系列的轉捩點。

到目前為止,我們的程式是這樣運作的:我們決定要做什麼,AI 負責處理文字。

從今天開始會反過來:AI 決定要做什麼,我們負責執行。


一、先破除一個誤解

很多人第一次聽到「AI 可以呼叫工具」,會以為 AI 真的有能力執行程式碼、連上網路、讀取你的檔案。

它不能。

模型還是只會做一件事:產生文字。所謂的 Tool Use,實際上是這樣的:

1. 我們告訴模型:「你有這些工具可以用」(工具定義)
2. 模型回應:「我想呼叫 get_weather,參數是 {"city": "臺北市"}」
   ↑ 這只是一段結構化的文字,什麼事都沒發生
3. 【我們的程式】看到這個請求,實際去執行 get_weather("臺北市")
   ↑ 真正做事的是我們
4. 我們把結果傳回給模型:「執行結果是:臺北市多雲,26-31度…」
5. 模型根據結果產生最終回答

**第 3 步永遠是我們的程式在做。**模型只是「說」它想呼叫什麼,執行與否、怎麼執行,完全由我們控制。

這個認知很重要,因為它直接關係到安全性:我們永遠掌握最後的否決權。


二、完整的四步流程

用程式碼看一遍。

第一步:帶著工具定義呼叫模型

import anthropic
import config

client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)

tools = [
    {
        "name": "get_weather",
        "description": (
            "查詢台灣某個縣市未來 36 小時的天氣預報,"
            "包含天氣現象、氣溫範圍、降雨機率與舒適度。"
            "當使用者詢問天氣、要不要帶傘、穿什麼衣服時使用。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "台灣的縣市名稱,例如「臺北市」",
                }
            },
            "required": ["city"],
        },
    }
]

messages = [{"role": "user", "content": "我明天在台北要不要帶傘?"}]

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=4096,
    tools=tools,                # ← 關鍵在這裡
    messages=messages,
)

這個 tools 的格式,就是 Day 15 我們寫的 to_schema() 產生的東西。當初設計的 description 和 input_schema,現在真的派上用場了。

第二步:檢查模型是否要求呼叫工具

print(response.stop_reason)
# tool_use

for block in response.content:
    print(block.type)
# text          ← 有時候會先說一句「我來查一下」
# tool_use      ← 這個就是工具呼叫請求

tool_use 區塊裡有三個東西:

tool_block = [b for b in response.content if b.type == "tool_use"][0]

print(tool_block.id)      # toolu_01A9B... ← 這次呼叫的唯一編號
print(tool_block.name)    # get_weather    ← 要呼叫哪個工具
print(tool_block.input)   # {'city': '臺北市'} ← 參數(已經是 dict)

注意模型自己把「台北」轉成了「臺北市」——因為我們在 description 裡寫了範例。

第三步:執行工具

from tools.weather_tool import WeatherTool

weather_tool = WeatherTool()
result = weather_tool.safe_run(tool_block.input)     # Day 15 寫的

print(result)
# {'ok': True, 'content': '【臺北市】未來 36 小時天氣\n...'}

**這一步完全是我們的程式在跑。**模型不知道、也不參與。

第四步:把結果回傳給模型

這裡有個格式要求,要小心:

# 1. 把模型剛剛的回應(包含 tool_use 區塊)加進歷史
messages.append({"role": "assistant", "content": response.content})

# 2. 把工具結果當作 user 訊息傳回去
messages.append({
    "role": "user",
    "content": [
        {
            "type": "tool_result",
            "tool_use_id": tool_block.id,       # ← 必須對應到原本的 id
            "content": result["content"],
            "is_error": not result["ok"],        # 失敗時標記
        }
    ],
})

# 3. 再呼叫一次模型
final = client.messages.create(
    model="claude-opus-5",
    max_tokens=4096,
    tools=tools,
    messages=messages,
)

print(final.content[0].text)

輸出:

明天台北白天降雨機率 60%,而且是短暫陣雨,建議帶把傘。
氣溫 25–30 度,中午偏悶熱,穿短袖就好,但早晚可以帶件薄外套。

AI 自己決定去查了天氣,然後根據真實資料回答。


三、三個容易踩的坑

坑一:tool_use_id 必須對應

"tool_use_id": tool_block.id      # ✅
"tool_use_id": "toolu_123"        # ❌ 亂填會報錯

模型可能一次要求呼叫多個工具,id 是用來配對的。

坑二:工具結果一定要以 user 角色傳回

雖然直覺上「工具的結果」不是使用者說的話,但 API 的格式就是這樣規定。role 要填 "user"。

坑三:assistant 訊息要放整個 content

messages.append({"role": "assistant", "content": response.content})    # ✅
messages.append({"role": "assistant", "content": "我來查一下"})          # ❌

必須把原始的 content list(包含 tool_use 區塊)整個放回去。只放文字的話,模型就不知道它剛剛要求了什麼。

這是我自己踩過最久的坑,症狀是模型一直重複呼叫同一個工具。


四、平行呼叫多個工具

模型可能在一次回應裡要求多個工具:

messages = [{"role": "user", "content": "幫我看明天台北跟台中的天氣,順便告訴我現在幾點"}]

回應的 content 可能是:

[
    TextBlock(text="我來查一下"),
    ToolUseBlock(id="toolu_01", name="get_weather", input={"city": "臺北市"}),
    ToolUseBlock(id="toolu_02", name="get_weather", input={"city": "臺中市"}),
    ToolUseBlock(id="toolu_03", name="get_current_datetime", input={}),
]

處理方式:全部執行完,把所有結果放在同一則訊息裡回傳:

tool_blocks = [b for b in response.content if b.type == "tool_use"]

results = []
for block in tool_blocks:
    tool = TOOLS[block.name]
    outcome = tool.safe_run(block.input)
    results.append({
        "type": "tool_result",
        "tool_use_id": block.id,
        "content": outcome["content"],
        "is_error": not outcome["ok"],
    })

messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": results})      # ← 全部一起

⚠️ **不要拆成多則訊息傳回去。**這會讓模型以後不敢平行呼叫工具,每次都變成一個一個慢慢來。


五、迴圈:Agent 的核心

上面的例子只跑了一輪。但模型可能在看到第一個工具的結果後,決定再呼叫另一個工具。

例如:

使用者:「幫我安排明天的行程」

第 1 輪:模型呼叫 get_current_datetime(先確認今天是幾號)
第 2 輪:模型呼叫 get_weather(查明天天氣)
第 3 輪:模型呼叫 list_todos(看有什麼事要做)
第 4 輪:模型綜合以上資訊,給出建議

所以我們需要一個迴圈:

def run_agent(client, tools_map, tool_schemas, user_input, max_turns=10):
    """執行 Agent 迴圈,直到模型給出最終回答。"""

    messages = [{"role": "user", "content": user_input}]

    for turn in range(max_turns):
        response = client.messages.create(
            model="claude-opus-5",
            max_tokens=4096,
            tools=tool_schemas,
            messages=messages,
        )

        # 模型講完了,沒有要呼叫工具
        if response.stop_reason != "tool_use":
            return extract_text(response)

        # 有工具要執行
        messages.append({"role": "assistant", "content": response.content})

        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            tool = tools_map.get(block.name)
            if tool is None:
                outcome = {"ok": False, "content": f"沒有名為 {block.name} 的工具"}
            else:
                outcome = tool.safe_run(block.input)
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": outcome["content"],
                "is_error": not outcome["ok"],
            })

        messages.append({"role": "user", "content": results})

    return "抱歉,這個任務太複雜了,我試了很多次還是沒辦法完成。"


def extract_text(response):
    return "\n".join(b.text for b in response.content if b.type == "text").strip()

**這個迴圈就是 Agent 的核心。**整個系列講了 21 天,濃縮起來就是這三十幾行。

max_turns 為什麼一定要有

沒有上限的話,可能發生:

  • 工具一直失敗,模型一直重試
  • 兩個工具互相觸發,形成迴圈
  • 模型誤解任務,無止盡地探索

**每一輪都是一次 API 呼叫,都在花錢。**沒有上限等於沒有煞車。

Day 17 寫的 trace 在這裡就很有用——你可以事後看到它在第幾輪卡住、重複呼叫了什麼。


六、完整範例

把工具接上:

"""agent_v1.py — 第一個真正的 Agent。"""

import logging

import anthropic

import config
from logger_setup import setup_logging
from tools.datetime_tool import DateTimeTool
from tools.todo_tool import AddTodoTool, ListTodosTool
from tools.weather_tool import WeatherTool

logger = logging.getLogger(__name__)

SYSTEM_PROMPT = """你是「小幫」,一個台灣使用者的生活助理。

使用提供的工具取得真實資訊,絕對不要憑印象編造天氣、日期或待辦內容。

工作原則:
- 處理任何跟「今天」「明天」相關的問題前,先呼叫 get_current_datetime
- 需要資訊時主動使用工具,不要反問使用者你自己查得到的事
- 工具失敗時,用白話告訴使用者原因,不要對同一個工具重試超過兩次
- 最後的回答用自然的口語,簡潔具體,三到五句話

你使用繁體中文與台灣用語。"""


def build_tools():
    tools = [
        DateTimeTool(),
        WeatherTool(),
        AddTodoTool(),
        ListTodosTool(),
    ]
    return {t.name: t for t in tools}, [t.to_schema() for t in tools]


def main():
    setup_logging()
    client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
    tools_map, tool_schemas = build_tools()

    print("=== 小幫 v1(有工具了!)===")
    print(f"可用工具:{', '.join(tools_map)}\n")

    while True:
        user_input = input("你:").strip()
        if user_input.lower() in ("quit", "exit"):
            break
        if not user_input:
            continue

        messages = [{"role": "user", "content": user_input}]

        for turn in range(10):
            response = client.messages.create(
                model="claude-opus-5",
                max_tokens=4096,
                system=SYSTEM_PROMPT,
                tools=tool_schemas,
                messages=messages,
            )

            if response.stop_reason != "tool_use":
                text = "\n".join(
                    b.text for b in response.content if b.type == "text"
                ).strip()
                print(f"小幫:{text}\n")
                break

            messages.append({"role": "assistant", "content": response.content})

            results = []
            for block in response.content:
                if block.type != "tool_use":
                    continue
                print(f"  🔧 使用工具 {block.name}({block.input})")
                tool = tools_map.get(block.name)
                outcome = (
                    tool.safe_run(block.input) if tool
                    else {"ok": False, "content": f"沒有名為 {block.name} 的工具"}
                )
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": outcome["content"],
                    "is_error": not outcome["ok"],
                })

            messages.append({"role": "user", "content": results})
        else:
            print("小幫:這個任務太複雜了,我沒能完成。\n")


if __name__ == "__main__":
    main()

實際跑起來:

=== 小幫 v1(有工具了!)===
可用工具:get_current_datetime, get_weather, add_todo, list_todos

你:我明天在台北要不要帶傘?
  🔧 使用工具 get_current_datetime({})
  🔧 使用工具 get_weather({'city': '臺北市'})
小幫:明天(10/6,星期一)台北白天有短暫陣雨,降雨機率 60%,建議帶傘。
     氣溫 25–30 度,中午偏悶熱但早晚舒適,短袖加薄外套就可以。

你:那幫我記一下明天要帶傘
  🔧 使用工具 add_todo({'title': '出門記得帶傘', 'priority': '高'})
小幫:好,已經記下來了。明天出門前記得看一下清單。

你:我現在有什麼事要做?
  🔧 使用工具 list_todos({'only_pending': True})
小幫:目前有 1 筆待辦:出門記得帶傘(高優先度)。

**注意第一個問題——我們只問了一句話,它自己決定要先查日期、再查天氣。**這個決策過程完全沒有寫在我們的程式碼裡。

這就是「自主決策」的雛形。


七、觀察:它「決定」了什麼

回頭看第一輪互動,模型做了這些決定:

決定 依據
要用工具(而不是直接回答) 它知道自己不知道明天的天氣
要先查日期 system prompt 教它的
「台北」→「臺北市」 tool description 裡的範例
查完天氣後就夠了,不用再查別的 它判斷資訊足夠了
回答要提到降雨機率的數字 system prompt 要求「具體」

這五個決定,沒有一個是 if-else 寫死的。

但同時也要注意:這五個決定全部都受我們寫的文字影響。system prompt、tool description、input_schema 的 description——這些都是我們的「程式碼」,只是用自然語言寫的。

這是 Agent 開發跟傳統開發最大的心態差異:

你不是在寫指令,你是在設計一個環境,讓模型在裡面做出好的決定。


八、還缺什麼

今天的 Agent 已經能動,但還很粗糙:

問題 什麼時候解決
工具是手動註冊的,加新工具要改好幾個地方 Day 22
只有單次對話,沒有記憶前面聊過什麼 Day 26
沒有 trace,出事不知道為什麼 Day 22 接上
危險操作沒有確認機制 Day 25
主迴圈的程式碼跟 UI 混在一起 Day 27 重構

明天先處理第一個:建立一個乾淨的工具註冊系統。


小結

  • 模型不會執行工具,它只會「說」它想呼叫什麼;真正執行的永遠是我們的程式
  • 四步流程:帶工具呼叫 → 檢查 stop_reason == "tool_use" → 執行 → 把結果傳回
  • tool_result 要以 user 角色傳回,tool_use_id 必須對應
  • assistant 訊息要放整個 response.content,不能只放文字
  • 多個工具呼叫的結果要放在同一則訊息裡
  • 用迴圈讓模型可以連續呼叫工具,一定要設 max_turns
  • Agent 的「決策」來自 system prompt、tool description、schema description——這些都是用自然語言寫的程式碼

明天:把工具系統做好,讓新增工具變成一件輕鬆的事。


上一篇
讓 AI 回傳程式能用的東西:結構化輸出
下一篇
工具註冊表:讓 Agent 的能力可以隨時擴充
系列文
從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言