LLM 集成已经成为后端标配能力:文本总结、结构化抽取、代码生成、客服助手……但 LLM 的调用形态与普通 REST 不同——流式、超时、令牌成本、重试策略都有讲究。本文以 OpenAI 与 Anthropic 为例,从 SDK 调用讲到 SSE 流式、退避重试、结构化输出与异步任务化。
1. LLM 集成全景
1.1 调用形态
同步对话(聊天/问答) → 用户等待,直连
流式输出(打字机/长文) → SSE 逐 token 推送
批量任务(摘要/抽取海量文) → 队列异步处理
工具调用(Function/Tool) → 模型决策 + 代码执行
1.2 SDK 与原生 HTTP 的选型
| 方案 | 优点 | 缺点 |
|---|---|---|
| 官方 SDK | 类型齐全、流式封装好、省心 | 多一层依赖 |
| 原生 fetch | 零依赖、可控 | 流式/重试要自己写 |
推荐 SDK:接口演进快,SDK 能帮你跟上 breaking change。两个 SDK 都基于底层 fetch,与本文主题天然衔接。
1.3 环境变量与密钥管理
export OPENAI_API_KEY=sk-xxx
export ANTHROPIC_API_KEY=sk-ant-xxx
密钥绝不写进代码仓库,进程内用环境变量读取,云端用 Secret Manager 注入。
一句话:LLM 集成 = SDK 直连 + 流式 SSE + 退避重试 + 令牌核算四个模块;密钥走环境变量,绝不入库。
2. OpenAI SDK 调用
2.1 最小调用
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: '你是一个简洁的中文助手。' },
{ role: 'user', content: '用一句话解释什么是索引。' },
],
max_tokens: 200,
});
console.log(res.choices[0].message.content);
2.2 参数详解
| 参数 | 作用 | 建议 |
|---|---|---|
| temperature | 随机性,越高越发散 | 抽取用 0,创意用 0.7 |
| max_tokens | 输出上限 | 按任务估算,防跑飞 |
| top_p | 核采样,与 temperature 二选一 | 一般不用 |
| presence/frequency_penalty | 重复惩罚 | 摘要场景可调 |
2.3 多轮对话
const messages = [{ role: 'system', content: '你是技术支持。' },
{ role: 'user', content: '我的服务 500 了。' },
{ role: 'assistant', content: '先看错误日志和最近变更。' },
{ role: 'user', content: '日志里全是连接超时。' }];
一句话:OpenAI 调用 =
chat.completions.create+ messages 数组;抽取任务temperature=0保稳定,max_tokens设上限防开销失控。
3. Anthropic SDK 调用
3.1 最小调用
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const res = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 500,
system: '你是严谨的中文写作助手。',
messages: [{ role: 'user', content: '帮我总结这段代码的职责。' }],
});
console.log(res.content[0].text);
3.2 与 OpenAI 的差异
| 维度 | OpenAI | Anthropic |
|---|---|---|
| 顶层 system | 放在 messages 里 | 独立 system 字段 |
| max_tokens | 可选默认大 | 必填 |
| 返回结构 | choices[0].message | content[0].text |
| 流式 | stream: true | stream: true |
| 工具 | tools | tools(新版) |
3.3 统一封装适配
多模型供应商并存时,封装一层适配器,业务侧只面对统一接口:
async function chat(provider, { system, user, maxTokens = 500 }) {
if (provider === 'openai') {
const res = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
max_tokens: maxTokens,
});
return res.choices[0].message.content;
}
const res = await anthropic.messages.create({
model: 'claude-sonnet-4-5', max_tokens: maxTokens,
system, messages: [{ role: 'user', content: user }],
});
return res.content[0].text;
}
一句话:Anthropic 的关键差异 =
system独立 +max_tokens必填;多供应商并存务必加一层适配器,业务代码不感知具体 SDK。
4. 流式输出 SSE
4.1 SDK 流式调用
const stream = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: '写一首四行诗' }],
stream: true, // 开启流式
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
4.2 原生 fetch 消费 SSE
不想用 SDK 时,SSE 就是一段 HTTP 流,按行解析 data: 事件:
import { createInterface } from 'node:readline';
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
body: JSON.stringify({ model: 'gpt-4o-mini', stream: true, messages: [] }),
});
for await (const line of createInterface({ input: res.body })) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (payload === '[DONE]') break;
const text = JSON.parse(payload).choices?.[0]?.delta?.content;
if (text) process.stdout.write(text);
}
4.3 流式转 WebSocket 下发
浏览器端打字机效果,通常由服务端把 SSE 转成 WebSocket/SSE 再下发客户端;注意客户端断连要取消上游流,避免 token 浪费。
一句话:流式 = SDK
stream:true逐 chunk 或 fetch + readline 解析data:行;客户端断连时立刻 cancel 上游流,防 token 空烧。
5. 超时重试与指数退避
5.1 LLM 特有错误
429 Too Many Requests 限流,要退避
500/502/503 上游抖动 可重试
400/401/403 参数/鉴权错误 不可重试,直接报错
5.2 指数退避封装
async function withRetry(fn, { retries = 3, baseDelay = 1000 } = {}) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
const status = err?.status;
if ([400, 401, 403, 404].includes(status)) throw err; // 永久错误
if (attempt === retries) throw err;
const delay = baseDelay * 2 ** attempt + Math.random() * 500; // 指数+抖动
await new Promise((r) => setTimeout(r, delay));
}
}
}
const text = await withRetry(() => callChat(question));
5.3 请求超时
const controller = new AbortController(); // SDK 支持 signal 透传
const timer = setTimeout(() => controller.abort(), 60 * 1000);
try {
const res = await client.chat.completions.create({ ..., signal: controller.signal });
} finally {
clearTimeout(timer);
}
一句话:LLM 重试 = 429/5xx 才重试,4xx 永久错误直接抛;
baseDelay * 2 ** attempt指数退避加抖动,避免集体重启风暴。
6. 提示词工程与结构化输出
6.1 提示词结构
角色(system) → 你是谁,什么语气
任务(user) → 具体做什么,输入在哪
约束(格式) → 输出格式、长度、禁忌
示例(few-shot) → 给一两个期望样例
6.2 JSON 结构化输出
让模型输出可解析的 JSON,并声明约束:
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
response_format: { type: 'json_object' }, // 强制 JSON
messages: [{ role: 'user', content: '从评论中抽取信息,输出 JSON,键为 sentiment 和 keywords。评论:服务很好但价格偏贵' }],
});
const data = JSON.parse(res.choices[0].message.content);
6.3 解析兜底
模型偶尔会输出带解释文字的 JSON,解析必须容错:
function safeParse(text) {
try {
return JSON.parse(text);
} catch {
const m = text.match(/\{[\s\S]*\}/); // 掐头去尾找对象
if (m) return JSON.parse(m[0]);
throw new Error('无法解析模型输出');
}
}
一句话:结构化输出 = 提示词定格式 +
response_format: json_object强制 +safeParse容错三层;宁可解析失败显式报错,也不静默吞掉脏数据。
7. 令牌成本控制与异步任务化
7.1 令牌与成本
function estimateTokens(text) { // 1 汉字约 1 到 1.5 token
return Math.ceil(text.length / 1.3);
}
调用前先估算输入 token,配合模型单价做预算;输出设 max_tokens 上限。真实计数建议用各家的 tokenizer(如 tiktoken)。
7.2 并发限流
LLM 接口有 RPM/TPM 限流,批量任务必须限并发:
async function mapLimit(items, limit, fn) {
const queue = [...items];
const workers = Array.from({ length: limit }, async () => {
while (queue.length) await fn(queue.shift());
});
await Promise.all(workers);
}
await mapLimit(posts, 5, async (post) => {
await summarize(post); // 最多 5 个并发调用
});
7.3 异步任务化
长文本批量摘要不该让 HTTP 请求等几分钟,而是入队异步处理:
客户端提交 → 写入队列 → Worker 消费调 LLM → 写回存储 → 客户端取结果
// Worker 侧示意:消费一条任务,调 LLM,写回
await queue.add('summarize', { articleId });
// worker 里 await callChat(...) 后写回数据库
一句话:成本控制 = 估算 token + 限并发 + max_tokens 上限;批量任务 = 入队异步处理,绝不阻塞在 HTTP 请求里(队列实操见 nodejs-message-queue)。
8. 踩坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
| 密钥写进仓库 | 泄露被滥用 | 环境变量 + Secret Manager |
| 4xx 反复重试 | 白白烧钱 | 4xx 永久错误直接抛 |
| 无退避重试 429 | 触发更严限流 | 指数退避 + 抖动 |
| 流式不取消 | 断连后 token 空烧 | 客户端断开立刻 cancel |
| 解析脏 JSON | 静默拿到 undefined | safeParse 容错 + 显式报错 |
| 无 max_tokens | 长文本费用爆炸 | 设输出上限 |
| 并发打爆 RPM | 429 连环 | 限流器 + 队列 |
| 忘传 system | 输出语气不可控 | 角色/任务/约束/示例四要素 |
| 同步等长任务 | HTTP 超时 | 入队异步化 |
9. 总结
| 环节 | 要点 |
|---|---|
| 选型 | 官方 SDK 为主,底层是 fetch |
| OpenAI | chat.completions + messages,温度 0 保稳定 |
| Anthropic | system 独立 + max_tokens 必填 |
| 流式 | stream:true 逐 chunk / SSE 按行解析 |
| 重试 | 429/5xx 退避,4xx 直接抛 |
| 提示词 | 角色 + 任务 + 约束 + 示例 |
| 结构化 | json_object + safeParse 兜底 |
| 成本 | 估算 token + 限并发 + 上限 |
| 异步 | 批量任务入队,Worker 消费 |
一句话记住:LLM 集成是「调用 + 流式 + 重试 + 成本」四位一体——SDK 封装调用形态,SSE 承载打字机体验,指数退避扛住限流,令牌核算守住预算;批量任务必须异步化,否则一个长摘要就把整个服务拖垮。
延伸阅读
- Node.js 异步与并发 — 流式消费与并发背后的异步模型
- Node.js 消息队列实践 — 批量 LLM 任务的队列落地
- Node.js 错误处理与日志 — LLM 调用的异常归类
- Node.js 缓存策略 — 相似提问结果缓存降成本
- Node.js 可观测性 — LLM 调用链路的追踪与指标
- Node.js 性能调优 — 集成层瓶颈定位
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。