FastAPI开发AI应用五:模型新增图片理解

本文将深入讲解如何在 FastAPI AI 聊天应用中实现图片理解功能,让 AI 能够理解和分析用户上传的图片内容。通过本教程,你将学会如何构建完整的多模态交互系统,包括图片上传、预处理、多模态消息格式化以及流式响应处理等核心技术。

📖 项目地址:github.com/wayn111/fas...

温馨提示:本文全文约八千字,看完约需 12 分钟。

本文概述

想象一下,当你向 AI 发送一张图片时,AI 不仅能看懂图片内容,还能基于图片进行深度分析和对话------就像一个拥有视觉能力的智能助手。这就是我们要实现的视觉理解功能!用户可以上传图片,AI 能够识别图片中的物体、场景、文字,并与用户进行基于图片内容的智能对话。

要实现的效果就是下图所示:

本文中,我们新增功能具备图片上传预览与管理、文本和图片混合消息处理、AI 识别分析图片元素、流式显示分析结果及图片格式与大小验证等核心能力,可实现多模态内容的高效交互与安全处理。

技术上采用 FastAPI 作为后端框架,结合 Pillow 进行图片处理,依托 doubao-seed-1-6、GPT-4o 等多模态 AI 模型实现图片理解,通过 Base64 编码传输图片数据,并利用 HTML5 File API 与 JavaScript 完成前端的图片上传和预览交互。

支持的图片模型

模型系列 代表模型 图片能力 特色
OpenAI GPT-4o gpt-4o, gpt-4o-mini 多模态融合 图文混合理解、实时交互
豆包视觉 doubao-seed-1.6 中文场景优化 中文OCR、本土化识别

图片理解能力详解

这里用豆包文档的图片理解说明,帮助大家理解大模型对图片理解的一些限制。

图片传入方式

图片理解模型支持两种图片传入方式:

  1. 图片 URL 方式:直接传入可访问的图片链接
  2. Base64 编码方式:将图片转换为 Base64 编码字符串传输

本项目采用 Base64 编码方式,确保图片数据的安全传输和处理。

图片格式与尺寸要求

支持的图片格式:

  • JPEG (.jpg, .jpeg)
  • PNG (.png)
  • GIF (.gif)
  • WebP (.webp)
  • BMP (.bmp)

图片尺寸限制:

根据不同模型版本,图片尺寸要求有所不同:

新版豆包模型(doubao-1.5-vision-pro-32k-250115 及以后版本):

  • 最小尺寸:宽 > 14px 且 高 > 14px
  • 像素范围:宽×高 在 [196, 3600万] 像素之间
  • 推荐尺寸:
    • 低精度模式:104万像素(1024×1024)
    • 高精度模式:401万像素(2048×1960)

图片数量限制

单次请求中可传入的图片数量受模型上下文长度限制:

计算公式:

复制代码
最大图片数量 = 模型上下文长度 ÷ 单张图片Token消耗

实际示例:

  • 高分辨率图片(1312 tokens/张):32k上下文可传入约 24 张
  • 低分辨率图片(256 tokens/张):32k上下文可传入约 125 张

注意事项:

  1. 图片数量过多会影响模型理解质量
  2. 建议单次请求控制在 5-10 张图片以内
  3. 对话API是无状态的,多次理解同一张图片需重复传入

理解深度控制

大部分图片模型支持两种理解深度:

低精度模式(detail: low)

  • 处理速度快,Token消耗少
  • 适合简单的图片识别和分类
  • 图片会被压缩到较小尺寸

高精度模式(detail: high)

  • 处理精度高,能识别更多细节
  • Token消耗较多,处理时间较长
  • 保持图片原始分辨率进行分析

在豆包模型中可以通过控制 detail 传参来实现。

ini 复制代码
# 在消息格式化时指定理解深度
content = [
    {
        "type": "image_url",
        "image_url": {
            "url": f"data:{image_type};base64,{image_data}",
            "detail": "high"  # 或 "low"
        }
    },
    {
        "type": "text",
        "text": "请详细分析这张图片的内容"
    }
]

核心理念

图片理解功能的实现基于三个核心设计原则:

1. 统一消息格式原则文本消息和图片消息使用统一的数据结构,确保系统能够无缝处理多模态内容。这样可以让现有的对话逻辑无需大幅修改就能支持图片。

2. 流式处理原则图片分析结果应该支持流式返回,让用户能够实时看到 AI 的分析过程。这不仅提升了用户体验,还保持了与纯文本对话的一致性。

3. 安全优先原则所有上传的图片都需要经过严格的格式验证和大小限制,确保系统安全稳定运行。

架构层次

图片理解功能的架构分为三个清晰的层次:

1. 前端交互层(HTML5 + JavaScript)

这一层负责用户的图片上传交互和预览展示:

csharp 复制代码
/**
 * 处理图片上传的核心函数
 * 包含文件验证、大小检查、格式转换等功能
 */
asyncfunction handleImageUpload(event) {
    const file = event.target.files[0];
    if (!file) return;

    // 检查文件类型
    if (!file.type.startsWith('image/')) {
        alert('请选择图片文件');
        return;
    }

    // 检查文件大小(限制为5MB)
    if (file.size > 5 * 1024 * 1024) {
        alert('图片文件大小不能超过5MB');
        return;
    }

    try {
        // 创建FormData对象
        const formData = new FormData();
        formData.append('file', file);

        // 上传图片
        const response = await fetch('/upload/image', {
            method: 'POST',
            body: formData
        });

        if (response.ok) {
            const result = await response.json();

            // 保存图片数据
            currentImageData = result.data.base64_data;
            currentImageType = result.data.content_type;

            // 显示图片预览
            showImagePreview(file, result.data.filename);

            console.log('图片上传成功:', result.message);
        } else {
            const error = await response.json();
            alert('图片上传失败: ' + (error.detail || '未知错误'));
        }
    } catch (error) {
        console.error('图片上传失败:', error);
        alert('图片上传失败: ' + error.message);
    }

    // 清空文件输入框
    event.target.value = '';
}

新增 image_data、image_type 接受前端上传图片 base64 内容以及图片格式。

python 复制代码
from dataclasses import dataclass
from typing import Optional

@dataclass
class AIMessage:
    """
    AI消息数据模型
    支持文本和图片的统一消息格式
    """
    role: str  # 消息角色:user, assistant, system
    content: str  # 文本内容
    timestamp: float  # 时间戳
    image_data: Optional[str] = None  # Base64编码的图片数据
    image_type: Optional[str] = None  # 图片MIME类型
    ...

核心特点:

  • 文件验证:严格检查文件类型和大小
  • 异步上传:使用 FormData 进行异步文件传输
  • 实时预览:上传成功后立即显示图片预览
  • 错误处理:完善的错误提示和异常处理

2. 后端处理层(FastAPI + Pillow)

这一层负责接收图片文件,进行验证和格式转换:

python 复制代码
@app.post("/upload/image")
asyncdef upload_image(file: UploadFile = File(...)):
    """
    图片上传API端点
    处理图片文件的接收、验证、转换和存储
    
    Args:
        file: 上传的图片文件
        
    Returns:
        dict: 包含上传结果和图片数据的响应
    """
    logger.info(f"接收图片上传请求 - 文件名: {file.filename}, 类型: {file.content_type}")

    try:
        # 检查文件类型
        ifnot file.content_type ornot file.content_type.startswith('image/'):
            raise HTTPException(status_code=400, detail="只支持图片文件")

        # 读取文件内容
        file_content = await file.read()

        # 检查文件大小(10MB = 10 * 1024 * 1024 bytes)
        max_size = 10 * 1024 * 1024# 10MB
        if len(file_content) > max_size:
            logger.warning(f"文件大小超出限制 - 文件名: {file.filename}, 大小: {len(file_content)} bytes, 限制: {max_size} bytes")
            raise HTTPException(status_code=413, detail=f"文件大小不能超过10MB,当前文件大小: {len(file_content) / (1024 * 1024):.2f}MB")

        # 验证图片格式和完整性
        try:
            image = Image.open(BytesIO(file_content))
            image.verify()  # 验证图片完整性
        except Exception as e:
            logger.error(f"图片验证失败: {e}")
            raise HTTPException(status_code=400, detail="无效的图片文件")

        # 转换为base64编码
        base64_data = base64.b64encode(file_content).decode('utf-8')

        logger.info(f"图片上传成功 - 文件名: {file.filename}, 大小: {len(file_content)} bytes")

        return {
            "success": True,
            "message": "图片上传成功",
            "data": {
                "filename": file.filename,
                "content_type": file.content_type,
                "size": len(file_content),
                "base64_data": base64_data
            }
        }

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"图片上传失败: {e}")
        raise HTTPException(status_code=500, detail=f"图片上传失败: {str(e)}")

关键功能:

  • 格式验证:使用 Pillow 验证图片格式和完整性
  • Base64 编码:将图片转换为 Base64 格式便于传输
  • 异常处理:完善的错误处理和日志记录
  • 安全检查:多层次的文件安全验证

3. 多模态消息层(OpenAI Compatible)

这一层负责将图片和文本组合成多模态消息格式:

python 复制代码
def format_messages(self, messages: List[AIMessage], system_prompt: str = None) -> List[Dict[str, Any]]:
    """
    格式化消息为提供商特定格式,支持多模态内容
    将文本和图片统一格式化为 OpenAI 兼容的消息格式
    
    Args:
        messages: 消息列表,包含文本和图片消息
        system_prompt: 系统提示词
        
    Returns:
        List[Dict[str, Any]]: 格式化后的消息列表
    """
    formatted_messages = []

    # 添加系统提示
    if system_prompt:
        formatted_messages.append({
            "role": "system",
            "content": system_prompt
        })

    # 处理历史消息
    for msg in messages:
        if msg.role in ["user", "assistant"]:
            # 检查是否包含图片数据
            if msg.image_data:
                # 多模态消息格式
                content = [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:{msg.image_type};base64,{msg.image_data}"
                        }
                    },
                    {
                        "type": "text",
                        "text": msg.content
                    },
                ]
                formatted_messages.append({
                    "role": msg.role,
                    "content": content
                })
            else:
                # 纯文本消息格式
                formatted_messages.append({
                    "role": msg.role,
                    "content": msg.content
                })

    return formatted_messages

采用统一数据结构处理文本和图片消息,不仅完全兼容 OpenAI 的多模态消息格式,还支持纯文本、纯图片、图文混合等多种消息类型,且易于扩展以支持更多模态类型。

到这里,我们的图片上传处理的核心逻辑就已经完成了。

踩坑点

大家知道标准的 EventSource API 设计时就只支持 GET,不支持 POST 请求,但是由于我们的聊天应用上传图片时采用base64 格式,导致上传内容很大,后端接收时,会出现参数截断现象,因此我们要修改 SSE 实现,改用 fetch post 请求来实现 SSE POST 请求。

ini 复制代码
// 构建请求体
const requestBody = {
    user_id: userId,
    session_id: currentSessionId,
    message: message,
    provider: provider,
    model: model,
    image_data: currentImageDataTmp,
    image_type: currentImageTypeTmp
};

// 发送POST请求获取流式响应
const response = await fetch('/chat/stream', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify(requestBody)
});

if (!response.ok) {
    thrownewError(`HTTP error! status: ${response.status}`);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();

let reasoningMessage = '';
let contentMessage = '';
let messageContainer = null;
let reasoningElement = null;
let contentElement = null;
let hasShownTypingIndicator = false;

// 处理流式响应
asyncfunction processStream() {
    try {
        while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            const chunk = decoder.decode(value, { stream: true });
            const lines = chunk.split('\n');

            for (const line of lines) {
                if (line.startsWith('data: ')) {
                    const jsonStr = line.slice(6);
                    if (jsonStr.trim() === '') continue;

                    try {
                        const data = JSON.parse(jsonStr);

                        if (data.type === 'chunk' || data.type === 'content' || data.type === 'reasoning') {
                            ...
                        }   
                    } catch (parseError) {
                        console.error('解析JSON失败:', parseError, 'JSON字符串:', jsonStr);
                    }
                }
            }
        }
    } catch (error) {
        console.error('处理流式响应失败:', error);
        hideTypingIndicator();
        addMessage('assistant', '❌ 抱歉,连接出现问题,请重试。');
        messageInput.disabled = false;
        document.getElementById('sendButton').disabled = false;
    }
}

// 开始处理流式响应
processStream();

再给大家演示下,看下我们上传图片的最终效果,

FastAPI开发AI应用系列文章

总结

本文详细讲解了在 FastAPI AI 聊天应用中实现图片理解功能的方法,包括构建多模态交互系统的核心技术、支持的图片模型及其实力、核心理念、各层次实现代码、设计亮点和踩坑点等内容。

相关推荐
量子位17 小时前
新Vidu Q3参考生,这是冲着「剧」来的!万物皆可参考:特效音效场景都备好了
openai
慧知AI17 小时前
Chrome Skills重磅上线!浏览器秒变"龙虾助理",开发者必看
openai
量子位17 小时前
刚刚,李飞飞世界模型新成果发布
openai
用户83562907805117 小时前
Python 操作 Word 文档节与页面设置
后端·python
西西弗Sisyphus17 小时前
Python 闭包的经典坑
python·闭包
西西弗Sisyphus17 小时前
Python 在dataclasses 里,field() 能给可变、不可变数据分别设置安全的默认值
python·field·dataclasses
西西弗Sisyphus17 小时前
Python @dataclass 有 `__post_init__` 和 无 `__post_init__` 的对比
python·dataclass·__post_init__
独隅17 小时前
PyCharm 开启硬换行的方法
ide·python·pycharm
無名路人17 小时前
用 codex AI 更新了下之前写的浏览器云书签标签页扩展
前端·openai·ai编程
weixin_4080996718 小时前
python请求文字识别ocr api
开发语言·人工智能·后端·python·ocr·api·ocr文字识别