多模态生图:从 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 协议和数据结构,把模型能力接进真实的软件里。