iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
AI Engineering

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

讓程式連上外面的世界:HTTP 與 API 基礎

  • 分享至 

  • xImage
  •  

前面十二天,我們的程式都只在自己的電腦裡打轉。今天要讓它走出去。

這一步為什麼重要?回想一下第一天講的目標:

使用者說「幫我安排明天的行程」,AI 不只是給一段文字,而是可以取得天氣、行程等資訊,再透過不同工具進行分析。

「取得天氣、行程等資訊」——這些資料不在我們的電腦裡,在別人的伺服器上。要拿到它們,就得學會用 HTTP 跟 API 溝通。


一、API 是什麼

API(Application Programming Interface),直譯是「應用程式介面」,但這個翻譯對理解沒什麼幫助。

我比較喜歡的比喻是餐廳的菜單:

  • 你(程式)不會直接衝進廚房自己煮
  • 你看菜單(API 文件),知道有哪些餐點可以點
  • 你跟服務生說「我要一份 A 餐」(發送請求)
  • 廚房做好,服務生端上來(回傳回應)
  • 你不需要知道廚房怎麼運作的

中央氣象署有一堆氣象資料,它不會讓你直接連進它的資料庫。但它提供一份菜單:「你告訴我城市名稱,我告訴你天氣」。這就是 API。


二、HTTP 的四個要素

程式跟 API 溝通用的協定是 HTTP。每一次請求都包含四個部分:

1. URL:要去哪裡

https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-C0032-001?locationName=臺北市&Authorization=KEY
└─┬─┘   └────────────┬────────────┘└──────────┬──────────────┘└──────────────┬──────────────────┘
協定              網域                        路徑                       查詢參數

? 後面的是查詢參數(query parameters),用 & 分隔多個。

2. Method:要做什麼

Method 意思 用途
GET 給我資料 查詢(最常用)
POST 這是新資料 建立、送出表單、呼叫 AI
PUT / PATCH 改這筆資料 更新
DELETE 刪掉這筆 刪除

我們這個系列主要會用到 GET(查天氣)和 POST(呼叫 AI)。

3. Headers:附加說明

像是信封上的資訊,最常見的是:

{
    "Content-Type": "application/json",   # 我送的是 JSON
    "Authorization": "Bearer sk-xxx",     # 我的身分憑證
    "User-Agent": "MyApp/1.0",            # 我是誰
}

4. Body:要送出去的內容

只有 POST / PUT 這類請求才有。通常是 JSON:

{"model": "claude-opus-5", "messages": [...]}

三、回應:狀態碼與內容

伺服器回應也有兩個部分:狀態碼(status code)和內容(body)。

狀態碼用三位數字表示結果,記住開頭數字就懂一半:

範圍 意思 常見代表
2xx 成功 200 OK、201 Created
3xx 重新導向 301、302
4xx 你的問題 400 參數錯、401 沒授權、403 沒權限、404 找不到、429 太頻繁
5xx 伺服器的問題 500 內部錯誤、503 暫時無法服務

這個區分很重要:

  • 4xx:重試也沒用,要改你的程式碼或參數
  • 5xx:對方出事了,等一下再試可能就好了

這個差別在寫重試邏輯時是關鍵。


四、requests:Python 的 HTTP 工具

Python 標準庫有 urllib,但沒人愛用。大家都用 requests:

pip install requests

最簡單的 GET

import requests

response = requests.get("https://api.github.com/users/github")

print(response.status_code)   # 200
print(response.json())        # 自動把 JSON 解析成 dict

response.json() 內建幫你做了 json.loads()(Day 10 學過),直接回傳 dict。

常用屬性

response.status_code   # 200
response.ok            # True(狀態碼 < 400)
response.text          # 回應的原始文字
response.json()        # 解析成 Python 物件(不是 JSON 就會噴錯)
response.headers       # 回應的標頭
response.url           # 實際請求的完整 URL(除錯很好用)
response.elapsed       # 花了多久

帶參數的 GET

不要自己拼字串:

# ❌ 不要這樣
url = f"https://api.example.com/search?q={keyword}&limit=10"

因為如果 keyword 裡有空白、中文、&、#,URL 就壞掉了。正確做法是用 params:

response = requests.get(
    "https://api.example.com/search",
    params={"q": "台北 天氣", "limit": 10},
)
print(response.url)
# https://api.example.com/search?q=%E5%8F%B0%E5%8C%97+%E5%A4%A9%E6%B0%A3&limit=10

requests 自動幫你做 URL 編碼。

帶 Header

response = requests.get(
    "https://api.example.com/me",
    headers={"Authorization": f"Bearer {api_key}"},
)

POST 一個 JSON

response = requests.post(
    "https://api.example.com/items",
    headers={"Content-Type": "application/json"},
    json={"title": "買牛奶", "priority": "高"},   # 用 json= 不是 data=
)

用 json= 參數,requests 會自動幫你序列化,也會自動設好 Content-Type。用 data= 的話送出去的是表單格式,不一樣。


五、一定要做的三件事

新手寫 API 呼叫最常漏掉這三件事,而它們在 Agent 裡都會出問題。

1. 設 timeout

requests.get(url)                 # ❌ 沒有 timeout,可能永遠卡住
requests.get(url, timeout=10)     # ✅

**requests 預設是沒有 timeout 的。**如果對方伺服器掛掉但沒回應,你的程式就會一直卡在那裡。對 Agent 來說這是災難——它會整個停住,使用者也不知道發生什麼事。

**每一個 requests 呼叫都要設 timeout。**沒有例外。

2. 檢查狀態碼

response = requests.get(url, timeout=10)

if not response.ok:
    print(f"請求失敗:{response.status_code}")
    return None

data = response.json()

或是用 raise_for_status(),讓它在 4xx/5xx 時丟出例外:

response = requests.get(url, timeout=10)
response.raise_for_status()      # 4xx/5xx 會 raise HTTPError
data = response.json()

3. 處理例外

import requests

try:
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    data = response.json()

except requests.Timeout:
    print("請求逾時")
except requests.ConnectionError:
    print("連線失敗,檢查網路")
except requests.HTTPError as e:
    print(f"HTTP 錯誤:{e.response.status_code}")
except ValueError:
    print("回應不是合法的 JSON")

requests.Timeout 和 requests.ConnectionError 都是 requests.RequestException 的子類別,想一次抓完可以用它。


六、包一個可靠的請求函式

把上面所有東西整合起來,加上重試機制:

"""http_client.py — 一個帶重試的 HTTP 工具。"""

import time
import requests

# 這些狀態碼代表「等一下再試可能會成功」
RETRYABLE_STATUS = {429, 500, 502, 503, 504}


def fetch_json(url, params=None, headers=None, timeout=10, retries=3):
    """發送 GET 請求並回傳 JSON。

    Args:
        url: 目標網址
        params: 查詢參數 dict
        headers: 標頭 dict
        timeout: 單次請求逾時秒數
        retries: 最多重試幾次

    Returns:
        dict: 解析後的 JSON

    Raises:
        RuntimeError: 重試後仍然失敗
    """
    last_error = None

    for attempt in range(retries):
        try:
            response = requests.get(
                url, params=params, headers=headers, timeout=timeout
            )

            # 4xx(除了 429)是我們的問題,重試沒用,直接放棄
            if response.status_code not in RETRYABLE_STATUS:
                response.raise_for_status()
                return response.json()

            last_error = f"HTTP {response.status_code}"

        except (requests.Timeout, requests.ConnectionError) as e:
            last_error = f"{type(e).__name__}: {e}"
        except requests.HTTPError as e:
            # 明確的客戶端錯誤,不重試
            raise RuntimeError(
                f"請求失敗({e.response.status_code}):{e.response.text[:200]}"
            ) from e
        except ValueError as e:
            raise RuntimeError("回應不是合法的 JSON") from e

        # 指數退避:1 秒、2 秒、4 秒
        if attempt < retries - 1:
            wait = 2 ** attempt
            print(f"第 {attempt + 1} 次失敗({last_error}),{wait} 秒後重試…")
            time.sleep(wait)

    raise RuntimeError(f"重試 {retries} 次後仍然失敗:{last_error}")

這裡有幾個設計值得說明:

指數退避(exponential backoff)
重試間隔是 1、2、4 秒,而不是每次都等 1 秒。如果對方伺服器正在忙,你一直用固定頻率打它只會讓情況更糟。越等越久是網路請求的標準做法。

區分「該重試」和「不該重試」
404 Not Found 重試一百次還是 404。503 Service Unavailable 等一下可能就好了。把這兩種分開處理,才不會浪費時間。

raise ... from e
保留原始的錯誤鏈,除錯時可以看到完整的錯誤來源。

錯誤訊息包含 response.text[:200]
API 通常會在回應裡說明錯誤原因,把它帶出來,比只看到一個 400 有用太多。截 200 字是避免一大堆 HTML 洗版。

用起來就很簡單:

data = fetch_json(
    "https://api.github.com/users/github",
    timeout=10,
)
print(data["name"], data["public_repos"])

七、練習:找一個不用 key 的 API 玩玩

在正式申請 key 之前,先用免費的公開 API 練手:

from http_client import fetch_json

# 1. 隨機貓咪事實
cat = fetch_json("https://catfact.ninja/fact")
print("貓咪小知識:", cat["fact"])

# 2. 匯率(以台幣為基準)
rates = fetch_json("https://open.er-api.com/v6/latest/TWD")
print(f"1 TWD = {rates['rates']['JPY']:.4f} JPY")

# 3. GitHub 使用者資訊
user = fetch_json("https://api.github.com/users/torvalds")
print(f"{user['name']} 有 {user['followers']} 個追蹤者")

建議的練習流程,我自己都是這樣做的:

  1. 先用瀏覽器打開 API 網址,看看回傳長什麼樣
  2. 把 JSON 貼到 jsonformatter.org 之類的工具,看清楚結構
  3. 再寫程式去抓,一層一層把需要的欄位挖出來

先看清楚資料結構,再寫程式。直接寫的話會一直在猜 key 的名字。


八、這跟 Agent 有什麼關係?

HTTP 是 Agent 的手腳。

回到第一天的那個例子——「幫我安排明天的行程」。Agent 要完成這件事,需要:

需要的資訊 從哪來
明天的天氣 氣象署 API
我的行事曆 Google Calendar API
通勤時間 地圖 API
判斷與建議 AI API

每一個都是 HTTP 請求。

而且注意最後一項——連「AI 本身」都是透過 HTTP 呼叫的。我們 Day 18 會用官方 SDK,但 SDK 底層做的事情就是今天學的這些:組一個 POST 請求,帶上 Authorization header,送一包 JSON 出去,收一包 JSON 回來。

所以今天講的 timeout、重試、狀態碼判斷,不是「等有空再補」的細節,而是 Agent 能不能穩定運作的基礎。一個 Agent 可能在一次任務裡呼叫五、六個 API,只要有一個沒處理好,整件事就做不完。


小結

  • API 就像餐廳菜單:你照規則點餐,不需要知道廚房怎麼運作
  • HTTP 請求有四個要素:URL、Method、Headers、Body
  • 狀態碼看開頭:2xx 成功、4xx 你的問題、5xx 對方的問題
  • requests 用 params= 帶查詢參數、json= 送 JSON,不要自己拼字串
  • 每一個請求都要設 timeout,這是最常被忽略也最致命的一件事
  • 重試要用指數退避,而且只重試 5xx 和 429

明天我們要拿這些工具去做真的事情——實作天氣查詢:中央氣象署 API。這會是我們生活助理的第一個真實資料來源。


上一篇
把鑰匙藏好:環境變數、.env 與 API Key 管理
下一篇
第一個真實資料來源:串接中央氣象署天氣 API
系列文
從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言