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.