最近在整理图片生成相关的能力,顺手把阿里云百炼里的千问图片接口接了一遍。之前我对这类接口的印象还停留在“传一句 prompt,返回一张图”,真正接到项目里之后才发现,接口本身不难,麻烦的是周边细节:图片地址多久过期、输入图怎么传、提示词要不要自动改写、生成失败时怎么给用户一个能理解的提示。

这篇就按我自己的接入过程写,不做很大的架构展开。示例用 Node.js 原生 fetch,因为官方 DashScope SDK 主要是 Python 和 Java,Node 项目直接走 HTTP 反而简单,代码也更容易看清楚。

先把接口地址和模型搞清楚

我这次主要用的是千问文生图和图像编辑。它们走的是同一个 HTTP 地址:

POST https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation

如果用新加坡地域,地址要换成:

POST https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation

这个地方很容易被忽略。北京和新加坡的 API Key 不通用,请求地址也不一样。如果你本地换过地域,最好把 DASHSCOPE_BASE_URL 单独放到环境变量里,不要写死在代码里。

模型上我没有一上来就做很复杂的选择。大多数中文海报、封面、信息图类需求,可以先试 qwen-image-2.0-pro。如果只是想要快一点,或者对效果要求没那么高,再测试 qwen-image-2.0。编辑类场景也可以先用 qwen-image-2.0-pro 跑通,再根据效果去试 qwen-image-edit-maxqwen-image-edit-plus

文生图和图片编辑最大的区别在 content。文生图只放一个 text;图片编辑要先放图片,再放一个 text 指令。注意这里是一个 text,不要拆成多段。

一个最小的文生图调用

下面这段就是我一开始用来验证接口的代码。Node 18 以后有全局 fetch,不需要额外装请求库。

const API_KEY = process.env.DASHSCOPE_API_KEY;
const BASE_URL =
  process.env.DASHSCOPE_BASE_URL ||
  "https://dashscope.aliyuncs.com/api/v1";

async function textToImage(prompt) {
  if (!API_KEY) {
    throw new Error("Missing DASHSCOPE_API_KEY");
  }

  const response = await fetch(
    `${BASE_URL}/services/aigc/multimodal-generation/generation`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_KEY}`,
      },
      body: JSON.stringify({
        model: "qwen-image-2.0-pro",
        input: {
          messages: [
            {
              role: "user",
              content: [{ text: prompt }],
            },
          ],
        },
        parameters: {
          size: "2048*2048",
          n: 1,
          prompt_extend: true,
          watermark: false,
          negative_prompt: "低清晰度,文字模糊,构图混乱,过度变形",
        },
      }),
    },
  );

  const data = await response.json();

  if (!response.ok || data.code) {
    throw new Error(`${data.code || response.status}: ${data.message || ""}`);
  }

  return data.output.choices[0].message.content
    .map((item) => item.image)
    .filter(Boolean);
}

const urls = await textToImage(
  "一张横版技术博客封面图,主题是 AI 图片生成接口接入,画面里有代码编辑器、图片预览窗口和后台任务队列,干净的浅色风格,中文标题清晰:图片生成接口实践",
);

console.log(urls);

这里有几个参数我觉得需要单独说一下。

prompt_extend 默认可以先开着。短 prompt 的时候,它会帮你补一些细节,效果通常更完整。但如果你写的是品牌海报、商品主图、活动 Banner,里面已经有明确的文字、构图和位置要求,我反而建议关掉。否则模型可能会“帮你发挥”,最后不是你要的版式。

negative_prompt 不用写得特别玄。我的习惯是只写几个明确不想要的问题,比如低清晰度、文字模糊、构图混乱、主体变形。写太长不一定更稳,还可能让自己后面排查变复杂。

size 要按模型支持范围来,不要随手传一个设计稿尺寸。比如你想做 16:9,可以先用官方推荐的比例尺寸跑通,再在业务侧裁剪或缩放。

图片编辑其实更看提示词

图片编辑的调用方式和文生图很像,只是 content 里要多传图片。图片可以是公网 URL,也可以是 Base64 Data URL。我一般不建议直接传业务内网地址,模型服务访问不到。更稳的方式是先把图片放到 OSS 或临时文件服务,再传一个可访问链接。

async function editImage(imageUrls, instruction) {
  if (!API_KEY) {
    throw new Error("Missing DASHSCOPE_API_KEY");
  }

  const content = [
    ...imageUrls.map((url) => ({ image: url })),
    { text: instruction },
  ];

  const response = await fetch(
    `${BASE_URL}/services/aigc/multimodal-generation/generation`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_KEY}`,
      },
      body: JSON.stringify({
        model: "qwen-image-2.0-pro",
        input: {
          messages: [{ role: "user", content }],
        },
        parameters: {
          size: "1536*1024",
          n: 1,
          prompt_extend: true,
          watermark: false,
          negative_prompt: "主体变形,错误文字,模糊,多余物体",
        },
      }),
    },
  );

  const data = await response.json();

  if (!response.ok || data.code) {
    throw new Error(`${data.code || response.status}: ${data.message || ""}`);
  }

  return data.output.choices[0].message.content
    .map((item) => item.image)
    .filter(Boolean);
}

const result = await editImage(
  [
    "https://example.com/product.png",
    "https://example.com/style-reference.png",
  ],
  "以第一张图片为主图,只替换背景和光影,不改变产品结构、比例和包装文字。参考第二张图片的色调,生成一张横版商品首图,右侧留白用于后续放标题。",
);

console.log(result);

我试下来,图片编辑最怕指令太含糊。比如“优化一下这张图”这种说法,模型不知道你要优化什么,可能把主体、背景、颜色全改了。更好的写法是把“保留什么”和“改变什么”分开说。

比如:

  • 保留产品结构、比例、包装文字。
  • 只替换背景,不新增人物和其他品牌。
  • 参考第二张图的色调,但不要复制第二张图里的物体。
  • 右侧留白,方便后续加标题。

如果是多图输入,还要说清楚每张图的角色。不要只说“参考这几张图”,最好写“第一张是底图,第二张是风格参考,第三张是姿势参考”。这句话看起来啰嗦,但能少出很多奇怪结果。

返回的图片 URL 要马上转存

这个点我觉得是接入时最该先处理的。接口返回的图片 URL 不是永久素材地址,官方文档里也说明了链接有有效期。也就是说,不能直接把这个 URL 存到文章、商品、海报记录里就结束。

最少也要在拿到结果后立即下载,然后上传到自己的对象存储或素材库。

import { writeFile } from "node:fs/promises";

async function saveImage(url, filePath) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`download failed: ${response.status}`);
  }

  const buffer = Buffer.from(await response.arrayBuffer());
  await writeFile(filePath, buffer);
  return filePath;
}

真实项目里我不会只保存图片。还会顺手保存这些字段:模型名、prompt、negative_prompt、size、seed、request_id、生成时间、操作者、原始输入图、最终文件地址。以后如果有人问“这张图为什么生成成这样”,至少能查回当时的输入,而不是只剩下一张结果图。

错误处理不要只弹“生成失败”

图片接口失败的原因挺多,统一提示“生成失败”对用户没什么帮助。我的做法是先把错误分成几类。

如果是 InvalidApiKey、鉴权失败、地域不匹配,这属于配置问题,应该进日志或告警,不应该让用户反复点击重试。

如果是参数问题,比如尺寸不支持、图片数量不对、content 里传了多个 text,这是代码或表单校验问题。能在请求前拦住,就不要等接口报错。

如果是审核相关错误,比如提示词触发了内容安全检查,这时重试基本没意义,应该提示用户改一下输入内容。

如果是限流或网络波动,可以放进队列里重试。图片生成本来就比文本慢,前端最好不要一直卡着页面等结果。更舒服的体验是提交任务后进入“生成中”,服务端完成后再刷新状态。

我最后会把它封装成任务,而不是按钮

一开始我只是想写个按钮:点一下,生成一张图。后来发现这类能力最好不要散落在各个页面里。文章封面、商品主图、活动图、风格改图,看起来入口不同,本质上都是“图片任务”。

所以更适合做成一个内部服务:前端提交业务意图,后端负责组装提示词、检查图片、调用模型、转存结果、记录日志、返回素材 ID。这样以后换模型、调整尺寸、加审核流程,都不用到处改页面代码。

我在整理 Dcoding Max 盾码无界系统里的内容素材链路时,也基本按这个思路处理。系统里有文章、站点、商品、分发这些模块,图片生成只是其中一个小环节。它真正有用的地方,不是单独生成一张好看的图,而是能跟文章标题、产品信息、发布渠道放在一起,被记录、复用和继续优化。

小结

阿里云千问图片接口接起来不复杂。文生图就是一个 text,图片编辑就是几张 image 加一个 text。真正要注意的是:地域和 Key 别混,prompt 不要写得太散,结果 URL 要及时转存,错误要分类处理,编辑图片时要明确哪些内容不能动。

如果只是写 demo,几十行代码就够了。如果要放进业务系统,我建议早点把它当成“任务”和“素材”来设计。这样后面不管是给文章配图、做商品主图,还是接到更完整的内容运营流程里,都不会推倒重来。

参考资料

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐