# 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 32–4096 | | **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 │ │ │ │ • buildSystemPrompt(chunks, binaryMeta) → String │ │ │ │ • expandCallGraph(functionId, hops) → List │ │ │ │ • deduplicateAndRank(results) → List │ │ │ └────────┬──────────────────────────────────────────────────┘ │ │ │ │ │ ┌────────┴────────────────┬──────────────────────────────┐ │ │ │ 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` (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>`) - 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