Otimização de Motor de Inferência e Lógica de Validação em Sistemas de Reconhecimento de Documentos Internacionais

Contexto e Desafios do Sistema

Em implementações anteriores de sistemas de extração de dados de documentos internacionais, a integração de modelos multimodais com cadeias de pensamento extendidas (como a variante Thinking do Qwen-VL) demonstrou alta precisão em tarefas de raciocínio lógico complexo. No entanto, para operações de OCR e tradução multilíngue em tempo real, o overhead computacional gerado pelo raciocínio prolongado resultou em latências inaceitáveis (entre 15 e 30 segundos por requisição), comprometendo a eficiência operacional.

Adicionalmente, a lógica de validação de documentos estrangeiros apresentava falhas conceituais. O sistema baseava-se em similaridade de vetores de características para classificar e, incorretamente, atestar a autenticdiade dos documentos. Devido à escassez de amostras de referência (problema de poucos dados), a similaridade cosmética não é um indicador confiável de autenticidade. Alertas de "documento suspeito" ou "nenhuma anomalia detectada" geravam falsos positivos e negativos, induzindo os operadores a erros.

Para resolver essas questões, a arquitetura do backend (FastAPI) passa por uma reestruturação focada em dois pilares: substituição do motor de inferência para uma variante otimizada para instrução (Instruct) e saneamento da lógica de resposta para documentos estrangeiros, removendo inferências de autenticidade e limitando o escopo à correspondência de layout e extração de texto.

Substituição do Motor de Inferência

Provisionamento dos Pesos do Modelo

O primeiro passo consiste em obter os pesos do modelo otimizado para seguimento de instruções. Em um ambiente com acesso à internet, utilizamos o utilitário de download do ModelScope:

# Download dos pesos do modelo Instruct
modelscope download --model Qwen/Qwen3-VL-8B-Instruct --local_dir /tmp/models/Qwen3-VL-8B-Instruct

Os arquivos baixados devem ser transferidos para o servidor de inferência com GPU, garantindo as permissões adequadas de leitura para o usuário do Docker.

Reconfiguração do Serviço vLLM

O script de inicialização do container Docker precisa ser ajustado para apontar para o novo diretório do modelo e atualizar o identificador do serviço exposto.

#!/bin/bash

# Definição de caminhos e variáveis de ambiente
HOST_MODEL_DIR="/data/models/Qwen3-VL-8B-Instruct"
CONTAINER_MODEL_DIR="/workspace/model"
SERVICE_NAME="qwen-vl-instruct-api"

# Limpeza de instâncias anteriores
docker rm -f $SERVICE_NAME 2>/dev/null

# Inicialização do container com vLLM
docker run -d \
    --name $SERVICE_NAME \
    --gpus all \
    --shm-size 16g \
    --network host \
    -v "${HOST_MODEL_DIR}:${CONTAINER_MODEL_DIR}" \
    vllm/vllm-openai:latest \
    --model $CONTAINER_MODEL_DIR \
    --served-model-name qwen-instruct-vl \
    --tensor-parallel-size 4 \
    --max-model-len 8192 \
    --gpu-memory-utilization 0.85 \
    --dtype bfloat16 \
    --enforce-eager

Adaptação do Cliente de Inferência

O módulo responsável por intermediar a comunicação com a API do vLLM deve ser atualizado para refletir o novo nome do modelo e otimizar o fluxo de chamadas assíncronas.

import base64
from openai import AsyncOpenAI

# Inicialização do cliente assíncrono
inference_client = AsyncOpenAI(
    api_key="not-needed",
    base_url="http://localhost:8000/v1"
)

async def extract_document_data(image_payload: bytes) -> str:
    """
    Envia a imagem codificada para o modelo Instruct e retorna a extração estruturada.
    """
    encoded_img = base64.b64encode(image_payload).decode("utf-8")
    
    payload_messages = [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Extraia e traduza os campos deste documento para o formato JSON."},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{encoded_img}"}}
            ]
        }
    ]

    completion = await inference_client.chat.completions.create(
        model="qwen-instruct-vl",
        messages=payload_messages,
        temperature=0.1,
        max_tokens=2048
    )
    
    return completion.choices[0].message.content

Refatoração da Lógica de Negócios

A endpoint principle de processamento de documentos sofre alterações significativas no fluxo de documentos estrangeiros. A validação de segurança (UV/IR) é desativada para estes casos, e a resposta é restrita aos dados de correspondência de template e extração textual.

import re
import base64
import pickle
import numpy as np
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from sklearn.metrics.pairwise import cosine_similarity

router = APIRouter()

@router.post("/process-document", response_model=DocumentProcessingResult)
async def process_identity_document(
    payload: DocumentRequest, 
    db: Session = Depends(get_database_session)
):
    # Fluxo para documentos domésticos (mantém validação de segurança)
    if payload.region_code in DOMESTIC_REGION_CODES:
        return await handle_domestic_document(payload, db)

    # Fluxo para documentos internacionais
    front_img_bytes = base64.b64decode(payload.front_image_b64)
    back_img_bytes = base64.b64decode(payload.back_image_b64) if payload.back_image_b64 else None

    # Extração de vetores de características
    query_vectors = {
        "front": extract_feature_vector(front_img_bytes),
        "back": extract_feature_vector(back_img_bytes) if back_img_bytes else None
    }

    # Busca de templates no banco de dados
    query_stmt = select(DocumentTemplate).where(
        DocumentTemplate.region_code == payload.region_code
    )
    available_templates = db.exec(query_stmt).all()

    if not available_templates:
        raise HTTPException(status_code=404, detail="Nenhum template encontrado para esta região.")

    # Cálculo de similaridade
    optimal_match = None
    highest_score = -1.0

    for tmpl in available_templates:
        tmpl_vectors = {
            "front": pickle.loads(tmpl.front_vector),
            "back": pickle.loads(tmpl.back_vector) if tmpl.back_vector else None
        }
        
        scores = []
        if query_vectors["front"] is not None and tmpl_vectors["front"] is not None:
            scores.append(cosine_similarity([query_vectors["front"]], [tmpl_vectors["front"]])[0][0])
        if query_vectors["back"] is not None and tmpl_vectors["back"] is not None:
            scores.append(cosine_similarity([query_vectors["back"]], [tmpl_vectors["back"]])[0][0])
            
        current_score = np.mean(scores) if scores else 0.0
        
        if current_score > highest_score:
            highest_score = current_score
            optimal_match = tmpl

    if highest_score < MIN_MATCH_THRESHOLD:
        raise HTTPException(status_code=400, detail="Similaridade insuficiente para classificação de layout.")

    # Extração via LLM (se habilitado)
    extracted_text = ""
    if payload.use_llm_extraction:
        combined_image = stitch_images_vertically(front_img_bytes, back_img_bytes)
        extracted_text = await extract_document_data(combined_image)

    # Formatação da resposta final
    sanitized_name = re.sub(r'\s+', ' ', re.sub(r'(?i)\bother\b', '', optimal_match.name)).strip()
    
    response_message = (
        f"Layout Identificado: {sanitized_name}\n"
        f"Índice de Correspondência: {highest_score:.2%}\n"
        f"[Aviso: A correspondência de layout não atesta a autenticidade do documento físico.]"
    )
    
    if extracted_text:
        response_message += f"\n\n--- Dados Extraídos ---\n{extracted_text}"

    return DocumentProcessingResult(
        status_code=200,
        message=response_message,
        template_front_b64=base64.b64encode(optimal_match.front_image).decode(),
        template_back_b64=base64.b64encode(optimal_match.back_image).decode() if optimal_match.back_image else None
    )

Validação Operacional

Após a implantação das alterações no ambiente de staging, os testes de integração devem focar nos seguintes critérios de aceitação:

  • Latência de Inferência: Ao processar um documento internacional com a extração de LLM ativada, o tempo total de resposta deve cair para a faixa de 4 a 7 segundos. O payload de resposta não deve conter tokens de raciocínio interno do modelo.
  • Integridade da Resposta: A interface do cliente deve exibir estritamente o nome do layout correspondente e a métrica de similaridade. Mensagens de alerta sobre autenticidade ou anomalias de segurança devem estar completamente ausentes para documentos não-domésticos, sendo substituídas pelo aviso de isenção de responsabilidade sobre a validação física.

Tags: FastAPI vLLM Qwen-VL Python Docker

Publicado em 7-8 03:22