# Melhorias no Pipeline de IA/RAG — Plano Exaustivo

## Índice

1. [Visão Geral](#1-visão-geral)
2. [Tool Calling (Function Calling)](#2-tool-calling-function-calling)
3. [Aprimoramentos no Retrieval (RAG)](#3-aprimoramentos-no-retrieval-rag)
4. [Estratégia de Chunking e Embedding](#4-estratégia-de-chunking-e-embedding)
5. [Prompt Engineering Avançado](#5-prompt-engineering-avançado)
6. [Workflows Agentic](#6-workflows-agentic)
7. [Gestão de Contexto e Orçamento de Tokens](#7-gestão-de-contexto-e-orçamento-de-tokens)
8. [Multi-Provider, Model Routing e Fallback](#8-multi-provider-model-routing-e-fallback)
9. [Caching Inteligente](#9-caching-inteligente)
10. [Estrutura de Avaliação (Evals)](#10-estrutura-de-avaliação-evals)
11. [Segurança e Robustez](#11-segurança-e-robustez)
12. [Observabilidade e Monitoramento](#12-observabilidade-e-monitoramento)
13. [Roteiro de Implementação](#13-roteiro-de-implementação)

---

## 1. Visão Geral

### 1.1 Estado Atual (Linha de Base)

O pipeline de IA do Decompile-AI possui uma arquitetura RAG sólida como fundação. O fluxo atual é:

```
Usuário pergunta → ChatService
  ├── Embedding da query (qwen3-embedding)
  ├── Busca semântica (pgvector cosine <=>)
  ├── Fallback textual (LIKE se < 40% do topK)
  ├── Call graph expansion (1-hop xrefs)
  ├── Montagem do system prompt com contexto
  └── Streaming da resposta (Gemma4:12b via Ollama)
```

Apesar de funcional, o pipeline atual opera no paradigma **"single-shot retrieval"**: todo o contexto é coletado _antes_ do LLM começar a gerar. Se a busca inicial não capturar a informação necessária, o LLM não tem como obtê-la depois.

### 1.2 Visão Futura (Objetivo)

```
Usuário pergunta → ChatService
  ├── Fase 1: Retrieval inicial (semântico + textual + call graph)
  ├── Fase 2: LLM raciocina e decide quais tools chamar
  │     ├── "Preciso ver os callers de sub_401000" → tool: get_callers
  │     ├── "Quero o assembly completo dessa função" → tool: get_assembly
  │     └── "Deixa eu buscar por strings com 'http'" → tool: search_strings
  ├── Fase 3: LLM gera resposta final com todo o contexto coletado
  └── Fase 4 (futuro): Agente planeja múltiplos passos autônomos
```

### 1.3 Métricas-Alvo

| Métrica | Atual (Estimado) | Alvo |
|---|---|---|
| **Precisão da resposta** (avaliação humana) | ~70% | ≥90% |
| **Recall do retrieval** (top-10) | ~60% | ≥85% |
| **Latência P50 (primeira resposta)** | 3-5s | 2-3s |
| **Tool calls por pergunta complexa** | 0 | 1-5 |
| **Cobertura de contexto relevante** | ~70% | ≥95% |
| **Taxa de "não tenho informação suficiente"** | ~15% | ≤5% |

---

## 2. Tool Calling (Function Calling)

### 2.1 Problema

Hoje, o `ChatService.chat()` (`ChatService.java:28`) injeta o contexto RAG no system prompt e o LLM gera uma resposta. Se o LLM precisar de mais informação — por exemplo, examinar os callers de uma função que acabou de descobrir ser interessante — ele não pode. Ele fica restrito ao que foi recuperado na fase inicial.

O `ToolCallingManager` já está instanciado em `AiConfig.java:46`, mas vazio (`ToolCallingManager.builder().build()`). Nenhuma tool está registrada.

### 2.2 Solução

Registrar tools de domínio no `ToolCallingManager` usando a anotação `@Tool` do Spring AI 1.0.1. O LLM, ao receber o system prompt instruindo-o a usar tools, decidirá autonomamente quais chamar para obter informações adicionais.

As tools são funções Java expostas ao LLM com descrições em linguagem natural. Spring AI serializa a definição da tool no formato OpenAI-compatible e a envia junto com a requisição de chat.

### 2.3 Catálogo de Tools Propostas

#### 2.3.1 Tools de Busca

```java
@Component
@ConditionalOnProperty(name = "app.ai.enabled", havingValue = "true", matchIfMissing = true)
public class AnalysisTools {

    private final EmbeddingChunkRepository embeddingChunkRepository;
    private final StaticFunctionRepository functionRepository;
    private final StaticStringRepository stringRepository;
    private final StaticImportRepository importRepository;
    private final StaticXrefRepository xrefRepository;
    private final EmbeddingService embeddingService;
    private final AiUtils aiUtils;

    // --- Busca Semântica ---

    @Tool(description = """
        Search for functions and code sections semantically related to a query.
        Use this when the user asks about functionality (e.g., "network code",
        "encryption", "file operations") rather than specific function names.
        Returns up to 10 most relevant code sections with their addresses and summaries.
        """)
    public List<CodeSectionResult> semanticSearch(
            @ToolParam(description = "Natural language query describing what to search for")
            String query,
            @ToolParam(description = "The binary ID being analyzed (UUID)")
            String binaryId) {
        var uuid = UUID.fromString(binaryId);
        var embedding = embeddingService.embed(query);
        var vectorStr = aiUtils.toVectorString(embedding);
        var chunks = embeddingChunkRepository.findSimilar(uuid, vectorStr, 0.5, 10);
        return chunks.stream()
            .map(c -> new CodeSectionResult(
                c.getChunkType(), c.getSourceType(), c.getSourceId(),
                truncateSummary(c.getContent(), 500)))
            .toList();
    }

    // --- Busca de Funções por Nome/Endereço ---

    @Tool(description = """
        Find functions by name or address. Use this when the user mentions a specific
        function name (e.g., "sub_401000", "WinMain") or addresses (e.g., "0x401000").
        Returns up to 10 matching functions with their basic metadata.
        """)
    public List<FunctionSummary> searchFunctions(
            @ToolParam(description = "Function name or address to search for")
            String nameOrAddress,
            @ToolParam(description = "The binary ID being analyzed (UUID)")
            String binaryId) {
        // ... busca por nome e endereço
    }

    // --- Busca de Strings ---

    @Tool(description = """
        Search for strings in the binary. Use this when the user asks about specific
        text patterns (e.g., URLs, error messages, registry keys, IP addresses).
        Returns matching strings with their addresses and reference counts.
        """)
    public List<StringResult> searchStrings(
            @ToolParam(description = "Text pattern to search for (case-insensitive substring match)")
            String pattern,
            @ToolParam(description = "The binary ID being analyzed (UUID)")
            String binaryId) {
        // ... busca por strings com LIKE
    }

    // --- Busca de Imports ---

    @Tool(description = """
        Search for imported API functions by name, DLL, or resolved library name.
        Use this when the user asks about specific API usage (e.g., "CreateFile",
        "socket", "RegSetValue"). Returns matching imports with their DLL and address.
        """)
    public List<ImportSummary> searchImports(
            @ToolParam(description = "API name, DLL name, or library name to search for")
            String name,
            @ToolParam(description = "The binary ID being analyzed (UUID)")
            String binaryId) {
        // ... busca por imports
    }
}
```

#### 2.3.2 Tools de Navegação (Call Graph)

```java
    // --- Callers ---

    @Tool(description = """
        Get all functions that CALL this function. Essential for understanding
        control flow — who invokes this function and under what conditions.
        Returns caller names, addresses, and call types.
        """)
    public List<XrefResult> getCallers(
            @ToolParam(description = "The function ID (UUID from searchFunctions result)")
            String functionId) {
        var xrefs = xrefRepository.findByCalleeId(UUID.fromString(functionId));
        return xrefs.stream().map(x -> new XrefResult(
            x.getCaller().getName(), x.getCaller().getAddress(),
            x.getType(), "caller")).toList();
    }

    // --- Callees ---

    @Tool(description = """
        Get all functions that this function CALLS. Essential for understanding
        what APIs or subroutines a function depends on. Returns callee names,
        addresses, and call types.
        """)
    public List<XrefResult> getCallees(
            @ToolParam(description = "The function ID (UUID from searchFunctions result)")
            String functionId) {
        var xrefs = xrefRepository.findByCallerId(UUID.fromString(functionId));
        return xrefs.stream().map(x -> new XrefResult(
            x.getCallee().getName(), x.getCallee().getAddress(),
            x.getType(), "callee")).toList();
    }

    // --- All Xrefs ---

    @Tool(description = """
        Get ALL cross-references (both callers and callees) for a function.
        Use this for a complete picture of how a function fits in the call graph.
        """)
    public XrefGraphResult getXrefGraph(
            @ToolParam(description = "The function ID (UUID from searchFunctions result)")
            String functionId) {
        var func = functionRepository.findById(UUID.fromString(functionId)).orElseThrow();
        var callers = xrefRepository.findByCalleeId(func.getId());
        var callees = xrefRepository.findByCallerId(func.getId());
        return new XrefGraphResult(/* ... */);
    }
```

#### 2.3.3 Tools de Detalhe (Deep Dive)

```java
    // --- Assembly ---

    @Tool(description = """
        Get the full assembly code of a function. Use this when you need to analyze
        a function's implementation in detail — instruction by instruction.
        The assembly is truncated to the most important 8000 characters.
        """)
    public AssemblyResult getFunctionAssembly(
            @ToolParam(description = "The function ID (UUID from searchFunctions result)")
            String functionId) {
        var func = functionRepository.findById(UUID.fromString(functionId)).orElseThrow();
        return new AssemblyResult(
            func.getName(), func.getAddress(),
            func.getAssembly()); // truncado a 8000 chars
    }

    // --- Decompiled Code ---

    @Tool(description = """
        Get the decompiled pseudocode of a function if available (from IDA Hex-Rays
        or Ghidra decompiler). Use this for high-level understanding of the function's
        logic in C-like syntax.
        """)
    public DecompiledResult getFunctionDecompiled(
            @ToolParam(description = "The function ID (UUID from searchFunctions result)")
            String functionId) {
        // ... retorna decompiled code
    }

    // --- Binary Metadata ---

    @Tool(description = """
        Get the complete binary metadata: filename, format (PE/ELF/Mach-O),
        architecture (x86/x64/ARM), compiler, entry point, segment layout,
        and top-level statistics (function count, import count, string count).
        Use this for overview questions about the binary.
        """)
    public BinaryMetadataResult getBinaryMetadata(
            @ToolParam(description = "The binary ID being analyzed (UUID)")
            String binaryId) {
        // ... retorna visão geral do binário
    }

    // --- Library Diagnostics ---

    @Tool(description = """
        Get library/framework diagnostics for the binary. Identifies which
        libraries are linked (e.g., kernel32, msvcrt, Qt5Core), detected
        class names, and API category breakdown (GUI, Crypto, Network, etc.).
        Use this when the user asks about frameworks, libraries, or technology stack.
        """)
    public LibraryDiagnosticsResult getLibraryDiagnostics(
            @ToolParam(description = "The binary ID being analyzed (UUID)")
            String binaryId) {
        // ... bibliotecas, classes detectadas, categorias
    }

    // --- Function Listing ---

    @Tool(description = """
        List functions in the binary, optionally filtered by name pattern or
        address range. Use this when the user asks "list all functions" or
        wants to browse the function catalog. Returns up to 50 results.
        """)
    public List<FunctionSummary> listFunctions(
            @ToolParam(description = "Optional: name pattern to filter (substring match)")
            String nameFilter,
            @ToolParam(description = "Optional: minimum address to filter (hex string like '0x401000')")
            String minAddress,
            @ToolParam(description = "Optional: maximum address to filter")
            String maxAddress,
            @ToolParam(description = "The binary ID being analyzed (UUID)")
            String binaryId) {
        // ... listagem de funções
    }
```

### 2.4 Implementação Técnica

#### 2.4.1 Registro das Tools no `AiConfig`

```java
// AiConfig.java — modificação na criação do OllamaChatModel

@Bean
OllamaChatModel ollamaChatModel(
        OllamaApi ollamaApi,
        ObservationRegistry observationRegistry,
        AnalysisTools analysisTools) {  // ← injetar o bean de tools
    return OllamaChatModel.builder()
        .ollamaApi(ollamaApi)
        .defaultOptions(OllamaOptions.builder()
            .model(ollamaChatModelName)
            .temperature(ollamaTemperature)
            .build())
        .toolCallingManager(
            ToolCallingManager.builder()
                .toolCallbacks(analysisTools)  // ← registrar as tools
                .build())
        .observationRegistry(observationRegistry)
        .build();
}
```

#### 2.4.2 Adaptação do `ChatService` para Loop de Tool Calls

O `ChatService.chat()` (`ChatService.java:28`) atualmente faz streaming direto. Com tool calling, o fluxo precisa ser modificado para suportar um loop onde o LLM pode solicitar múltiplas tool calls antes de gerar a resposta final.

**Abordagem recomendada: usar `ChatClient` do Spring AI ao invés de `StreamingChatModel` diretamente.**

O Spring AI 1.0.1 fornece `ChatClient` que abstrai o loop de tool calls:

```java
// ChatService.java — versão com tool calling

private final ChatClient.Builder chatClientBuilder;
private final AnalysisTools analysisTools;

public Flux<String> chat(UUID sessionId, String userMessageContent) {
    var session = sessionService.getSession(sessionId);
    var binary = binaryService.getBinary(session.getBinaryId());

    sessionService.saveMessage(session, "USER", userMessageContent);

    var systemPrompt = buildInitialSystemPrompt(session, binary);
    var historyMessages = buildHistoryMessages(session);
    var assistantContent = new AtomicReference<>("");
    Sinks.Many<String> sink = Sinks.many().multicast().directBestEffort();

    var chatClient = chatClientBuilder.build();

    chatClient.prompt()
        .system(systemPrompt)
        .messages(historyMessages)
        .user(userMessageContent)
        .tools(analysisTools)  // ← tools disponíveis para esta conversa
        .stream()
        .chatResponse()  // Flux<ChatResponse> — inclui tool call requests
        .doOnNext(response -> {
            if (response.hasToolCalls()) {
                // O ChatClient do Spring AI gerencia tool calls automaticamente
                // se configurado. Alternativamente, podemos logar para debug.
                log.debug("Tool calls requested: {}", response.getResults()
                    .stream().flatMap(r -> r.getOutput().getToolCalls().stream())
                    .map(tc -> tc.name()).toList());
            }
            // tokens normais da resposta
            var content = response.getResult().getOutput().getContent();
            if (content != null) {
                assistantContent.updateAndGet(c -> c + content);
                sink.tryEmitNext(content);
            }
        })
        .doOnComplete(sink::tryEmitComplete)
        .doOnError(error -> {
            log.error("Chat error for session {}", sessionId, error);
            sink.tryEmitError(error);
        })
        .doFinally(_ -> saveAssistantMessage(session, assistantContent))
        .subscribe();

    return sink.asFlux();
}
```

**Nota importante**: O `ChatClient` do Spring AI 1.0.1 já gerencia o loop de tool calls internamente quando configurado com `.tools()`. Ele:
1. Envia a mensagem do usuário + tools disponíveis para o LLM
2. Se o LLM responder com tool calls, executa-as automaticamente
3. Reenvia os resultados das tools como mensagens `ToolResponseMessage`
4. Continua até o LLM gerar uma resposta final (sem mais tool calls)

**Alternativa manual** (para maior controle): Implementar o loop manualmente no `ChatService`, útil para:
- Limitar número máximo de tool calls por mensagem
- Timeout por tool call
- Logging detalhado de cada tool call
- Validação de parâmetros antes de executar

#### 2.4.3 Adaptação do System Prompt para Tool Calling

O system prompt em `ChatContextBuilder.buildSystemPrompt()` (`ChatContextBuilder.java:73`) precisa instruir o LLM a usar as tools:

```java
public String buildSystemPrompt(/* ... */) {
    return String.format("""
        You are an expert reverse engineer and malware analyst.
        
        [BINARY OVERVIEW]
        Filename: %s
        Format: %s
        Architecture: %s
        Compiler: %s
        Binary ID: %s    ← NOVO: ID do binário para tool calls
        
        [AVAILABLE TOOLS]
        You have access to tools that let you explore the binary's analysis data.
        IMPORTANT: Use these tools proactively when you need more information:
        - If the user asks about a specific function, use searchFunctions or getFunctionAssembly
        - If you need to understand control flow, use getCallers or getCallees
        - If the user asks about specific APIs, use searchImports
        - If you find an interesting function and want to see who calls it, use getCallers
        - If you need full assembly for detailed analysis, use getFunctionAssembly
        - ALWAYS provide the binaryId "%s" when calling tools
        
        %s    ← contexto RAG inicial (reduzido para ~1000 tokens)
        
        [GUIDELINES]
        - Use tools to GET information, don't guess or hallucinate
        - When you retrieve data via tools, cite function addresses
        - Explain assembly in natural language when useful
        - Be concise but thorough in technical analysis
        - The initial context below is a starting point — use tools to dig deeper
        """,
        // ... parâmetros
    );
}
```

### 2.5 Considerações Sobre o Modelo

O `gemma4:12b` suporta tool calling via Ollama. Verificar:

```bash
# Testar suporte a tool calling no modelo atual
ollama run gemma4:12b --format json
# Enviar requisição com tools definidas e verificar se o modelo
# retorna tool_calls no response
```

Se o suporte for limitado, considerar fallback para:
- **DeepSeek-chat** (tem excelente suporte a tool calling)
- **Llama 3.3** (suporte robusto, disponível via Ollama)
- **Qwen 2.5** (bom suporte a tool calling, roda via Ollama)

### 2.6 Limitações e Mitigações

| Limitação | Mitigação |
|---|---|
| **Latência**: cada tool call = +1 round trip ao LLM | Tools leves (SQL queries <50ms), cache de resultados |
| **Tool calls em cascata**: LLM pede ferramenta A → resultado → pede B → ... | Limitar a 5 tool calls por mensagem; abortar se detectar loop |
| **Hallucinação de tool calls**: LLM inventa nomes de funções | Validar UUIDs antes de executar; retornar erro descritivo |
| **Tokens gastos em tool results**: resultados grandes consomem contexto | Truncar resultados a 4000 tokens; incluir aviso "[truncated]" |
| **Modelo não suporta tool calling bem**: tool calls mal formatadas | Feature flag `app.ai.tool-calling.enabled`; fallback para modo sem tools |

---

## 3. Aprimoramentos no Retrieval (RAG)

### 3.1 Problema

O retrieval atual (`ChatContextBuilder.java:60`) tem três pontos de melhoria:

1. **Embedding único**: a query do usuário é embeddada e comparada diretamente com os chunks. Não há expansão de query, reformulação ou decomposição.
2. **Threshold fixo**: 0.6 de similaridade de cosseno é one-size-fits-all. Algumas queries precisam de threshold mais baixo para capturar relações indiretas.
3. **Sem re-ranking**: os resultados do pgvector são usados na ordem de similaridade, sem re-ranqueamento cross-encoder.

### 3.2 Solução 3.2.1: Query Rewriting / Expansão

Antes de embeddar a query do usuário, gerar variações para melhorar o recall:

```java
// ChatContextBuilder.java — novo método
private List<String> expandQuery(String userQuery) {
    var expansions = new ArrayList<String>();
    expansions.add(userQuery); // query original

    // 1. Extrair termos técnicos específicos
    if (userQuery.toLowerCase().contains("network") || userQuery.toLowerCase().contains("rede")) {
        expansions.add("socket connect send recv WS2_32 WinSock InternetOpen UrlDownload");
    }
    if (userQuery.toLowerCase().contains("crypto") || userQuery.toLowerCase().contains("cripto")) {
        expansions.add("encrypt decrypt AES RSA XOR RC4 hash CryptEncrypt CryptDecrypt");
    }
    if (userQuery.toLowerCase().contains("file") || userQuery.toLowerCase().contains("arquivo")) {
        expansions.add("CreateFile ReadFile WriteFile DeleteFile fopen fwrite fread");
    }
    if (userQuery.toLowerCase().contains("registry") || userQuery.toLowerCase().contains("registro")) {
        expansions.add("RegOpenKey RegSetValue RegQueryValue RegCreateKey");
    }

    // 2. Sinônimos de domínio (ex: "malware" → "malicious trojan backdoor rat")
    // 3. Tradução de termos (ex: "cryptografy" → "criptografia" e vice-versa)

    // Embeddar todas as variações e buscar
    // Concatenar e deduplicar resultados
    return expansions;
}
```

**Outra abordagem: usar o próprio LLM para expandir a query** (gasto de 1 chamada extra, mas mais flexível):

```java
private String rewriteQueryWithLLM(String userQuery) {
    // Prompt: "You are a query rewriter for a reverse engineering RAG system.
    // Rewrite the following user query into 3 alternative search queries
    // that would help find relevant assembly code, function names, and API
    // imports. Include technical synonyms and related API names.
    // Original query: {query}"
    // Chama LLM com temperatura baixa (0.1) para obter variações
}
```

### 3.3 Solução 3.2.2: Re-ranking com Cross-Encoder

Após recuperar top-20 chunks via pgvector, re-ranquear com um modelo cross-encoder para melhorar a precisão:

```java
// Novo componente: RerankingService
@Component
public class RerankingService {
    // Opção 1: Usar Ollama com modelo de reranking (ex: bge-reranker-v2-m3)
    // Opção 2: Usar API externa (Cohere Rerank, Voyage AI)
    // Opção 3: Heurística baseada em regras (mais leve, menos precisa)

    @Value("${app.ai.reranking.strategy:heuristic}")
    private String strategy;

    public List<EmbeddingChunk> rerank(List<EmbeddingChunk> candidates, String query) {
        return switch (strategy) {
            case "cross-encoder" -> rerankWithCrossEncoder(candidates, query);
            case "heuristic" -> rerankHeuristic(candidates, query);
            default -> candidates;
        };
    }

    private List<EmbeddingChunk> rerankHeuristic(
            List<EmbeddingChunk> candidates, String query) {
        var queryLower = query.toLowerCase();
        var queryTokens = extractTokens(queryLower);

        return candidates.stream()
            .sorted((a, b) -> {
                // Score composto: função mencionada diretamente na query → prioridade máxima
                int scoreA = computeHeuristicScore(a, queryLower, queryTokens);
                int scoreB = computeHeuristicScore(b, queryLower, queryTokens);
                return Integer.compare(scoreB, scoreA); // decrescente
            })
            .toList();
    }

    private int computeHeuristicScore(
            EmbeddingChunk chunk, String queryLower, List<String> queryTokens) {
        var content = chunk.getContent().toLowerCase();
        int score = 0;

        // Bônus por chunk type (FUNCTION > METADATA > STRING > outros)
        if ("FUNCTION".equals(chunk.getChunkType())) score += 10;
        else if ("METADATA".equals(chunk.getChunkType())) score += 5;

        // Bônus por match exato de tokens da query no conteúdo
        for (var token : queryTokens) {
            if (content.contains(token)) score += 3;
        }

        // Bônus por função mencionada na query aparecer nos callers/callees
        // (extrair de metadata JSON)

        return score;
    }
}
```

### 3.4 Solução 3.2.3: Hybrid Search (Dense + Sparse)

Combinar busca vetorial (dense, pgvector `<=>`) com busca lexical (sparse, PostgreSQL full-text search `tsvector`/`tsquery`):

```sql
-- Nova coluna na tabela embedding_chunk
ALTER TABLE embedding_chunk ADD COLUMN content_tsv tsvector
    GENERATED ALWAYS AS (to_tsvector('english', content)) STORED;
CREATE INDEX idx_embedding_chunk_tsv ON embedding_chunk USING GIN (content_tsv);

-- Query híbrida: combina score semântico com score lexical
SELECT *, 
    (0.7 * (1 - (embedding <=> CAST(:queryEmbedding AS vector)))  -- peso 70% semântico
   + 0.3 * ts_rank(content_tsv, plainto_tsquery('english', :queryText)))  -- peso 30% lexical
    AS hybrid_score
FROM embedding_chunk
WHERE binary_id = :binaryId
ORDER BY hybrid_score DESC
LIMIT :limit;
```

**Benefício**: queries com termos técnicos exatos (ex: `"WS2_32.send"`) ganham peso extra via busca lexical, enquanto queries conceituais (ex: `"código de rede"`) se beneficiam da busca semântica.

### 3.5 Solução 3.2.4: Multi-Hop Retrieval (Expansão em 2-3 níveis)

Hoje a expansão de call graph é 1-hop (`DEFAULT_CALL_GRAPH_HOPS = 1`, `ChatContextBuilder.java:39`). Expandir para 2-3 hops sob demanda:

```java
@Value("${app.ai.rag.max-call-graph-hops:3}")
private int maxCallGraphHops;

private void enrichWithCallGraph(/* ... */) {
    var hops = detectQueryComplexity(query);
    // queries complexas ("explique o fluxo completo de inicialização") → 3 hops
    // queries simples ("o que faz sub_401000?") → 1 hop

    var visitedFunctions = new HashSet<UUID>();
    var queue = new ArrayDeque<FunctionHop>();
    // BFS a partir das funções detectadas na query
    for (var func : detectedFunctions) {
        queue.add(new FunctionHop(func, 0));
    }

    while (!queue.isEmpty() && inlineSections.size() < MAX_INLINE_SECTIONS) {
        var current = queue.poll();
        if (!visitedFunctions.add(current.func.getId())) continue;
        if (current.hop > hops) continue;

        inlineSections.add(buildFunctionContext(current.func));
        if (current.hop < hops) {
            // expandir callers e callees
            var callees = xrefRepository.findByCallerId(current.func.getId());
            for (var callee : callees) {
                queue.add(new FunctionHop(callee.getCallee(), current.hop + 1));
            }
        }
    }
}
```

### 3.6 Solução 3.2.5: Threshold Adaptativo

O threshold de similaridade não deveria ser fixo:

```java
private double computeAdaptiveThreshold(String query) {
    // Queries específicas (contêm endereços, nomes de função) → threshold mais baixo
    if (FUNCTION_PATTERN.matcher(query).find()) {
        return 0.3; // mais permissivo para buscar funções relacionadas
    }
    // Queries curtas (1-3 palavras) → threshold mais baixo (menos contexto)
    var wordCount = query.split("\\s+").length;
    if (wordCount <= 3) {
        return 0.4;
    }
    // Queries longas e descritivas → threshold padrão
    return similarityThreshold; // 0.6 default
}
```

### 3.7 Diagrama do Retrieval Melhorado

```
Usuário: "Como o malware se comunica com o C2?"
    │
    ├── 1. Query Rewriting (LLM ou regras)
    │     "socket connect HTTP POST C2 command control beacon"
    │
    ├── 2. Hybrid Search
    │     ├── Embedding → pgvector cosine (dense, peso 0.7)
    │     └── Full-text search → ts_rank (sparse, peso 0.3)
    │     └── Result: top-30 chunks
    │
    ├── 3. Re-ranking (cross-encoder ou heurístico)
    │     ├── Score por relevância semântica
    │     ├── Score por match de tokens
    │     └── Result: top-10 chunks ordenados
    │
    ├── 4. Call Graph Expansion (1-3 hops)
    │     ├── Detecta funções mencionadas na query
    │     ├── BFS a partir delas
    │     └── Assembly inline para funções-chave
    │
    └── 5. Montagem do contexto (com orçamento de tokens)
```

---

## 4. Estratégia de Chunking e Embedding

### 4.1 Problemas Atuais

O chunking atual (`EmbeddingChunkBuilder.java:57`) gera um chunk por função, truncado a 3000 caracteres. Problemas:

1. **Funções muito grandes (>3000 chars)** perdem o final do assembly, que pode conter a lógica mais importante
2. **Funções muito pequenas (<200 chars)** geram embeddings com pouco contexto semântico
3. **Sem sobreposição (overlap)**: chunks são independentes; se a fronteira entre duas funções é relevante para uma query, nenhum chunk captura isso
4. **Chunks de METADATA únicos**: toda a informação de visão geral fica em um único chunk; se a similaridade for baixa, toda a visão geral é perdida

### 4.2 Solução 4.1: Chunking Hierárquico

Criar dois níveis de granularidade:

```
Nível 1 (Coarse): Chunk METADATA (visão geral do binário)
Nível 2 (Medium): Chunk FUNCTION (assembly + decompiled + call graph)
Nível 3 (Fine):   Chunk BASIC_BLOCK (blocos básicos individuais)

Exemplo para função sub_401000 (800 linhas de assembly):

Nível 2 — FUNCTION chunk:
  [FUNCTION] sub_401000 at 0x401000 (2048 bytes)
  Callers: WinMain | Callees: CreateFileA, WriteFile, CloseHandle
  [ASSEMBLY] ... (assembly completo, sem truncagem, até 12000 chars)

Nível 3 — BASIC_BLOCK chunks (3 chunks):
  [BASIC_BLOCK] sub_401000 @ 0x401000 — Entry
  [ASSEMBLY] push ebp; mov ebp, esp; sub esp, 0x40; ...

  [BASIC_BLOCK] sub_401000 @ 0x401050 — Loop body
  [ASSEMBLY] mov eax, [esi]; cmp eax, 0; je 0x401080; ...

  [BASIC_BLOCK] sub_401000 @ 0x401080 — Cleanup & return
  [ASSEMBLY] mov esp, ebp; pop ebp; ret 4
```

Na busca:
- Queries conceituais (ex: "função de criptografia") → usam Nível 2 (visão completa)
- Queries específicas (ex: "loop de XOR em sub_401000") → usam Nível 3 (bloco exato)
- O re-ranking pode combinar resultados de ambos os níveis

**Implementação no `EmbeddingChunkBuilder`**:

```java
public List<EmbeddingChunk> buildBasicBlockChunks(
        StaticFunction func, Binary binary, FunctionGraphContext ctx) {
    var blocks = parseBasicBlocks(func.getAssembly());
    // parseBasicBlocks: divide o assembly em blocos por labels/jumps
    return blocks.stream()
        .map(block -> EmbeddingChunk.builder()
            .chunkType("BASIC_BLOCK")
            .sourceType("STATIC_FUNCTION")
            .sourceId(func.getId())
            .content(String.format(
                "[BASIC_BLOCK] %s @ %s — %s\n%s",
                func.getName(), block.address(), block.label(),
                block.assembly()))
            .build())
        .toList();
}
```

### 4.3 Solução 4.2: Sobreposição (Overlap) e Sliding Window

Para funções grandes, em vez de truncar, gerar múltiplos chunks com sobreposição:

```java
public List<EmbeddingChunk> buildSlidingWindowChunks(
        StaticFunction func, Binary binary, int windowSize, int overlap) {
    var assembly = func.getAssembly();
    var lines = assembly.split("\n");
    var chunks = new ArrayList<EmbeddingChunk>();

    for (int start = 0; start < lines.length; start += (windowSize - overlap)) {
        var end = Math.min(start + windowSize, lines.length);
        var window = String.join("\n", 
            Arrays.copyOfRange(lines, start, end));
        chunks.add(buildChunk(func, binary, window, start, end));
        if (end >= lines.length) break;
    }
    return chunks;
}
// Exemplo: windowSize=100 linhas, overlap=20 linhas
// Chunk 1: linhas 1-100
// Chunk 2: linhas 81-180
// Chunk 3: linhas 161-260 ...
```

### 4.4 Solução 4.3: Chunks por Tipo de Dado (Multifacetado)

Hoje cada chunk tem um tipo (`FUNCTION`, `STRING`, `IMPORT`, etc.), mas as queries não consideram isso. Melhorar:

1. **Indexar tipos separadamente**: criar índices por `chunk_type`:
   ```sql
   CREATE INDEX idx_embedding_chunk_type ON embedding_chunk (binary_id, chunk_type);
   ```

2. **Busca ponderada por tipo**:
   - Se a query menciona "string", "texto", "URL" → peso extra para chunks `STRING`
   - Se a query menciona "API", "DLL", "importa" → peso extra para chunks `IMPORT`
   - Se a query menciona "função", "sub_", endereço → peso extra para chunks `FUNCTION`

3. **Busca multi-vetorial**: embeddar a query uma vez e buscar contra todos os tipos, depois combinar com pesos:

```java
private List<EmbeddingChunk> weightedMultiTypeSearch(UUID binaryId, String queryEmbedding) {
    var allResults = new ArrayList<ScoredChunk>();

    // Buscar cada tipo separadamente
    var funcChunks = embeddingChunkRepository.findSimilarByType(
        binaryId, queryEmbedding, "FUNCTION", similarityThreshold, topK);
    var stringChunks = embeddingChunkRepository.findSimilarByType(
        binaryId, queryEmbedding, "STRING", similarityThreshold, topK / 2);
    var importChunks = embeddingChunkRepository.findSimilarByType(
        binaryId, queryEmbedding, "IMPORT", similarityThreshold, topK / 2);

    // Aplicar pesos baseados na query
    var weights = computeTypeWeights(query); // ex: {FUNCTION: 1.0, STRING: 0.5, IMPORT: 0.3}
    // Combinar e ordenar por score ponderado
}
```

### 4.5 Solução 4.4: Modelo de Embedding Alternativo

O modelo atual (`qwen3-embedding:latest`, 4096 dimensões) é bom, mas há opções mais especializadas para código:

| Modelo | Dimensões | Tamanho | Vantagem |
|---|---|---|---|
| `qwen3-embedding:latest` (atual) | 4096 | 4.7 GB | Multilíngue, bom para texto geral |
| `nomic-embed-text` | 768 | 274 MB | Rápido, leve, bom para inglês técnico |
| `codegemma:7b` (adaptado com LoRA) | 3072 | 5 GB | Treinado especificamente para código |
| `starcoder2:7b` (adaptado) | 4096 | 5 GB | Código multilíngue |
| `voyage-code-2` (API) | 1536 | — | Estado-da-arte para busca de código |

**Recomendação de curto prazo**: testar `nomic-embed-text` (via Ollama) para reduzir dimensionalidade para 768 (menos storage, busca mais rápida) e comparar qualidade com o Qwen3.

**Recomendação de longo prazo**: fine-tune do `qwen3-embedding` ou `codegemma` com os próprios dados de assembly usando LoRA. Um modelo fine-tuned no domínio de assembly/decompilação teria recall significativamente melhor.

### 4.6 Solução 4.5: Indexação Incremental e Delta

Hoje a re-indexação é full (delete + rebuild). Para binários grandes ou re-análises frequentes, implementar indexação delta:

```java
@Transactional
public int indexBinaryDelta(UUID binaryId, UUID analysisId) {
    // 1. Buscar análise mais recente
    var analysis = analysisRepository.findById(analysisId).orElseThrow();
    var functions = analysis.getFunctions();

    // 2. Identificar funções novas ou modificadas
    var existingChunks = embeddingChunkRepository.findByBinaryId(binaryId);
    var existingIds = existingChunks.stream()
        .filter(c -> "STATIC_FUNCTION".equals(c.getSourceType()))
        .map(EmbeddingChunk::getSourceId)
        .collect(Collectors.toSet());

    var newFunctions = functions.stream()
        .filter(f -> !existingIds.contains(f.getId()))
        .toList();

    var modifiedFunctions = functions.stream()
        .filter(f -> existingIds.contains(f.getId()) && isModified(f, existingChunks))
        .toList();

    // 3. Deletar chunks das funções modificadas
    for (var func : modifiedFunctions) {
        embeddingChunkRepository.deleteBySourceId(func.getId());
    }

    // 4. Re-indexar apenas funções novas/modificadas
    var deltaChunks = new ArrayList<EmbeddingChunk>();
    deltaChunks.addAll(chunkBuilder.buildFunctionChunks(newFunctions, binary));
    deltaChunks.addAll(chunkBuilder.buildFunctionChunks(modifiedFunctions, binary));
    embedAndPersistChunks(deltaChunks);

    log.info("Delta indexing: {} new, {} modified, {} total chunks for binary {}",
        newFunctions.size(), modifiedFunctions.size(), deltaChunks.size(), binaryId);
    return deltaChunks.size();
}
```

---

## 5. Prompt Engineering Avançado

### 5.1 Problema

O system prompt atual (`ChatContextBuilder.java:73-107`) é um template fixo em Java. Isso gera três problemas:

1. **Dificuldade de iteração**: alterar o prompt requer recompilar e redeploy
2. **Sem versionamento**: prompts evoluem junto com código, difícil fazer A/B testing
3. **Sem personalização por tipo de binário**: o mesmo prompt serve para malware, game, driver, etc.
4. **Sem few-shot examples**: o LLM não tem exemplos de respostas de qualidade esperada

### 5.2 Solução 5.1: Sistema de Templates Externo

Mover prompts para arquivos externos versionados, com suporte a variáveis de template:

```
src/main/resources/prompts/
├── system/
│   ├── default.md            # Prompt padrão
│   ├── malware-analysis.md   # Especializado em malware
│   ├── vulnerability.md      # Especializado em vulnerabilidades
│   └── general-reverse.md    # Engenharia reversa geral
├── few-shot/
│   ├── function-analysis.md  # Exemplos de análise de função
│   ├── call-graph.md         # Exemplos de navegação de call graph
│   └── malware-detection.md  # Exemplos de detecção de malware
└── guidelines/
    ├── assembly.md           # Guidelines para explicar assembly
    └── security.md           # Guidelines para análise de segurança
```

**Exemplo: `prompts/system/malware-analysis.md`**:

```markdown
You are an expert malware analyst specializing in {architecture} binaries.
You are analyzing a {format} file compiled with {compiler}.

## Analysis Framework
1. Identify the malware's primary objective (keylogger, ransomware, RAT, etc.)
2. Trace the infection vector and persistence mechanisms
3. Map C2 communication protocols
4. Catalog anti-analysis / anti-debugging techniques
5. Document IOCs (Indicators of Compromise)

## Available Data
- Entry point: {entry_point}
- Linked libraries: {libraries}
- Detected API categories: {categories}

{context}

## Response Guidelines
- Start with an executive summary of the malware's purpose
- For each function you analyze, explain its role in the attack chain
- Highlight suspicious patterns: encoded strings, dynamic API resolution, process injection
- Use the tools available to dig deeper into suspicious functions
- Flag IOCs: IP addresses, URLs, mutex names, registry keys

## Few-Shot Examples

### Example 1: Credential Harvester
User: "Analyze the function at 0x401500 that calls InternetOpenUrl"
Assistant:
**Analysis of sub_401500 at 0x401500: Credential Exfiltration**
1. **Purpose**: This function harvests stored browser credentials and exfiltrates them...
2. **Flow**:
   - Calls `InternetOpenUrl` to connect to `http://evil-c2.com/collect.php`
   - Uses POST with base64-encoded data...
...
```

### 5.3 Solução 5.2: Template Engine

```java
// PromptTemplateService.java
@Component
public class PromptTemplateService {
    private final Map<String, String> templateCache = new ConcurrentHashMap<>();

    @Value("${app.ai.prompt.template:default}")
    private String defaultTemplate;

    public String render(String templateName, Map<String, Object> variables) {
        var template = loadTemplate(templateName);
        return processTemplate(template, variables);
    }

    private String loadTemplate(String name) {
        return templateCache.computeIfAbsent(name, key -> {
            var path = "prompts/system/" + key + ".md";
            var resource = new ClassPathResource(path);
            try {
                return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
            } catch (IOException e) {
                log.warn("Template {} not found, using default", key);
                return loadTemplate("default");
            }
        });
    }

    private String processTemplate(String template, Map<String, Object> vars) {
        var result = template;
        for (var entry : vars.entrySet()) {
            result = result.replace("{" + entry.getKey() + "}", 
                Objects.toString(entry.getValue(), ""));
        }
        // Suporte a condicionais simples
        // {% if has_decompiled %}...{% endif %}
        return result;
    }
}
```

### 5.4 Solução 5.3: Few-Shot Examples Dinâmicos

Incluir exemplos relevantes no prompt baseado no tipo de query:

```java
private String selectFewShotExamples(String query) {
    var sb = new StringBuilder();
    sb.append("\n## Examples of Good Responses\n\n");

    // Detectar padrões na query e incluir exemplos relevantes
    if (query.toLowerCase().matches(".*(analyz|explain|what does).*(function|sub_|0x).*")) {
        sb.append("""
            ### Example: Function Analysis
            User: "Analyze sub_401000"
            Assistant:
            **Analysis of sub_401000 at 0x401000 (256 bytes)**
            
            **Purpose**: This function implements a XOR-based string decryption routine.
            
            **Parameters**:
            - [ebp+8]: pointer to encrypted buffer
            - [ebp+12]: buffer length
            
            **Logic**:
            1. Prologue sets up stack frame (push ebp; mov ebp, esp)
            2. Loop iterates [ebp+12] times
            3. XOR each byte with 0x55
            4. Result written back to original buffer
            
            **Called by**: WinMain (initialization phase)
            **Calls**: None (leaf function)
            """);
    } else if (query.toLowerCase().matches(".*(malware|malicious|suspicious|trojan|virus).*")) {
        sb.append("""
            ### Example: Malware Classification
            ...
            """);
    }
    return sb.toString();
}
```

### 5.5 Solução 5.4: A/B Testing de Prompts

```java
@Value("${app.ai.prompt.ab-test-enabled:false}")
private boolean abTestEnabled;

public String buildSystemPrompt(/* ... */) {
    String templateName;
    if (abTestEnabled) {
        // 50% dos usuários recebem template A, 50% template B
        templateName = Math.random() < 0.5 ? "default" : "experimental-v2";
        log.debug("A/B test: using template={} for binary={}", templateName, binaryId);
    } else {
        templateName = defaultTemplate;
    }
    var vars = buildTemplateVars(context, binary);
    return promptTemplateService.render(templateName, vars);
}
```

### 5.6 Solução 5.5: Chain-of-Thought (CoT) Implícito

Instruir o LLM a raciocinar passo a passo antes de responder:

```markdown
## Reasoning Protocol
Before answering, reason through these steps internally:
1. **Understand the question**: What exactly is being asked?
2. **Assess available context**: What data do I have from the initial context?
3. **Identify gaps**: What additional information would help?
4. **Use tools**: Call appropriate tools to fill gaps
5. **Synthesize**: Combine tool results with initial context
6. **Answer**: Provide a thorough technical analysis

Do NOT skip step 4 — always check if tools can provide better information
than what's in the initial context.
```

---

## 6. Workflows Agentic

### 6.1 Problema

O sistema atual é reativo: responde uma pergunta por vez. Não há capacidade de executar tarefas multi-step autônomas, como:

- "Analise este binário e me diga se é malware" → precisa de múltiplos passos de raciocínio
- "Compare as funções de rede dos binários A e B" → precisa de contexto de dois binários
- "Explique todo o fluxo de execução desde a entry point" → precisa de exploração multi-hop

### 6.2 Solução 6.1: ReAct Agent (Reasoning + Acting)

Implementar um agente ReAct que alterna entre pensamento (Thought), ação (Action/Tool call) e observação (Observation):

```
Pergunta: "Este binário é malware?"

Thought: Preciso verificar indicadores de malware.
  - Preciso da visão geral do binário (metadados)
  - Preciso verificar imports suspeitos (crypto, network, process injection)
  - Preciso verificar strings suspeitas (URLs, IPs, nomes de mutex)
  
Action: getBinaryMetadata(binaryId)
Observation: PE32, x86, MSVC, 450 funções, entry 0x402100

Thought: Entry point em 0x402100. Vou examinar a entry point.
Action: getFunctionAssembly(entryPointFunctionId)
Observation: [assembly da entry point...]

Thought: Vejo chamadas para VirtualAlloc, WriteProcessMemory, CreateRemoteThread.
  Essas são APIs típicas de process injection. É um forte indicador de malware.
  Vou verificar as strings para URLs e IPs.
Action: searchStrings("http")
Observation: "http://malicious-c2.com/panel/gate.php"

Thought: Confirmado. URL de C2 hardcoded + process injection = malware.
  Vou classificar como RAT (Remote Access Trojan).

Answer: Sim, este binário é malware. Classificação: RAT...
```

**Implementação com Spring AI**:

```java
// ReActAgentService.java
public Flux<String> analyzeWithAgent(UUID sessionId, String task) {
    // Spring AI não tem suporte nativo a ReAct na versão 1.0.1,
    // mas podemos implementar o loop manualmente.

    var REACT_PROMPT = """
        You are a malware analyst agent. Use the following format:
        
        Question: the input question you must answer
        Thought: you should always think about what to do
        Action: the action to take, must be one of: [searchFunctions, getCallers, 
                getCallees, getFunctionAssembly, searchStrings, searchImports, 
                getBinaryMetadata, getLibraryDiagnostics]
        Action Input: the parameters for the action (JSON)
        Observation: the result of the action
        ... (this Thought/Action/Action Input/Observation can repeat N times)
        Thought: I now know the final answer
        Final Answer: the final answer to the original input question
        
        Tools available:
        {tools}
        
        Begin!
        Question: {question}
        """;

    // Loop ReAct: max 10 iterations
    for (int i = 0; i < 10; i++) {
        var response = chatModel.call(new Prompt(/* ... */));
        var parsed = parseReActResponse(response);
        
        if (parsed.isFinalAnswer()) {
            return Flux.just(parsed.answer());
        }
        
        var toolResult = executeAction(parsed.action(), parsed.actionInput());
        // Adiciona ao prompt como Observation
    }
}
```

### 6.3 Solução 6.2: Plan-and-Execute

Para tarefas complexas, primeiro gerar um plano e depois executá-lo passo a passo:

```
Pergunta: "Analise este binário e produza um relatório de malware"

Fase 1 — Planejamento:
  Plano:
  1. Coletar metadados do binário (getBinaryMetadata)
  2. Identificar entry point e analisar (getFunctionAssembly)
  3. Listar todos os imports agrupados por categoria (getLibraryDiagnostics)
  4. Buscar strings suspeitas: URLs, IPs, paths, registry keys (searchStrings)
  5. Rastrear call graph a partir da entry point até 3 hops (getCallees recursivo)
  6. Identificar técnicas MITRE ATT&CK
  7. Compilar relatório final

Fase 2 — Execução:
  Executar cada passo, coletar resultados.

Fase 3 — Síntese:
  Gerar relatório final consolidado.
```

### 6.4 Solução 6.3: CrewAI / Multi-Agent (Futuro Distante)

Múltiplos agentes especializados colaborando:

- **Agent Disassembler**: focado em análise de assembly linha a linha
- **Agent Decompiler**: focado em entender lógica de alto nível (pseudocódigo)
- **Agent Malware Hunter**: focado em detectar padrões maliciosos
- **Agent Network Analyst**: focado em protocolos de rede e C2
- **Agent Orchestrator**: coordena os agentes e consolida respostas

**Nota**: Este é um objetivo de longo prazo (6-12 meses). Começar com tool calling + ReAct é mais pragmático.

---

## 7. Gestão de Contexto e Orçamento de Tokens

### 7.1 Problema

Hoje, `max-context-tokens: 8000` está configurado mas não é rigorosamente aplicado. O sistema confia na truncagem de chunks (3000 chars cada) e no limite de histórico (20 mensagens), mas não há garantia de que o prompt total caiba na janela de contexto.

Com tool calling, o problema se agrava: cada resultado de tool adiciona tokens ao contexto, podendo estourar a janela.

### 7.2 Solução 7.1: Token Counter Preciso

Substituir o estimador atual (caracteres / 3.5, `AiUtils.java:26`) por um tokenizer real:

```java
// Opção 1: Se Spring AI expuser o tokenizer do modelo
// Opção 2: Usar uma biblioteca de tokenização (ex: JTokkit, HuggingFace Tokenizers Java)
// Opção 3: Usar o endpoint /api/tokens do Ollama (se disponível)

// Exemplo com JTokkit:
// import com.knuddels.jtokkit.Encodings;
// import com.knuddels.jtokkit.api.Encoding;

public static int countTokens(String text, String modelName) {
    var encoding = switch (modelName) {
        case "gemma4:12b" -> Encodings.newDefaultEncoding(); // Gemma usa SentencePiece
        case "deepseek-chat" -> Encodings.newCl100kBaseEncoding(); // OpenAI-compatible
        default -> Encodings.newDefaultEncoding();
    };
    return encoding.countTokens(text);
}
```

### 7.3 Solução 7.2: Orçamento de Tokens Dinâmico

Calcular e aplicar orçamento de tokens em tempo real:

```java
// TokenBudgetManager.java
@Component
public class TokenBudgetManager {
    private static final int SYSTEM_PROMPT_RESERVE_TOKENS = 1000;
    private static final int RESPONSE_RESERVE_TOKENS = 2000;
    private static final int TOOL_RESULTS_RESERVE_TOKENS = 4000;

    public TokenBudget calculate(UUID sessionId, String modelName) {
        var maxContext = getModelMaxContext(modelName);  // ex: gemma4 → 256K
        var configMax = maxContextTokens;  // 8000 do application.yml
        var effectiveMax = Math.min(maxContext, configMax);

        return new TokenBudget(
            effectiveMax,
            SYSTEM_PROMPT_RESERVE_TOKENS,
            RESPONSE_RESERVE_TOKENS,
            TOOL_RESULTS_RESERVE_TOKENS);
    }

    public List<EmbeddingChunk> fitChunksInBudget(
            List<EmbeddingChunk> chunks, TokenBudget budget, String modelName) {
        var usedTokens = 0;
        var selected = new ArrayList<EmbeddingChunk>();
        var available = budget.availableForContext();

        for (var chunk : chunks) {
            var chunkTokens = chunk.getTokenCount() != null 
                ? chunk.getTokenCount() 
                : countTokens(chunk.getContent(), modelName);
            if (usedTokens + chunkTokens > available) {
                log.debug("Token budget exceeded; {} tokens used, {} available, skipping chunk {}", 
                    usedTokens, available, chunk.getChunkType());
                break;
            }
            usedTokens += chunkTokens;
            selected.add(chunk);
        }
        return selected;
    }

    private int getModelMaxContext(String modelName) {
        return switch (modelName) {
            case "gemma4:12b" -> 256_000;
            case "deepseek-chat" -> 64_000;
            case "llama3.3:70b" -> 128_000;
            case "qwen2.5:14b" -> 128_000;
            default -> 8_000; // conservador
        };
    }
}
```

### 7.4 Solução 7.3: Contexto com Sumarização Progressiva

Quando o histórico de conversa fica muito longo, sumarizar as mensagens antigas:

```java
// HistorySummarizer.java
@Component
public class HistorySummarizer {
    private final ChatModel chatModel; // modelo rápido e barato

    public String summarize(List<ChatMessage> oldMessages) {
        if (oldMessages.size() < 10) {
            return buildCompactHistory(oldMessages);
        }

        var conversationText = oldMessages.stream()
            .map(m -> m.getRole() + ": " + m.getContent())
            .collect(Collectors.joining("\n"));

        var prompt = """
            Summarize the following conversation about a binary analysis.
            Keep all technical details: function names, addresses, findings.
            Output a concise summary (< 500 tokens).
            
            Conversation:
            %s
            
            Summary:
            """.formatted(conversationText);

        return chatModel.call(prompt);
    }
}
```

O prompt final fica:

```java
var messages = new ArrayList<Message>();
messages.add(new SystemMessage(systemPrompt));
if (historyRequiresSummarization) {
    messages.add(new SystemMessage(
        "[PREVIOUS CONVERSATION SUMMARY]\n" + historySummary));
}
messages.addAll(recentHistory); // últimas 5-10 mensagens intactas
messages.add(new UserMessage(currentQuery));
```

---

## 8. Multi-Provider, Model Routing e Fallback

### 8.1 Problema

A interface `StreamingModelProvider` (`StreamingModelProvider.java:6`) e sua implementação `OllamaStreamingModelProvider` são um bom começo, mas:

1. **Só há um provider implementado** (Ollama)
2. **Não há fallback** — se Ollama estiver offline, o chat falha
3. **Não há roteamento por tipo de tarefa** — tool calling pode exigir modelo diferente de chat simples
4. **Não há seleção dinâmica** baseada em latência, custo ou capacidade

### 8.2 Solução 8.1: Router com Seleção Dinâmica

```java
// ModelRouter.java
@Component
public class ModelRouter {
    private final Map<String, StreamingModelProvider> providers;

    public StreamingChatModel route(RoutingContext ctx) {
        // 1. Se o provider específico foi selecionado na sessão, usar ele
        if (ctx.sessionModel() != null) {
            return providers.get(ctx.sessionModel()).resolve();
        }

        // 2. Tool calling → preferir modelos com bom suporte
        if (ctx.requiresToolCalling()) {
            return tryProviders("deepseek-chat", "gemma4:12b");
        }

        // 3. Tarefa simples (chat básico) → preferir local (zero custo)
        if (ctx.isSimpleQuery()) {
            return tryProviders("gemma4:12b", "deepseek-chat");
        }

        // 4. Análise complexa / relatório → preferir modelo mais capaz
        return tryProviders("deepseek-chat", "gemma4:12b");
    }

    private StreamingChatModel tryProviders(String... providerNames) {
        for (var name : providerNames) {
            try {
                var model = providers.get(name).resolve();
                if (model != null) {
                    log.info("Routed to provider: {}", name);
                    return model;
                }
            } catch (Exception e) {
                log.warn("Provider {} unavailable: {}", name, e.getMessage());
            }
        }
        throw new IllegalStateException("No AI providers available");
    }
}
```

### 8.3 Solução 8.2: Circuit Breaker e Health Checks

Usar Spring Retry + Resilience4j (já configurado via `@EnableResilientMethods`):

```java
@Retryable(retryFor = {IOException.class, TimeoutException.class}, 
           maxAttempts = 3, backoff = @Backoff(delay = 1000))
@CircuitBreaker(name = "ollama-chat", fallbackMethod = "fallbackChat")
public StreamingChatModel resolve() {
    // verificar health do Ollama
    var health = ollamaApi.checkHealth();
    if (!health.isHealthy()) {
        throw new IllegalStateException("Ollama is unhealthy");
    }
    return ollamaChatModel;
}

public StreamingChatModel fallbackChat(Throwable t) {
    log.warn("Ollama unavailable, falling back to cloud provider");
    return deepSeekProvider.resolve();
}
```

### 8.4 Solução 8.3: Modelos por Papel (Role-Based Models)

Diferentes modelos para diferentes etapas do pipeline:

| Papel | Modelo | Motivo |
|---|---|---|
| **Chat principal** | `gemma4:12b` | Local, rápido, zero custo |
| **Tool calling** | `deepseek-chat` | Excelente suporte a tools |
| **Raciocínio complexo** | `deepseek-reasoner` ou `qwen2.5:32b` | Cadeia de pensamento longa |
| **Sumarização** | `gemma4:12b` ou `llama3.2:3b` | Tarefa simples, modelo leve basta |
| **Query rewriting** | `gemma4:12b` | Baixa latência, tarefa curta |
| **Classificação de intenção** | `llama3.2:3b` (via Ollama) | Modelo leve, tarefa de classificação |

Configuração:

```yaml
app:
  ai:
    models:
      chat: ${AI_CHAT_MODEL:gemma4:12b}
      tool-calling: ${AI_TOOL_MODEL:deepseek-chat}
      reasoning: ${AI_REASONING_MODEL:deepseek-reasoner}
      summarization: ${AI_SUMMARIZE_MODEL:gemma4:12b}
      lightweight: ${AI_LIGHT_MODEL:llama3.2:3b}
```

---

## 9. Caching Inteligente

### 9.1 Problema

Cada mensagem do usuário dispara:
1. Embedding da query (chamada HTTP ao Ollama)
2. Busca vetorial no pgvector (query SQL)
3. Chamada ao LLM (tokens gerados)

Muitas operações são repetitivas e cacheáveis:
- Queries similares geram embeddings similares (mas são recalculados)
- O mesmo binário é analisado múltiplas vezes na mesma sessão
- Tools chamadas com os mesmos parâmetros retornam os mesmos resultados

### 9.2 Solução 9.1: Cache de Embedding de Query (LRU)

```java
// QueryEmbeddingCache.java
@Component
public class QueryEmbeddingCache {
    private final Cache<String, float[]> cache;

    public QueryEmbeddingCache(
            @Value("${app.ai.cache.query-embedding-size:100}") int maxSize) {
        this.cache = Caffeine.newBuilder()
            .maximumSize(maxSize)
            .expireAfterWrite(Duration.ofMinutes(30))
            .build();
    }

    public float[] getOrCompute(String query, Function<String, float[]> computer) {
        var normalized = normalizeQuery(query); // lowercase, trim, remove pontuação
        return cache.get(normalized, computer);
    }

    private String normalizeQuery(String query) {
        return query.toLowerCase().trim().replaceAll("[^a-z0-9\\s]", "");
    }
}
```

### 9.3 Solução 9.2: Cache de Resultados de Tool Calls

Durante uma sessão de chat, tool calls com os mesmos parâmetros podem ser cacheadas:

```java
// ToolResultCache.java
@Component
public class ToolResultCache {
    private final Cache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(500)
        .expireAfterWrite(Duration.ofMinutes(10))
        .build();

    public <T> T getOrCompute(String toolName, String params, Supplier<T> computer) {
        var key = toolName + ":" + params;
        return (T) cache.get(key, k -> computer.get());
    }
}
```

### 9.4 Solução 9.3: Cache de Chunks de Embedding (Segundo Nível)

Os embeddings dos chunks (`float[4096]`) são lidos do banco a cada busca. Podem ser cacheados em memória:

```java
// EmbeddingVectorCache.java
@Component
public class EmbeddingVectorCache {
    private final Cache<UUID, List<EmbeddingVector>> cache = Caffeine.newBuilder()
        .maximumWeight(10_000) // máximo 10K vetores
        .weigher((UUID key, List<EmbeddingVector> val) -> val.size())
        .expireAfterWrite(Duration.ofHours(1))
        .build();

    @EventListener
    public void onEmbeddingGenerated(EmbeddingCompletedEvent event) {
        // Invalida cache do binário quando novos embeddings são gerados
        cache.invalidate(event.getBinaryId());
    }
}
```

**Nota**: pgvector com índice HNSW já é rápido (<100ms). Este cache pode não ser necessário a menos que o volume de queries seja muito alto.

### 9.5 Solução 9.4: Cache de Respostas (Semantic Cache)

Para perguntas comuns ou repetidas, cachear a resposta final usando similaridade semântica:

```java
// SemanticResponseCache.java
@Component
public class SemanticResponseCache {
    // Usa o mesmo pgvector para cache de respostas
    // Tabela: response_cache (query_embedding vector(4096), response TEXT)

    public Optional<String> get(String query) {
        var embedding = embeddingService.embed(query);
        var vectorStr = AiUtils.toVectorString(embedding);
        var cached = responseCacheRepository.findSimilar(vectorStr, 0.95, 1);
        // Se similaridade > 0.95 (quase idêntica), retorna a resposta cacheada
        if (!cached.isEmpty()) {
            return Optional.of(cached.getFirst());
        }
        return Optional.empty();
    }

    public void put(String query, String response) {
        var embedding = embeddingService.embed(query);
        var vectorStr = AiUtils.toVectorString(embedding);
        responseCacheRepository.save(query, vectorStr, response);
    }
}
```

---

## 10. Estrutura de Avaliação (Evals)

### 10.1 Problema

Não há medição objetiva da qualidade do pipeline. Mudanças no prompt, chunking ou modelo são feitas "no escuro", sem saber se melhoraram ou pioraram os resultados.

### 10.2 Solução 10.1: Dataset de Avaliação

Criar um dataset de perguntas e respostas esperadas (ground truth) para binários de teste:

```
test/resources/eval/
├── binaries/
│   ├── test-malware.exe          # Binário real de teste
│   ├── test-game.dll             # DLL de jogo
│   └── test-utility              # ELF utilitário Linux
├── datasets/
│   ├── malware-questions.json    # Perguntas sobre malware
│   ├── reverse-eng-questions.json
│   └── factual-questions.json    # Perguntas factuais (metadados)
└── expected/
    ├── malware-answers.json      # Respostas esperadas (avaliadas por humanos)
    └── scoring-rubric.json       # Rubrica de pontuação
```

**Exemplo: `malware-questions.json`**:

```json
[
  {
    "id": "q001",
    "binary": "test-malware.exe",
    "question": "Qual é a função da entry point?",
    "expected_answer": "A entry point (0x402100) implementa a desofuscação de strings...",
    "evaluation_criteria": ["Menciona endereço 0x402100", 
                             "Explica desofuscação de strings",
                             "Identifica chamadas para VirtualAlloc"],
    "difficulty": "medium",
    "category": "function_analysis"
  },
  {
    "id": "q002",
    "binary": "test-malware.exe",
    "question": "Este binário é malware?",
    "expected_answer": "Sim, classificado como RAT...",
    "evaluation_criteria": ["Classifica como malware/RAT", 
                             "Identifica C2 communication",
                             "Menciona técnicas de persistência"],
    "difficulty": "hard",
    "category": "classification"
  }
]
```

### 10.3 Solução 10.2: Métricas de Avaliação

```java
// EvalRunner.java
@Component
public class EvalRunner {
    private final ChatService chatService;
    private final ChatSessionService sessionService;
    private final ObjectMapper objectMapper;

    public EvalReport runEvaluation(String datasetPath) {
        var questions = loadDataset(datasetPath);
        var results = new ArrayList<EvalResult>();

        for (var q : questions) {
            var session = sessionService.createSession(q.binaryId(), null, "gemma4:12b");
            var answer = chatService.chat(session.getId(), q.question())
                .collectList().block().stream()
                .collect(Collectors.joining());

            var score = evaluateAnswer(q, answer);
            results.add(new EvalResult(q.id(), score, answer));
        }

        return new EvalReport(results);
    }

    private EvalScore evaluateAnswer(EvalQuestion q, String answer) {
        // Método 1: LLM-as-Judge (usar outro modelo para avaliar)
        var judgePrompt = buildJudgePrompt(q, answer);
        var judgeResponse = judgeModel.call(judgePrompt);
        var score = parseScore(judgeResponse);

        // Método 2: Métricas automáticas
        var rougeScore = computeRougeL(answer, q.expectedAnswer());
        var containsKeywords = checkKeywords(answer, q.expectedKeywords());

        return new EvalScore(score.llmJudge(), rougeScore, containsKeywords);
    }
}
```

### 10.4 Métricas-Chave

| Métrica | Descrição | Como Medir |
|---|---|---|
| **Precision@K** | Fração de chunks relevantes nos top-K | Julgamento humano dos chunks |
| **Recall@K** | Fração de todos os chunks relevantes recuperados | Dataset com chunks anotados |
| **MRR** (Mean Reciprocal Rank) | Posição média do primeiro chunk relevante | Ranking dos chunks |
| **Answer Accuracy** | Resposta está correta? | LLM-as-Judge + avaliação humana |
| **Answer Completeness** | A resposta cobre todos os aspectos? | Rubrica de critérios |
| **Hallucination Rate** | Fração de afirmações incorretas | Verificação manual |
| **Latency P50/P95** | Tempo até primeira resposta / resposta completa | Métricas de aplicação |
| **Tool Call Precision** | Fração de tool calls que foram úteis | Análise de logs |

### 10.5 CI/CD para Avaliação

```yaml
# .github/workflows/ai-eval.yml
name: AI Pipeline Evaluation
on:
  pull_request:
    paths:
      - 'src/main/java/ai/decompile/ai/**'
      - 'src/main/resources/prompts/**'
jobs:
  eval:
    runs-on: self-hosted  # precisa de GPU para Ollama
    steps:
      - name: Run RAG evaluation
        run: ./mvnw test -Dtest=EvalRunnerTest
      - name: Check regression threshold
        run: |
          if [ $(cat eval-report.json | jq '.overall_score') -lt 0.80 ]; then
            echo "Eval score below threshold!"
            exit 1
          fi
```

---

## 11. Segurança e Robustez

### 11.1 Problemas Potenciais

1. **Prompt Injection**: usuário pode injetar instruções maliciosas via mensagens de chat
2. **Tool Injection**: usuário pode tentar forçar tool calls via mensagens (ex: "call getCallers with functionId='malicious'")
3. **Data Leakage**: informações de outros binários podem vazar via ferramentas mal configuradas
4. **DoS via Tool Loops**: LLM pode entrar em loop chamando tools infinitamente
5. **Output malicioso**: LLM pode gerar código malicioso ou instruções de exploração

### 11.2 Mitigações

```java
// InputSanitizer.java
@Component
public class InputSanitizer {
    private static final Pattern PROMPT_INJECTION_PATTERN = Pattern.compile(
        "(ignore|forget|override|disregard)\\s+(all|previous|above|system|instructions)",
        Pattern.CASE_INSENSITIVE);
    private static final int MAX_MESSAGE_LENGTH = 10000;

    public String sanitize(String userMessage) {
        // 1. Limitar comprimento
        if (userMessage.length() > MAX_MESSAGE_LENGTH) {
            throw new IllegalArgumentException("Message too long");
        }

        // 2. Detectar padrões de prompt injection
        if (PROMPT_INJECTION_PATTERN.matcher(userMessage).find()) {
            log.warn("Potential prompt injection detected in message");
            // Não rejeitar, mas adicionar warning no log para monitoramento
        }

        return userMessage.trim();
    }
}

// ToolExecutionGuard.java
@Component
public class ToolExecutionGuard {
    private final Map<UUID, AtomicInteger> toolCallCounts = new ConcurrentHashMap<>();
    private static final int MAX_TOOL_CALLS_PER_MESSAGE = 10;
    private static final int MAX_TOOL_CALLS_PER_SESSION = 50;
    private static final Duration TOOL_CALL_TIMEOUT = Duration.ofSeconds(5);

    public <T> T executeGuarded(UUID sessionId, String toolName, Supplier<T> execution) {
        // 1. Limitar tool calls por sessão
        var sessionCount = toolCallCounts.computeIfAbsent(
            sessionId, k -> new AtomicInteger(0));
        if (sessionCount.incrementAndGet() > MAX_TOOL_CALLS_PER_SESSION) {
            throw new ToolExecutionException("Tool call limit exceeded for session");
        }

        // 2. Timeout por tool call
        try {
            return CompletableFuture.supplyAsync(execution)
                .get(TOOL_CALL_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            throw new ToolExecutionException("Tool '" + toolName + "' timed out");
        }
    }
}

// Data isolation: todas as queries de tool incluem binaryId
@Tool(description = "...")
public List<FunctionSummary> searchFunctions(
        @ToolParam(description = "Function name or address")
        String nameOrAddress,
        @ToolParam(description = "Binary ID (REQUIRED for data isolation)")
        String binaryId) {
    // binaryId SEMPRE validado; nunca buscar cross-binary
}
```

---

## 12. Observabilidade e Monitoramento

### 12.1 Métricas

```java
// AiMetrics.java
@Component
public class AiMetrics {
    private final MeterRegistry meterRegistry;

    // Contadores
    private final Counter chatRequests;
    private final Counter toolCalls;
    private final Counter toolCallFailures;
    private final Counter embeddingsGenerated;

    // Temporizadores
    private final Timer queryEmbeddingTime;
    private final Timer vectorSearchTime;
    private final Timer chatResponseTime;
    private final Timer toolExecutionTime;

    // Histogramas
    private final DistributionSummary contextTokensUsed;
    private final DistributionSummary chunksRetrieved;
    private final DistributionSummary toolCallsPerMessage;
}
```

### 12.2 Logs Estruturados

```java
// Log informativo de cada interação
log.info("chat_request session={} binary={} model={} query_length={}",
    sessionId, binaryId, model, query.length());

// Log de cada tool call
log.info("tool_call session={} tool={} params={} duration_ms={} result_size={}",
    sessionId, toolName, params, duration, resultSize);

// Log de métricas de retrieval
log.info("rag_retrieval binary={} semantic_results={} text_results={} callgraph_functions={} total_tokens={}",
    binaryId, semanticCount, textCount, callGraphCount, totalTokens);
```

### 12.3 Tracing Distribuído

Integrar com Micrometer Tracing (já configurado via ObservationRegistry):

```java
@Bean
ObservationRegistry observationRegistry() {
    return ObservationRegistry.create();
}

// No ChatService:
var observation = Observation.createNotStarted("chat.send_message", registry)
    .lowCardinalityKeyValue("model", session.getChatModel())
    .highCardinalityKeyValue("session_id", sessionId.toString());

observation.observe(() -> {
    // streaming e tool calls
});
```

### 12.4 Dashboard de Qualidade

Métricas para dashboard (Grafana / Prometheus):

| Painel | Métricas |
|---|---|
| **Latência** | P50/P95/P99 de chat response time, embedding time, vector search time |
| **Qualidade** | Hallucination rate, answer accuracy (via evals periódicos) |
| **Tool Usage** | Tool calls por mensagem, tool call success rate, top tools usadas |
| **Custos** | Tokens consumidos por modelo, custo estimado (cloud models) |
| **Erros** | Taxa de erro do Ollama, timeout de tools, rate limiting |

---

## 13. Roteiro de Implementação

### Fase 1 — Fundação (4-6 semanas)
**Objetivo**: Tool calling básico + melhorias no retrieval

| Semana | Tarefa | Impacto |
|---|---|---|
| 1-2 | Implementar `AnalysisTools` com 6 tools principais | **Alto**: LLM ganha capacidade de explorar binários |
| 2-3 | Integrar `ToolCallingManager` no `AiConfig` e adaptar `ChatService` | **Alto**: pipeline de tool calling funcional |
| 3-4 | Query rewriting com regras de domínio + expansão de sinônimos | **Médio**: melhora recall em ~15% |
| 4-5 | Re-ranking heurístico no `ChatContextBuilder` | **Médio**: melhora precisão do top-10 |
| 5-6 | Testes, ajustes de prompt, documentação | — |

### Fase 2 — Aprimoramento (6-10 semanas)
**Objetivo**: Melhorias de qualidade e robustez

| Semana | Tarefa | Impacto |
|---|---|---|
| 6-7 | Chunking hierárquico (BASIC_BLOCK) + sliding window | **Médio**: melhor granularidade |
| 7-8 | Hybrid search (pgvector + PostgreSQL full-text) | **Alto**: combina precisão lexical com recall semântico |
| 8-9 | Sistema de templates de prompt externos | **Médio**: facilita iteração |
| 9-10 | Token budget management com tokenizer preciso | **Alto**: evita estouro de contexto |

### Fase 3 — Avançado (10-16 semanas)
**Objetivo**: Agentes, avaliação, multi-provider

| Semana | Tarefa | Impacto |
|---|---|---|
| 10-12 | Model router com circuit breaker e health checks | **Médio**: resiliência |
| 12-14 | Estrutura de avaliação (evals) + dataset | **Alto**: permite medir progresso |
| 14-16 | Caching (query embedding, tool results) | **Médio**: reduz latência |

### Fase 4 — Futuro (16+ semanas)
**Objetivo**: Agentes autônomos, fine-tuning, multi-modal

| Tarefa | Impacto |
|---|---|
| ReAct Agent para tarefas multi-step | **Alto**: análise autônoma |
| Fine-tune do modelo de embedding com dados de assembly | **Alto**: recall significativamente melhor |
| Suporte a imagens (CFG graphs, entropy plots) via modelo multi-modal | **Baixo-Médio**: melhora UX |
| Agentes colaborativos (multi-agent) | **Alto**: cobertura abrangente |

---

## Referências

- [Spring AI 1.0.1 Documentation — Tool Calling](https://docs.spring.io/spring-ai/reference/api/tools.html)
- [Ollama Tool Support](https://ollama.com/blog/tool-support)
- [pgvector Documentation](https://github.com/pgvector/pgvector)
- [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629)
- [RAGAS: Evaluation framework for RAG](https://docs.ragas.io/)
- [JTokkit: Java Tokenizer](https://github.com/knuddelsgmbh/jtokkit)

---

> **Nota**: Este documento é um plano vivo. Cada fase deve ser reavaliada após a conclusão da anterior, com métricas de avaliação guiando as prioridades.
