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 服务器 / 后端
    participant DB as 目录/外部 API

    User->>Chat: "挑选礼物……"
    Chat->>App: 决定调用 tool `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 服务器与 tools 的处理器。

Insight: mcp-tools 数量限制

在 MCP 服务器中,工具数量是一项与内存或上下文 token 一样受限的指标。形式上你可以注册几十甚至上百个 tools,但平台与模型并非线性利用它们:每新增一个工具都会增加路由时的“噪音”。

实务经验给出这些参考:

  • 硬上限 对于 ChatGPT ≈ 每个服务器至多 128 个 MCP-tools
  • 推荐工作区间——不超过 50 个工具。再往上质量会明显下降:模型会混淆描述相近的工具,更少记起冷门工具,更容易选错。

Anthropic 的情况类似:上限约 最多 100 个 tools,而他们自己也推荐控制在 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           # 根布局,Bootstrap 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;
  • 处理器——一个异步函数,参数中包含 input

不过目前还没有校验、规范化的结构化输出,也没有授权。接下来我们补齐这些。

4. 输入校验与分层

为什么仅有 JSON Schema 还不够

没错,平台会根据模式自动校验基础内容:字段类型、必填属性等。但:

  • 模型可能传来逻辑上不正确的数据(比如预算 −100,或者包含 1000 个元素的兴趣列表);
  • 你有业务约束(预算上限、支持的货币等);
  • 有时 ChatGPT 或其他客户端可能行为异常,发来完全意外的内容。

因此在处理器内部仍需要额外的逻辑校验。

代码分层:handler ↔ 业务逻辑

为了避免服务端代码“面条化”,建议将业务逻辑单独存放。比如创建 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: "礼物推荐小部件",
  mimeType: "text/html+skybridge",
  // 下面:提供 HTML 的方式(内置模板或文件)
});

而在工具的响应中:

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

这样 ChatGPT 不仅理解结果的结构,还会加载对应的小部件 HTML/JS,而我们在 iframe 内的 React 组件会读取 window.openai.toolOutput 并渲染礼物列表。

关于 UI 的细节我们会在同模块的 ToolOutput → UI 讲解中展开,因此这里仅强调关联:工具处理器不仅要产出业务数据,还需要指定要绑定的 UI 模板,并设计好放进 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,然后在处理器中检查 token 与上下文。

最简单的示例:

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 不会“替你猜”校验逻辑。它只转发 token 和上下文,你必须自己完成授权校验。
  • 特殊字段 _meta["mcp/www_authenticate"] 会告诉平台:“需要给用户展示登录/刷新 token 的 UI”。否则 ChatGPT 只会看到错误。

关于授权的复杂性我们会在模块 10 单讲,这里先建立基本概念:在处理器中验证 token,别盲目信任模型。

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 写单元测试,而不用启动 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 及其模式,决定调用它并构造 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。
开发者常想:“既然有模式,输入就一定有效。”但模型可能给出奇怪的值,外部客户端更是如此。不能只依赖类型与 JSON Schema——还需要逻辑校验(预算边界、数组长度、允许值等)。

错误三:把所有内容堆进 content,忽视 structuredContent。
有人把巨大的 JSON 以字符串形式塞进 content“以防万一”。这会让模型提示变得嘈杂且耗费 token,UI 端也很痛苦,因为不得不先解码字符串才能使用结构。更好的做法是让 content 简洁,把细节放在 structuredContent

错误四:结构化输出格式不稳定。
今天 items 是带 idtitleprice 的对象数组,明天你突然把 price 改名为 amount,小部件就挂了。或者增加了新的嵌套层级。可以改,但要么对契约做版本化,要么以更小的步子演进模式,否则 UI 与测试会一直被打断。

错误五:缺少有意义的错误处理。
抛出异常并指望平台“自己处理”并非上策。模型看到的是莫名其妙的 JSON‑RPC 错误,用户看到的是红色错误条,而你丢失了问题的上下文。更好的方式是返回明确的 isErrorerrorCode 与用户可读的信息,同时在服务器端记录详细日志。

错误六:忽视授权并信任模型。
有时开发者认为:“模型很聪明,用户没登录它就不会调用这个工具。”实际上模型并不了解你的 ACL 与配额,它只看到工具描述。所有权限校验都必须在服务器处理器中完成,不论工具如何描述。

错误七:把所有内容都记录到日志,包括 PII。
很容易顺手把完整的 input 记录下来。对于 ChatGPT App,这可能包含 PII(姓名、邮箱、地址等),既违反 OpenAI 政策,也不符合常识。更好的方式是只记录聚合/去标识的信息:关系类型、预算区间、兴趣数量。

错误八:与外部 API 交互时没有超时与重试。
若工具在处理器中对外部 API 执行 fetch 而没有超时与重试,任何该 API 的延迟都会表现为“ChatGPT 卡住”。用户会以为整个应用坏了。服务器端需要设置时间限制,处理超时,并返回有意义的错误。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION