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/call、resources/list、handshake 等。
典型的專案結構:
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 我們會:
- 建立 MCP 伺服器實例(透過 @modelcontextprotocol/sdk);
- 註冊工具(server.registerTool(...));
- 定義 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",
},
};
}
}
這樣的分層有幾個優點。
- 可測性:可以針對 suggestGifts 與 fetchGiftsFromApi 寫 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/outputTemplate、openai/widgetCSP、openai/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 也要友善易懂。
範例(假設我們已把工具的 title、description、inputSchema 等中介資料抽到變數 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 應用的一條鏈。
- 使用者在 ChatGPT 中輸入:
「幫我替朋友挑一個禮物,他喜歡桌遊,預算 50 美元以內」。 - 模型知道工具 suggest_gifts 與其 schema,決定呼叫它,並組出 tool_call:
{ "tool": "suggest_gifts", "arguments": { "relationship": "friend", "maxBudget": 50, "interests": ["board games"], "age": null } } - 平台將此 JSON‑RPC 發到我們的 MCP 伺服器(POST /app/mcp),Next.js 把請求本文交給 server.handle(...)。
- 我們的處理器 suggest_gifts:
- 驗證 interests 非空;
- 呼叫 suggestGifts(payload);
- 取得陣列 GiftItem[](依 score 取前 3);
- 將其放入 structuredContent.items,並加入 _meta["openai/outputTemplate"] = "ui://widget/gifts.html"。
- ChatGPT 收到回應,把 structuredContent 放入上下文,載入小工具的 HTML 資源 gifts.html,並將 toolOutput 傳入。
- 我們的 React 小工具讀取 window.openai.toolOutput.items,渲染禮物列表;模型則根據 content 與 structuredContent,向使用者說明為何這些禮物合適。
- 使用者按下例如「顯示更多」的小工具按鈕——小工具透過 SDK 呼叫 callTool → 再次進入我們的處理器,但引數已不同(例如提高預算)。
整個鏈條仰賴於伺服端工具的實作:
- 依約定的 JSON Schema 接受結構化的 input;
- 謹慎地驗證資料;
- 呼叫隔離的商業邏輯;
- 回傳穩定的結構化輸出;
- 必要時指定 UI 樣板與中介資料。
13. 伺服端工具實作的常見錯誤
錯誤一:「一切都寫一起」——巨型處理器。
當所有邏輯與外部 API 的互動都寫在 server.registerTool(..., async () => { ... }) 中,程式很快就會膨脹成難以閱讀的巨石。一點小改動就可能全面崩壞。更好的做法是把商業邏輯抽到獨立的函式/模組,讓處理器成為薄適配層。
錯誤二:盲目信任 JSON Schema。
開發者常想:「有 schema 就表示輸入一定有效」。但模型可能送來奇怪的值,外部客戶端更是如此。不能只依賴型別與 JSON Schema——還需要邏輯驗證(預算範圍、陣列長度、允許值等)。
錯誤三:把所有東西都丟進 content,而忽略 structuredContent。
有人會把巨大 JSON 以字串形式塞進 content「以防萬一」。這會讓模型的提示變得吵雜且昂貴,同時 UI 也會受苦,因為它得先解碼字串才能取得結構。更好的方式是讓 content 保持精簡,把細節放進 structuredContent。
錯誤四:不穩定的結構化輸出格式。
今天 items 是有 id、title、price 的物件陣列,明天你卻把 price 改名為 amount,小工具就掛了。或是多加了一層巢狀。你當然可以這麼做,但務必要進行契約版本化、或以更小的步驟演進 schema。否則 UI 與測試會不斷壞掉。
錯誤五:缺乏有意義的錯誤處理。
丟出例外並期待平台「自己處理」並非最佳策略。模型只會看到難懂的 JSON‑RPC error、使用者只會看到紅色錯誤條,而你會失去問題的脈絡。更好的方式是回傳明確的 isError、errorCode 與人類可讀的訊息,並在伺服器端記錄詳情。
錯誤六:忽略授權並信任模型。
有些開發者會想:「模型很聰明,若使用者沒登入就不會呼叫這個工具」。事實上模型並不了解你的 ACL 與配額限制,它只看得到工具描述。所有權限檢查都必須在伺服端處理器中實作,無論工具如何描述。
錯誤七:無節制的日誌,包含 PII。
很容易習慣性地把整個 input 都記錄下來。在 ChatGPT App 的情境中,這可能包含 PII(姓名、email、住址等),不但違反 OpenAI 政策,也欠缺風險控管。請只記錄聚合/去識別後的資訊:關係類型、預算範圍、興趣數量。
錯誤八:呼叫外部 API 時缺乏逾時與重試機制。
若處理器內對外部 API 進行 fetch 時沒有逾時與重試,任何外部 API 的延遲都會被使用者感知為「ChatGPT 當掉了」。使用者會以為整個應用故障。請在伺服器端設定逾時,處理逾時例外,並回傳有意義的錯誤。
GO TO FULL VERSION