【實戰】Day 24:Mode A 實作:用 Pydantic AI 打造鞋墊廠訂單決策 Agent

【實戰】Day 24:Mode A 實作:用 Pydantic AI 打造鞋墊廠訂單決策 Agent

從設計圖到真實程式碼。本篇完整實作 Day 20 規格書中的 Mode A 單一決策 Agent:Pydantic AI + FastAPI 五層架構全部串通,並說清楚前後端為何必須分離、API 如何成為唯一橋樑。


💡 數據為虛擬數據,並為了方便演示,毛利率改由品牌決定,而非庫存。

在 Day 20,我們寫了一份系統規格書,畫了漂亮的架構圖,定義了雙模式決策、五層架構。

今天,我們要把它變成真正能跑的程式碼

本篇是這個系列最重要的一篇實作文章,因為它把前面十幾天談過的所有概念——Pydantic AI、FastAPI、Tool Layer、User Context——第一次同時活在同一個專案裡


先回答一個最常被問的問題

「前端 Next.js 和後端 Python,是不是不同的環境?要用 API 接?」

是的。兩套完全獨立的環境,唯一的橋樑是 HTTP API。

┌──────────────────────────────┐     HTTP API      ┌──────────────────────────────┐
Frontend (Node.js)          │ ←──────────────→  │  Backend (Python 3.10)Next.js + Shadcn            │                   │  FastAPI + Pydantic AI│  port: 3000                  │                   │  port: 8000│  套件管理:npm / node_modules │                   │  套件管理:pip / venv          │
└──────────────────────────────┘                   └──────────────────────────────┘

這不是「前後端分離」的選擇題,而是現實——最強大的 AI 框架(Pydantic AI、LangGraph、CrewAI)全都在 Python 生態系。前端的 JavaScript 不能直接呼叫它們。

Next.js 的職責是:拿到漂亮的 JSON,畫出漂亮的畫面。
FastAPI 的職責是:接受前端的問題,驅動 AI,把結果回傳成 JSON。


整體專案結構

ERP-AI-Agent/
├── backend/Python 3.10 venv
│   ├── venv/                          ← 獨立 Python 環境
│   ├── data/Mock JSON(替代 Oracle ERP│   │   ├── orders.json15 筆訂單
│   │   ├── factories.json3 廠區
│   │   ├── customers.json6 客戶
│   │   └── inventory.json             ← 原料庫存
│   ├── app/
│   │   ├── main.pyFastAPI 入口(最先 load_dotenv)
│   │   ├── data_loader.py             ← 讀 JSON 的統一入口
│   │   ├── routers/
│   │   │   ├── orders.py/api/v1/orders(Day 19Business API│   │   │   └── agent.py/api/v1/agent/query(今天新增)
│   │   ├── tools/
│   │   │   └── order_tools.pyTool Layer(Day 21,今天真正實作)
│   │   ├── agents/
│   │   │   └── order_agent.pyPydantic AI Agent(今天核心)
│   │   └── context/
│   │       └── user_context.pyUser Context Layer(Day 22,今天真正實作)
│   ├── .envAPI Key(不進 git)
│   └── requirements.txt
└── frontend/
    └── next-shadcn-dashboard-starter-main/Day 23Next.js 模板

完整系統架構圖

這是 Day 18 到今天為止,所有層次的實際對應關係:

Loading Diagram...

請求時序圖

架構圖說明「有哪些層」,時序圖說明「一次請求怎麼走過這些層」:

Loading Diagram...
步驟發生什麼事對應技術
① 前後端橋樑前端透過 HTTP POST 發問,跨越 Node.js / Python 環境邊界FastAPI CORS + fetch
② UserContext 注入FastAPI 取出使用者角色,注入 Agent 的 system_prompt,決定回答重點與格式get_agent_deps()AgentDeps
③ ReAct 工具決策Pydantic AI Agent 讀取工具的 Docstring,自主決定呼叫哪個 @tool_plain、傳入什麼參數Pydantic AI ReAct loop
④ 資料回傳工具從 JSON 計算結果回傳 dict,Agent 生成自然語言,FastAPI 序列化為 JSONresult.dataAgentResponse

啟動環境

# 第一次安裝
cd ERP-AI-Agent/backend
py -3.10 -m venv venv
.\venv\Scripts\pip install -r requirements.txt

# 設定 API Key(複製 .env.example → .env,填入真實 key)
cp .env.example .env
# 編輯 .env:OPENAI_API_KEY=sk-你的真實金鑰

# 啟動
.\venv\Scripts\uvicorn app.main:app --reload --port 8000

核心實作一:Tool Layer

app/tools/order_tools.py 是純粹的 Python 函式,不依賴任何 AI 框架。它們只負責從 JSON 撈資料、計算、回傳 dict。

def calculate_gross_margin(brand_name: str) -> dict:
    """
    用途:計算指定品牌的訂單毛利率。
    使用時機:當主管詢問利潤、毛利、賺不賺錢時呼叫。
    回傳:毛利率百分比,並標示是否低於公司紅線 15%。
    """
    orders = [o for o in get_orders() if o["brand_name"].lower() == brand_name.lower()]
    total_revenue = sum(o["total_amount"] for o in orders)
    total_cost = sum(o["unit_price"] * 0.35 * o["order_qty"] for o in orders)
    gross_profit = total_revenue - total_cost
    margin_pct = round(gross_profit / total_revenue * 100, 1)
    return {
        "brand_name": brand_name,
        "gross_margin_pct": margin_pct,
        "below_redline": margin_pct < 15.0,  # 低於紅線要觸發警報
    }

關鍵設計原則:AI 不負責算數學,它只負責決定呼叫哪個函式。 數字計算 100% 在 Python 裡完成,永遠準確。


核心實作二:Pydantic AI Agent

app/agents/order_agent.py 是整個後端的大腦。

from pydantic_ai import Agent
from dataclasses import dataclass

@dataclass
class AgentDeps:
    user_role: str   # "general_manager" | "sales_manager" | "production_manager"
    user_name: str

order_agent = Agent(
    "openai:gpt-4o-mini",
    deps_type=AgentDeps,
    system_prompt=(
        "你是鞋墊廠的專業訂單決策 AI 幕僚,使用繁體中文回答。\n"
        "根據使用者角色給出不同重點:\n"
        "- general_manager:著重毛利率、整體營收,回答簡潔(200字內)。\n"
        "- sales_manager:著重訂單達標率、交期、具體數字,提供行動建議。\n"
        "- production_manager:著重產能稼動率、交期、備料狀況。\n"
        "毛利率 < 15% 必須標示 🚨 低利潤警報。\n"
        "廠區稼動率 > 85% 必須標示 ⚠️ 產能警告。"
    ),
)

@order_agent.tool_plain
def tool_query_orders_by_brand(brand_name: str) -> dict:
    """查詢指定品牌的所有訂單明細、出貨率與總金額。"""
    return query_orders_by_brand(brand_name)

@order_agent.tool_plain
def tool_calculate_gross_margin(brand_name: str) -> dict:
    """計算指定品牌的毛利率,並標示是否低於公司紅線 15%。"""
    return calculate_gross_margin(brand_name)

@order_agent.tool_plain
def tool_get_factory_capacity(factory_code: str) -> dict:
    """查詢廠區產能與稼動率。factory_code: TW-01 / VN-02 / ID-03"""
    return get_factory_capacity(factory_code)

@order_agent.tool_plain
def tool_get_orders_summary() -> dict:
    """取得全廠訂單整體摘要:各品牌營收、各廠出貨量。"""
    return get_orders_summary()

@order_agent.tool_plain 裝飾器做了一件很重要的事:它把函式的 Docstring 轉成 LLM 的工具說明書。LLM 讀 Docstring 決定要呼叫哪個工具、傳入什麼參數——你寫得越清楚,AI 命中率越高。


核心實作三:User Context

# app/context/user_context.py
USER_PROFILES = {
    "gm_harry":    {"name": "Harry", "role": "general_manager"},
    "sales_alice": {"name": "Alice", "role": "sales_manager"},
    "prod_bob":    {"name": "Bob",   "role": "production_manager"},
}

def get_agent_deps(user_id: str) -> AgentDeps:
    profile = USER_PROFILES.get(user_id, DEFAULT_PROFILE)
    return AgentDeps(user_role=profile["role"], user_name=profile["name"])

這個 AgentDeps 會被注入進 Agent 的 System Prompt,讓同一個問題在不同使用者身上產生截然不同的回答——這正是 Day 22 定義的行為。


核心實作四:FastAPI 端點

# app/routers/agent.py
@router.post("/agent/query", response_model=AgentResponse)
async def agent_query(request: AgentRequest):
    deps = get_agent_deps(request.user_id)          # 取 user context
    result = await order_agent.run(request.message, deps=deps)  # 跑 agent
    return AgentResponse(
        reply=result.data,
        user_role=deps.user_role,
        user_name=deps.user_name,
    )

三行有效邏輯:取 context → 跑 agent → 回傳結果。其餘的複雜性,都被封裝在各自的層裡。


為什麼 main.py 必須先 load_dotenv?

# app/main.py ── 順序非常重要
from dotenv import load_dotenv
load_dotenv()  # ← 必須在所有 app 模組 import 之前!

from app.routers import orders, agent  # ← 這行 import 會觸發 Agent 初始化

Pydantic AI 在 Agent(...) 被呼叫的瞬間,就會嘗試初始化 OpenAI client。如果這時 OPENAI_API_KEY 還沒被載入,程式會直接崩潰。load_dotenv() 必須是整個程式的第一行。


呼叫測試

在瀏覽器 http://localhost:8000/docs,可以看到 Swagger UI,進行找 POST /api/v1/agent/query → 點 Try it out → 填入以下內容。

# 總經理視角
curl -X POST http://localhost:8000/api/v1/agent/query \
  -H "Content-Type: application/json" \
  -d '{"message": "幫我評估 Nike 的訂單獲利狀況", "user_id": "gm_harry"}'

# 業務主管視角(同樣的問題,不同角色)
curl -X POST http://localhost:8000/api/v1/agent/query \
  -H "Content-Type: application/json" \
  -d '{"message": "幫我評估 Nike 的訂單獲利狀況", "user_id": "sales_alice"}'

總經理回答(gm_harry):

Nike 訂單財務摘要:
- 總營收:$211,150
- 毛利率:65%(健康,高於 15% 紅線)
- 越南廠 VN-02 稼動率 91% ⚠️ 產能警告,建議謹慎接急單
總經理結果

業務主管回答(sales_alice):

Nike 訂單詳細狀況:
1.4 筆訂單,總量 195,000 雙,出貨率 87%
2.1 筆部分出貨(ORD-101455,000雙僅出30,000   → 建議確認越南廠交期,主動聯絡客戶說明延遲原因
3. 行動建議:PU 鞋墊佔 64%,可向 Nike 推廣 EVA 材質擴大品項...
業務主管結果

同一個問題,兩個完全不同的回答。這就是 User Context Layer 的實際效果。


實用建議:三個起步行動

步驟 1:把真實 API Key 填進 .env

# .env
OPENAI_API_KEY=sk-你的真實金鑰
LLM_MODEL=openai:gpt-4o-mini  # 便宜、夠用

步驟 2:先測 Tool 函式,再測 Agent

在開 Agent 之前,直接在 Python shell 跑 from app.tools.order_tools import calculate_gross_margin; print(calculate_gross_margin("Nike")),確認工具的計算結果正確。工具對了,Agent 才不會拿到錯誤的資料去分析。

步驟 3:用 curl 或 Swagger UI 測試不同 user_id

FastAPI 自動生成 Swagger UI(http://localhost:8000/docs),在裡面直接測試 user_id = "gm_harry"user_id = "sales_alice" 的差異,感受 Context Layer 的效果。


我的反思

從 Day 18 到今天,我們做了一件很紮實的事:

把六篇文章的設計,真正變成了一個可以跑的系統。

回顧這條路:

Day做了什麼架構層
18Oracle → JSON 去識別化Layer 5 Data
19FastAPI REST APILayer 4 Business API
20系統規格書、五層架構設計整體藍圖
21Tool Layer 封裝概念Layer 3 Tool Layer
22User Context 概念Layer 2 Context
23Next.js Shadcn 前端模板Frontend
24(今天)全部真正串起來Layer 1 AI Agent

現在後端五層架構全部就位,API 可以接受自然語言提問並回傳決策建議。

下一步:把前端的對話抽屜接上這個 API,讓主管不用開 curl,直接在戰情室裡問問題。


參考資料

官方資源