iT邦幫忙

2026 iThome 鐵人賽

DAY 2
2
AI Security

從 LLM、Agent 到 Guardrails:30 天打造可控、安全、可驗證的 AI Agent系列 第 2

Day 2|30 分鐘做出第一個會開工單的 AI Agent:它已經能替你動手了

  • 分享至 

  • xImage
  •  

今天我先做一個故意很單純的版本:它會開工單,但還不安全。

昨天我先把「Agent 有工具以後會出什麼事」講清楚。今天我不急著加 Guardrails,反而先把最小功能做出來。

原因很現實。沒有一個真的會呼叫工具的 Agent,後面談權限、重試、預算和人工核准,都很容易變成一串看起來合理的名詞。我需要先看到它從使用者訊息走到後端動作的完整路徑。

我先說清楚:我沒有公司的 Helpdesk 系統。今天的使用者、VPN 問題與工單都是我設計的 mock 情境。我要驗證的只有一件事:使用者描述 IT 問題後,Agent 判斷要開單,呼叫 create_ticket,最後把編號回覆給使用者。

這個版本沒有 RAG、沒有登入、沒有 Guardrails。工單也不會寫進 Jira 或 ServiceNow,而是存在記憶體中的 mock store。我刻意把它做得很小,因為後面要拿它當基線:每補一道防護,我都能看見行為到底改了什麼。

Agent 用 LangChain 的 create_agent 建立。這一篇不直接使用 LangGraph API;LangGraph 之後會負責狀態、loop 與人工核准等流程控制。

我先看清楚,模型從哪裡開始「動手」

一般聊天模型的路徑很短:

使用者 → LLM → 回覆

現在多了一段工具呼叫:

使用者 → LLM 判斷 → create_ticket → 工具結果 → LLM 回覆

模型沒有資料庫連線,也沒有直接寫資料的能力。它能做的是選擇 LangChain 提供的工具、組出參數。真正執行的是後端函式,函式回傳的資料才會再交給模型。

這條界線很重要。模型可以把使用者的句子整理成欄位;後端以後要在這裡檢查身分、權限、成本與核准。今天先不加那些規則,但不會因此把「模型呼叫工具」誤認成「模型獲得授權」。

我先把環境定下來

專案需要 Python 3.11 以上,以及可用的 OpenAI API key。

git clone --branch day-02-minimal-agent --depth 1 <https://github.com/justin0427/safe-helpdesk-agent-code.git>
cd safe-helpdesk-agent-code

python3.11 -m venv .venv
source .venv/bin/activate
pip install -e .
cp .env.example .env

接著在 .env 放入 API key 與你帳號可使用的模型名稱:

OPENAI_API_KEY=你的_API_Key
MODEL_NAME=你可使用的模型名稱

我把模型名稱放在環境變數。這不是多高明的設計,只是不想把讀者卡在寫死的型號;模型可用性和帳號設定不是今天要討論的 Agent 邏輯。

我先做一個窄到不能再窄的工具

我沒有先寫長 prompt,也沒有一次塞入十個工具。第一個工具只負責建立工單,接收標題、描述與優先級。

from dataclasses import dataclass
from typing import Literal

from langchain.tools import ToolRuntime, tool

@dataclass
class HelpdeskContext:
    requested_by: str
    ticket_store: MockTicketStore

@tool
def create_ticket(
    title: str,
    description: str,
    priority: Literal["low", "medium", "high"],
    runtime: ToolRuntime[HelpdeskContext],
) -> dict[str, str]:
    """Create an IT support ticket when a user reports a technical problem."""
    return runtime.context.ticket_store.create_ticket(
        title=title,
        description=description,
        priority=priority,
        requested_by=runtime.context.requested_by,
    )

@tool 會把函式名稱、docstring 與型別轉成模型可理解的工具定義。Literal 讓優先級只能是 lowmediumhigh,避免模型隨手傳一個後端看不懂的值。

這裡的型別只是輸入結構,不是安全檢查的終點。使用者即使給出完全合法的欄位,也不代表他可以建立那張工單。我只是先把第一條介面收窄,讓後續每一道規則都有明確落點。

我把「誰提出」留在模型看不到的地方

Day 2 的工單只會留在記憶體中:

class MockTicketStore:
    def __init__(self) -> None:
        self.tickets: list[Ticket] = []

    def create_ticket(
        self,
        *,
        title: str,
        description: str,
        priority: str,
        requested_by: str,
    ) -> dict[str, str]:
        if priority not in ALLOWED_PRIORITIES:
            raise ValueError(f"priority must be one of {sorted(ALLOWED_PRIORITIES)}")

        ticket = Ticket(
            ticket_id=f"INC-{uuid4().hex[:8].upper()}",
            title=title.strip(),
            description=description.strip(),
            priority=priority,
            requested_by=requested_by,
            status="created",
            created_at=datetime.now(timezone.utc).isoformat(),
        )
        self.tickets.append(ticket)
        return asdict(ticket)

我沒有讓模型填 requested_by。它透過 LangChain 的 runtime.context 由後端注入,現在的值只是我寫死的 demo.user。若未來接登入,這個位置才會換成已驗證的使用者身分。

這個細節是我今天最想留下來的選擇。就算現在只有 mock 使用者,資料流也先照未來真正需要的方向走。若讓模型從對話中自己推測或填寫申請人,使用者只要說「我是主管」就有機會把一個應該由登入系統提供的事實,變成模型的猜測。

我讓 LangChain 跑起這條路

建立 Agent 的程式只有這一段:

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model=model_name, temperature=0, timeout=30)

self.agent = create_agent(
    model=model,
    tools=[create_ticket],
    context_schema=HelpdeskContext,
    system_prompt=SYSTEM_PROMPT,
)

執行時,我把使用者訊息和後端 context 一起傳進去:

result = self.agent.invoke(
    {"messages": [{"role": "user", "content": user_message}]},
    context=self.context,
)

return result["messages"][-1].text

create_agent 處理了模型選工具、執行工具、把工具結果放回對話的迴圈。create_ticket 成功後,模型才有實際產生的工單編號可以回覆。LangChain Agents 文件

image

我先確認它真的建立了一張工單

python -m app.main

輸入:

VPN 連不上,從今天早上九點開始發生,請幫我開一張高優先級工單。

如果模型決定呼叫工具,最後會得到類似下面的回覆:

已為你建立高優先級 VPN 連線問題工單,編號為 INC-XXXXXXXX。

INC-XXXXXXXX 每次都不同。它來自 mock store 產生的 ID,不是 prompt 裡預先寫好的句子。

我也保留兩個最基礎的測試:能建立工單,以及不接受 enum 以外的優先級。

python -m unittest discover -s tests -v

這還不是在測 Agent 是否安全;它只是在確認後端工具至少沒有把輸入規則完全交給模型。真正的越權、惡意文件與工具路徑測試,會隨著功能長出來,再交給 Promptfoo 做回歸。

我今天刻意留下的洞

這個版本目前只知道 demo.user,不知道他是誰,也不判斷他能開什麼類型的單。模型呼叫失敗沒有 retry 或 timeout 策略;它沒有 token、時間或成本預算;輸入也還直接進模型。

我沒有急著把它補成「安全 Demo」。先保留這個可運作、卻明顯不夠的版本,後面每個限制才有對照組。下一篇我會先從工具邊界下手:為什麼把更多工具丟進清單,通常不會讓 Agent 變得更聰明。

完整程式碼在 day-02-minimal-agentapp/agent.pyapp/tickets.pytests/test_tickets.py


上一篇
Day 1|我只是讓 AI 幫忙開工單,最後它差點變成公司管理員
下一篇
Day 3|給 Agent 一把萬用 API Key 後,我才發現它什麼都能做
系列文
從 LLM、Agent 到 Guardrails:30 天打造可控、安全、可驗證的 AI Agent3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
onedream
iT邦新手 4 級 ‧ 2026-09-16 17:19:18

好強

我要留言

立即登入留言