大家好,鐵人賽堂堂邁入第十七天!
在過去幾天,我們學會了如何用 adk-mcp
在本地端建立和測試 MCP 伺服器與工具。但一個真正的 AI 工具,不能只活在開發者的電腦裡,它需要被部署到雲端,成為一個穩定、公開、可供全球任何 AI Host 呼叫的強大服務。
今天,我們將走完這「最後一哩路」。我們將以 Day 15 的天氣工具為基礎,使用 FastAPI 將其封裝成一個專業的 Web API,用 Docker 將其容器化,並將其免費部署到 Render.com 雲端平台,最終在我們熟悉的 OpenWebUI 中,成功呼叫這個部署在雲端的自訂工具!
這將是一段充滿成就感的旅程,讓我們開始吧!
目標:將 Python 天氣查詢功能,從一個單純的函式,封裝成一個可透過網路呼叫的專業 API 服務。
一個結構清晰的專案是成功的開始。我們建立如下的目錄結構:
weather_tool/
├── app/
│ ├── .env # 存放機敏資訊,如 API 金鑰
│ ├── main.py # FastAPI 伺服器核心程式碼
│ └── requirements.txt # 專案依賴的 Python 套件
├── .dockerignore # Docker 建置時忽略的檔案
├── .gitignore # Git 版本控制忽略的檔案
└── Dockerfile # 容器化設定檔
app/requirements.txt
)我們需要以下幾個核心套件:
fastapi
: 我們的主角,高效能的 Web 框架。uvicorn
: ASGI 伺服器,負責運行 FastAPI 應用。requests
: 用於呼叫外部 API (中央氣象署)。python-dotenv
: 方便在本地開發時讀取 .env
檔案。pydantic
: FastAPI 底層使用的資料驗證利器。app/main.py
)這是我們專案的心臟。我們使用 FastAPI 框架來建立一個 Web 應用。
(您的 main.py
程式碼寫得非常棒,結構清晰、錯誤處理也很完善,我會直接沿用並在文章中強調其優點)
import os
import urllib.parse
import requests
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from dotenv import load_dotenv
# 載入 .env 檔案 (主要用於本地測試,部署時會直接用服務的環境變數)
load_dotenv()
app = FastAPI(
title="台灣天氣預報 API (Taiwan Weather Forecast API)",
description="一個簡單的 API,用於查詢台灣各縣市的即時天氣預報。",
version="1.0.0",
)
# --- 核心天氣查詢邏輯 ---
TAIWAN_CITIES = [
"臺北市", "新北市", "桃園市", "臺中市", "臺南市", "高雄市",
"基隆市", "新竹市", "嘉義市", "新竹縣", "苗栗縣", "彰化縣",
"南投縣", "雲林縣", "嘉義縣", "屏東縣", "宜蘭縣", "花蓮縣",
"臺東縣", "澎湖縣", "金門縣", "連江縣"
]
def get_weather(city: str) -> dict:
"""根據城市名稱查詢天氣,並以結構化字典格式回傳。"""
normalized_city = city.replace("台", "臺")
if normalized_city not in TAIWAN_CITIES:
raise HTTPException(status_code=404, detail=f"找不到城市 '{city}' 的天氣資訊。")
auth_token = os.getenv("CWA_AUTH_TOKEN")
if not auth_token:
raise HTTPException(status_code=500, detail="伺服器未設定 CWA_AUTH_TOKEN。")
base_url = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-C0032-001"
encoded_location_name = urllib.parse.quote(normalized_city)
full_api_url = f"{base_url}?Authorization={auth_token}&locationName={encoded_location_name}"
try:
response = requests.get(full_api_url, timeout=10)
response.raise_for_status()
data = response.json()
location_data = data['records']['location'][0]
weather_elements = location_data['weatherElement']
time_info = weather_elements[0]['time'][0]
# 提取所有天氣元素
weather_condition = next(p['parameter']['parameterName'] for p in weather_elements[0]['time'] if p['startTime'] == time_info['startTime'])
rain_prob = next(p['parameter']['parameterName'] for p in weather_elements[1]['time'] if p['startTime'] == time_info['startTime'])
min_temp = next(p['parameter']['parameterName'] for p in weather_elements[2]['time'] if p['startTime'] == time_info['startTime'])
max_temp = next(p['parameter']['parameterName'] for p in weather_elements[4]['time'] if p['startTime'] == time_info['startTime'])
return {
"city": city,
"start_time": time_info['startTime'],
"end_time": time_info['endTime'],
"weather_condition": weather_condition,
"rain_probability_percent": int(rain_prob),
"min_temp_celsius": int(min_temp),
"max_temp_celsius": int(max_temp),
}
except (requests.exceptions.RequestException, KeyError, IndexError, StopIteration) as e:
raise HTTPException(status_code=503, detail=f"查詢天氣資料失敗:{e}")
# --- API 端點定義 ---
class WeatherRequest(BaseModel):
city: str
@app.post("/get_weather_forecast", summary="取得天氣預報")
def api_get_weather(request: WeatherRequest):
"""
接收一個城市名稱,回傳該城市的天氣預報。
這是給 Claude 使用的主要工具函式。
"""
return get_weather(request.city)
@app.get("/")
def read_root():
return {"status": "ok", "message": "天氣預報 API 正在運行"}
程式碼亮點解析:
- 關注點分離: 核心查詢邏輯 (
get_weather
) 與 API 端點定義 (@app.post
) 分開,程式碼更易於維護和測試。- 環境變數: CWA 的
auth_token
從環境變數讀取,避免了將機敏資訊寫死在程式碼中的安全風險。- 自動化文件: FastAPI 最強大的功能之一!只要我們的程式碼寫好,它就會自動在
/docs
和/openapi.json
路徑,生成符合 OpenAPI 標準的互動式 API 說明書。這正是接下來 OpenWebUI 能「讀懂」我們工具的關鍵!
.gitignore
)建立 .gitignore
檔案,確保 app/.env
、venv/
和 __pycache__/
等檔案不會被上傳到 GitHub,保障我們的 API 金鑰安全。
# Git and Docker files
.git
.gitignore
Dockerfile
.dockerignore
# Python (與 .gitignore 重疊)
__pycache__/
*.pyc
venv/
.venv/
# Secrets (與 .gitignore 重疊)
app/.env
目標:將我們的 FastAPI 應用程式及其所有複雜的依賴項,打包成一個標準化、輕量且可移植的 Docker 映像檔。
Dockerfile
就像一張「安裝說明書」,告訴 Docker 如何一步步建置我們的應用程式環境。
(您的 Dockerfile 寫得很專業,我會直接沿用並說明)
# 使用輕量的 Python 基礎映像檔
FROM python:3.11-slim
# 設定工作目錄
WORKDIR /code
# 複製 Python 應用程式碼與依賴項文件
COPY ./app/requirements.txt /code/requirements.txt
COPY ./app /code/app
# 安裝依賴項
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# 開放 80 port (Render 預設會連接到容器的 80 port)
EXPOSE 80
# 容器啟動時執行的命令
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
在專案根目錄下,執行 docker build -t my-weather-tool .
指令。Docker 就會按照說明書,為我們打造一個包含所有程式碼和依賴項的應用程式。
目標:將 Docker 映像檔部署到公開的雲端平台 (Render.com),讓它獲得一個全球皆可訪問的穩定網址。
將整個 weather_tool
專案(除了被 .gitignore
忽略的檔案)推送到您個人的 GitHub 倉庫。
Render 是一個對開發者非常友善的雲端平台,尤其對 Docker 專案的支援極佳。
weather_tool
倉庫。Dockerfile
並選擇以 Docker 方式部署。Key
為 CWA_AUTH_TOKEN
的變數,並將您有效的 API 金鑰作為 Value
填入。這是確保雲端伺服器能成功呼叫 CWA API 的關鍵步驟。部署成功後,從 Render 儀表板複製您的服務公開網址,例如 https://my-weather-tool.onrender.com
。
萬事俱備,只欠東風!現在,我們要讓本地的 OpenWebUI,去呼叫我們部署在雲端的個人專屬天氣工具。
Settings
-> Tools
。Add a tool
,並填寫您部署在 Render 上的服務位址:
https://my-weather-tool.onrender.com/openapi.json
None
。/openapi.json
,並理解我們 API 的所有能力。現在,你可以正式對你的 AI 下達指令了!
幫我查一下臺中的天氣怎麼樣?
AI 模型會接收到你的問題,發現它需要天氣資訊,然後跨越網際網路,去呼叫我們部署在 Render 雲端上的 FastAPI 服務,獲取即時、準確的天氣資料,最終生成完美的回答!
今天,我們完成了一項令人振奮的、端到端的全流程實戰。我們從一個本地的 Python 函式開始,最終將它變成了一個全球可用的雲端 AI 工具。
我們掌握了:
這不僅僅是 MCP 的一次實踐,更是現代 AI 應用開發 (LLM Ops) 的一個縮影。你現在所擁有的,不僅是一個聊天機器人,而是一個具備了雲端擴充能力的、真正屬於你自己的 AI 平台。