FDEAI 工程實戰課AI Engineering for FDEs
中級篇 · 第 4 章Intermediate · Chapter 4

工具呼叫:查個人資料Tool calls: personal data

「我今年還剩幾天特休?」手冊只寫規定,寫不出你個人的餘額。這種問題要查人事系統。這一章讓 AI 可以「請你的程式幫它查」,這個機制叫工具呼叫(tool calling)。

"How many days of annual leave do I have left?" The handbook states rules, not your personal balance; that needs the HR system. This chapter lets the AI ask your code to look things up, a mechanism called tool calling.

⏱ 25 分鐘min🧪 tools.py · bot.py

這章要加的功能What this chapter adds

  • 兩個工具:查特休餘額、查今天日期。
  • 工具迴圈:AI 要用工具 → 你的程式執行 → 結果交回 → AI 繼續回答。
  • 三道防護:輪數上限、錯誤也回給 AI、查誰的資料由登入者決定。
  • Two tools: leave balance and today's date.
  • The tool loop: AI asks for a tool → your code runs it → result goes back → AI continues.
  • Three safeguards: a round limit, errors returned to the AI, and "whose data" decided by the login.

AI 不會自己執行工具The AI doesn't run tools itself

一句話重點Key point

AI 只會說「我想呼叫 get_leave_balance」。真正去查資料的,永遠是你的程式。所以你完全控制它能查什麼、查誰的。

The AI only says "I'd like to call get_leave_balance". Your code is always what actually fetches the data. So you fully control what it can look up, and whose.

定義工具Defining tools

工具就是一份說明:名稱、做什麼、需要哪些參數(用 JSON Schema 描述)。AI 讀這份說明來決定要不要用、怎麼用,所以 description 要寫清楚。

A tool is a description: name, purpose and parameters (as JSON Schema). The AI reads it to decide whether and how to use the tool, so write the description carefully.

labs/intermediate/tools.py
TOOLS = [
    {
        "name": "get_leave_balance",
        "description": "查詢目前登入員工今年的特休:總天數、已休天數、剩餘天數。只能查登入者本人,不需要任何參數。",
        "input_schema": {"type": "object", "properties": {}, "required": [], "additionalProperties": False},
        "strict": True,
    },
    ...
]


def run_tool(name: str, args: dict, user_id: str, today=None) -> tuple[str, bool]:
    """執行工具,回傳 (結果文字, 是否為錯誤)。錯誤也要回給 AI,讓它知道發生什麼事。"""
    if name == "get_leave_balance":
        emp = EMPLOYEES.get(user_id)
        if emp is None:
            return f"找不到員工 {user_id} 的資料", True
        remaining = emp["annual_leave_total"] - emp["annual_leave_used"]
        return json.dumps({"total": ..., "used": ..., "remaining": remaining}), False
    ...
    return f"沒有這個工具:{name}", True
最重要的設計決定The most important design decision

get_leave_balance 沒有 employee_id 參數。如果有,使用者只要說「幫我查老闆還剩幾天特休」,AI 就可能乖乖把老闆的編號填進去。查誰的資料,應該由登入系統決定(user_id 由你的程式傳入),而不是由 AI 決定。

get_leave_balance has no employee_id parameter. If it did, a user could say "check my boss's remaining leave" and the AI might dutifully fill in the boss's ID. Whose data is read should be decided by the login system (your code passes user_id), never by the AI.

工具迴圈The tool loop

每次呼叫 API,回應裡的 stop_reason 會告訴你它為什麼停下來。如果是 tool_use,代表它在等工具的結果:

Each API response carries a stop_reason saying why it stopped. tool_use means it's waiting for tool results:

labs/intermediate/bot.py
reply = self._call(messages, turn)
rounds = 0
while reply.stop_reason == "tool_use" and reply.tool_calls:
    rounds += 1
    if rounds > config.MAX_TOOL_ROUNDS:                                  # ① 輪數上限
        turn.error = f"工具呼叫超過 {config.MAX_TOOL_ROUNDS} 輪,已停止"
        return self._finish(turn)
    messages.append({"role": "assistant", "content": reply.content})     # ② 原封不動放回去
    results = []
    for call in reply.tool_calls:
        content, is_error = run_tool(call.name, call.input, self.user_id) # ③ user_id 來自登入
        results.append({"type": "tool_result", "tool_use_id": call.id,
                        "content": content, "is_error": is_error})       # ④ 錯誤也回報
    messages.append({"role": "user", "content": results})                # ⑤ 所有結果放同一則
    reply = self._call(messages, turn)

if reply.stop_reason in ("max_tokens", "refusal"):                       # ⑥ 其他停止原因
    turn.error = f"沒有可用的回答(stop_reason={reply.stop_reason})"
  1. 輪數上限:模型可能一直要求用工具。沒有上限,就是一個會燒錢的無窮迴圈。
  2. 原封不動放回:AI 的回應(包含 tool_use 區塊)要完整加回對話,不要只挑文字。
  3. user_id 來自登入:見上面的說明。
  4. 錯誤也回報:查不到資料時,用 is_error 告訴 AI,讓它可以跟使用者解釋,而不是整個程式當掉。
  5. 同一則訊息:如果 AI 一次要求多個工具,所有結果放在同一則 user 訊息裡回傳,每個結果的 tool_use_id 要對應到原本的請求。
  6. 其他停止原因:max_tokens(被截斷)或 refusal(拒答)時,不要把內容當成正常答案。很多範例只判斷 end_turn,遇到這些情況會出錯。
  1. Round limit: the model may keep asking for tools. Without a cap, that's an infinite loop that burns money.
  2. Append unchanged: add the AI's full response (tool_use blocks included) back to the conversation; don't keep only the text.
  3. user_id from the login: see above.
  4. Report errors: when lookup fails, tell the AI via is_error so it can explain to the user instead of the program crashing.
  5. One message: if the AI requests several tools at once, return all results in a single user message, each tool_use_id matching its request.
  6. Other stop reasons: on max_tokens (truncated) or refusal, don't treat the content as a normal answer. Many examples only check end_turn and break in these cases.

動手試:一步步看訊息怎麼來回Try it: watch the messages go back and forth

demo

「我今年還剩幾天特休?」的完整過程The full exchange for 「我今年還剩幾天特休?」

這份紀錄是實際執行 labs/intermediate 時攔截下來的(使用離線假模型),不是手寫的範例。注意第二次請求時,訊息從 1 則變成 3 則。

This record was captured from an actual run of labs/intermediate (with the offline fake), not hand-written. Notice the second request grows from 1 message to 3.


  

練習Exercises

  1. 用 python -m labs.intermediate.chat --fake --user E002 登入另一位員工,問同樣的問題,確認結果不同。
  2. 新增一個工具 get_overtime_hours(查本月加班時數),記得也不要有 employee_id 參數,並寫一個測試。
  3. 想一想:如果要新增「送出請假申請」這種會改資料的工具,還需要加什麼保護?(提示:進階篇第 06 章的人工核准。)
  1. Run python -m labs.intermediate.chat --fake --user E002 as another employee, ask the same question and confirm the result differs.
  2. Add a tool get_overtime_hours (this month's overtime), again without an employee_id parameter, plus a test.
  3. Think: if you added a tool that changes data, like "submit a leave request", what extra protection would it need? (Hint: human approval, core chapter 06.)

小測驗Quiz

1. 為什麼 get_leave_balance 不接受 employee_id 參數?1. Why doesn't get_leave_balance take an employee_id parameter?
AI 會照使用者的話做事,所以權限不能交給它判斷。把權限放在程式碼裡,而不是 prompt 裡。
The AI follows what users say, so it can't be trusted with permission decisions. Put permissions in code, not in the prompt.
2. 工具迴圈只寫了 while stop_reason != "end_turn"。會有什麼問題?2. A tool loop is written as while stop_reason != "end_turn". What's the problem?
正確寫法是「stop_reason 是 tool_use 才繼續」,再加上輪數上限,並另外處理其他停止原因。
Continue only while stop_reason is tool_use, add a round limit, and handle the other stop reasons explicitly.