HyDE é opt-in (
CHATCLI_QUALITY_HYDE_ENABLED=true) para manter o steady-state sem custo adicional. Phase 3a custa +1 LLM call cheap; Phase 3b requer configurar um embedding provider.O problema que HyDE resolve
O retrieval dememory.Fact pré-pipeline era keyword-only: o scorer bate tokens extraídos de mensagens recentes contra tags e content dos facts armazenados. Funciona bem quando o vocabulário bate exatamente — falha quando o usuário usa sinônimos ou faz perguntas abstratas.
Exemplo do gap:
- Sem HyDE
- Com HyDE 3a
- Com HyDE 3b
Usuário:
Keywords extraídas:
Fact armazenado:
Match: ❌ — “fazer” e “go” não aparecem literalmente no fact.
como fazer X em Go?Keywords extraídas:
[fazer, go]Fact armazenado:
"use goroutines for concurrency in X pipelines"Match: ❌ — “fazer” e “go” não aparecem literalmente no fact.
Phase 3a — Hypothesis-based keyword expansion
LLM gera hipótese curta
Prompt: “Write a 2-4 sentence plausible answer that uses the technical nouns that would appear in any matching note. Bilingual if the query mixes languages.”
ExtractKeywords da hipótese
O mesmo extractor já usado no chat mode (stop words en+pt, min 3 chars).
Merge unique + lower-case
Keywords originais + top-N da hipótese, cap configurável via
CHATCLI_QUALITY_HYDE_NUM_KEYWORDS (default 5).Phase 3b — Vector embeddings + ranking fundido
Adiciona busca por cosine similarity sobre embeddings de facts — e, desde a refatoração de ranking, funde o score de cosseno com os sinais lexical e temporal num único ranking.O que mudou (PR #1027): antes, os hits vetoriais eram dissolvidos de volta em keywords e o score de cosseno era descartado — pagava-se uma chamada de embedding e jogava-se fora justamente o sinal semântico. Agora o cosseno flui direto para o ranker fundido
SearchBlended, então um fato achado só por paráfrase (zero overlap lexical) consegue rankear.Arquitetura
Ranking fundido (SearchBlended)
Três sinais independentes e complementares, cada um normalizado min-max sobre o conjunto de candidatos antes da soma ponderada:
| Sinal | Origem | Captura |
|---|---|---|
| semantic | cosseno do vector store | sinonímia, paráfrase |
| lexical | overlap de keyword/tag | termos exatos, identificadores, nomes de arquivo |
| temporal | decaimento por recência × frequência de acesso | o que o usuário realmente usa |
DefaultRankWeights): semantic 0.55 · lexical 0.30 · temporal 0.15 — semantic-first, porque a chamada de embedding já foi paga. A fusão é aditiva (não multiplicativa) de propósito: um fato com cosseno alto e zero keyword ainda rankeia — um produto o zeraria. A normalização min-max é o que torna os pesos provider-agnostic: cosseno Voyage 1024-d e OpenAI 1536-d caem ambos em [0,1] após normalizar.
Providers suportados
- Voyage (recomendado)
- OpenAI
- Bedrock (Titan + Cohere)
- Null (default)
voyage-3, 1024-dim) é o sweet spot geral.Vector store pure-Go — primitivo genérico vindex
Sem CGO, sem SQLite-vec, sem dependências externas. Só
float32[] + cosseno + persistência JSON em ~/.chatcli/memory/vector_index.json.llm/embedding/vindex, extraído quando um segundo consumidor (o retrieval semântico de /context) apareceu. O memory é só um adapter fino sobre ele — sem máquina vetorial duplicada por pacote:
k, não com o tamanho do corpus — o teto de escala deixa de ser “centenas de facts”. Para o caso típico do chatcli a busca linear completa em microssegundos; sem HNSW ou IVFFlat.
Auto-migração de provider/dimensão
Trocar de provider ou dimensão (Voyage 1024 → OpenAI 1536, ou voyage→cohere no mesmo 1024) não exige maisrm manual. No load, o índice detecta o mismatch — de dimensão (cosseno entre arities diferentes é indefinido) ou de provider (dois espaços de embedding distintos não são comparáveis, mesmo na mesma dimensão) — e auto-limpa o cache, removendo o arquivo para o backfill repopular:
Lazy backfill
Ao retrieve uma fact, se ela não tem vetor (fact pré-existe à ativação de embeddings), o index spawna goroutine detached para embedar as top-500 facts visíveis:Avaliação — provando o retrieval (não supondo)
Antes não havia como medir se o retrieval era bom. Agora há um harness de avaliação dependency-free emcli/workspace/memory/eval — métricas padrão de IR macro-averaged:
| Métrica | O que mede |
|---|---|
| recall@k | fração dos facts relevantes recuperados no top-k |
| precision@k | fração do top-k que era relevante |
| MRR | rank recíproco médio do primeiro acerto |
| nDCG@k | ganho cumulativo descontado normalizado |
ranking_test.go, com um provider de embedding determinístico) compara keyword-only vs. ranking fundido sobre queries que usam sinônimos ausentes do texto dos facts:
O harness roda igual em CI (provider determinístico, reproduzível) ou contra um backend real — ele nunca importa um provider, então fica neutro entre os 14 suportados. É o que transforma “parece bom” em “é bom, medido”, e guarda contra regressões.
Tunables de ranking (sem novos env vars)
Os parâmetros do ranking fundido vivem como campos deConfig com defaults fortes — não foram criados novos env vars (e os CHATCLI_MEMORY_* que já existiam, antes ignorados no caminho estruturado, agora são aplicados via ConfigFromEnv + clamp):
Campo Config | Default | O que controla |
|---|---|---|
RankWeights | {0.55, 0.30, 0.15} | pesos semantic / lexical / temporal |
MinCosineScore | 0.25 | floor de cosseno no top-K |
VectorTopK | 12 | candidatos vetoriais por query |
BackfillBatchMax | 500 | teto de facts embeddados por retrieve |
Configuração completa
| Env var | Default | O que faz |
|---|---|---|
CHATCLI_QUALITY_HYDE_ENABLED | false | Master switch (phase 3a) |
CHATCLI_QUALITY_HYDE_USE_VECTORS | false | Liga phase 3b (requer provider) |
CHATCLI_QUALITY_HYDE_NUM_KEYWORDS | 5 | Cap de keywords da hipótese em phase 3a |
CHATCLI_EMBED_PROVIDER | — | voyage / openai / bedrock / null — única fonte de verdade para selecionar o provider |
CHATCLI_EMBED_MODEL | provider default | Voyage: voyage-3. OpenAI: text-embedding-3-small / -large. Bedrock: amazon.titan-embed-text-v2:0 (default), amazon.titan-embed-text-v1, cohere.embed-english-v3, cohere.embed-multilingual-v3. |
CHATCLI_EMBED_DIMENSIONS | nativa do modelo | OpenAI: trunca via Matryoshka. Bedrock Titan v2: 256 / 512 / 1024 (rejeita outros). Bedrock Titan v1 / Cohere v3: dimensão fixa, ignorada. |
BEDROCK_REGION / AWS_REGION | us-east-1 | Região AWS — só usado quando CHATCLI_EMBED_PROVIDER=bedrock. |
AWS_PROFILE | — | Profile AWS — só usado quando CHATCLI_EMBED_PROVIDER=bedrock. |
/config quality expõe o estado
Integração com Reflexion
HyDE amplifica o valor de Reflexion: as lições persistidas pela #3 são recuperadas com muito mais recall quando a próxima tarefa não usa exatamente as mesmas keywords. Workflow:Turn 1: refactor auth.go falha (timeout)
Reflexion persiste lesson:
"use Edit tool for large files", tags [go, refactor, edit-tool].Turn 5 (dias depois): 'me ajuda a dividir pkg/engine'
Query não contém
refactor ou edit. Keyword-only perderia a lesson.HyDE 3a gera hipótese
"To split a Go package, identify logical groupings and use refactor patterns with Edit tool for surgical changes..."Keywords extraídas:
[split, package, refactor, edit, patterns, …]Caveats e tuning
Fallback gracioso: se o LLM fail ou o provider embedding retornar erro, o retrieval cai para keyword-only silenciosamente. Nenhum turn é abortado por falha de HyDE.
Leia também
#3 Reflexion
As lições que HyDE recupera com mais recall.
Bootstrap Memory
A camada embaixo: como memory.Fact é populada e mantida.
Persistent Context
/context attach para contextos explícitos de arquivos.Configuração completa
Todos os env e slashes.