backend/docs/ai/architecture.md
Rodrigo Verdiani 90ee74b3b9 docs: add AI module documentation and AGENTS.md
Comprehensive documentation for the AI module covering architecture,
API design, data model, RAG pipeline, configuration, integration points,
and frontend integration guide. Includes IDA5 extraction points reference
and AGENTS.md with project conventions and coding standards.
2026-06-08 20:13:21 -03:00

18 KiB
Raw Blame History

Arquitetura do Módulo de IA

Visão Geral

O módulo ai/ implementa um sistema de Retrieval-Augmented Generation (RAG) que permite ao usuário interagir com uma LLM através de um chat, fazendo perguntas sobre funções, estruturas, protocolos de rede e outros aspectos de um binário que passou por análise estática.

O sistema combina busca semântica via embeddings (pgvector) com navegação estruturada via call graph (xrefs) para fornecer contexto relevante à LLM durante a geração de respostas.

Stack Tecnológica

Componente Tecnologia Papel
Framework Spring Boot 4.0.6 + Spring Modulith 2.0.6 Base da aplicação, injeção de dependências, eventos
AI Framework Spring AI 1.0.x Abstração de LLM providers, embeddings, chat streaming
Chat Local Gemma 4 12B (via Ollama) LLM para chat executando localmente, 256K contexto
Chat Cloud DeepSeek (deepseek-chat) LLM alternativa via API cloud (OpenAI-compatible)
Embeddings Qwen3 Embedding 8B (via Ollama) Geração de embeddings para busca semântica, dims 324096
Vector Store pgvector (PostgreSQL 18) Armazenamento e busca por similaridade de cosseno
Vector Index HNSW (Hierarchical Navigable Small World) Índice para busca vetorial rápida
Message Broker RabbitMQ 4 Processamento assíncrono de jobs de embedding
Arquitetura Spring Modulith Módulo desacoplado com eventos de domínio

Diagrama de Componentes

┌──────────────────────────────────────────────────────────────────────────┐
│                        ai.decompile.ai (Módulo AI)                        │
│                                                                            │
│  ┌──────────────────────┐    ┌──────────────────────┐                    │
│  │    EmbeddingService   │    │     ChatService       │                    │
│  │                      │    │                      │                    │
│  │  • embed(text)       │    │  • createSession()   │                    │
│  │  • embedBatch()      │    │  • chat(sessionId,   │                    │
│  │  • indexBinary(id)   │    │         message)      │                    │
│  │  • searchSimilar()   │    │  • getHistory()       │                    │
│  │  • deleteByBinary()  │    │  • deleteSession()   │                    │
│  └────────┬─────────────┘    └──────────┬───────────┘                    │
│           │                             │                                 │
│  ┌────────┴─────────────────────────────┴──────────────────┐             │
│  │                     RagService                            │             │
│  │                                                           │             │
│  │  • retrieveContext(binaryId, query) → List<Chunk>        │             │
│  │  • buildSystemPrompt(chunks, binaryMeta) → String        │             │
│  │  • expandCallGraph(functionId, hops) → List<Function>    │             │
│  │  • deduplicateAndRank(results) → List<Chunk>             │             │
│  └────────┬──────────────────────────────────────────────────┘             │
│           │                                                                │
│  ┌────────┴────────────────┬──────────────────────────────┐               │
│  │  EmbeddingChunkRepo     │  ChatSessionRepo             │               │
│  │  • findSimilar(vector,  │  • findByBinaryId()          │               │
│  │    binaryId, topK)      │  • findById()                │               │
│  │  • deleteByBinaryId()   │                              │               │
│  │                          │  ChatMessageRepo             │               │
│  │                          │  • findBySessionId()         │               │
│  └──────────────────────────┴──────────────────────────────┘               │
│                                                                            │
│  ┌──────────────────────────────────────────────────────────┐             │
│  │                  ChatController (REST + SSE)               │             │
│  │                                                            │             │
│  │  POST   /binaries/{id}/chat/sessions    — criar sessão    │             │
│  │  GET    /binaries/{id}/chat/sessions    — listar sessões  │             │
│  │  GET    /chat/sessions/{id}/messages    — histórico       │             │
│  │  POST   /chat/sessions/{id}/messages    — enviar (SSE)   │             │
│  │  DELETE /chat/sessions/{id}             — deletar sessão  │             │
│  └──────────────────────────────────────────────────────────┘             │
└──────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│                      Infraestrutura Externa                                │
│                                                                            │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐                  │
│  │   Ollama     │   │  DeepSeek    │   │  PostgreSQL  │                  │
│  │  (localhost) │   │  (Cloud API) │   │  (pgvector)  │                  │
│  │              │   │              │   │              │                  │
│  │ qwen3-embed  │   │ deepseek-    │   │ embedding_   │                  │
│  │ gemma4:12b   │   │ chat         │   │ chunk        │                  │
│  └──────────────┘   └──────────────┘   │ chat_session │                  │
│                                        │ chat_message │                  │
│  ┌──────────────┐                      └──────────────┘                  │
│  │  RabbitMQ    │                                                         │
│  │              │                                                         │
│  │ decompile.   │                                                         │
│  │ jobs.queue   │                                                         │
│  └──────────────┘                                                         │
└──────────────────────────────────────────────────────────────────────────┘

Decisões Arquiteturais

1. Uso do Spring AI como Camada de Abstração

Decisão: Utilizar Spring AI 1.0.x como camada de abstração para providers de LLM e embeddings.

Justificativa:

  • Fornece interfaces padronizadas (ChatClient, StreamingChatClient, EmbeddingModel)
  • Suporte nativo a streaming via Flux<String> (Reactor)
  • Auto-configuração para Ollama e OpenAI (DeepSeek é compatível com API da OpenAI)
  • Integração com ecossistema Spring Boot

Trade-off: Spring AI é relativamente novo e pode ter breaking changes entre versões. Mitigamos encapsulando os chamados diretos atrás dos nossos serviços (EmbeddingService, ChatService).

2. Embedding Store Próprio (não Spring AI VectorStore)

Decisão: Implementar repositório pgvector próprio (EmbeddingChunkRepository) em vez de usar spring-ai-pgvector-store.

Justificativa:

  • Schema altamente específico ao domínio (FK para binary_id, source_id para static_function.id)
  • Necessidade de campos de metadados customizados em jsonb
  • Semântica de deleção específica (hard delete por binary_id na re-indexação)
  • Suporte a múltiplas estratégias de retrieval: busca semântica (pgvector <=>), busca textual (ILIKE fallback), call graph expansion

2.1 Hybrid Retrieval

Decisão: Pipeline de retrieval com três camadas: semântica → textual fallback → call graph.

Fluxo:

  1. Semântica: pgvector cosine similarity (top-K)
  2. Fallback: se < 40% do topK preenchido, busca textual via LIKE nos chunks
  3. Call Graph: detecta funções/endereços na query, expande 1-hop via xrefs, inclui assembly inline

Justificativa:

  • Queries com nomes específicos de função (sub_f5, 0x401000) frequentemente falham na busca semântica (nome curto, sem contexto)
  • O fallback textual captura menções diretas ao nome da função nos chunks
  • A expansão por call graph garante que a função requisitada SEMPRE apareça no contexto, mesmo sem embedding

3. Indexação Assíncrona via Job Separado

Decisão: Geração de embeddings como job assíncrono (GENERATE_EMBEDDINGS) via RabbitMQ, desacoplado do job de análise estática.

Justificativa:

  • Análise estática e embedding são operações com perfis de falha diferentes
  • Permite re-indexar sem re-analisar (ex: trocar modelo de embedding)
  • Permite retry independente se a API de embedding falhar
  • Segue o padrão arquitetural já estabelecido (ANALYZE_FILE, STATIC_ANALYSIS)

4. Multi-Provider com Seleção por Sessão

Decisão: Suporte a múltiplos providers de LLM (Ollama/Gemma4 + DeepSeek) com seleção no momento da criação da sessão.

Justificativa:

  • Usuário pode escolher entre privacidade/local (Ollama) ou qualidade/cloud (DeepSeek)
  • Providers são "pluggable" — adicionar novo provider requer apenas implementar a interface e registrar
  • A seleção é persistida na chat_session.chat_model e usada em todas as mensagens daquela sessão

5. Streaming via Server-Sent Events (SSE)

Decisão: Respostas do chat utilizam SSE para streaming de tokens.

Justificativa:

  • UX muito melhor que esperar resposta completa
  • Nativo do Spring WebMVC / Reactor (Flux<ServerSentEvent<String>>)
  • Compatível com StreamingChatClient do Spring AI
  • Fácil de consumir no frontend (EventSource API)

6. Chunking por Função (não por tamanho fixo)

Decisão: Cada função estática vira um chunk de embedding. Funções muito grandes são truncadas em chunk-max-chars (default 3000).

Justificativa:

  • Alinha com a unidade semântica natural da análise de binários (a função)
  • Permite que metadados de call graph (callers/callees) enriqueçam cada chunk
  • Facilita a expansão via navegação de xrefs (saber exatamente qual função foi recuperada)
  • Simplifica o tracking (source_type=STATIC_FUNCTION, source_id=function.id)

Módulos Spring Modulith

Dependências do módulo ai/

ai/
 ├── allowedDependencies:
 │   ├── workspace::entities    (Binary, Project para relações FK)
 │   ├── analysis::entities     (StaticFunction, StaticAnalysis, etc.)
 │   ├── analysis::services     (AnalysisService para buscar funções/xrefs)
 │   └── common                 (exceções, configurações compartilhadas)

Dependências do módulo job/ (atualizado)

job/
 ├── allowedDependencies (novos):
 │   ├── ...
 │   ├── ai::services           (EmbeddingService para o handler de embeddings)
 │   ├── ai::dto                (tipos de dados do módulo AI)
 │   └── ai::events             (eventos publicados pelo módulo AI)

Dependências do módulo analysis/ (atualizado)

analysis/
 ├── allowedDependencies (novos):
 │   ├── ...
 │   └── ai::events             (para publicar StaticAnalysisCompletedEvent)
      └── NOTA: o evento será definido no analysis/event para evitar dependência circular

Fluxo de Eventos Inter-Módulos

workspace        analysis           job              ai
   │                │                │                │
   │  BinaryUp-     │                │                │
   │  loadedEvent───┤                │                │
   │                │                │                │
   │                │  StaticAnalysisRequestedEvent   │
   │                ├───────────────►│                │
   │                │                │  STATIC_       │
   │                │                │  ANALYSIS      │
   │                │                │  (handler)     │
   │                │                │                │
   │                │◄───────────────┤  saveResult()  │
   │                │                │                │
   │                │  StaticAnalysisCompletedEvent   │
   │                ├────────────────────────────────►│
   │                │                │                │
   │                │                │  GENERATE_     │
   │                │                │  EMBEDDINGS    │
   │                │                │  (handler)────►│
   │                │                │                │
   │                │                │◄───────────────│
   │                │                │  indexBinary() │

Nota: O StaticAnalysisCompletedEvent será definido em analysis.event e o subscriber residirá em job.service (padrão consistente com StaticAnalysisRequestedEvent + StaticAnalysisEventSubscriber).

Configuração Multi-Provider

O módulo suporta dois providers de chat e um provider de embeddings, todos abstraídos pelo Spring AI:

                    ┌─────────────────────┐
                    │   ChatClientProvider │  (interface própria)
                    │                     │
                    │ getClient(model)─────┤
                    └──────────┬──────────┘
                               │
               ┌───────────────┼───────────────┐
               │                               │
    ┌──────────┴──────────┐     ┌──────────────┴──────────┐
    │ DeepSeekChatClient   │     │ OllamaChatClient        │
    │ (OpenAiChatModel)   │     │ (OllamaChatModel)       │
    │                     │     │                         │
    │ base-url:           │     │ base-url:               │
    │ api.deepseek.com    │     │ localhost:11434         │
    │ model: deepseek-chat│     │ model: gemma4:12b       │
    └─────────────────────┘     └─────────────────────────┘

                    ┌─────────────────────┐
                    │  EmbeddingModel     │  (Spring AI interface)
                    │                     │
                    │  embed(docs)        │
                    └──────────┬──────────┘
                               │
                    ┌──────────┴──────────┐
                    │ OllamaEmbeddingModel │
                    │                      │
                    │ base-url:            │
                    │ localhost:11434      │
                    │ model: qwen3-embedding│
                    └──────────────────────┘

Considerações de Performance

Aspecto Estratégia
Embedding em batch Funções enviadas em lotes de 20 (app.ai.embedding.batch-size)
Busca vetorial Índice HNSW com vector_cosine_ops — sub-100ms para milhares de vetores
Contexto da LLM Truncagem de chunks + histórico para caber em max-context-tokens (8000 default)
Streaming Tokens fluem via SSE assim que gerados, sem buffer completo
Conexões Ollama Pool de conexões HTTP gerenciado pelo Spring AI
Virtual Threads Habilitados (spring.threads.virtual.enabled: true) — I/O de rede não bloqueia

Segurança

  • API keys (DeepSeek) armazenadas exclusivamente em .env, nunca no repositório
  • .env listado no .gitignore
  • Nenhuma chave ou segredo em plaintext no application.yml
  • Variáveis de ambiente referenciadas como ${DEEPSEEK_API_KEY} com fallback vazio