CodeGym /課程 /ChatGPT Apps /認識代理:角色、run 迴圈、確定性、冪等性

認識代理:角色、run 迴圈、確定性、冪等性

ChatGPT Apps
等級 12 , 課堂 0
開放

1. 代理到底是什麼,為什麼需要它

代理不是 ChatGPT App 的必備部分:你可以在不使用 LLM 代理的情況下,打造再多酷炫的應用。然而,我有三個充分的理由要和你談談它們。

代理是為你的應用後端加入智慧的極佳方式。像是聰明的禮物挑選、解析使用者的文字偏好;複雜的搜尋、分析、處理與總結情境——這些用 LLM 代理都能很輕鬆地做到。

ChatGPT 發布了自己的 AgentsSDK(TS 與 Python)。它非常好用,代理的編排開箱即用。面對一道複雜任務,不只一個代理在工作,而是一整個團隊。這是非常有前景的方向。

此外還有教學目的。ChatGPT 呼叫 mcp-tools 的方式,和 LLM 代理呼叫自己的 tools 完全一樣。一旦你弄懂 LLM 代理如何運作,就能理解例如如何在應用的模型端實作狀態機。同樣地,學習 AgentsSDK 也能讓你對未來的 ChatGPT SDK 如何工作有清晰概念。

那就開始吧。

什麼是 LLM 代理

如果把 ChatGPT App 看成是在 ChatGPT 內部呈現你服務的美觀、好用的「前端」,而 MCP‑伺服器是帶有工具與商業邏輯的「引擎」,那麼代理就像是一個聰明的調度員,能夠:

  • 讀懂目標;
  • 自行決定要呼叫哪些工具、以什麼順序;
  • 必要時詢問額外資料;
  • 出錯時重試步驟;
  • 走到清楚明確的最終結果。

用接近官方 Agents SDK 文件的表述,代理是一個程式。擁有對 LLM 與一組工具的存取後,它能自主規劃步驟以達成目標,並且透過 tool‑calls 執行這些步驟。

與你現有的東西類比:

  • 在一般的 ChatGPT App 中,ChatGPT 的模型會直接編排呼叫你的 MCP‑工具。
  • 後端上的 LLM 代理同樣擁有任務描述與一組工具,並自行決定要呼叫哪些 tools、執行多少步、何時停止,以及要回傳什麼結果。

在我們的 GiftGenius 情境中,可能是這樣:

  • 沒有代理的應用: 模型直接呼叫 searchGifts,再呼叫 filterByBudget,然後 getDetails——每次都重新思考。
  • 有代理的應用: ChatGPT 呼叫 mcp‑tool,而後端給代理一個任務:「幫某個輪廓找到前 5 名的禮物」。 代理會做多步:蒐集補充資訊、呼叫不同的搜尋工具、過濾、排序、建立最終卡片,然後回傳結構化的結果。

ChatGPT 與後端上的 LLM 代理就像公司的主管與員工。ChatGPT 的自由度更高:它與使用者互動,並決定啟動哪些策略性任務(呼叫 mcp tools)。LLM 代理只在後端運作,不和使用者互動,但也會「思考」並可呼叫自己的 tool可以把它想成「精簡版的 ChatGPT」。

2. 代理由哪些部分構成:LLM、指令、工具與狀態

把代理想像成幾個層級會很方便。

首先,底層還是同一個 LLM。這可以是 GPT‑5.1 或 Agents SDK 使用的其他模型。它負責產生文字、規劃步驟、挑選工具——總之是「思考」,但已經是在你的編排語境中。

其次,模型之上是指令。這是代理的系統提示(system prompt),用來設定它的角色、邊界、溝通風格與工具的使用方式。你在 ChatGPT App 已經做過類似的事,但現在它套用在一個獨立的代理上。

第三,是一組代理的工具。可能包括:

  • TypeScript 函式(典型的 function calling);
  • HTTP/REST 工具;
  • 對你的 MCP‑tools 的封裝,讓代理能存取與 ChatGPT App 相同的後端;
  • OpenAI 自帶的「hosted」工具(例如你啟用的 web‑search)。

最後,還有狀態與步驟的處理規則:如何保存工作階段狀態(session state)、如何保存中間結果、如何限制循環。我們會在下一講的記憶與狀態中更深入,但現在就要記得,代理不是「一次性請求」,而是可能保存進度的長程序。

從 TypeScript 開發者的視角來看,可以在腦海裡想像一個大致這樣的物件(接近 Agents SDK TS 的偽代碼):

const giftAgent = new Agent({
  model: "gpt-5.1",
  systemPrompt: giftAgentPrompt,
  tools: { searchGifts, filterGifts, checkoutDraft },
  // 此處也可設定記憶、步驟上限等
});

先不深入 API 細節,重點是這個形象:模型、指令、工具與行為設定集中在同一處。

3. 訊息角色:system / user / assistant / tool 在代理世界中的含義

你已熟悉 Chat Completions 中的經典角色 systemuserassistanttool。在 Agents SDK 中它們仍然存在,但更具有務實的意義。

system 角色設定代理的個性與使命。比如對 GiftGenius 代理,可以是:「你是禮物挑選代理。你的目標是用最少的步驟,根據收禮者輪廓與預算挑出 3–7 個相關選項,然後為小工具準備結構化的 JSON。」你也會在這裡寫下限制:它不該做的事(例如,不經過額外步驟就進行真實購買)以及如何使用工具。

在代理的語境中,user 角色不一定是「真人」。更多時候它是代理的「任務」:由你的 App、服務或另一個代理所表述的目標。舉例來說,ChatGPT App 可以用 user 訊息呼叫代理:「替一位同事開發者挑 5 個禮物點子,預算 50 美元,場合是生日。」

assistant 角色是代理內部的模型「說」的內容。這裡可能包含中間的推理與規劃,也可能是最終回答。你的任務是把系統提示設計好,讓這些訊息在需要時能被記錄並具有用處。

tool 角色(或特定 SDK 的對應角色)描述工具呼叫的結果:「透過 MCP 找到 50 個商品」、「API 回傳逾時錯誤」、「資料庫交回使用者輪廓」。這些訊息與 assistant 訊息一起,構成代理 run 迴圈的歷史。

把它整理成一個小表:

角色 誰在說話 GiftGenius 中的例子
system
您(代理的開發者) 「你是禮物挑選代理……」
user
外部呼叫(App 或另一個代理) 「挑 5 個 50 美元以內的禮物……」
assistant
代理內的模型 「計畫:1)先詢問細節……」
tool
工具呼叫的結果 「searchGifts 回傳了 20 個選項……」

這個結構很重要,因為它正是我們今天主角——run 迴圈——的基礎。

4. LLM 如何呼叫你後端的函式

當你習慣了「問與答」模式,會覺得 LLM 似乎按簡單的流程在工作:收到文字 → 模型回文字。事實上底層更複雜一些,這也是 function calling 能運作的原因。

模型拿到的不是單一問題,而是訊息清單——對話歷史。那裡包含所有先前的話:系統指令(「你是誰、能/不能做什麼」)、你的訊息、模型過去的回覆、工具的結果。每一步,模型都會把整條訊息串當成聊天紀錄審視,然後決定:「我現在應該在末尾新增哪一則訊息?」

關鍵觀念是:LLM 永遠做單一步——把下一則訊息寫到歷史末尾。它不會「改變過去」,不會編輯舊訊息,只會延續清單。你寫了一個問題,模型回答;你再寫第二個問題,模型再次回答,但會考慮整段對話的歷史(所有訊息)。

Function calling 也是基於這個原理。模型不是直接「執行函式」,而是這樣做:

  • 在對話歷史之外,還能看到可用的工具/tools及其描述;
  • 決定:「現在與其直接回文字,不如先呼叫某個工具」
  • 接著,作為下一則訊息,不寫一般文字回答,而是特別格式的訊息: 「我想呼叫某個 tool,並帶上這些參數」。

之後就不是模型,而是你的後端讀取這則新訊息(位於歷史末尾),理解它是函式呼叫的請求,並呼叫對應的工具。然後把另一則訊息——tool 的結果——加回歷史,再把完整的訊息清單傳給模型。模型再次看完整串,寫下下一步:要嘛再呼叫一個工具,要嘛產出最終的人類可讀回覆。

也就是說:

  • 一般 Q&A:下一則訊息 = 文字回答;
  • function calling:下一則訊息 = 呼叫函式的指示使用工具後的回覆

沒有任何獨立的「魔法指令」來呼叫函式——那只是一種特殊形式的下一則訊息,模型把它附加在鏈的尾端。

模型並不透過公開 API 呼叫你後端的函式。它只是「在聊天裡寫下」想要呼叫的函式與參數。然後你的後端呼叫本機函式,並把回應再寫回聊天中。如此重複進行。

5. 代理的 run 迴圈:它如何一步步「思考」

本質上,LLM 代理就是你伺服器上的一個物件/演算法,會啟動代理的 run 迴圈——這是加強版的「提問 → 思考 → 也許採取動作 → 再思考 → … → 最終回覆」。在 OpenAI 的文件裡,這有時被稱為agent loop或 ReAct 模式(Reason + Act + Observe)。

概念層面上一個代理的 run 看起來像這樣:

  1. 代理接收輸入:系統指令、任務(user 訊息),也可能有——當前狀態。
  2. 模型產生一步:要嘛是文字回覆,要嘛是規劃與決定呼叫一個或多個工具。
  3. 如果模型選擇 tool‑call,代理會在程式碼裡呼叫對應工具(可能是本機函式、MCP‑tool、HTTP 請求、資料庫存取等)。
  4. 工具的結果以 tool 訊息的形式加入歷史。
  5. 迴圈帶著新上下文回到模型。模型決定接下來要做什麼:繼續規劃、呼叫其他工具,或用最終回覆結束任務。
  6. 當模型顯式或依照停止條件完成 run 時,代理把最終結果回傳給呼叫方。

可以用一張小圖示意:

flowchart TD
    A[Run 開始:目標 + system] --> B[呼叫模型]
    B --> C{模型要
直接回覆文字
還是呼叫 tool?} C --> D["文字回覆
(assistant)"] D --> E{任務已完成?} E -->|是| F[最終結果] E -->|否| B C --> G["Tool-call
(呼叫描述)"] G --> H[呼叫函式 / MCP / HTTP] H --> I["Tool 結果
(tool message)"] I --> B

把它轉成簡化的 TypeScript 偽代碼(和真實 API 有距離,但邏輯正確)大概是:

async function runAgent(goal: string) {
  let context = buildInitialContext(goal);

  while (!isFinished(context)) {
    const decision = await callLLM(context); // 代理的一步

    if (decision.type === "tool_call") {		// 要呼叫函式嗎?
      const toolResult = await callTool(decision.tool, decision.args);	// 呼叫本機函式
      context = appendToolResult(context, toolResult);		// 把結果加到列表尾端
    } else {
      context = appendAssistantMessage(context, decision.message); 
    }

    enforceLimits(context); // 步數/時間/循環限制
  }

  return extractFinalResult(context);
}

Agents SDK 會替你處理大部分瑣事:保存歷史、封送 tool‑calls、重試邏輯等等。你只需設定組態並實作工具本身。

Run vs step

要分清兩個概念:

  • run——代理針對某個目標的一次啟動:「為此情境挑選禮物」。
  • step——run 迴圈中的一步:一次具體的模型呼叫,可能產生文字回覆或 tool‑call。

在監控中,你會看到一次 run 中的多個 step;而安全與成本的限制,常常是「以 run」或「以 step」為單位設定。

既然已經明白代理在一次 run 中如何沿著 run 迴圈行進,接下來看看哪些地方值得上代理,哪些地方用簡單的 tools 就夠了。

5. GiftGenius 中哪些地方需要代理,哪些不需要

在到處上代理之前,先問自己一個誠實的問題:「這裡真的需要它嗎?」

適合代理的情境,是帶有分支、重試與邏輯的多步任務,而把這些全塞在提示裡會很不方便。

在 GiftGenius,這樣的任務可以是「智慧禮物精靈」,它會:

  • 追問重要細節(收禮者性別、嗜好、親近程度);
  • 能查詢多個商品來源(透過不同廠商的 MCP‑tools);
  • 過濾並排序結果;
  • 當來源出錯時重試,或走備援路徑;
  • 回傳的不只是文字清單,而是包含解釋與 product feed 中 SKU 連結的結構化候選列表。

在這裡,代理作為「編排器」會非常有用,特別是當你之後想補上語音/Realtime 情境或更複雜的商務(ACP)。

但對於簡單的 getGiftDetails(giftId) 呼叫就不需要代理:由 ChatGPT 直接呼叫的 MCP‑tool 就能完全覆蓋。像「根據商品卡說明寫一段描述」這種「單步驟」的場景也一樣。

一般的務實準則是:如果能把場景描述為「一個正常的 tool」,多半不需要代理;如果你開始明確地規劃多步 workflow 並帶有檢查與重試,很可能代理會帶給你助益。

6. 確定性:如何讓代理行為更可預測

在 LLM 代理的世界裡,「確定性」很微妙。理論上,在相同輸入與設定下,你希望得到相同的行動計畫與一致的 tool‑calls 序列;實務上模型仍具隨機性,不過你手上有幾個控制可預測性的槓桿。

首先是經典:溫度(temperature)等生成參數。溫度越低,創造性越少,模型越「聽話」。對禮物挑選代理,你大概會希望不是 0,但也不要太高,不然模型每天都會發明一套呼叫同一工具的新方法。

其次是清晰的系統指令。如果你模糊描述如「你可以叫各種工具、做你想做的」,別意外代理一下跳 API、一下又嘗試「憑空回覆」。更好的做法是明確寫出在什麼情況應呼叫工具、哪些參數允許、如何解讀錯誤、何時該結束任務。

例如,GiftGenius 代理的系統提示可以包含:

如果你沒有完整的收禮者個人檔(年齡、性別、場合、預算),
先透過對使用者的渠道提出澄清問題並等待回答。
只有之後才呼叫工具 search_gifts,且需帶上完整的個人檔。
不要憑空捏造商品,務必以工具的結果為依據。

這類指示會降低決策的變異,使行為更具確定性。

第三是工具本身的設計。如果你有三個工具「大致都在找禮物」,模型必然有時選這個、有時選那個。最好將工具設計成職責清晰且不重疊,並在描述中加以說明。

最後,可以使用guardrails——用來檢查代理動作與模型結果的規則與綱要。Agents SDK 內建對檢查與限制的支援,包括對輸出資料結構的限制。如果模型試圖產生不符合結構的內容,你可以柔性修正,甚至重做該步。

迷你範例:固定結果格式

假設你需要代理總是回傳含有 gifts 欄位的 JSON,而其中的物件必須包含 idtitlescore。你可以:

  • 在代理層級描述這個結構;
  • 指定最終輸出必須符合它;
  • 若違反——重試該步或回傳安全錯誤。

偽代碼:

const giftResultSchema = z.object({
  gifts: z.array(z.object({
    id: z.string(),
    title: z.string(),
    score: z.number().min(0).max(1),
  }))
});

// 在代理的設定中
const agent = new Agent({
  /* ... */
  outputSchema: giftResultSchema,
});

當模型嘗試回傳奇怪的東西時,runner 會回報驗證錯誤,你可以重請模型或記錄事件。

7. 冪等性:為什麼代理可能會把你的 API 呼叫兩次

如果確定性講的是「相同輸入得到相同計畫」,那麼冪等性講的就是重試的安全性。在代理情境中它至關重要,理由有二。

其一,你多了一層重試:不只有 HTTP 用戶端與負載平衡器,代理本身也可能在收到錯誤或不完整結果時決定重試工具呼叫。其二,真實的生產場景中還會有 webhook、佇列、串流通道——你可能會不小心把同一個邏輯步驟處理多次。

你已在 MCP‑tools 層面討論過冪等性:避免重複扣款、不重複建立相同訂單、在請求中使用 idempotency keys。現在同樣的事,放到代理的多步特性上再加成。

假設 GiftGenius 有個工具 create_checkout_session,會根據已選的禮物建立 ACP/Stripe 的 draft 結帳。若代理因網路錯誤決定重試這個呼叫,你當然不希望出現兩筆訂單與兩次扣款。

因此你需要:

  • 為每個邏輯動作設計外部idempotency key(例如 runId + stepIndex,或明確生成的 checkoutDraftId);
  • 把它傳入你的後端/ACP endpoint;
  • 在後端檢查此鍵是否已處理過,如已處理則回傳保存結果,避免再次執行。

TypeScript 偽範例:

async function createCheckoutDraft(runId: string, payload: DraftPayload) {
  const key = `gift-checkout-${runId}`;

  const existing = await findDraftByKey(key);
  if (existing) return existing;

  const draft = await stripe.checkout.sessions.create({
    /* ... */,
    idempotencyKey: key, // 或自行在外面加一層
  });

  await saveDraftWithKey(key, draft);
  return draft;
}

如此一來,即便代理因某些原因用同一個 runId 呼叫這個工具兩次,你的程式碼仍具冪等性:同一個邏輯步驟 → 得到相同的實際結果。

「先檢查,再執行」

第二個常見的冪等模式是先檢查狀態再執行。例如,在建立訂單之前,先檢查是否已存在相同 clientReferenceId 或相同參數的訂單。這在冗長的 workflow 裡特別有用,因為代理可能「忘了」自己在前一步做過什麼。

Safe‑mode/Fake‑mode

在開發階段,對危險工具準備「安全模式」很有幫助:不做真實動作,只記錄若執行會做什麼,並回傳模擬結果。對代理而言,這是用實際環境演練 run 迴圈、又不冒金錢或資料風險的方式。

8. 小實作:用自然語言描述 GiftGenius 代理

我們談了 run 迴圈、確定性與工具的冪等性。現在先離開程式碼一分鐘,看看這些如何拼成真實情境。

現在做個小練習(紙上或腦中),不寫程式。

想像你要描述一個簡單的代理:

  • system
    : 你是禮物挑選助手;你會先確認重要細節、絕不憑空捏造商品,只使用工具的結果。
  • user
    : 我想給同事準備一份不超過 $50 的禮物。

用文字描述,這樣的代理應該做哪些步驟。

典型流程可能像這樣:

  1. 先檢查資訊是否足夠。不足時提出澄清問題:同事大概做什麼(設計、開發、管理)、有沒有禁忌(酒類、玩笑型禮物)、配送是否有限制。答案要麼存入 session state,要麼成為工具呼叫的參數。
  2. 然後用完整輪廓呼叫 search_gifts 工具:「同事是開發者,預算 50,類別是小工具與辦公室」。工具回傳帶價格、類別與商品 ID 的候選清單。
  3. 接著,如果發現部分商品無法配送到目標地區,代理可以呼叫額外工具 filter_gifts_by_constraints,或在自己的提示中手動過濾。之後依相關性與價格排序,必要時加上評論(「若同事喜歡咖啡很合適」、「遠端工作者的好選擇」)。
  4. 最後,代理為 ChatGPT App 準備最終的結構化回覆:列出 5–7 個禮物,附上簡短說明、使用建議,以及 Checkout 連結(或下一步——建立 draft 結帳)。

哪些地方需要 tool‑calls?很明顯是在商品搜尋與過濾、可用性檢查、建立 draft 結帳。哪些步驟必須冪等?首要是所有與訂單與金流相關的操作——建立 draft 結帳,還有可能是寫入資料庫的歷史記錄。

9. 初學代理時的常見錯誤

錯誤 №1:把代理當成「沒有邊界的第二個 ChatGPT」。
有時你可能只是再給模型一段提示,然後稱它為「代理」。結果就是一個產出很多文字、隨機呼叫工具、難以控制的東西。避免這點的關鍵:在 system 中明確描述代理的角色,限制工具清單,把它當作帶明確使命的編排者,而不是「另一個文字生成宇宙」。

錯誤 №2:工具缺乏冪等性。
開發者常把舊的 HTTP handlers 原封不動搬到代理之下,忽略 runner 現在可能自動重試呼叫。對支付與訂單而言,這會造成非常糟糕的後果。正確方式是從一開始就設計工具為冪等:用相同邏輯鍵重複呼叫不會重複執行動作。

錯誤 №3:模型設定過於「有創意」。
高溫度很適合寫祝酒詞或詩,但對必須可靠編排多步流程的代理而言,這是不可預測行為的溫床:模型會每次挑不同工具、產生不一樣的計畫,甚至偶爾忘了自己有 tools。把代理視為「服務型」元件,讓它在更嚴謹的模式下運作。

錯誤 №4:做一個「萬用工具」。
有時會想做一個通用工具,比如 execute_any_sqldo_anything_with_orders,然後交給代理。結合 LLM 的創造性,幾乎註定是安全風險。比起一個擁有一切權限的「神級工具」,多個職責清晰、權限明確的專用工具更好。

錯誤 №5:沒有明確的 run 結束準則。
若不告訴代理何時該停止,它可能陷入無窮或半無窮迴圈:再檢查一次結果、再問一次使用者、在同樣錯誤下再試一次。這常在壓力情境才浮現,比如某個相依服務不穩定。正確方式是設定步數上限、run 時間與相同錯誤的重試次數,並在 system 中寫清楚:當合理選項用盡,就要「誠實地放棄」。

錯誤 №6:把一切都塞進代理狀態。
因為 Agents SDK 讓 session state 很好用,就容易什麼都往裡放:大文件、未處理的日誌、敏感資料。這會膨脹上下文、增加成本並帶來安全風險。代理狀態只應保存延續工作所需的內容;其他都放到資料庫、日誌等層,並注意隱私。

錯誤 №7:在明明一個 MCP‑tool 就足夠的地方使用代理。
有時開發者在任務只是呼叫一個函式並回傳結果時,就從代理入手。這是在不需要的地方增加複雜度:出現 run 迴圈、狀態、更多日誌與潛在故障點。如果情境只需一次 tool‑call 而無複雜 workflow,最好就保持如此,只有當真正出現多步性時才引入代理。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION