接入教程

Anthropic TypeScript SDK 使用指南

Ethan Cole

使用 Anthropic TypeScript SDK 的流程很直接:安装 @anthropic-ai/sdk,在服务端用 new Anthropic({ apiKey }) 构造客户端(密钥绝不下发到浏览器),再调用 client.messages.create() 传入模型 id、max_tokens 和 messages。同一个客户端还提供流式输出、按状态码细分的错误类、重试与超时配置,以及接网关或中转端点的 baseURL 选项。

安装 Anthropic TypeScript SDK

官方包名是 @anthropic-ai/sdk,类型声明随包发布,所以不论项目用 TypeScript 还是纯 JavaScript 都装同一个包。按你项目现有的包管理器选一条命令即可:

npm install @anthropic-ai/sdk
pnpm add @anthropic-ai/sdk
bun add @anthropic-ai/sdk

SDK 底层依赖标准 fetch,而不是 Node 专有的 http 模块,因此除了现代 Node.js,也能跑在 Cloudflare Workers、Vercel Edge Functions、Deno 这类边缘运行时。如果你在较老的 Node LTS 上启动时报 fetch is not defined,正确做法是升级 Node,而不是打 polyfill 补丁;官方文档列出了支持的运行时版本。

如果你之前是用裸 HTTP 调过 Claude API,可以把 SDK 理解成对同一个 POST /v1/messages 接口做的一层带类型的薄封装,请求和响应结构完全沿用。想先补底层请求模型的话,可以看我们的 Claude API 使用指南。

构造客户端与密钥管理

不传参数时,客户端会自动读取环境变量 ANTHROPIC_API_KEY,这是最常见的写法。显式传入 apiKey 适合密钥来自密钥管理服务或框架自带配置层的场景:

import Anthropic from "@anthropic-ai/sdk";

// 自动读取 process.env.ANTHROPIC_API_KEY
const client = new Anthropic();

// 或者自己注入
const explicit = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

Node 与服务端框架。 密钥放环境变量或密钥管理服务,客户端只在服务端代码里构造:Express 路由、Next.js 的 Route Handler 或 Server Action、Nuxt 的 server 路由、Fastify 插件都可以。浏览器只跟你的服务端通信,服务端再去调 Anthropic。

边缘运行时。 Cloudflare Workers 这类平台没有 process.env,要从平台的 bindings 里读密钥,再作为 apiKey 传进构造函数,其余用法不变。

绝对不要把密钥打进浏览器。 打包进前端 JavaScript 的密钥,任何人打开 DevTools 都能看到,很快就会被抓取并盗用。SDK 默认拒绝在浏览器环境里运行,除非你显式设置 dangerouslyAllowBrowser: true;这个开关只为内部工具这类所有使用者本来就持有密钥的窄场景而存在。面向公众的产品,正确模式永远是由服务端持有密钥并转发请求。

如果客户端构造正常但请求一律返回 401,通常是密钥填错、已吊销或设在了另一个 shell 里,可以对照 Anthropic API key 不能用的排查清单 逐项核对。

第一次 messages.create 调用

一次请求需要三样东西:模型 id、max_tokens 输出上限、以 user 轮开头的 messages 数组。system 提示词是可选的,放在顶层参数而不是 messages 里面:

const response = await client.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  system: "你是一个面向开发者文档站的简洁助手。",
  messages: [
    { role: "user", content: "用两句话解释什么是 content block。" },
  ],
});

console.log(response.id, response.stop_reason, response.usage);

把 claude-sonnet-4-5 换成你的账号或网关实际暴露的模型 id。模型 id 会随时间变化,建议当成配置项而不是写死在代码里的常量。

返回值是 Anthropic.Message 类型,其中 content 字段是一个块数组而不是单个字符串。这是从其他聊天类 API 转过来的开发者最常踩的第一个坑。

用类型收窄读取 content block

response.content 的类型是可辨识联合:每个块都有 type 字段,取值如 "text"、"tool_use"、"thinking",而只有 text 变体才带 .text 属性。所以 TypeScript 会直接拒绝 response.content[0].text,必须先收窄类型,这恰恰是你想要的保护:

for (const block of response.content) {
  if (block.type === "text") {
    process.stdout.write(block.text);
  } else if (block.type === "tool_use") {
    console.log("模型请求调用工具:", block.name, block.input);
  }
}

// 只需要纯文本时,把所有 text 块拼成一个字符串
const text = response.content
  .filter((b) => b.type === "text")
  .map((b) => b.text)
  .join("");

同时务必检查 response.stop_reason:"end_turn" 是正常结束,"max_tokens" 表示回答被你设的上限截断,"tool_use" 表示模型要你先执行工具再继续。

用 client.messages.stream() 做流式输出

只要是实时展示给用户的内容,或者输出很长、非流式请求可能撞上 HTTP 超时的场景,都应该用 client.messages.stream()。它接受与 create() 相同的参数,返回一个 MessageStream,有两种消费方式。

事件助手。 流对象本身是事件发射器,text 事件只给你增量文本,finalMessage() 在流结束后解析出完整的 Anthropic.Message:

const stream = client.messages.stream({
  model: "claude-sonnet-4-5",
  max_tokens: 4096,
  messages: [{ role: "user", content: "写一条简短的更新日志。" }],
});

stream.on("text", (delta) => {
  process.stdout.write(delta);
});

const finalMessage = await stream.finalMessage();
console.log("\nstop_reason:", finalMessage.stop_reason);
console.log("输出 token 数:", finalMessage.usage.output_tokens);

用 for await 处理原始事件。 需要拿到每一条服务端事件时(比如把工具输入的 JSON 边到边渲染),直接迭代流对象并按 event.type 分支:

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}

不要自己用 new Promise() 包一层 .on("text") 来等结束,finalMessage() 已经处理了完成、中止和出错三种状态。要提前取消,调用 stream.abort() 或在请求选项里传入 AbortSignal。事件顺序以及如何通过 SSE 把流转发到浏览器,在 Claude API 流式输出详解 里有完整说明。

工具调用(tool use)基础

工具让模型可以请求你的代码执行某个函数。每个工具用名称、描述和 JSON Schema 格式的 input_schema 来定义;模型想调用时,响应里会出现 tool_use 块且 stop_reason 为 "tool_use"。你执行函数,把结果包成 tool_result 块并带上对应的 tool_use_id 发回去,再调一次接口:

const tools = [
  {
    name: "get_order_status",
    description: "按订单 id 查询订单的履约状态。",
    input_schema: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
  },
];

const messages = [{ role: "user", content: "订单 A-1042 现在到哪了?" }];

const first = await client.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  tools,
  messages,
});

if (first.stop_reason === "tool_use") {
  const toolUse = first.content.find((b) => b.type === "tool_use");
  const result = await lookupOrder(toolUse.input.order_id); // 你自己的函数

  messages.push({ role: "assistant", content: first.content });
  messages.push({
    role: "user",
    content: [
      { type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(result) },
    ],
  });

  const second = await client.messages.create({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    tools,
    messages,
  });
}

两个习惯能让这个循环保持健康:一是始终把 toolUse.input 当结构化数据解析,不要对序列化字符串做匹配;二是如果模型在同一轮里给出多个 tool_use 块,要把所有 tool_result 放进同一条 user 消息里一次性返回。TypeScript 项目还可以用 SDK 的 beta 版 tool runner 配合 Zod schema 让它替你跑循环,上面的手写循环则是不依赖任何 beta 标志的通用版本。

需要捕获的错误类

SDK 抛出的所有错误都继承自 Anthropic.APIError,带数字型 status 和服务端返回的 message。每个 HTTP 状态码都有对应子类,可以直接按类型分支而不用去匹配字符串:

  • Anthropic.AuthenticationError(401):密钥错误、已吊销或缺失。
  • Anthropic.PermissionDeniedError(403):密钥有效但无权执行此操作。
  • Anthropic.NotFoundError(404):模型 id 不存在或路径不对,baseURL 配错时特别常见。
  • Anthropic.BadRequestError(400):参数不合法,例如 messages 没有以 user 轮开头。
  • Anthropic.RateLimitError(429):触发限流,SDK 其实已经自动重试过了。
  • Anthropic.InternalServerError(5xx)与 Anthropic.APIConnectionError(网络故障或超时)。

判断顺序从最具体到最泛化,并且要把 APIConnectionError 放在基类 APIError 之前检查,因为在 TypeScript SDK 里它是 APIError 的子类:

try {
  const res = await client.messages.create({ /* ... */ });
} catch (err) {
  if (err instanceof Anthropic.RateLimitError) {
    // 退避、排队,或向用户展示繁忙状态
  } else if (err instanceof Anthropic.AuthenticationError) {
    // 快速失败:重试救不了错误的密钥
  } else if (err instanceof Anthropic.APIConnectionError) {
    // 网络或超时问题,可以限次重试
  } else if (err instanceof Anthropic.APIError) {
    console.error(err.status, err.message);
  } else {
    throw err;
  }
}

maxRetries 与 timeout 配置

客户端默认会对连接错误以及 408、409、429、5xx 响应做指数退避重试,默认重试两次。请求超时默认十分钟,而且在 TypeScript SDK 里单位是毫秒,从 Python SDK(单位是秒)转过来的人很容易在这里栽跟头。两者都可以在客户端级别设置,也可以通过任意方法的第二个参数按请求覆盖:

const client = new Anthropic({
  maxRetries: 3,
  timeout: 60_000, // 60 秒,单位毫秒
});

// 按请求覆盖:健康检查用,不重试、短超时
const probe = await client.messages.create(
  {
    model: "claude-sonnet-4-5",
    max_tokens: 16,
    messages: [{ role: "user", content: "ping" }],
  },
  { maxRetries: 0, timeout: 5_000 },
);

超时本身也会触发重试,所以最坏情况的总耗时大约是 timeout * (maxRetries + 1)。面向用户的链路建议用流式加合理超时,而不是给非流式请求设一个很大的 max_tokens,后者正是「SDK 卡住不动」的最常见原因。

用 baseURL 接入网关或中转端点

baseURL 选项会替换默认的 API 源地址。企业内网代理、用于日志和配额控制的自建网关、兼容 Anthropic 协议的第三方中转端点,都靠它接入。SDK 会在你传入的地址后面自动拼接 /v1/messages 这类路径,所以除非网关文档另有说明,传源地址(origin)而不是完整接口地址:

const client = new Anthropic({
  apiKey: process.env.GATEWAY_API_KEY,
  baseURL: process.env.ANTHROPIC_BASE_URL, // 例如你的网关或中转端点源地址
});

SDK 同样会读取环境变量 ANTHROPIC_BASE_URL,因此可以代码不动、按部署环境切换目标。如果某个网关对官方端点能跑通的请求返回 404,多半是 /v1 段重复拼接了;把错误 message 里的实际 URL 打出来对比即可定位。另外有些网关只实现了 OpenAI 风格的请求格式,那种情况要改用 OpenAI 客户端而不是本 SDK。

ROIBest AI 是一个同时兼容 Anthropic 与 OpenAI 协议的 API 中转端点。如果你通过它转发请求,上面的 baseURL 选项就是配置该端点的位置,详情见 ai.roibest.com。

常见问题

Anthropic TypeScript SDK 能在纯 JavaScript 项目里用吗?

可以,两者用的是同一个包。TypeScript 用户得到类型检查和自动补全,JavaScript 用户直接调用同样的函数,只是没有类型标注。

为什么 response.content[0].text 会报类型错误?

因为 content 是由多种块类型组成的数组,只有 text 块才有 .text 属性。先用 block.type === "text" 收窄类型,或者先对数组做过滤。

SDK 能跑在 Cloudflare Workers 等边缘运行时上吗?

可以。SDK 基于标准 fetch,边缘平台都支持。由于那里没有 process.env,需要从平台的 secret bindings 读取密钥并作为 apiKey 传入。

怎么让 SDK 走中转或网关端点?

在构造函数里设置 baseURL(或环境变量 ANTHROPIC_BASE_URL)为网关的源地址,并把该网关签发的密钥作为 apiKey 传入,其余代码完全不用改。