iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
ChatGPT & Codex

ChatGPT + Codex 打造高效能 AI 開發工作流系列 第 18

Day 18: 實戰應用:Line Chatbot + AI Agent 打造企業級 ERP 查詢助手

  • 分享至 

  • xImage
  •  

Day 18: 實戰應用:Line Chatbot + AI Agent 打造企業級 ERP 查詢助手 (LINE Chatbot ERP Assistant)

本日核心價值 (Core Focus): LINE webhook 驗簽後,用 Intent + Function Calling 呼叫白名單內的 ERP 唯讀 API(訂單、庫存),再組官方回覆 JSON;模型不准發明並執行 SQL,且每個 user-id 都要限流與綁定授權。

概念說明與實戰情境 (Overview)

企業要的不是「會聊天的客服」,而是員工在 LINE 問「SO-20260817-001 出了沒」「料號 A-100 庫存」能拿到 ERP 真資料。架構應固定為:LINE webhook(C# 或 Python Flask)→ 驗簽 → 辨識意圖 → Function Calling 只打 allowlist 內的唯讀 API → 把工具結果填進回覆。模型負責選工具與整理文句,不負責組 SQL、不負責寫入 ERP。Webhook 必須驗證 X-Line-Signature;查詢必須綁 LINE userId 到員工帳號;同一 user 要做 rate limit,避免把 ERP 查詢接口打爆。

關鍵操作與範例 (Implementation & Example)

把 Bot 當 ERP 的只讀投影,而不是第二套後台。訂單狀態、庫存數字只能來自 tool 回傳;模型沒打工具就作答,視為實作缺陷。Channel secret、ERP token 只存在伺服器環境變數。Webhook URL 用 HTTPS,且只接受 LINE 平台的 POST。以下用 Flask 示範驗簽與限流,C# 給對等 HMAC;工具分派兩邊相同。

實作順序建議:先接通驗簽與「未授權」回覆,再接 ERP GET,最後才接 LLM。沒有前兩段就上模型,會把授權漏洞藏在自然語言裡,之後很難測。

資料流如下:

LINE Platform
  -> HTTPS POST /webhook  (raw body + X-Line-Signature)
  -> 驗簽、辨識 userId、rate limit
  -> LLM + tools(僅 query_order / query_inventory)
  -> ERP read API(訂單、庫存)
  -> LINE Reply API JSON

工具白名單只放唯讀查詢。名稱、參數、後端實作都由你定義;模型只能填參數,不能新增工具、不能把 sql 當參數。

Tool 允許參數 後端實際呼叫 禁止
query_order order_id GET /erp/orders/{id} 任意 SQL、更新狀態
query_inventory sku GET /erp/inventory/{sku} 匯出全表、改庫存

Python Flask 驗簽與 webhook 骨架(務必用 原始 bytes 算 HMAC,不可先 request.json):

import base64
import hashlib
import hmac
import os
import time
from collections import defaultdict, deque

from flask import Flask, abort, jsonify, request

app = Flask(__name__)
CHANNEL_SECRET = os.environ["LINE_CHANNEL_SECRET"]
WINDOW_SEC = 60
MAX_CALLS = 8
_hits: dict[str, deque[float]] = defaultdict(deque)

# Map LINE userId -> ERP employee id. Missing => deny.
EMPLOYEES = {"Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": "EMP-0042"}


def verify_signature(body: bytes, signature: str) -> bool:
    digest = hmac.new(
        CHANNEL_SECRET.encode("utf-8"),
        body,
        hashlib.sha256,
    ).digest()
    expected = base64.b64encode(digest).decode("utf-8")
    return hmac.compare_digest(expected, signature or "")


def rate_limit(user_id: str) -> bool:
    now = time.monotonic()
    q = _hits[user_id]
    while q and now - q[0] > WINDOW_SEC:
        q.popleft()
    if len(q) >= MAX_CALLS:
        return False
    q.append(now)
    return True


@app.post("/webhook")
def webhook():
    body = request.get_data()
    if not verify_signature(body, request.headers.get("X-Line-Signature", "")):
        abort(400)

    payload = request.get_json(force=True, silent=True) or {}
    events = payload.get("events") or []
    # Handle message events: bind userId, call agent, then reply.
    return jsonify({"ok": True, "events": len(events)})

C# 對等驗簽(ASP.NET Minimal API 同樣先讀 raw body):

static bool VerifyLineSignature(byte[] body, string? signature, string channelSecret)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(channelSecret));
    var expected = Convert.ToBase64String(hmac.ComputeHash(body));
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signature ?? string.Empty));
}

Function Calling 的 tool schema 不要出現 sql 欄位。參數用嚴格型別,後端再用自己的 HTTP client 打 ERP:

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "Read one sales order by id. Read-only.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "pattern": r"^SO-[0-9]{8}-[0-9]{3}$"}
                },
                "required": ["order_id"],
                "additionalProperties": False,
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "query_inventory",
            "description": "Read on-hand qty for one SKU. Read-only.",
            "parameters": {
                "type": "object",
                "properties": {
                    "sku": {"type": "string", "pattern": r"^[A-Z0-9-]{3,32}$"}
                },
                "required": ["sku"],
                "additionalProperties": False,
            },
        },
    },
]

ALLOWED = {"query_order", "query_inventory"}


def dispatch(name: str, args: dict, employee_id: str) -> dict:
    if name not in ALLOWED:
        return {"error": "tool_not_allowed"}
    # Call ERP HTTP APIs with service credentials; never exec model-written SQL.
    if name == "query_order":
        return erp_get(f"/orders/{args['order_id']}", employee_id)
    return erp_get(f"/inventory/{args['sku']}", employee_id)

模型若回傳不在 ALLOWED 的函式名稱,直接拒絕。ERP 端用員工代號做資料範圍(業務只能看自己的訂單),不要把 LINE userId 當資料庫主鍵散落到每張表。

LINE 回覆必須走 Reply API 的 JSON,而不是自己印文字到 webhook response。範例:

{
  "replyToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "messages": [
    {
      "type": "text",
      "text": "訂單 SO-20260817-001:已出貨。料號 A-100 可用庫存 42。"
    }
  ]
}

Intent 不要做成「自由聊天再看要不要查 ERP」。使用者一則文字進來後,先做授權與限流,再讓模型只能在兩個 tool 裡選一個或拒絕。查不到就說查不到,不要用訓練資料補訂單狀態。replyToken 時效短,工具呼叫與 ERP HTTP 都要設 timeout(建議各 3–5 秒),逾時回「查詢逾時,請稍後再試」,避免 webhook 重試把同一張訂單查很多次。群組訊息的 source.userId 與 1:1 不同,授權表必須用真正的發送者,而不是 groupId。ERP 服務帳號放在後端設定,不進 LINE 訊息、不進 Prompt。

上線前用簽名單元測試擋回歸:正確 X-Line-Signature 必須進業務流程,錯誤簽名必須 400。授權表建議放資料庫或 IdP,範例裡的 EMPLOYEES 字典只適合本機。查詢結果要做資料分類:訂單編號可以回,客戶電話與成本單價不要回。模型整理文句時只能用 tool 回傳的欄位,缺欄就說「系統未提供」。rate limit 除了每 userId 滑動視窗,也要對整個 Bot 做全局限流,避免群組洗版打掛 ERP。寫 log 時記錄 employee_id 與 tool 名稱即可,不要把完整聊天內容當訂單副本存下來。

System prompt 寫成政策,而不是「盡量幫忙」:

You are an ERP query assistant.
You may only call query_order and query_inventory.
Never invent SQL. Never guess order ids or stock qty.
If the tool returns not_found, say so. If the user asks to update ERP, refuse.

注意事項與常見失敗 (Pitfalls)

  • request.json 再序列化去驗簽:JSON 空白不同就會驗簽失敗或被繞過。修法:HMAC 只打 raw body,比對 X-Line-Signaturecompare_digest
  • 讓模型產出 SQL 並 execute():一次 Prompt Injection 就能讀走整庫。修法:只有 HTTP GET 白名單;SQL 留在 ERP 服務內部。
  • 未綁定 userId:任何人加 Bot 就能查訂單。修法:EMPLOYEES 對照表(或 IdP);未註冊一律回「未授權」。
  • 沒有 rate limit:群組訊息或重試會打爆 ERP。修法:每 userId 滑動視窗(上例 60 秒 8 次),超限回「請稍後再查」。
  • webhook 回 200 但沒消耗 replyToken:使用者看不到答案,還以為系統當掉。修法:先驗簽與限流,再在 token 有效時間內呼叫 Reply API。
  • 把可寫 API(出貨、調帳)也掛進 tools:聊天介面不適合做變更。修法:本篇範圍僅查詢;寫入走 ERP 畫面與既有權限。
  • 把 ERP 錯誤原文(連線字串、內部表名)回進 LINE:等於把後端結構洩給所有對話者。修法:工具結果先對應成 not_found / forbidden / timeout / ok,再讓模型組使用者看懂的句子。
  • 用同一個 Bot 服務多家公司卻共用一把 ERP 金鑰:授權邊界會糊掉。修法:tenant 由 userId 對照表決定,HTTP client 帶對應的服務憑證,模型看不到憑證內容。

本日總結 (Takeaways)

  • 架構固定為 webhook 驗簽 → 授權與限流 → allowlist Function Calling → ERP 唯讀 API → LINE Reply JSON。
  • 驗簽用 channel secret 對 raw body 做 HMAC-SHA256;C# / Python 都先讀 bytes。
  • 工具只要 query_orderquery_inventory 這類唯讀查詢;禁止模型發明並執行 SQL。
  • userId 必須對到員工帳號,查詢範圍由 ERP 授權決定,不是由 Prompt 決定。
  • 每個 user 做 rate limit;更新類意圖直接拒絕,不要在聊天室完成寫入。

明日預告 (Next)

明天把「唯讀查詢」換成批次資料移動:用 Python / Codex 做大規模 ETL 與 Data Migration,重點是分塊、Pydantic 驗證、冪等鍵,以及沒有 checksum 就不 load。


上一篇
Day 17: CI/CD Pipeline 整合:將 AI 程式碼檢視與測試自動化嵌入 GitHub Actions
下一篇
Day 19: 大規模資料處理工作流:使用 Python/Codex 自動化 ETL 與 Data Migration
系列文
ChatGPT + Codex 打造高效能 AI 開發工作流21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言