Autores: @hi_im_isaac_, @learnwdaniel, @gaozenghao
nota: a versão interativa do blog técnico completo está disponível em: https://www.cerebras.ai/blog/how-we-built-our-knowledge-base
Os funcionários fazem mais de 15.000 perguntas por dia à nossa base de conhecimento interna. Ela se tornou uma das ferramentas internas mais adotadas na empresa desde seu lançamento, há 3 meses. Usada por humanos, automações e agentes.
Na Cerebras, nossas equipes trabalham em operações de data center, design de chips, hardware, treinamento, inferência, plataforma de nuvem e muito mais. Com centenas de novos funcionários entrando a cada ano, nossos canais de comunicação estavam sendo preenchidos com as mesmas perguntas:
- “Onde encontro X?”
- “Quem é o especialista em Y?”
- “O que é Z?”

Construímos o Cerebras Knowledge para ajudar pessoas a conectar pessoas e sistemas a informações úteis.
Encontrando os dados onde eles estão
Encontrar informações dentro de uma organização é difícil. Os dados estão espalhados por ferramentas, e a cada trimestre alguém propõe a mesma solução brilhante: vamos gravar tudo em uma única plataforma, para que todas as informações estejam em um só lugar. O sonho de uma fonte única da verdade, claro, raramente funciona na prática.
As informações são geradas onde é conveniente e ergonômico: edições sugeridas em um documento, threads no Slack, referências de código no GitHub e metadados de status no Jira. Essas plataformas são feitas sob medida para seus domínios específicos, otimizadas por anos de engenharia de produto e análise. Discutir um pull request no Google Docs seria uma experiência terrível.
Então, nos propusemos a projetar um sistema que exigisse uma mudança mínima no comportamento existente. Do lado da coleta de dados, isso significava extrair dados de cada plataforma diretamente.
Anatomia de uma base de conhecimento
Nossa base de conhecimento oferece três coisas:
- Uma plataforma para coletar e armazenar dados internos.
- Uma plataforma para consultar esses dados.
- Uma camada que impõe autenticação e autorização, com auditoria e análise.
No centro, há uma única tabela Postgres que contém embeddings, resumos brutos e metadados de várias fontes. O sistema ingere continuamente dados de toda a empresa e mantém um armazenamento de dados pronto para consulta.
Queríamos uma interface de dados simples, mas que funcionasse com a maioria dos formatos de dados. Também queríamos que outros desenvolvedores na Cerebras pudessem criar conectores personalizados. O resultado é deliberadamente simples: cada fonte, desde threads do Slack até netlists, chega na mesma tabela de embeddings, e qualquer coisa nessa tabela é imediatamente consultável pela mesma interface:

Cada fonte de dados define o que são os dados, como conectar-se a eles e com que frequência devem ser buscados. Cada linha de embedding resultante segue a mesma interface, independentemente de ter vindo do Slack, de um repositório de código, de um sistema de documentos ou de um banco de dados personalizado.
Como processamos conversas não estruturadas do Slack
O Slack foi a fonte de dados mais importante que precisávamos projetar. É onde as discussões de engenharia mais atualizadas acontecem em toda a empresa.

Inicialmente testamos se embeddings simples sobre texto bruto teriam um desempenho suficiente. Rapidamente percebemos que a busca vetorial sozinha era insuficiente para encontrar todos os dados relevantes.
As mensagens do Slack apresentam vários desafios:
- A densidade de informação varia enormemente: "ei, sim, claro, Mike" e uma explicação detalhada de kernel são ambas mensagens.
- O comprimento das mensagens varia, e mensagens mais curtas frequentemente vencem mensagens mais longas e detalhadas na similaridade de cosseno.
- O significado de uma mensagem muitas vezes depende da conversa ao redor.
Precisávamos de uma abordagem híbrida. Construímos a ingestão do Slack para que cada thread seja recuperável por várias técnicas de busca ao mesmo tempo, onde cada técnica compensa as fraquezas das outras:
- A busca por texto completo captura os tokens exatos que os embeddings tendem a borrar: strings de erro, nomes de flags, nomes de host. Quando um engenheiro cola uma mensagem de erro literal, uma correspondência lexical exata é quase sempre a melhor evidência, e nenhuma similaridade semântica deve superá-la.
- A busca por embeddings captura paráfrases. A pessoa que pergunta "restauração trava após carregar manifesto" e a pessoa que responde "checkpoint trava no mount NFS" podem nunca compartilhar vocabulário. A similaridade vetorial é o que conecta uma pergunta a uma resposta escrita em palavras diferentes.(1)
- A frequência inversa de documentos separa sinal de ruído. Uma mensagem curta construída em torno de tokens raros, como uma flag de configuração obscura, merece ser ranqueada. "Parece bom, obrigado!" fica próximo de muitas consultas no espaço de embeddings, mas pontua perto de zero quando a raridade dos termos é levada em conta.
- A decadência temporal codifica que as respostas do Slack expiram. Duas threads podem responder à mesma pergunta, e a de seis meses atrás pode descrever uma infraestrutura que não existe mais. Quando a relevância é igual, a thread mais nova vence.

Nenhum pontuador individual é confiável por si só. Cada técnica produz sua própria visão ranqueada do mesmo corpus, e essas visões são fundidas no momento da consulta (veja Reclassificação).
Modo Socket
Para coletar dados em tempo real, instalamos um bot do Slack em nosso workspace e o executamos no Modo Socket. O Slack envia cada evento de mensagem para nós por meio de um WebSocket persistente, então recebemos atualizações em tempo real sem precisar consultar a Web API e queimar seus limites de taxa.
Quando um evento chega, imediatamente o reconhecemos, deduplicamos usando o ID de evento estável e marcamos a mensagem para o consumidor de ingestão.
O consumidor de ingestão não salva uma nova mensagem isoladamente. Ele resolve a thread à qual a mensagem pertence e busca novamente toda a conversa, incluindo o pai e todas as respostas, da API do Slack. Em seguida, escreve toda a thread de volta como uma única linha. Uma resposta a uma thread existente, portanto, puxa novamente o pai e todos os irmãos, de modo que o conteúdo armazenado, a lista de participantes e o timestamp da última atividade sempre reflitam a conversa completa.
Cada canal do Slack em nosso sistema tem sua própria fonte de dados. Isso fornece ajuste fino para a atualização dos dados. Uma equipe pode optar por ingerir um canal de incidentes movimentado com mais frequência, por exemplo.
Threads e mensagens
O texto bruto do Slack é pesquisável por palavras-chave assim que chega, porque mantemos um índice de texto completo (GIN) do Postgres sobre o conteúdo bruto. Para permitir uma busca vetorial útil, no entanto, fazemos algum processamento adicional.(8)
Durante a destilação, um LLM extrai dados estruturados da thread completa:
- Uma pergunta de uma linha que um engenheiro realmente pesquisaria.
- Um resumo curto.
- A resolução.
- Os sistemas e referências de código mencionados.

Incorporamos esses pontos de dados e os escrevemos na tabela compartilhada de embeddings. O transcript original não é incorporado diretamente. Em nossos experimentos, a precisão aumentou significativamente quando a thread foi normalizada em um formato consistente.(7,9) Os metadados adicionais também fornecem mais sinal útil para a correspondência semântica.
Bursting
Nesse ponto, a busca no Slack estava boa, mas continuávamos encontrando o mesmo problema: mensagens importantes dentro de threads longas nem sempre eram representadas no resumo no nível da thread.
Para aumentar o sinal de mensagens individuais, usamos o bursting. Um burst é uma sequência de mensagens consecutivas do mesmo autor. Incorporamos bursts individuais com o tópico da thread anexado como contexto(2) porque, às vezes, a resposta está em uma mensagem tangente cujo vocabulário nunca aparece no resumo da thread. Os embeddings de burst tornam essa mensagem encontrável por si só.
Para evitar que dados de baixo sinal cheguem ao banco de dados, cada burst é pontuado contra uma combinação ponderada de sinais e deve ultrapassar um limiar antes de ser incorporado:
- Contém um token relativamente raro no corpus, com IDF de pelo menos 4,0.
- O burst combinado tem pelo menos 200 caracteres.
- Uma ou mais mensagens no burst contêm reações, fornecendo um impulso social.

Após a destilação, os bursts qualificados são incorporados e armazenados na tabela de embeddings junto com o registro no nível da thread.
Repositórios de código
Inicialmente debatemos se era necessário incorporar repositórios de código. Com o aumento do Claude Code e outras ferramentas de linha de comando, criar embeddings de código parecia contraintuitivo quando parecia que "grep é tudo que você precisa". Depois de conversar com outros no setor e ler as descobertas da Cursor sobre busca semântica em grandes bases de código, decidimos tentar.
Temos muitos repositórios internos, alguns com mais de 40 GB. Nossa principal preocupação era como mantê-los atualizados de forma eficiente.
Usando @cocoindex_io para manter embeddings de código
Após vários experimentos, optamos pelo CocoIndex, um framework de código aberto para incorporação de documentos especializado em vetorizar bases de código.
Para cada repositório, dividimos o código usando limites de regex específicos da linguagem, ordenados do mais grosseiro ao mais fino. O divisor tenta primeiro limites de nível mais alto, como classes. Se um pedaço resultante ainda for muito grande, ele recorre a limites de método e, em seguida, blocos menores. Incorporamos os pedaços resultantes e escrevemos os vetores no Postgres. Um único arquivo pode gerar vários embeddings em diferentes níveis de especificidade, como registros em nível de arquivo e em nível de função.

O CocoIndex rastreia metadados de sincronização no Postgres. Em cada commit, ele reincorpora e reexporta apenas os pedaços de código alterados, em vez de recomputar todo o repositório. Isso funcionou especialmente bem para nós, porque o estado de sincronização e o armazenamento de embeddings estão no mesmo banco de dados.
À medida que o número de bases de código crescia, movemos a integração de repositórios para arquivos de configuração que as próprias equipes podem enviar, incluindo listas de permissão e bloqueio no nível do caminho do arquivo.
Fontes de dados personalizadas
Algumas equipes já tinham seus próprios bancos de dados e não queriam mover dados para o Slack ou um sistema de documentos apenas para participar da base de conhecimento. Elas queriam a mesma superfície de consulta sobre suas tabelas existentes.
Para suportar isso, tratamos as fontes personalizadas como scripts de plugin. Uma equipe abre um pull request com um pequeno módulo Python que sabe ler de seu sistema e emitir linhas no formato de nossa tabela de embeddings, além de uma entrada de fonte de dados correspondente.
Contanto que o script escreva no banco de dados compartilhado usando o mesmo esquema de qualquer outra linha de embedding, o restante da pilha funciona inalterado. Os dados se tornam consultáveis juntamente com Slack, código e documentos, sem tratamento especial em nenhum outro lugar do sistema.
Planejamento e distribuição de ferramentas
Para cada consulta, primeiro executamos uma passagem curta de planejamento onde um LLM decide quais ferramentas e fontes de dados provavelmente são relevantes. As principais ferramentas:
- subsystem_index: resumos de LLM por arquivo.
- search: o pipeline vetorial unificado em Slack, wiki, código e outras fontes indexadas, mesclado e reclassificado internamente.
- search_slack: recuperação direta do Slack.
- search_code: ripgrep sobre repositórios de origem.
- recent_prs: pull requests recentes relevantes para a pergunta.
- who_knows: pessoas com experiência demonstrada em um tópico.
O planejador trabalha com uma descrição compacta do que indexamos: quais projetos existem, quais fontes estão disponíveis em cada projeto e para que cada fonte é boa em responder. Dada a consulta do usuário e o escopo ativo, ele emite seleções de ferramentas que o executor distribui em paralelo, normaliza em um formato de evidência comum e passa para um LLM de síntese final.(4)

Reclassificação
Um documento pode aparecer no topo simplesmente por compartilhar vocabulário com a consulta enquanto responde a uma pergunta diferente. Antes da reclassificação, combinamos as listas de resultados incompatíveis dos recuperadores com a fusão de rank recíproco, ou RRF. Para cada documento, adicionamos peso / (60 + rank) para cada lista em que aparece, com um peso padrão de 1,0 e uma constante de suavização de 60.

A constante de suavização faz com que o consenso importe mais do que um único voto forte: um documento que aparece perto do topo em vários recuperadores pode vencer um que fica em primeiro em apenas um deles. Em seguida, mesclamos pedaços duplicados de volta a uma fonte, limitamos quantos resultados cada arquivo pode contribuir e terminamos com um top vinte mais diversificado.
Enviamos a consulta original e esses candidatos para um modelo de reclassificação pequeno. Ele dá a cada documento uma pontuação de zero a dez, e mantemos os dez primeiros.(6)
Depois que o ranking é finalizado, adicionamos contexto de volta aos vencedores. Por exemplo, se correspondermos a uma seção da wiki, puxamos as duas seções vizinhas para que o título, pré-condições e ressalvas que a divisão em pedaços separou não sejam perdidos. Isso dá aos leitores um snippet completo em vez de um parágrafo solitário que está faltando contexto importante.
Portanto, a saída da busca é um pacote rico de evidências: resultados fundidos de diferentes recuperadores, deduplicados no nível da fonte, reclassificados contra a pergunta real e só então expandidos com o contexto ao redor.
MCP
Na integração MCP, expomos blocos de construção de recuperação como ferramentas diretas, em vez de escondê-los atrás de um endpoint "responda a esta pergunta". Essas ferramentas são intencionalmente simples e tão livres de LLM quanto possível, para que os clientes possam consultá-las de forma rápida e barata.(5)
Cada ferramenta MCP corresponde a um primitivo de recuperação subjacente, como search_slack, search_code, search ou who_knows. As entradas e saídas das ferramentas são estreitas, estruturadas e estáveis, facilitando a chamada de qualquer cliente ou agente sem incorporar lógica de orquestração adicional dentro da própria ferramenta.
A maioria das ferramentas executa um pipeline de consulta, como busca vetorial, busca lexical ou ripgrep, aplica heurísticas de pontuação leves e retorna linhas de evidência brutas.
O Claude Code, ou qualquer agente compatível com MCP, torna-se o motor de orquestração. Ele decide quais ferramentas chamar, em que ordem e como montar os resultados em uma resposta final ou edição de código. A própria camada de recuperação não depende dessas decisões do LLM para atender às solicitações.
Interface Web
Na interface web, as mesmas ferramentas existem, mas estão conectadas a um pipeline de consulta completo que é executado de ponta a ponta para cada pergunta do usuário. O agente da interface é responsável pelas etapas de planejador e executor.
- Planejador: Uma passagem leve de LLM inspeciona a consulta e o projeto ativo, depois escolhe quais ferramentas de recuperação invocar, como search, search_slack e subsystem_index.
- Executor: O sistema distribui essas chamadas de ferramenta em paralelo, coleta os resultados e os normaliza em um esquema de evidência compartilhado com pontuações, atualidade e dicas de fonte.
- Síntese: Uma passagem final de LLM pega o pacote de evidências tipadas e a pergunta original, depois produz a resposta mostrada na interface, incluindo citações, ressalvas e síntese entre fontes.
Da perspectiva do usuário, a interface web é simplesmente "faça uma pergunta e obtenha uma resposta". Por baixo dos panos, ela executa o mesmo padrão planejador → executor → sintetizador que os clientes MCP podem recriar explicitamente.

Organização
À medida que o corpus crescia, "buscar tudo em todos os lugares" rapidamente deixou de ser útil. Engenheiros de equipes de compilador não queriam runbooks de infraestrutura em seus resultados, e vice-versa. Projetos são como tornamos a busca relevante por padrão.
Projetos e busca com escopo
Introduzimos projetos como a principal forma de organizar o espaço de trabalho sobre o qual uma consulta é executada. Um projeto é um pacote nomeado de fontes de dados: canais específicos do Slack, repositórios de código, bancos de dados internos e espaços de documentos relevantes para uma equipe ou iniciativa.
Os projetos são intencionalmente leves. A mesma fonte de dados, como um canal de incidentes compartilhado ou um repositório de plataforma central, pode ser referenciada por vários projetos em vez de ser duplicada.

Integração e padrões
Durante a integração, os usuários são solicitados a selecionar ou criar um projeto padrão que corresponda à forma como trabalham, como Infraestrutura de Treinamento ML, Compilador ou Operações de Data Center.
Esse projeto padrão é armazenado no perfil do usuário e define o escopo das consultas automaticamente. Um novo engenheiro obtém respostas de alto sinal sem primeiro precisar aprender quais canais do Slack, repositórios ou espaços de documento são importantes.
Considerações Finais
No final, a base de conhecimento funciona porque encontra as pessoas onde a informação já está, em vez de forçar tudo em um sistema rígido. Ao combinar várias técnicas de busca, podemos trazer evidências rapidamente. O resultado é uma experiência de busca que permanece flexível o suficiente para dados reais de empresa, mas estruturada o suficiente para permanecer útil à medida que a Cerebras continua crescendo.
Se você leu até aqui e achou interessante, o time de ai/growth está contratando – entre em contato se tiver interesse @learnwdaniel
Referências
- Malkov e Yashunin, Busca Aproximada de Vizinhos Mais Próximos Eficiente e Robusta Usando Grafos de Pequeno Mundo Navegáveis Hierárquicos, arXiv:1603.09320 / IEEE TPAMI 2018.
- Anthropic, Apresentando a Recuperação Contextual, 2024.
- Cormack, Clarke e Büttcher, [*Fusão de Classificação Recíproca Supera Métodos de Aprendizado de Classificação de Condorcet e Individuais





