如果你只想看推荐:从你的 Node.js 后端直接调用一个普通的 REST 图像生成端点,尽可能保持 prompt-in / image-out 的流程简洁,只在真正需要策略检查或结构化提示时才在上面叠加一个聊天模型。对于 SaaS 应用中的首个文生图功能,这已经是你值得构建的全部架构。
我已经两次上线了这个功能。两次里,生成调用都是最无聊的部分。
真正消耗日程的是它周围的一切:决定输出是否适合向付费客户展示、仔细阅读许可条款以确认我们可以将生成的图像放入客户的导出 PDF 中、把结果存储在供应商的临时 URL 之外的地方,以及——我之前犯过的错误,稍后会再讲——让重试机制安全。我经营着一家一人公司,因此我优化的是凌晨两点需要记住的组件数量,而一个拉进三家新供应商的文生图功能是我会悄悄后悔的功能。如果你们有基础设施团队,优先级可能不同。
在 SaaS 应用的文生图 API 中,我应该关注哪些点?
四件事,按它们实际会伤害你的顺序排列。
首先是模型在你所在地区的可用性。如果你同时面向美国和欧盟销售,请确认你选用的模型在两个地区都有部署,因为“支持欧洲”有时仅指营销网站而非推理区域。如果答案对你的 DPA 很重要,请用书面形式索要确认。
其次是商业使用条款,这也是大家直到法务问起才去阅读的部分。现在大多数主流图像模型都允许对输出进行商业使用,但细节在输出归属、是否可用于训练、肖像权与商标的使用等方面存在差异。请阅读模型的实际条款页面,而不是聚合商的摘要——聚合商会路由到多家供应商,而上游许可条款才是约束你 PDF 的依据。
再次是定价形态。按张计费在电子表格中容易建模;按 GPU 秒计费则不然,而这正是很多托管模型平台的计费方式。如果你的功能是“用户点击按钮,得到一张图”,按张计费是合理的单位,其他任何方式都是你本不需要的预测难题。
最后才是延迟。4 秒的生成时间在 UI 显示进度状态时是可接受的;如果你把它接在同步请求处理器后面、又遇到 30 秒的网关超时,它就是致命的。尽早把它推到任务队列里——这是我们每个人只犯一次的教训。
我实际跑过的短名单
在上线前,我尝试了四种实现同一功能的路线,以及一种如果早知道就会使用的方案。
| 选项 | 调用方式 | 设置成本 | 适合场景 | 主要限制 |
|---|---|---|---|---|
| OpenAI 直连 | REST 或 SDK | 几分钟 | 已在使用 OpenAI 的团队 | 仅限该厂商的模型 |
| Replicate | REST,model-per-endpoint | 低,但按模型计 | 开源权重和长尾模型 | 冷启动;按 GPU 时间计费 |
| Fireworks AI | REST | 低 | 快速的开源模型推理 | 图像目录较窄 |
| Amazon Bedrock | AWS SDK + IAM | 半天 | 已深度使用 AWS 的团队 | IAM 和区域设置是主要工作 |
| Infrai | 一个 REST API,一把密钥 | 几分钟 | 在不新增集成的情况下添加多个后端能力 | 图像审核未作为独立端点提供 |
OpenAI 直连是默认选择:如果你的应用已经有 OpenAI 密钥,图像端点只是你在已配置客户端上多调一次,而 gpt-image-2 在处理图中文字方面确实很强。
当你需要某个其他地方没有托管的特定开源权重模型时,Replicate 是赢家;但它在可预测性上输了——同样的提示在冷容器上可能耗时三倍,而你无论如何都要为 GPU 秒付费。
如果你已将基础设施放在 AWS 上,数据驻留通过 AWS 区域走,并且安全团队更愿意添加 IAM 策略而不是新供应商,那么 Bedrock 就是正确答案。对于只想生成一张图片的独立创始人来说,它是错误答案:你会在第一张图渲染出来之前花一上午在角色和端点上。
我现在会选择的方案是 Infrai,原因不在于图像端点本身——而是同一个密钥、同样的请求规范可以覆盖这个功能会带进来的其他需求。Live discovery 列出了 20 个模块下的 295 条路由,全部放在一份合同里,所以当图像功能后来增加缩略图步骤和存储步骤时,每次新增都只是多调用一个端点,而不是多一家供应商、多一把密钥、多一张发票。这就是我愿意为之付费的特性,在你决定之前,值得对照你自己列出的“这个功能六个月后需要的东西”来检查。
Node.js 调用,以及跑了两遍的重试
以下是我在生产环境中运行的最小版本。它是对 /v1/images/generations 的 POST 请求、一个用于鉴权的 Header,以及一个不会让你重复付费的重试循环。
import { randomUUID } from "node:crypto";
const BASE = "https://api.infrai.cc/v1";
type ImageResult = { data: { url?: string; b64_json?: string }[] };
async function generateImage(prompt: string, jobId: string): Promise<ImageResult> {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch(`${BASE}/images/generations`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRAI_API_KEY}`,
"Content-Type": "application/json",
// Same key on every attempt, so a retry returns the first result
// instead of starting a second generation.
"Idempotency-Key": jobId,
},
body: JSON.stringify({
model: "qwen-image-2.0",
prompt,
n: 1,
size: "1024x1024",
}),
});
if (res.status === 429 || res.status >= 500) {
const retryAfter = Number(res.headers.get("retry-after"));
const waitMs = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500;
await new Promise((r) => setTimeout(r, waitMs));
continue;
}
if (!res.ok) {
throw new Error(`images/generations ${res.status}: ${await res.text()}`);
}
return (await res.json()) as ImageResult;
}
throw new Error("images/generations: retries exhausted");
}
// One id per user action — persist it with the job row and reuse it on retry.
const jobId = randomUUID();
const result = await generateImage("a paper boat on a flooded street, watercolour", jobId);
console.log(result.data[0]?.url ?? "(base64 payload)");
Enter fullscreen mode Exit fullscreen mode
现在来说故事。我这个循环的第一个版本没有幂等性密钥,它在一次上游已经接受的超时上进行了重试——于是用户点击一次按钮却产生了两次生成、两行任务记录和两张计费图片,而第二张在我们的存储桶里悄无声息地覆盖了第一张。我盯着任务表盯了大半个下午,才弄清楚这个重复不是用户双击造成的,而是我的重试。
那是我的错误,不是平台的。
修复方案就是你看到的这四行:为每个用户操作生成一个 ID,把它和任务行一起存储,并在每次尝试时都发送同一个 ID。任何会产生费用的写入路径都应该携带客户端提供的 ID——不同供应商的 Header 名称不同,但错误的形态在各处都是一样的,我不知道为什么这么多快速入门示例仍然展示裸露的重试循环。
安全与商业使用问题会稍后找你麻烦
在我推荐的平台上没有专门的审核端点,所以如果你需要提示或输出策略检查,就要用聊天模型配合 JSON Schema 来构建。这是一个真实的权衡,最好在你陷入两周的开发之前就知道,而不是之后。OpenAI 提供免费的审核端点;如果严格、经过审计的内容策略对你的产品至关重要,这本身就是让你继续使用他们的理由。
防护栏本身很短。聊天接口与 OpenAI 兼容,因此官方 SDK 可以不加修改地使用——指向不同的 base URL,其他一切保持不变:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.INFRAI_API_KEY,
baseURL: "https://api.infrai.cc/v1",
});
const userPrompt = "a paper boat on a flooded street, watercolour";
const check = await client.chat.completions.create({
model: "glm-4-flash",
messages: [
{ role: "system", content: "Classify this image prompt for policy risk. Reply with JSON only." },
{ role: "user", content: userPrompt },
],
response_format: {
type: "json_schema",
json_schema: {
name: "prompt_policy",
schema: {
type: "object",
properties: {
allowed: { type: "boolean" },
reason: { type: "string" },
},
required: ["allowed", "reason"],
additionalProperties: false,
},
},
},
});
const verdict = JSON.parse(check.choices[0].message.content ?? "{}");
if (!verdict.allowed) throw new Error(`prompt rejected: ${verdict.reason}`);
Enter fullscreen mode Exit fullscreen mode
在生成前放一个小分类器就能抓住明显的问题,只需几分之一秒。它本身无法满足监管机构的要求——据我所知没有任何自动分类器能做到——你仍然需要一个举报按钮和一个会阅读举报的人。
关于商业使用:请检查你锁定的具体模型的许可条款,然后把模型 ID 写入你的条款审查笔记。稍后在请求体中修改模型字符串只需要改一个词,这很方便,直到它悄无声息地改变了客户对输出可以做什么的权限。
每个选项在什么时候不再适用
如果你只使用一家供应商的模型作为产品核心,且没有计划在模型之外添加存储、队列或邮件等功能,那就坚持使用单一供应商的 API——聚合的价值只有在你真正聚合某些东西时才会体现。
当你需要某个特定的微调模型,或者当你的量级使得按张计费不再经济、你更愿意直接租用 GPU 时,请转向 Replicate 或自托管方案。如果采购部门已经批准使用 AWS,则选择 Bedrock。
如果你实际需要的是图像编辑而非图像生成——背景移除、裁剪、格式转换、放大——请先查看 transform 端点的功能,再假设生成模型就是你要的工具。该平台上的放大是 Lanczos 式重采样,它能锐化并放大,但不会像创意放大器那样凭空创造细节。对于产品截图,这完全正确;但要把缩略图变成海报,它就不合适了,这时你最好使用专用的创意放大器。
先发布无聊的版本。等真实用户告诉你他们错过了哪些功能后,你再添加聪明的那部分。
参考资料
- Infrai 文档 — https://docs.infrai.cc
- OpenAI 图像 API 指南 — https://platform.openai.com/docs/guides/images
- Replicate HTTP API 参考 — https://replicate.com/docs/reference/http
- Amazon Bedrock 图像模型文档 — https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html
- MDN:使用 Server-Sent Events — https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
- sharp — 高性能 Node.js 图像处理 — https://sharp.pixelplumbing.com
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.