CodeGym /課程 /ChatGPT Apps /多步驟流程:由模型自動編排與循環控制

多步驟流程:由模型自動編排與循環控制

ChatGPT Apps
等級 12 , 課堂 3
開放

1. 什麼是多步驟 run,與「單次」請求有何不同

當你只使用 ChatGPT App 與 MCP tools 時,常見的圖景相當線性:收到使用者請求 → GPT 決定呼叫一個或多個工具 → 你把結果回覆給使用者。即使工具裡面做得更複雜,這仍可視為「一個邏輯步驟」。

對代理而言,run 就是目標 + 一系列步驟。我們不再以「一個 prompt — 一個回覆」來思考,而是把任務視為由代理從頭到尾推進的小型專案。

可以這樣對比:

互動類型 模型做什麼 邏輯位於何處
在 ChatGPT App 中的普通工具呼叫 判斷是否要呼叫工具、填入參數、依據結果組合回覆 主要商業邏輯與步驟順序集中在單一工具或 backend
代理式 run(Agents SDK) 規劃多個步驟、決定何時呼叫哪個 tool、分析中間結果,並可修正計畫 「如何達成目標」的邏輯部分在代理的 system 指令中,部分由模型自行生成

這裡有個重點:你不必把規劃完全交給模型。通常會採混合式:你以程式碼嚴格定義主要階段(例如「先蒐集需求,再挑禮物,然後準備卡片」),而在每個階段之內,允許代理較自由地使用工具。

小比喻

單次工具呼叫就像叫快遞:「請取一份文件並送到辦公室」。

多步驟的代理式 run 則像私人助理:「幫我準備一份送給同事的生日禮物:先了解他的喜好,挑幾個方案,確認配送,最後做成好看的簡報」。助理會自行決定沿途要做哪些動作。

在接下來的內容中,我們也會看看這類多步驟的 run 如何嵌入你熟悉的 Apps SDK → MCP → backend 技術棧,讓對 ChatGPT 與小工具而言,代理邏輯看起來就像一個乾淨的 MCP 工具。

2. 模型如何自行規劃步驟:鳥瞰

以 Agents SDK 的術語來說,每個 run 可便利地想成三件事:

  1. Goal(目標):會放入代理的 system/user 指令中的任務文字描述。
  2. Tools:具備良好描述與 JSON Schema 的可用工具集合。
  3. State:你在外部(資料庫、Redis 等)保存的步驟歷史與結構化狀態。

接著是熟悉的 run 循環:模型查看目標與可用工具,在每一步決定:

  • 「我現在資訊已足夠 —— 可以把最終結果回覆給使用者」;
  • 或「我需要以這些參數呼叫工具 X」;
  • 或「我拿到工具結果,現在需要詮釋、過濾,或許呼叫另一個工具」。

在偽代碼層面,概念大致如下(提醒:這是心智模型,而非真實 API):

while (!done && steps < MAX_STEPS) {
  const modelResponse = await callModel({
    system: agentPolicy,
    messages: history,
    tools,
  });

  if (modelResponse.type === "tool_call") {
    const toolResult = await callTool(modelResponse.toolName, modelResponse.args);
    history.push({ role: "tool", content: toolResult });
  } else {
    // 最終回覆
    done = true;
    return modelResponse.content;
  }

  steps++;
}

在實際的 Agents SDK 中,整個循環都已實作並「封裝」在函式庫裡。你以宣告式描述代理,SDK 就會在模型與工具之間循環,直到拿到最終回覆,或碰到步驟/時間限制為止。

架構師的任務是:

  • 設計 goal 與 system 指令,使模型能規劃出合理步驟;
  • 建立沒有語義重疊的工具集;
  • 設定步驟與時間的限制;
  • 思考哪些步驟可以平行化。

當我們有了目標、工具與狀態觀念,下一個問題就是要以哪些步驟達成目標。步驟並不相同:有些必須嚴格順序,有些可以平行處理。

3. 順序步驟與平行步驟

在理解代理的 run 循環之後,重要的是釐清流程中的步驟型態。在代理的 workflow 中,大致有兩種:順序與平行。

順序步驟

當步驟 A 的結果對步驟 B 至關重要時,就是順序步驟。例如在我們的教學專案 GiftGenius

  1. 先理解送禮對象是誰:同事/親友、年齡、興趣。
  2. 接著透過 tool search_gifts 挑選候選清單。
  3. 然後依據預算與限制進行過濾。
  4. 接著為小工具把卡片排版美化。
  5. 最後才可能引導至結帳(checkout)。

每個後續步驟都依賴前一步的資料,因此執行必須嚴格依序進行。

在代理行為的偽計畫中,可能看起來像這樣:

1. 詢問使用者關於收禮者與預算的細節
2. 呼叫 tool search_gifts(profile, budget)
3. 呼叫 tool filter_by_constraints(gifts, constraints)
4. 整理最終清單與描述

模型並不會真的用程式碼寫出這樣的清單,但我們可以透過 system 指令、對話範例與工具描述,推動它朝這個結構前進。

平行步驟

有時步驟可以獨立執行。比方我們想同時比對三家商店的禮物候選:

  • search_gifts_amazon
  • search_gifts_etsy
  • search_gifts_local_store

對代理而言,這是三個彼此獨立的工具呼叫,可以平行啟動,以縮短總回應時間。

在 Agents SDK(以及現代代理框架)中,當模型在同一步提出多個呼叫時,往往內建支援工具的平行呼叫。典型情境是:模型在回覆中列出要呼叫的工具清單,SDK 便並發執行、收集結果,並把它們整理成一組 tool 訊息,提供給模型的下一步。

在規劃層面,大致如下:

// 代理步驟:模型決定呼叫三個工具
const calls = [
  { name: "search_gifts_amazon", args: {...} },
  { name: "search_gifts_etsy", args: {...} },
  { name: "search_gifts_local_store", args: {...} },
];

const results = await Promise.all(
  calls.map(c => callTool(c.name, c.args))
);

// 接著把所有結果加到脈絡,供模型的下一步使用

如果你寫過 JS/TS 前端,應該對平行請求不陌生:例如用 Promise.all 同時發出多個 fetch()。現在同樣的想法出現在代理的 run 循環內,只是關於哪些能平行執行的決策,常常由模型來做。

4. GiftGenius 的 workflow 範例:步驟、目標與工具

在順序步驟一節中,我們已直覺地把 GiftGenius 的行為切成幾個階段。現在把它正式化為代理 workflow:描述目標、步驟,並綁定到工具與代理設定。我們先不依附特定 Agents SDK API,只用結構說明並加入一些假想的 TypeScript 代碼輔助理解。

目標(goal)

目標可以這樣表述:

協助使用者為特定收禮者挑選 3–5 個禮物方案,考量預算、場合與配送限制,並輸出供 GiftGenius 小工具使用的結構化禮物卡片清單。

主要步驟

先描述一個由 4 個步驟組成的最小版本:

  1. 釐清收禮者的脈絡
    目標:收集送給誰(年齡、性別、興趣、與贈與者關係)、預算與活動日期等資訊。
    工具:可能完全不需要工具,純模型 ↔ 使用者對話。
  2. 搜尋與初步篩選禮物
    目標:取得「原始」的禮物候選集。
    工具:search_gifts(profile, budget) —— 連到我們的目錄/搜尋系統並回傳候選清單的 tool。
  3. 篩選與排序
    目標:剔除不合適的選項(無法配送至目標地、超出預算、不符限制),並依相關性排序。
    工具:filter_and_score_gifts(candidates, constraints) —— 乾淨且具冪等性的工具。
  4. 為小工具格式化結果
    目標:把資料整理成適合 UI 的格式:標題、短描述、圖片、價格、CTA。
    工具:format_gift_cards(gifts) —— 可為程式型工具(產生結構)或 LLM 工具(產生文案)。

在代理設定中可能長這樣

想像有個代理建構器(偽代碼):

import { createAgent } from "@acme/agents-sdk";
import { tools } from "./gift-tools";

export const giftAgent = createAgent({
  name: "gift-guru",
  system: `
    你是 GiftGenius 代理,協助挑選禮物。
    目標:提供 3–5 個確實可購買的選項,
    並考量收禮者輪廓、預算與配送限制。
    先釐清重要細節,再使用搜尋與篩選工具。
    若尚未知曉預算或關鍵興趣,請不要呼叫工具。
    當你擁有清晰的禮物卡片清單時再結束工作。
  `,
  tools, // 這裡會有 search_gifts、filter_and_score_gifts、format_gift_cards
  maxSteps: 12,
  timeoutMs: 15000,
});

請注意幾點:

  • 在 system 指令中,我們明確要求代理先釐清細節,再呼叫搜尋工具。這能降低模型在脈絡過於模糊時就亂呼叫工具的風險。
  • 我們限制了 maxSteps,避免代理陷入無窮迴圈。
  • timeoutMs 用於避免整個 run 拖太久而影響使用者體驗。

5. 由模型自動編排:哪些交給模型,哪些要嚴格固定

代理的本質是模型自由度與你所設定的嚴格結構之間的平衡。

若給模型過多自由且未設邊界,你會得到「創意亂流」:多餘的 tool 呼叫、重複步驟、不明所以的循環。反之,若把所有東西都在 backend 以有限狀態機硬編碼,模型就淪為文字裝飾,而非能幹的任務執行者。

通常交給模型決定的事項

GiftGenius 與類似情境中,合理交給模型的部分包括:

  • 向使用者提問的措辭(如何釐清興趣、如何禮貌詢問預算);
  • 判斷何時資訊已足夠可以啟動搜尋;
  • 在單一階段中選擇要用哪些工具(例如有多個商店搜尋工具時,選擇其一);
  • 產生描述文字、解釋與比較內容。

更適合硬性規定的部分

同時,建議預先固定:

  • 主要階段(「資訊蒐集」→「搜尋」→「篩選」→「格式化」→「完成」);
  • 步驟與時間的上限;
  • 代理必須停止並如實告知無法完成的條件(例如預算只有 5 美元,卻想在明天前送達昂貴電子產品);
  • 工具的冪等性政策與重試策略。

混合式範例:以狀態管理階段,細節交給模型

可以在代理的 state 中設一個 phase 欄位,取值為 "collect_profile" | "search" | "filter" | "format" | "done"。如此一來,你的 backend(或支援自訂狀態機的 Agents SDK)即可控制各階段允許的工具。

偽程式碼:

type Phase = "collect_profile" | "search" | "filter" | "format" | "done";

interface GiftAgentState {
  phase: Phase;
  profile?: UserProfile;
  candidates?: GiftCandidate[];
  finalGifts?: GiftCard[];
}

代理的 system 指令可以包含各階段的簡述,而你在程式中則依據當前階段限制提供給模型的 tools 清單。這就是 tool gating 的例子,會在 workflow 模組中更詳細討論。

6. 控制無窮循環與無效重複

若讓代理的 run 循環毫無節制,遲早會像學生死線前一樣:一直「再確認與重寫」,就是不交作業。我們的任務是避免它卡住。

無窮循環常見於三種來源:

  1. 模型對答案不確定,持續用些微差異反覆向工具發出同類請求。
  2. 工具穩定地回傳錯誤或空結果,而代理固執地「再試一次」。
  3. 代理在兩個工具之間來回卡住,不斷互相呼叫,卻無法邁向最終答案。

步驟上限(maxSteps)

最簡單且必備的機制是限制步驟數。在大多數 Agents SDK 實作中,你可以在啟動 run 或代理設定時指定 maxSteps。一旦達到上限,SDK 會以特殊狀態(例如 aborted_by_max_steps)結束 run。接著你決定如何呈現給使用者。

GiftGenius 中,我們可假設合理的禮物挑選能在約 10 個步驟內完成(幾次釐清、幾次搜尋、一次篩選與格式化)。可以把上限設在 12–15 步,並謹慎處理達上限的情況:

const run = await giftAgent.run({
  input: userGoal,
  maxSteps: 12, // 覆寫預設值
});

if (run.status === "max_steps_exceeded") {
  // 向使用者顯示清楚的訊息
}

時間上限(timeout)

有時問題不在步驟數,而是總耗時。工具可能很慢,或網路不穩。因此同時在單次 tool 呼叫與整個 run 層級設置 timeoutMs 很有幫助。

例如你可以規定:

  • 每次外部 API 呼叫(向合作夥伴搜尋禮物)不應超過 3–5 秒;
  • 整個禮物挑選的 run 應在 15 秒內完成。

若觸發 timeout,你要妥善終止 run,也許呈現部分結果,並說明「部分來源未及時回應」。

偵測重複

更進一步(且實用)的作法,是偵測以相同參數重複呼叫的工具。如果你看到代理連續三次以完全相同參數呼叫 search_gifts(profile, budget),那就是卡住的訊號。

你可以在 state 中加入以 (toolName, argsHash) 為鍵的呼叫計數器,當超過門檻時,要嘛:

  • 中止 run 並回傳可理解的錯誤給使用者;
  • 要嘛向模型補充指令:「你已三度以相同參數呼叫此工具,請嘗試改變策略或詢問使用者」。

偽代碼:

function shouldAbortToolCall(toolName: string, args: unknown, state: GiftAgentState) {
  const key = `${toolName}:${hashArgs(args)}`;
  const count = state.toolCallCounts[key] ?? 0;

  if (count >= 3) return true;

  state.toolCallCounts[key] = count + 1;
  return false;
}

其中 hashArgs 是任何具決定性的參數序列化函式(例如以排序鍵的 JSON.stringify)。

7. 明確的完成準則

「玩具級」代理與生產級代理的關鍵差異之一,是是否具備清楚可理解的完成準則。若沒有,模型可能過早結束(「給你一些禮物,自己看著辦」),或反之無止盡地「打磨」結果。

GiftGenius 中,可以制定簡單規則:

  • 當代理擁有 3 到 5 個禮物,且欄位已填齊: idtitleshortDescriptionpriceimageUrlpurchaseUrl, 並已通過預算與配送的過濾,代理即可結束。
  • 若在最多 N 次搜尋與篩選後,合適禮物少於 3 個,代理需如實告知找不到理想選項,並建議擴大預算或放寬限制。

你可以把這些準則寫入代理的 system 指令,與/或在 run 結束後檢查結果。

run 結束後的結果檢查範例:

if (run.status === "completed") {
  const gifts = run.output.gifts; // 假設代理回傳結構化的 JSON

  if (!gifts || gifts.length < 3) {
    // 代理「完成」了,但結果偏弱 —— 你可以:
    // 1) 顯示坦誠的說明,
    // 2) 建議使用者調整條件。
  } else {
    // 一切正常 —— 顯示禮物小工具
  }
}

別指望模型能魔法般理解你的商業成功條件。身為開發者,你必須明確定義「可接受結果」的條件並加以驗證。

8. 編排究竟在哪裡實作:代理、後端、元件

我們先前提過,編排可以存在於不同層級:代理、後端、小工具。

就多步驟流程而言,邏輯大致如下。

代理(Agents SDK)負責「思考式」的 workflow:

  • 如何把目標拆成步驟;
  • 要呼叫哪些工具、順序為何;
  • 要向使用者追加哪些問題。

Backend通常提供:

  • 工具實作(搜尋、過濾、商務等);
  • 狀態與檢查點的保存;
  • 嚴格的商業限制(預算上限、權限、區域可用性)。

小工具(Apps SDK)負責:

  • 進度呈現(步驟指示器、進度條、「第 2/4 步」);
  • 輸入表單;
  • UX 細節,例如當資料未填齊時停用按鈕。

良好實務是:代理編排工具與對話,小工具編排使用者的視覺體驗。兩者透過結構化資料(ToolOutput、agent run output)協作。

9. 迷你程式碼範例:從 MCP 工具啟動 GiftGenius 多步驟代理

如同本講一開始所述,現在把新概念與你熟悉的 Apps SDK → MCP → backend 技術棧串起來,展示一個小範例,說明 MCP 工具如何啟動代理的 run

假設在你的 app/mcp/route.ts 中,有個 tool run_gift_workflow,它會:

  • 接收使用者的文字請求(他的目標);
  • 啟動代理 giftAgent
  • 回傳給小工具的結構化結果。

程式碼雖經過簡化與假設,但能表達串接方式:

// app/mcp/route.ts
import { server } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";
import { giftAgent } from "@/agents/giftAgent";

server.registerTool(
  "run_gift_workflow",
  {
    title: "挑選禮物",
    description: "啟動多步驟的禮物挑選代理",
    inputSchema: {
      userGoal: z
        .string()
        .describe("使用者的任務,例如:想要為同事挑一份 50 美元以內的禮物"),
    },
  },
  async ({ userGoal }) => { 		
    const run = await giftAgent.run({		// 在這裡以 12 個步驟與 15 秒的 timeout 啟動代理
      input: userGoal,
      maxSteps: 12,
      timeoutMs: 15000,
    });

    return {
      status: run.status,
      gifts: run.output?.gifts ?? [],
      debug: run.debugInfo, // 之後可以移除
    };
  }
);

接著 ChatGPT App 可以像調用其他工具一樣呼叫此 MCP tool,而你的 GiftGenius 小工具則可根據 gifts 建構 UI。你在「引擎蓋下」得到多步驟的 workflow,而對 ChatGPT 而言,外觀仍是一個乾淨的 tool。

10. 設計多步驟流程的常見錯誤

錯誤 №1:「讓模型自己看著辦,我只要把所有工具都給它」。
當代理拿到十來個語義重疊的 tools,且沒有清楚的 system 指令與階段劃分時,模型會左右搖擺:用不同方式重複呼叫相同工具、重複請求、陷入循環。更好的做法是花時間設計:把情境分成階段、在每個階段限制工具清單,並在 system prompt 中明確指示策略。

錯誤 №2:缺少步驟與時間的上限。
若不設定 maxStepstimeout,在生產環境很快會出現「迷航」的 run,既消耗資源,使用者又無所適從。限制不是「可選項」,而是基本衛生。同時,超出限制時要妥善處理,而不是丟個沉默的 500。

錯誤 №3:沒有明確的完成準則。
模型會在它覺得「夠了」時結束 run,但這個「夠了」常與商業需求相去甚遠。若不形式化成功標準(需要多少禮物、哪些欄位、通過哪些過濾)並檢查,你將得到不穩定的 UX:今天五個很棒的選項,明天一個平庸加三個重複。

錯誤 №4:不追蹤重複的工具呼叫。
代理可能卡在「收到錯誤 → 改兩個字重發請求 → 再呼叫同一工具」的模式。若你不依 (toolName, args) 追蹤重複呼叫,這些循環會在你看日誌前完全隱形。簡單的計數器與參數雜湊能大幅改善。

錯誤 №5:把編排與商業邏輯實作混在同一個工具中。
有時會把整個 workflow 塞進單一 MCP tool 或代理函式:同時做搜尋、篩選、格式化與決策。結果代理失去意義 —— 模型無法逐步掌控流程,你也失去透明度與組件化重用。更好的做法是把各階段拆成獨立 tools,提供給代理組合。

錯誤 №6:缺少狀態與檢查點的連結。
沒有保存中間狀態與檢查點的多步驟流程,會變成脆弱的巨石:中途一旦失敗,使用者只能重來。這對會在步驟間來回或隔一段時間回訪的情境尤其致命。請使用狀態儲存,保存階段、輪廓、候選,並允許代理從斷點續跑。

錯誤 №7:忽視 UX 層。
開發者有時沉浸於代理的內部 workflow,而忘了使用者只看到小工具與聊天訊息。若 UI 缺少清楚的進度、狀態(「正在搜尋禮物…」、「正在篩選候選…」),使用者就會以為 App「當機」或「沒在做事」,即便代理在背後編排著複雜流程。規劃多步驟的 run 時,務必同步思考它在介面上的呈現。

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