多模态生图:从 Vite 工程化到前端调用 Qwen Image

多模态生图:从 Vite 工程化到前端调用 Qwen Image

本篇文章我第一次把"多模态"从一个听起来很大的 AI 概念,具体放到了一个前端项目里。这次同样也是调用 api 进行 llm 使用,但本次我们不再只是向大模型发送一段文字,而是把图片和文字一起放进请求体,让模型根据多张参考图生成一张新图。

多模态:不只是文字对话

以前调用大模型时,我们更多是在做文本输入和文本输出:用户发一句话,模型回一段话。但多模态模型处理的不再只有文字,它还可以理解图片、音频、视频等不同类型的信息。

本次聚焦的是生图模型,也就是让模型根据输入内容生成图片。更具体一点,我们使用的是 Qwen Image 的多模态生成接口。

在示例项目里,请求内容不是简单的一句 prompt,而是由多张图片和一段文字共同组成:

javascript 复制代码
content: [
  {
    image: 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/thtclx/input1.png'
  },
  {
    image: 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/iclsnx/input2.png'
  },
  {
    image: 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/gborgw/input3.png'
  },
  {
    text: '图1的女生穿着图2中的黑色裙子按图3的姿势坐下'
  }
]

这段代码体现了多模态的关键:模型接收的不是单一字符串,而是一组结构化内容。图片负责提供视觉参考,文本负责描述生成目标,模型再把这些信息综合起来生成结果。

为什么先讲 Vite?

在正式调用多模态接口之前,继续进行基础知识的补充,一块前端工程化知识:Vite。

如果只是写一个传统 HTML 页面,我们可以直接在浏览器里运行 <script>。但一旦项目开始涉及模块化、环境变量、依赖管理、开发服务器和构建流程,就需要一个工程化工具来接管项目。

Vite 在这里扮演的角色就是"前端项目的大管家":

  • 启动开发服务器,让项目可以通过 npm run dev 运行。
  • 支持 ESM 模块化,让浏览器可以使用 import
  • 管理环境变量,让配置可以从 .env.local 读取。
  • 负责开发和构建流程,让前端项目不再只是几个零散文件。

示例项目的 package.json 很简单:

json 复制代码
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "^8.1.0"
  }
}

这里的 npm run dev 本质上就是让 Vite 接管整个项目。它不是业务代码的一部分,却决定了项目如何启动、如何加载模块、如何读取配置。

.env.local:前端项目怎么管理配置

调用大模型接口绕不开 API Key。问题是:如果把 Key 直接写在前端代码里,它就会进入源码,风险非常高。

这里使用了 Vite 的环境变量机制:

javascript 复制代码
const apiKey = import.meta.env.VITE_QWEN_API_KEY

这行代码背后有几个关键点:

  • .env.local 用来存放本地环境变量,通常不提交到代码仓库。
  • Vite 要求暴露给前端的变量必须以 VITE_ 开头。
  • import.meta.env 是 Vite 在构建时注入的环境变量对象。
  • VITE_QWEN_API_KEY 最终会变成前端代码可以读取的字符串。

也就是说,.env.local 解决的是"不把 Key 直接写死在源码里"的问题。它适合本地开发和学习演示,但并不等于真正安全。

原因很简单:只要 API Key 被前端代码直接使用,它最终就有机会出现在浏览器运行环境或网络请求里。真正的生产环境应该把 API Key 放在后端,由前端请求自己的后端服务,再由后端去调用大模型接口。

所以这节课可以这样理解:Vite 的 .env.local 是前端工程化里的配置管理手段;后端代理才是生产环境里保护 API Key 的更可靠方案。

fetch:从前端发起多模态请求

项目中真正调用 Qwen Image 的函数是 generateImage

javascript 复制代码
const generateImage = async (prompt) => {
  const response = await fetch(
    'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'qwen-image-2.0-pro',
        input: {
          messages: [
            {
              role: 'user',
              content: [
                { image: 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/thtclx/input1.png' },
                { image: 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/iclsnx/input2.png' },
                { image: 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/gborgw/input3.png' },
                { text: '图1的女生穿着图2中的黑色裙子按图3的姿势坐下' }
              ]
            }
          ]
        },
        parameters: {
          n: 1,
          size: '1024*1536'
        }
      })
    }
  )

  const data = await response.json()
  return data.output.choices[0].message.content[0].image
}

这段代码可以拆成四层理解。

第一层是请求地址,也就是 Qwen Image 的多模态生成 endpoint。前端通过 fetch 向这个地址发送 HTTP 请求。

第二层是请求方法。这里使用 POST,因为生图请求需要传递比较复杂的请求体,里面有模型名、图片、文字和生成参数。GET 请求没有请求体,也不适合传递这类数据。

第三层是请求头。Authorization 用来携带 API Key,Content-Type: application/json 告诉服务器请求体是 JSON 格式。

第四层是请求体。body 里不能直接传 JS 对象,而要通过 JSON.stringify 序列化成 JSON 字符串。浏览器发送的是文本数据,服务器接收后再按 JSON 解析。

JSON.stringify:对象上网前要先打包

这节课和上一节 Ajax 的知识是连在一起的。无论是 XHR、fetch,还是 axios,只要要通过 HTTP 传输结构化数据,就绕不开序列化。

在 JavaScript 里,我们写的是对象:

javascript 复制代码
{
  model: 'qwen-image-2.0-pro',
  parameters: {
    n: 1,
    size: '1024*1536'
  }
}

但网络传输不能直接传一个 JS 对象。对象必须先变成 JSON 字符串:

javascript 复制代码
body: JSON.stringify({
  model: 'qwen-image-2.0-pro',
  input: {
    messages: []
  }
})

所以 JSON.stringify 的作用可以理解成"打包":把浏览器内存里的 JS 对象,打包成 HTTP 请求可以传输的文本。

后端处理完请求后,也会返回 JSON。前端再通过:

javascript 复制代码
const data = await response.json()

把响应内容解析回 JS 对象。整个过程还是上一节 Ajax 里讲过的链路:对象 → JSON 字符串 → HTTP 请求 → JSON 响应 → JS 对象。

多模态请求体:messages 为什么这样设计?

Qwen Image 的请求体里最核心的是 input.messages

它的结构和聊天模型很像:

javascript 复制代码
input: {
  messages: [
    {
      role: 'user',
      content: [
        { image: '图片地址' },
        { text: '生成要求' }
      ]
    }
  ]
}

role: 'user' 表示这条消息来自用户。content 是一个数组,里面可以放不同类型的内容。文本模型的 content 往往只是一段字符串,但多模态模型的 content 可以同时包含图片和文字。

这其实是 AI API 设计里的一个趋势:把用户输入抽象成"消息",再把消息内容抽象成多个"内容块"。每个内容块都有自己的类型,比如 image 或 text。

这样设计的好处是扩展性强。今天可以放图片和文字,未来也可以扩展到音频、视频、文件等更多输入类型。

渲染结果:把模型返回的图片显示到页面

拿到模型返回的数据后,项目从响应结构里取出图片地址:

javascript 复制代码
return data.output.choices[0].message.content[0].image

这里的层级比较深,但可以按语义理解:

  • output:模型输出结果。
  • choices:候选结果数组。
  • message:模型返回的一条消息。
  • content:消息里的内容块。
  • image:最终生成图片的地址。

取到图片地址后,页面渲染就很直接:

javascript 复制代码
const renderImage = (imageUrl) => {
  root.innerHTML = `<img src="${imageUrl}" alt="生成的图片" />`
}

最后在 main 函数里把生成和渲染串起来:

javascript 复制代码
const main = async () => {
  const imageUrl = await generateImage('一个猫')
  renderImage(imageUrl)
}

main()

这里的 await 很关键。生图是一个异步过程,前端必须等接口返回结果,才能拿到图片地址并渲染页面。

不过当前代码里的 prompt 参数还没有真正传入请求体,实际生图描述仍然写死在 content 的 text 字段里。如果要把它改成可复用函数,可以把 text 改成:

javascript 复制代码
{ text: prompt }

这样 generateImage('一个猫') 才会真正影响本次生成内容。

从工程化到多模态:我现在怎么理解这节课

这节课表面是在做一个 Qwen Image 生图 Demo,但真正学到的是一条前端调用 AI 服务的完整链路:

Vite 工程化 → 环境变量 → fetch 请求 → JSON 序列化 → 多模态消息 → 图片渲染

Vite 解决的是项目怎么跑起来的问题;.env.local 解决的是配置怎么管理的问题;fetch 解决的是前端怎么发请求的问题;JSON.stringify 解决的是对象怎么通过网络传输的问题;多模态 content 结构解决的是图片和文字怎么一起交给模型的问题;最后 DOM 渲染把模型结果展示到页面上。

最重要的不是记住某个接口地址,而是理解:AI 能力进入前端项目,本质上还是工程问题。模型再强,也要通过 HTTP 接口被调用;请求再复杂,也要组织成清晰的数据结构;Demo 能跑起来之后,还要继续思考 API Key、安全边界和生产环境架构。

从这个角度看,多模态并不是一个遥远的概念。它就是在已有的 Web 开发基础上,把"输入内容"从文字扩展到了图片,把"页面展示"从文本扩展到了生成结果。

这也让我对 AI 应用开发有了更实际的理解:我们不是在凭空创造一个智能系统,而是在用前端工程化、HTTP 协议和数据结构,把模型能力接进真实的软件里。

相关推荐
java小白小1 小时前
SpringBoot(09):缓存实战——穿透、雪崩、击穿的解决方案
后端
MobotStone1 小时前
AI项目越多,为什么越容易失控
人工智能·aigc
陳陈陳1 小时前
从Token到Embedding:一篇文章搞懂大模型的「文字数学变形记」
前端·javascript·ai编程
十有八七1 小时前
AI时代的置身X内
前端·人工智能
java小白小1 小时前
SpringBoot(08):Redis 集成——5 分钟给你的项目加上缓存
后端
Lkstar1 小时前
A2A协议深度解析|Agent2Agent通信标准,智能体互联网的"HTTP"
人工智能·llm
用户938515635071 小时前
从 O(n²) 到 O(nlogn):一文读懂快速排序的“快”与“妙”
javascript·算法
百度Geek说1 小时前
当代码越来越便宜,什么在变贵?
人工智能
橘子星1 小时前
LLM 无状态架构实践:从原理到代码落地
前端·javascript·人工智能