CodeGym /課程 /ChatGPT Apps /伺服端工具實作:從呼叫到回應

伺服端工具實作:從呼叫到回應

ChatGPT Apps
等級 4 , 課堂 2
開放

1. 全貌:經由伺服端的工具呼叫流程

在動手寫程式之前,先固定好架構。這能幫助我們不會淹沒於細節。

用 Apps SDK + MCP 的術語來說:我們有一個 MCP 伺服器(在本課程中是 Next.js 的 Route Handler app/mcp/route.ts),它會註冊工具與資源,並為這些工具實作處理器。

高層次示意:

sequenceDiagram
    participant User as 使用者
    participant Chat as ChatGPT(模型)
    participant App as ChatGPT App
    participant MCP as MCP 伺服器 / backend
    participant DB as 目錄/外部 API

    User->>Chat: 「幫我挑選禮物…」
    Chat->>App: 決定呼叫工具 `suggest_gifts`
    App->>MCP: JSON-RPC call_tool(名稱 + 引數)
    MCP->>MCP: 驗證、授權
    MCP->>DB: 查詢目錄/過濾
    DB-->>MCP: 候選清單
    MCP-->>App: structuredContent + content + _meta
    App-->>Chat: 將結果傳給模型與小工具
    Chat-->>User: 解釋選擇,顯示小工具

重點:伺服端並不知道模型的「魔法」。它只看見一般請求:工具名稱 + 引數,並且必須回傳結構化的回應。而模型看不到你的程式碼,它只看見:

  • 有哪些工具及其結構描述;
  • 它自己組出的引數;
  • 你回傳的 JSON 回應。

因此本講的目標——妥善實作中間那一段:MCP 伺服器與工具的處理器。

Insight: mcp-tools limit

在 MCP 伺服器裡,工具的數量就像記憶體或 context token 一樣是受限資源。形式上你可以註冊數十甚至上百個工具,但平台與模型對它們的處理不是線性的:每新增一個工具,都會增加路由時的「噪音」。

實務建議:

  • 硬上限(ChatGPT)約為每個伺服器 最多 128 個 MCP-tools
  • 建議範圍——最多 50 個工具。再往上,品質會明顯下滑:模型會混淆描述相近的工具,較少想起冷門工具,更常選錯工具。

在 Anthropic 也差不多:上限約 100 個工具,而他們建議控制在 50 個以內

2. 在 Next.js + Apps SDK 樣板中,伺服端邏輯位於何處

在模組 2 中我們已經部署過官方的 ChatGPT App Next.js 樣板,並快速瀏覽其結構。現在看看 MCP 伺服器位於哪裡,以及它如何與小工具串接。

若你使用該樣板,MCP 伺服器通常實作於 app/mcp/route.ts(App Router)。ChatGPT 的 JSON‑RPC 呼叫會送至此處:tools/callresources/listhandshake 等。

典型的專案結構:

my-chatgpt-app/
├─ app/
│  ├─ mcp/
│  │  └─ route.ts          # MCP 伺服器 + 工具註冊
│  ├─ page.tsx             # React 小工具(UI)
│  ├─ layout.tsx           # Root 版型,啟動 SDK
│  └─ globals.css          # 全域樣式
│
├─ proxy.ts                # CORS 等其他設定
├─ next.config.ts
├─ package.json
├─ tsconfig.json
└─ .env

route.ts 我們會:

  1. 建立 MCP 伺服器實例(透過 @modelcontextprotocol/sdk);
  2. 註冊工具(server.registerTool(...));
  3. 定義 HTTP 處理器,接收來自 ChatGPT 的請求並轉交給 MCP 伺服器。

接下來將以 TypeScript 撰寫程式,依據此結構進行。

3. 最小化 MCP 伺服器與工具處理器

先從最簡單的開始:建立伺服器並加入教學用工具 suggest_gifts,先回傳占位結果。

假設已經安裝 MCP SDK:

pnpm add @modelcontextprotocol/sdk

接著建立簡單的 app/mcp/route.ts

// app/mcp/route.ts
import { NextRequest } from "next/server";
import { McpServer } from "@modelcontextprotocol/sdk/server";

const server = new McpServer({ name: "giftgenius-mcp" });

// 使用最小化的結構註冊工具
server.registerTool(
  "suggest_gifts",
  {
    title: "禮物推薦",
    description: "依據興趣與預算推薦禮物。",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "對收禮者的簡短描述。" },
      },
      required: ["query"],
    },
  },
  async ({ input }) => {
    // 這裡會放商業邏輯
    return {
      content: [
        {
          type: "text",
          text: `占位:為 "${input.query}" 挑選的禮物。`,
        },
      ],
      structuredContent: {},
    };
  }
);

// Next.js HTTP 處理器
export async function POST(req: NextRequest) {
  const body = await req.text(); // JSON-RPC 字串
  const response = await server.handle(body);
  return new Response(response, {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
}

這已是可運作的版本:ChatGPT 能呼叫 suggest_gifts,伺服器會回傳文字占位結果。

重點在於 server.registerTool 接收:

  • 工具名稱;
  • 中介資料與輸入的 JSON Schema;
  • handler——一個非同步函式,會收到 input 引數。

但目前既沒有進一步的驗證、也沒有良好的結構化輸出,更沒有授權。接下來就來補上。

4. 輸入驗證與分層

為什麼僅有 JSON‑Schema 還不夠

沒錯,平台會依據 schema 自動驗證基本要件:欄位型別、必要屬性等。然而:

  • 模型可能傳來邏輯上不正確的資料(例如預算 −100,或 1000 個興趣);
  • 你有商業限制(最大預算、支援的幣別等);
  • 有時 ChatGPT 或其他客戶端會表現反常,送來完全出乎意料的東西。

因此在處理器內仍需要額外驗證。

拆分程式:處理器 ↔ 商業邏輯

為避免伺服端程式變成義大利麵,建議將商業邏輯獨立存放。例如建立 app/mcp/gifts.ts

// app/mcp/gifts.ts
export type SuggestGiftsInput = {
  age?: number | null;
  relationship: "friend" | "partner" | "colleague";
  maxBudget: number;
  interests: string[];
};

export type GiftItem = {
  id: string;
  title: string;
  price: number;
  currency: "USD";
  score: number;
  tags: string[];
  shortDescription: string;
};

// 簡單的「禮物目錄」
const CATALOG: GiftItem[] = [
  {
    id: "board-game-1",
    title: "桌上遊戲《星際策略》",
    price: 39,
    currency: "USD",
    score: 0.93,
    tags: ["board_games", "strategy", "2-4_players"],
    shortDescription: "很適合喜愛桌遊的人。",
  },
  // ...
];

export function suggestGifts(input: SuggestGiftsInput): GiftItem[] {
  if (input.maxBudget <= 0) {
    throw new Error("預算必須是正數。");
  }

  const filtered = CATALOG.filter(
    (item) => item.price <= input.maxBudget
  );

  // 簡化:依 score 排序並取前 3 名
  return filtered.sort((a, b) => b.score - a.score).slice(0, 3);
}

現在在 MCP 工具的處理器中,我們要做:

  • 解析 input
  • 對應到型別 SuggestGiftsInput
  • 安全地呼叫 suggestGifts
  • 將結果打包為 ChatGPT 與我們 UI 都能理解的格式。

5. 實作處理器:從 input 到 structuredContent

route.ts 中重寫 registerTool,使用我們的商業邏輯:

// app/mcp/route.ts (片段)
import { suggestGifts, SuggestGiftsInput } from "./gifts";

server.registerTool(
  "suggest_gifts",
  {
    title: "禮物推薦",
    description:
      "當需要依據興趣、預算與關係類型來挑選禮物時使用。",
    inputSchema: {
      type: "object",
      properties: {
        age: {
          type: "integer",
          minimum: 0,
          maximum: 120,
          description: "收禮者年齡(若已知)。",
        },
        relationship: {
          type: "string",
          enum: ["friend", "partner", "colleague"],
          description: "與收禮者的關係類型。",
        },
        maxBudget: {
          type: "number",
          minimum: 1,
          description: "最高預算(美元)。",
        },
        interests: {
          type: "array",
          items: { type: "string" },
          description: "收禮者的興趣(例如:board games、hiking)。",
        },
      },
      required: ["relationship", "maxBudget", "interests"],
    },
  },
  async ({ input }) => {
    // 基本邏輯驗證
    if (!Array.isArray(input.interests) || input.interests.length === 0) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: "至少需要提供一個收禮者的興趣。",
          },
        ],
        structuredContent: { errorCode: "NO_INTERESTS" },
      };
    }

    const payload: SuggestGiftsInput = {
      age: input.age ?? null,
      relationship: input.relationship,
      maxBudget: input.maxBudget,
      interests: input.interests,
    };

    const items = suggestGifts(payload);

    if (items.length === 0) {
      return {
        content: [
          {
            type: "text",
            text:
              "在該預算下我找不到合適的禮物。請試著提高預算或調整興趣。",
          },
        ],
        structuredContent: {
          items: [],
          emptyReason: "NO_MATCHES",
        },
      };
    }

    return {
      content: [
        {
          type: "text",
          text: `找到 ${items.length} 個合適的禮物選項。`,
        },
      ],
      structuredContent: {
        items: items.map((item) => ({
          id: item.id,
          title: item.title,
          price: item.price,
          currency: item.currency,
          shortDescription: item.shortDescription,
          tags: item.tags,
        })),
      },
    };
  }
);

這裡有幾個重點。

首先,我們明確檢查 interests 不是空陣列。即使 JSON Schema 形式上允許空陣列,對我們而言這樣的請求仍無意義。比起嘗試組出隨機清單,不如直接回傳清楚的錯誤。

其次,我們回傳兩種資料:

  • content——給模型看的。它是簡短的文字摘要:「找到 N 個選項」。模型會在回覆使用者時使用它。
  • structuredContent——同時給模型與 UI。這是結構化的 JSON,包含禮物列表;我們的小工具可以用卡片方式呈現。

常見錯誤是把一整坨 JSON 都塞進 content。不建議這樣做:模型會為此消耗大量 token,並且容易混亂。最好讓 content 保持精簡,把細節放在 structuredContent

6. 加入 UI 樣板與 _meta/openai/outputTemplate

在 Apps SDK 的層級,伺服器也會告訴 ChatGPT,應使用哪個 UI 樣板來視覺化工具結果。做法是透過資源與 _meta["openai/outputTemplate"]:伺服器註冊一個 mimeType"text/html+skybridge" 的 HTML 資源,工具的回應則引用它。

在 Next.js 樣板裡通常有更友善的包裝,但簡化後看起來像這樣:

// 在 MCP 伺服器初始化的某處
server.registerResource("ui://widget/gifts.html", {
  name: "Gift suggestions widget",
  mimeType: "text/html+skybridge",
  // 接著:回傳 HTML 的方式(內嵌樣板或檔案)
});

而在工具的回應中:

return {
  content: [{ type: "text", text: `找到 ${items.length} 個禮物。` }],
  structuredContent: { items: /* ... */ },
  _meta: {
    "openai/outputTemplate": "ui://widget/gifts.html",
  },
};

如此一來,ChatGPT 不僅能理解結果的結構,還會載入對應的 HTML/JS 小工具;而我們的 React 元件會在 iframe 中讀取 window.openai.toolOutput,並將禮物列表渲染出來。

關於 UI 的細節我們會在本模組中關於 ToolOutput → UI 的講次再深入討論,此處只強調關聯:工具處理器不僅負責商業資料,還要指定要綁定哪個 UI 樣板。我們此刻從 MCP 伺服器的視角出發:要指定哪個樣板,並在 structuredContent 中放入哪些資料。

Insight

ChatGPT 的設計者把小工具視為用來呈現 JSON 的樣板。因此名稱才會是 outputTemplate。原始設計概念是:ChatGPT 呼叫 mcp-tool,mcp-tool 回傳 JSON,有時也會回傳小工具。如果沒有小工具,ChatGPT 會自行決定如何呈現 JSON。

若指定了小工具,ChatGPT 就會顯示小工具,將 JSON 當作 toolOutput 傳給小工具,小工具負責呈現 JSON。小工具是用來呈現 JSON 的樣板。也因為如此,它會在應用註冊階段就被快取在 Store 中。

你可以依需求自由使用小工具:其中可以呼叫 fetch()。但若理解 ChatGPT 開發者的原始設計思路,你會更容易接受某些限制以及未來可能的變更。

7. 處理器中的授權與存取

前面我們假裝世界上的資料都是公開的。實務上,部分工具需要授權:例如存取使用者的帳號、訂單、付款、文件等。

在 Apps SDK / MCP 的術語中,工具可以設定 securitySchemes,然後在處理器裡檢查權杖與上下文。

最簡單的範例:

server.registerTool(
  "list_user_orders",
  {
    title: "使用者訂單列表",
    description: "回傳已登入使用者的最新訂單。",
    inputSchema: { type: "object", properties: {}, additionalProperties: false },
    _meta: {
        securitySchemes: [{ type: "oauth2", scopes: ["orders.read"] }],        
    }  
  },
  async ({ auth }) => {
    if (!auth?.accessToken) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: "需要先登入帳號才能查看訂單。",
          },
        ],
        _meta: {
          // 請求 ChatGPT 顯示 OAuth UI
          "mcp/www_authenticate": [
            'Bearer resource_metadata="https://your-mcp.example.com/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="請先登入以繼續。"',
          ],
        },
      };
    }

    // 在此檢查 token、issuer、audience、scope...
    const orders = await fetchUserOrders(auth.accessToken);

    return {
      content: [
        {
          type: "text",
          text: `找到 ${orders.length} 筆最近的訂單。`,
        },
      ],
      structuredContent: { orders },
    };
  }
);

這裡要理解:

  • ChatGPT 不會「自動猜測」你的檢查機制。它只會傳遞權杖與上下文,而你必須實作完善的授權。
  • 特殊欄位 _meta["mcp/www_authenticate"] 告訴平台:「需要向使用者顯示登入/更新權杖的 UI」。否則 ChatGPT 只會看到錯誤。

關於授權的複雜性我們會在模組 10 另行討論。此處先記住核心概念:在處理器中檢查權杖,不要相信模型的「直覺」。

8. 與外部 API 與資料庫互動:分層與實務

「把所有事都寫在處理器裡」很誘人:解析引數、查詢資料庫、過濾、映射成 structuredContent、記錄日誌,還順便來點哲學——都寫進一個 150 行的函式。這就像把整個應用都寫在 pages/index.tsx 裡——能做,但會很痛苦。

更好的做法是分層:

// gifts-repository.ts
import type { GiftItem } from "./gifts";

export async function fetchGiftsFromApi(
  maxBudget: number,
  interests: string[]
): Promise<GiftItem[]> {
  const resp = await fetch("https://example.com/api/gifts", {
    method: "POST",
    body: JSON.stringify({ maxBudget, interests }),
    headers: { "Content-Type": "application/json" },
  });

  if (!resp.ok) {
    throw new Error(`Gift API error: ${resp.status}`);
  }

  const data = (await resp.json()) as GiftItem[];
  return data;
}
// gifts.ts(更新後)
import { fetchGiftsFromApi } from "./gifts-repository";

export async function suggestGifts(input: SuggestGiftsInput): Promise<GiftItem[]> {
  if (input.maxBudget <= 0) {
    throw new Error("預算必須是正數。");
  }

  const items = await fetchGiftsFromApi(input.maxBudget, input.interests);

  return items.sort((a, b) => b.score - a.score).slice(0, 3);
}
// route.ts(處理器片段)
  async ({ input }) => {
    try {
      const payload: SuggestGiftsInput = {
        age: input.age ?? null,
        relationship: input.relationship,
        maxBudget: input.maxBudget,
        interests: input.interests,
      };

      const items = await suggestGifts(payload);

      // ...
    } catch (err) {
      console.error("suggest_gifts failed", err);
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: "挑選禮物時發生錯誤。請稍後再試。",
          },
        ],
        structuredContent: {
          errorCode: "INTERNAL_ERROR",
        },
      };
    }
  }

這樣的分層有幾個優點。

  • 可測性:可以針對 suggestGiftsfetchGiftsFromApi 寫 unit test,而不需要啟動 MCP 伺服器。
  • 可讀性:處理器保持為 MCP(協定)與你邏輯之間的薄適配層。
  • 可重用性:若之後要在其他地方(例如獨立的 REST API)提供相同的禮物推薦,不必從 MCP 中「挖出」邏輯。

9. 日誌與基礎可觀測性

工具的伺服端實作是建立最小可觀測性的好切入點。在生產環境你會想知道:

  • 哪些工具被呼叫;
  • 使用了哪些引數(當然不要記錄 PII);
  • 處理花了多少時間;
  • 錯誤量與錯誤類型。

目前我們先理解 ChatGPT App 的運作,因此先不使用專業記錄器。最簡單的包裝記錄器可長這樣:

// simple-logger.ts
export function logToolInvocationStart(tool: string, args: unknown) {
  console.log(
    JSON.stringify({
      level: "info",
      event: "tool_invocation_started",
      tool,
      timestamp: new Date().toISOString(),
      // 在生產環境切勿記錄 PII!
      args,
    })
  );
}

export function logToolInvocationEnd(tool: string, ms: number, success: boolean) {
  console.log(
    JSON.stringify({
      level: "info",
      event: "tool_invocation_finished",
      tool,
      durationMs: ms,
      success,
      timestamp: new Date().toISOString(),
    })
  );
}
// route.ts(處理器包裝)
import { logToolInvocationStart, logToolInvocationEnd } from "./simple-logger";

server.registerTool(
  "suggest_gifts",
  { /* ...meta... */ },
  async ({ input }) => {
    const startedAt = Date.now();
    logToolInvocationStart("suggest_gifts", {
      relationship: input.relationship,
      maxBudget: input.maxBudget,
      interestsCount: Array.isArray(input.interests)
        ? input.interests.length
        : 0,
    });

    try {
      // ... 主要邏輯 ...
      const duration = Date.now() - startedAt;
      logToolInvocationEnd("suggest_gifts", duration, true);
      return result;
    } catch (err) {
      const duration = Date.now() - startedAt;
      logToolInvocationEnd("suggest_gifts", duration, false);
      throw err;
    }
  }
);

未來在談到度量、SLO 與監控的模組時,你可以基於這些日誌建立圖表與警示。不過現在就養成記錄的習慣會更好。

10. 伺服端結果如何進入小工具(以及反向)

在第 6 節中,我們已透過 _meta["openai/outputTemplate"] 把工具的結果綁定到 UI 樣板。現在從另一側看看——structuredContent 是如何進到 React 小工具,以及在 UI 中要如何處理。

即使本講聚焦在伺服端,你仍需要理解:你設計的既是「給模型的 API」,也是「給 UI 的 API」。伺服器會回傳:

  • structuredContent——模型與小工具(透過 toolOutput)都能看到的資料;
  • content——面向模型的「精簡」結果描述;
  • _meta——小工具私有的欄位:openai/outputTemplateopenai/widgetCSPopenai/widgetDomain 等。

在 React 小工具裡你會寫出類似下列的程式:

// app/page.tsx(片段)
type ToolOutput = {
  items?: {
    id: string;
    title: string;
    price: number;
    currency: string;
    shortDescription: string;
    tags: string[];
  }[];
  emptyReason?: string;
};

declare global {
  interface Window {
    openai?: {
      toolOutput?: ToolOutput;
    };
  }
}

export default function GiftWidget() {
  const output = typeof window !== "undefined"
    ? window.openai?.toolOutput
    : undefined;

  if (!output) {
    return <div>正在等待禮物推薦結果…</div>;
  }

  if (!output.items || output.items.length === 0) {
    return <div>沒有合適的禮物。請嘗試調整條件。</div>;
  }

  return (
    <ul>
      {output.items.map((item) => (
        <li key={item.id}>
          <strong>{item.title}</strong> — {item.price} {item.currency}
        </li>
      ))}
    <ul>
  );
}

這也是為何讓 structuredContent 維持穩定的契約、並對 UI 友善很重要:清楚的欄位、避免 10 層深的巢狀結構。

這個路徑我們會在模組 4 的另一堂課詳談。此處先記住:伺服器與小工具共用同一份 structuredContent 結構。

11. 伺服端錯誤處理:格式與策略

在第 8–9 節我們已經稍微提過處理器裡的錯誤與日誌。現在把它們整理成一致的格式:要如何回傳工具錯誤,讓模型與 UI 都能正確處理。

處理器中的錯誤無可避免:外部 API 可能失敗、輸入可能不好、你自己也可能打錯字。關鍵是不要讓它們變成「500 Internal Server Error 但沒有說明」,讓模型與使用者都一頭霧水。

良好的伺服端工具實作會:

  • 區分使用者/模型的驗證錯誤與內部錯誤;
  • 回傳清楚的 isError 欄位與可理解的 errorCode(在 structuredContent 中);
  • 給人看的 content 也要友善易懂。

範例(假設我們已把工具的 titledescriptioninputSchema 等中介資料抽到變數 meta 以避免重複):

function makeErrorResult(message: string, code: string) {
  return {
    isError: true,
    content: [
      {
        type: "text",
        text: message,
      },
    ],
    structuredContent: {
      errorCode: code,
    },
  };
}

server.registerTool(
  "suggest_gifts",
  meta,
  async ({ input }) => {
    try {
      if (input.maxBudget > 10000) {
        return makeErrorResult(
          "預算過高。請調整請求(最多 10000 USD)。",
          "BUDGET_TOO_HIGH"
        );
      }

      const items = await suggestGifts({
        age: input.age ?? null,
        relationship: input.relationship,
        maxBudget: input.maxBudget,
        interests: input.interests,
      });

      if (!items.length) {
        return {
          content: [
            {
              type: "text",
              text:
                "在這個預算下找不到禮物。請嘗試調整興趣或提高預算。",
            },
          ],
          structuredContent: {
            items: [],
            emptyReason: "NO_MATCHES",
          },
        };
      }

      return {/* 正常結果 */};
    } catch (err) {
      console.error(err);
      return makeErrorResult(
        "伺服器在挑選禮物時發生內部錯誤。",
        "INTERNAL_ERROR"
      );
    }
  }
);

這種格式對模型有幫助(它可以嘗試改變引數),對 UI 也有幫助(小工具可針對不同的 errorCode 顯示特定訊息)。

更深入的韌性、冪等性與安全的工具設計,會在接下來的課程說明。不過從現在開始就養成習慣:與其默默做出奇怪的事,不如明確回傳錯誤。

在本講最後,還會把這些要點整理成伺服端工具實作的常見錯誤清單,方便作為檢查表使用。

12. 端到端短範例:從請求到回應

把上述內容串成我們的 GiftGenius 應用的一條鏈。

  1. 使用者在 ChatGPT 中輸入:
    「幫我替朋友挑一個禮物,他喜歡桌遊,預算 50 美元以內」。
  2. 模型知道工具 suggest_gifts 與其 schema,決定呼叫它,並組出 tool_call
    {
      "tool": "suggest_gifts",
      "arguments": {
        "relationship": "friend",
        "maxBudget": 50,
        "interests": ["board games"],
        "age": null
      }
    }
    
  3. 平台將此 JSON‑RPC 發到我們的 MCP 伺服器(POST /app/mcp),Next.js 把請求本文交給 server.handle(...)。
  4. 我們的處理器 suggest_gifts
    • 驗證 interests 非空;
    • 呼叫 suggestGifts(payload)
    • 取得陣列 GiftItem[](依 score 取前 3);
    • 將其放入 structuredContent.items,並加入 _meta["openai/outputTemplate"] = "ui://widget/gifts.html"
  5. ChatGPT 收到回應,把 structuredContent 放入上下文,載入小工具的 HTML 資源 gifts.html,並將 toolOutput 傳入。
  6. 我們的 React 小工具讀取 window.openai.toolOutput.items,渲染禮物列表;模型則根據 contentstructuredContent,向使用者說明為何這些禮物合適。
  7. 使用者按下例如「顯示更多」的小工具按鈕——小工具透過 SDK 呼叫 callTool → 再次進入我們的處理器,但引數已不同(例如提高預算)。

整個鏈條仰賴於伺服端工具的實作:

  • 依約定的 JSON Schema 接受結構化的 input
  • 謹慎地驗證資料;
  • 呼叫隔離的商業邏輯;
  • 回傳穩定的結構化輸出;
  • 必要時指定 UI 樣板與中介資料。

13. 伺服端工具實作的常見錯誤

錯誤一:「一切都寫一起」——巨型處理器。
當所有邏輯與外部 API 的互動都寫在 server.registerTool(..., async () => { ... }) 中,程式很快就會膨脹成難以閱讀的巨石。一點小改動就可能全面崩壞。更好的做法是把商業邏輯抽到獨立的函式/模組,讓處理器成為薄適配層。

錯誤二:盲目信任 JSON Schema。
開發者常想:「有 schema 就表示輸入一定有效」。但模型可能送來奇怪的值,外部客戶端更是如此。不能只依賴型別與 JSON Schema——還需要邏輯驗證(預算範圍、陣列長度、允許值等)。

錯誤三:把所有東西都丟進 content,而忽略 structuredContent。
有人會把巨大 JSON 以字串形式塞進 content「以防萬一」。這會讓模型的提示變得吵雜且昂貴,同時 UI 也會受苦,因為它得先解碼字串才能取得結構。更好的方式是讓 content 保持精簡,把細節放進 structuredContent

錯誤四:不穩定的結構化輸出格式。
今天 items 是有 idtitleprice 的物件陣列,明天你卻把 price 改名為 amount,小工具就掛了。或是多加了一層巢狀。你當然可以這麼做,但務必要進行契約版本化、或以更小的步驟演進 schema。否則 UI 與測試會不斷壞掉。

錯誤五:缺乏有意義的錯誤處理。
丟出例外並期待平台「自己處理」並非最佳策略。模型只會看到難懂的 JSON‑RPC error、使用者只會看到紅色錯誤條,而你會失去問題的脈絡。更好的方式是回傳明確的 isErrorerrorCode 與人類可讀的訊息,並在伺服器端記錄詳情。

錯誤六:忽略授權並信任模型。
有些開發者會想:「模型很聰明,若使用者沒登入就不會呼叫這個工具」。事實上模型並不了解你的 ACL 與配額限制,它只看得到工具描述。所有權限檢查都必須在伺服端處理器中實作,無論工具如何描述。

錯誤七:無節制的日誌,包含 PII。
很容易習慣性地把整個 input 都記錄下來。在 ChatGPT App 的情境中,這可能包含 PII(姓名、email、住址等),不但違反 OpenAI 政策,也欠缺風險控管。請只記錄聚合/去識別後的資訊:關係類型、預算範圍、興趣數量。

錯誤八:呼叫外部 API 時缺乏逾時與重試機制。
若處理器內對外部 API 進行 fetch 時沒有逾時與重試,任何外部 API 的延遲都會被使用者感知為「ChatGPT 當掉了」。使用者會以為整個應用故障。請在伺服器端設定逾時,處理逾時例外,並回傳有意義的錯誤。

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