O seu assistente de IA não precisa de uma cópia do seu banco de dados

Engineering

O discurso padrão de um bot de suporte com IA é assim: conecte as suas fontes de dados, nós vamos indexá-las, e o seu bot vai responder com base nelas.

Leia de novo com olhos de engenheiro. Conecte as suas fontes de dados significa construir uma exportação. Nós vamos indexá-las significa que os registros dos seus clientes agora também ficam no sistema de um fornecedor. E “responder com base nelas” significa responder com o que a última sincronização conseguiu copiar.

Para documentação, essa troca é aceitável: os artigos mudam toda semana, e um índice é um formato razoável para eles. Para dados que mudam em tempo real, é uma troca ruim. Preços mudam. O estoque muda. O status do pedido muda enquanto o cliente digita. Um índice é uma fotografia de algo que se move.

A outra opção

Existe uma ideia bem mais antiga à mão, e ela não envolve levar os seus dados para lugar nenhum: dê uma função ao modelo e deixe que ele a chame.

A chamada de ferramentas (tool calling) é exatamente isso desde que surgiu. O incomum é onde a função roda. Na maioria das implementações, ela roda nos servidores do fornecedor, e você volta a entregar credenciais, liberar os IPs deles e construir uma integração.

No Kookee, ela roda no navegador do visitante.

JavaScript
KookeeChat.init({
  apiKey: 'YOUR_API_KEY',
  tools: [{
    name: 'search_orders',
    description: "Look up the signed-in customer's orders.",
    parameters: { type: 'object', properties: { status: { type: 'string' } } },
    handler: ({ status }) =>
      fetch('/api/my/orders?status=' + status, { credentials: 'include' })
        .then((r) => r.json()),
  }],
});

Veja o que é esse handler. É um fetch para a sua API, com credentials: 'include', rodando na sua página, como o visitante que já está logado.

Daí vêm três consequências, e são elas que importam:

Você não construiu nenhuma integração. Nenhuma chave de API trocada, nenhuma allowlist de IPs, nenhum webhook, nenhum app OAuth. O endpoint que você chama é um que o seu frontend já chama.

O Kookee nunca guarda os seus dados. O nome, a descrição e o schema de parâmetros da ferramenta chegam ao Kookee: o suficiente para o modelo saber que a ferramenta existe e o que passar para ela. O handler roda no navegador; o resultado passa pelo Kookee até o modelo, para aquela resposta, e não é armazenado. A sua tabela de pedidos não está no banco de dados do Kookee, porque eu nunca pedi essa tabela.

A autenticação já está resolvida. A pergunta mais difícil em “IA que vê dados de clientes” é qual cliente. Aqui isso nem chega a ser uma pergunta. A requisição leva o cookie de sessão do próprio visitante, então devolve os pedidos dele e de mais ninguém, com a regra aplicada pelo mesmo código de autorização que protege o resto do seu app.

Como isso aparece para o visitante

Visitante: cadê o meu último pedido?

Verificando os seus pedidos…

Assistente: O seu pedido #4821 foi enviado em 21 de agosto e deve chegar na terça. Rastreio: DHL 0034…

A pausa no meio é a parte interessante. O turno de fato para: o modelo pede uma ferramenta, a resposta para, o navegador roda a sua função, o resultado volta e a resposta continua. Para o visitante, é uma resposta contínua com uma linha de status.

Como acabei construindo assim

A minha primeira abordagem devolvia o histórico da conversa ao navegador entre as duas metades e deixava o cliente mandá-lo de volta. Sem estado, sem memória no servidor, elegante no quadro branco.

Dois revisores derrubaram a ideia, por motivos que vale repetir:

Vazava. A busca de artigos de ajuda do próprio Kookee roda no servidor e devolve o texto completo dos artigos, inclusive os chatbot_only, que ficam fora da central de ajuda pública. Passar as mensagens do turno pelo navegador colocaria esses textos na aba de rede de qualquer pessoa que abrisse o devtools.

Dava para forjar. Se o cliente guarda o estado da conversa, o cliente pode editá-lo. Remova algumas entradas e o contador de rodadas zera: um loop sem limite de chamadas ao modelo, pago por outra pessoa.

Por isso o turno pausado fica no servidor, no Redis, atrás de um token de uso único com validade curta. O navegador recebe só o que precisa: quais funções chamar, com quais argumentos, e um token opaco para retomar. Ele não vê os resultados das ferramentas do servidor e não consegue reescrever o turno.

Essa checagem de token tem a sua própria sutileza, e eu errei na primeira versão: o código original apagava o token antes de verificar a qual projeto ele pertencia. Ninguém conseguiria adivinhar um token aleatório de 24 bytes, mas uma requisição do projeto errado ainda destruiria um turno legítimo em andamento. Ler, verificar, depois apagar.

Proteções, e onde eu paro

O que o Kookee garante:

  • Os resultados das ferramentas são tratados como dados não confiáveis, nunca como instruções.

  • Os marcadores de citação são removidos deles, para que o resultado de uma ferramenta não consiga inventar uma fonte.

  • Os handlers expiram em 15 segundos; os resultados são limitados a 4.000 caracteres.

  • Uma ferramenta que lança um erro devolve esse erro ao modelo, em vez de derrubar a resposta.

E uma coisa que eu deliberadamente não faço: não existe confirmação automática antes de uma ferramenta rodar. Se você registrar algo com efeitos colaterais, como cancelar um pedido ou enviar um e-mail, isso é executado quando o modelo decidir chamar.

Pensei em adicionar um diálogo de confirmação para toda ferramenta e desisti. Uma confirmação numa busca de produtos só de leitura é atrito sem nenhum ganho, e o Kookee não tem como distinguir uma da outra a partir de um JSON schema. Você tem. Então: registre ferramentas de leitura à vontade e coloque um window.confirm dentro de qualquer handler que altera dados.

Quando não usar isso

Se a resposta está na sua documentação, escreva um artigo. Ferramentas são para dados que mudam mais rápido do que você consegue publicar: preços, estoque, status de pedidos, estado da conta. Qualquer coisa estável fica mais barata, mais rápida e mais confiável como artigo de ajuda, e é citada com um link que o visitante pode abrir.

A regra prática que eu uso: se mudar a resposta exige um novo deploy, é uma ferramenta. Se basta editar uma página, é um artigo.


Já disponível no @kookee/react 1.5.0 e no @kookee/sdk 1.5.0, e na tag script, sem instalar nada. Dez ferramentas por widget, mais do que qualquer assistente de suporte que eu já vi realmente precisa.