多模态 AI(图片/语音/视频)

AI 不再只是文字对话。用户可以上传图片让 AI 分析、用语音代替打字、让 AI 生成图片。本章讲清多模态 AI 在 Next.js 中的集成方式——Vision(图片理解)、Image Generation(图片生成)、TTS/STT(语音合成/识别)。

1. 图片理解(Vision)

1.1 原理

GPT-4o、Claude 3.5、Gemini 等模型原生支持图片输入——你可以在消息中同时传文本和图片,AI 能"看懂"图片内容。

图片是如何被模型理解的? 模型会把图片切成小块(patch),每个小块转换成一个“图像 Token”,然后和文本 Token 一起送入 Transformer 处理。这意味着:

  • 图片会消耗 Token 配额,而且通常比文本贵得多——一张 1024x1024 的图片大约消耗 765 Token(GPT-4o)
  • 图片分辨率越高,消耗的 Token 越多。建议在上传前压缩图片到合理尺寸
  • 模型对图片的理解能力很强(OCR、场景描述、图表解读),但不能精确计数或识别很小的文字

1.2 发送图片消息

AI SDK 的消息格式支持多模态内容——content 字段可以是文本和图片的数组:

// app/api/chat/route.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
 
export async function POST(request: Request) {
  const { messages } = await request.json()
 
  const result = streamText({
    model: openai('gpt-4o'),  // GPT-4o 支持 Vision
    messages,
  })
 
  return result.toDataStreamResponse()
}

客户端发送图片消息。这里用 experimental_attachments 把图片转成 base64 Data URL 附加到消息中。base64 会让数据体积增大约 33%,但胜在简单——不需要单独的文件上传服务。生产环境建议先上传到 CDN,然后传 URL(见 1.3 节):

'use client'
 
import { useChat } from 'ai/react'
 
export function MultimodalChat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
 
  async function handleImageUpload(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (!file) return
 
    // 转为 base64
    const reader = new FileReader()
    reader.onload = () => {
      const base64 = reader.result as string
 
      // 发送包含图片的消息
      handleSubmit(undefined, {
        experimental_attachments: [
          {
            name: file.name,
            contentType: file.type,
            url: base64,
          },
        ],
      })
    }
    reader.readAsDataURL(file)
  }
 
  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>
          {/* 渲染附件(图片) */}
          {msg.experimental_attachments?.map((attachment) => (
            <img
              key={attachment.name}
              src={attachment.url}
              alt={attachment.name}
              className="max-w-sm rounded-lg"
            />
          ))}
          {msg.content && <p>{msg.content}</p>}
        </div>
      ))}
 
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input type="file" accept="image/*" onChange={handleImageUpload} />
        <input value={input} onChange={handleInputChange} placeholder="描述图片或提问..." />
        <button type="submit">发送</button>
      </form>
    </div>
  )
}

1.3 图片 URL 方式

URL 方式适合图片已存储在 CDN 的场景。模型会从 URL 下载图片,不需要编码在请求体里,请求体积小得多:

const messages = [
  {
    role: 'user',
    content: [
      { type: 'text', text: '这张图片里有什么?' },
      { type: 'image', image: new URL('https://cdn.example.com/photo.jpg') },
    ],
  },
]
 
const result = streamText({ model: openai('gpt-4o'), messages })

1.4 实用场景

场景Prompt 示例
OCR 文字识别"提取这张图片中的所有文字"
图片描述"详细描述这张图片的内容"
代码截图解析"这段代码有什么 bug?"
产品分析"这个 UI 设计有什么改进建议?"
数据提取"读取这张表格/图表中的数据"

2. 图片生成

2.1 DALL-E 集成

DALL-E 3 基于扩散模型(Diffusion Model)原理:从纯噪声图片开始,通过多步去噪(受文本 Prompt 引导)逐渐生成清晰图片。DALL-E 3 还会自动改写你的 Prompt(revised_prompt)——它先用 GPT-4 把你的简短描述扩展成详细的图片描述,再用这个详细描述生成图片。这就是为什么生成结果中会返回 revised_prompt

// app/api/generate-image/route.ts
import OpenAI from 'openai'
 
const openai = new OpenAI()
 
export async function POST(request: Request) {
  const { prompt, size = '1024x1024' } = await request.json()
 
  const response = await openai.images.generate({
    model: 'dall-e-3',
    prompt,
    n: 1,
    size: size as '1024x1024' | '1792x1024' | '1024x1792',
    quality: 'standard',
  })
 
  const imageUrl = response.data[0].url
  const revisedPrompt = response.data[0].revised_prompt
 
  return Response.json({ imageUrl, revisedPrompt })
}

2.2 作为工具调用

把图片生成作为 AI 工具——AI 自己决定什么时候生成图片:

const generateImage = tool({
  description: '生成图片。当用户要求创建、绘制、画一张图片时调用',
  parameters: z.object({
    prompt: z.string().describe('图片描述,用英文,尽量详细'),
    size: z.enum(['1024x1024', '1792x1024', '1024x1792']).default('1024x1024'),
  }),
  execute: async ({ prompt, size }) => {
    const response = await openai.images.generate({
      model: 'dall-e-3',
      prompt,
      n: 1,
      size,
    })
    return {
      imageUrl: response.data[0].url,
      revisedPrompt: response.data[0].revised_prompt,
    }
  },
})

前端渲染图片结果:

function ImageResultCard({ data }: { data: { imageUrl: string; revisedPrompt: string } }) {
  return (
    <div className="space-y-2">
      <img src={data.imageUrl} alt={data.revisedPrompt} className="rounded-xl max-w-md" />
      <p className="text-xs text-gray-400">Prompt: {data.revisedPrompt}</p>
    </div>
  )
}

3. 语音合成(TTS)

3.1 OpenAI TTS

TTS(Text-to-Speech)把文本转成自然语音。OpenAI 提供两个模型:tts-1(快,适合实时)和 tts-1-hd(慢,音质更好)。它们基于深度学习语音合成,能生成接近真人的语音,支持多种音色(alloy、echo、nova 等)。每 1M 字符约 $15。

将 AI 回复转成语音——实现语音播报:

// app/api/tts/route.ts
import OpenAI from 'openai'
 
const openai = new OpenAI()
 
export async function POST(request: Request) {
  const { text, voice = 'alloy' } = await request.json()
 
  const response = await openai.audio.speech.create({
    model: 'tts-1',
    voice: voice as 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer',
    input: text,
    response_format: 'mp3',
  })
 
  // 返回音频流
  const buffer = Buffer.from(await response.arrayBuffer())
  return new Response(buffer, {
    headers: {
      'Content-Type': 'audio/mpeg',
      'Content-Length': buffer.length.toString(),
    },
  })
}

3.2 客户端播放

'use client'
 
export function SpeakButton({ text }: { text: string }) {
  const [isPlaying, setIsPlaying] = useState(false)
 
  async function speak() {
    setIsPlaying(true)
 
    const response = await fetch('/api/tts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text, voice: 'nova' }),
    })
 
    const audioBlob = await response.blob()
    const audioUrl = URL.createObjectURL(audioBlob)
    const audio = new Audio(audioUrl)
 
    audio.onended = () => {
      setIsPlaying(false)
      URL.revokeObjectURL(audioUrl)
    }
 
    audio.play()
  }
 
  return (
    <button onClick={speak} disabled={isPlaying} className="text-gray-400 hover:text-gray-600">
      {isPlaying ? '🔊 播放中...' : '🔈 朗读'}
    </button>
  )
}

4. 语音识别(STT)

4.1 OpenAI Whisper

STT(Speech-to-Text)把语音转成文字。Whisper 是 OpenAI 开源的语音识别模型,支持 99 种语言,对中文识别效果很好。它基于 Transformer 架构,把音频转换成频谱图(mel spectrogram)后进行序列到序列的转录。指定 language: 'zh' 可以提高中文识别准确率,因为模型不需要花精力判断语言。

将用户语音转成文字——实现语音输入:

// app/api/stt/route.ts
import OpenAI from 'openai'
 
const openai = new OpenAI()
 
export async function POST(request: Request) {
  const formData = await request.formData()
  const audioFile = formData.get('audio') as File
 
  const transcription = await openai.audio.transcriptions.create({
    model: 'whisper-1',
    file: audioFile,
    language: 'zh',  // 指定中文
  })
 
  return Response.json({ text: transcription.text })
}

4.2 浏览器录音

浏览器通过 MediaRecorder API 录音。注意几个关键点:

  • 需要用户授权麦克风(getUserMedia),首次会弹出权限请求
  • 录音格式用 audio/webm(浏览器原生支持),Whisper API 能直接处理
  • 录音结束后记得释放麦克风(stream.getTracks().forEach(track => track.stop())),否则浏览器会一直显示录音指示器
'use client'
 
export function VoiceInput({ onTranscription }: { onTranscription: (text: string) => void }) {
  const [isRecording, setIsRecording] = useState(false)
  const mediaRecorder = useRef<MediaRecorder | null>(null)
  const chunks = useRef<Blob[]>([])
 
  async function startRecording() {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
    const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' })
 
    recorder.ondataavailable = (e) => chunks.current.push(e.data)
 
    recorder.onstop = async () => {
      const audioBlob = new Blob(chunks.current, { type: 'audio/webm' })
      chunks.current = []
 
      // 发送到服务端识别
      const formData = new FormData()
      formData.append('audio', audioBlob, 'recording.webm')
 
      const response = await fetch('/api/stt', { method: 'POST', body: formData })
      const { text } = await response.json()
      onTranscription(text)
 
      // 释放麦克风
      stream.getTracks().forEach((track) => track.stop())
    }
 
    recorder.start()
    mediaRecorder.current = recorder
    setIsRecording(true)
  }
 
  function stopRecording() {
    mediaRecorder.current?.stop()
    setIsRecording(false)
  }
 
  return (
    <button
      onMouseDown={startRecording}
      onMouseUp={stopRecording}
      className={`p-3 rounded-full ${isRecording ? 'bg-red-500 animate-pulse' : 'bg-gray-200'}`}
    >
      🎤 {isRecording ? '松开结束' : '按住说话'}
    </button>
  )
}

4.3 集成到聊天

export function ChatUI() {
  const { append } = useChat()
 
  function handleVoiceInput(text: string) {
    // 语音识别结果直接发送到对话
    append({ role: 'user', content: text })
  }
 
  return (
    <div>
      {/* ... 消息列表 ... */}
      <div className="flex gap-2">
        <input ... />
        <VoiceInput onTranscription={handleVoiceInput} />
        <button type="submit">发送</button>
      </div>
    </div>
  )
}

5. 文件处理

5.1 PDF 分析

用户上传 PDF → 提取文本 → 作为上下文发送给 AI:

// app/api/analyze-pdf/route.ts
import pdf from 'pdf-parse'
 
export async function POST(request: Request) {
  const formData = await request.formData()
  const file = formData.get('file') as File
  const question = formData.get('question') as string
 
  const buffer = Buffer.from(await file.arrayBuffer())
  const pdfData = await pdf(buffer)
 
  const result = await generateText({
    model: openai('gpt-4o'),
    messages: [
      {
        role: 'system',
        content: `以下是一份 PDF 文档的内容,请根据文档内容回答用户问题。\n\n文档内容:\n${pdfData.text.slice(0, 50000)}`,
      },
      { role: 'user', content: question },
    ],
  })
 
  return Response.json({ answer: result.text })
}

5.2 多模态消息统一处理

生产环境中,用户可能同时发送文本、图片和文件。需要一个统一的处理层把不同类型的附件转换成 AI SDK 的多模态消息格式。核心思路:图片转 base64 作为 image 类型,PDF 提取文本作为 text 类型注入上下文:

export async function POST(request: Request) {
  const formData = await request.formData()
  const messagesJson = formData.get('messages') as string
  const messages = JSON.parse(messagesJson)
  const attachments = formData.getAll('attachments') as File[]
 
  // 处理附件 → 转成多模态消息
  const lastMessage = messages[messages.length - 1]
  if (attachments.length > 0) {
    const content: any[] = [{ type: 'text', text: lastMessage.content }]
 
    for (const file of attachments) {
      if (file.type.startsWith('image/')) {
        const base64 = Buffer.from(await file.arrayBuffer()).toString('base64')
        content.push({
          type: 'image',
          image: `data:${file.type};base64,${base64}`,
        })
      }
      // PDF/文本文件 → 提取内容注入上下文
      if (file.type === 'application/pdf') {
        const pdfData = await pdf(Buffer.from(await file.arrayBuffer()))
        content.push({
          type: 'text',
          text: `\n\n[文件: ${file.name}]\n${pdfData.text.slice(0, 20000)}`,
        })
      }
    }
 
    lastMessage.content = content
  }
 
  const result = streamText({ model: openai('gpt-4o'), messages })
  return result.toDataStreamResponse()
}

6. 模型能力对比

能力GPT-4oClaude 3.5Gemini 2.0
文本
图片输入
图片生成✅(DALL-E)✅(Imagen)
音频输入
音频输出✅(TTS)
视频理解
PDF 直接输入❌(需提取文字)✅(原生)✅(原生)

选择模型时考虑多模态能力。几个实用建议:

  • 如果只需要图片理解,GPT-4o / Claude 3.5 都很好
  • 如果需要 PDF 原生支持(不用提取文本),Claude 和 Gemini 更方便
  • 如果需要视频理解,目前只有 Gemini 支持
  • 图片生成 + 理解都需要时,GPT-4o(DALL-E)是最全面的选择
  • 多模态的 Token 成本比纯文本高得多,要注意控制图片分辨率和调用频率

本章小结

  • Vision:GPT-4o/Claude/Gemini 原生支持图片输入,用 experimental_attachments 发送
  • 图片生成:DALL-E 3 + 工具调用,AI 自动决定何时生成图片
  • TTS:OpenAI TTS API → 返回音频流 → 前端 Audio 播放
  • STT:浏览器 MediaRecorder 录音 → Whisper API 转文字 → 作为消息发送
  • 文件处理:PDF 提取文字注入上下文,图片转 base64 作为多模态消息
  • 模型选择:根据多模态能力选模型,GPT-4o 最全面

下一章讲 AI 应用安全与成本控制。