Integrar um modelo de linguagem a um sistema Java parece exigir um framework novo, uma biblioteca específica ou uma arquitetura diferente. Não exige. Do ponto de vista do backend, a API de um LLM é uma API HTTP que recebe JSON e devolve JSON. O que muda é o que vai dentro da requisição e o cuidado com custo, limites e falhas.
Este artigo faz parte da trilha Aprenda IA construindo software e usa como exemplo o Porto, o assistente que responde dúvidas dos visitantes neste site. Ele chama a Messages API da Anthropic direto, com o RestClient do Spring, sem SDK.
Por que sem SDK
Os provedores oferecem SDKs oficiais, e eles são uma boa escolha quando você usa muitos recursos da API. Para um assistente que envia mensagens e recebe texto, o SDK acrescenta uma dependência, com seu próprio ciclo de atualização, para economizar umas trinta linhas. Chamando a API direto, cada campo da requisição fica visível no código, o que ajuda muito a entender o que está sendo enviado e quanto isso custa. Para aprender, é o melhor caminho.
A requisição, peça por peça
A chamada tem três cabeçalhos e um corpo JSON. Este é o cliente, configurado uma vez:
this.restClient = RestClient.builder()
.baseUrl("https://api.anthropic.com")
.defaultHeader("x-api-key", apiKey)
.defaultHeader("anthropic-version", "2023-06-01")
.build();
A chave vem de uma propriedade da aplicação, que em produção é preenchida por variável de ambiente. Ela nunca aparece no código nem no repositório. O artigo sobre segredos e variáveis de ambiente cobre o porquê com mais detalhe.
O corpo tem quatro partes que importam:
- model: qual modelo responde. O Porto usa um modelo da família Haiku, o menor e mais barato, porque a tarefa é orientar o visitante dentro do site, não resolver problemas difíceis. Escolher o modelo pela tarefa é a decisão de custo mais importante.
- max_tokens: o teto da resposta. No Porto, 1024. Sem teto, uma resposta longa demais custa caro e cansa quem lê.
- system: as instruções fixas, a personalidade e as regras.
- messages: a conversa até aqui, alternando usuário e assistente.
O system prompt é onde mora o comportamento
O modelo não sabe o que é este site, o que ele pode responder ou o que não deve fazer. Tudo isso vai no system prompt. O do Porto tem quatro blocos: idioma, personalidade, o que é o site e regras obrigatórias. Algumas das regras reais:
REGRAS OBRIGATORIAS:
2. Nunca invente artigo, hub, material, preco ou funcionalidade
que voce nao tem certeza que existe.
5. Respostas curtas: no maximo 3 a 4 paragrafos curtos.
6. Se nao tiver certeza da resposta, diga que nao sabe
e direcione para a pagina /contato.
A regra 2 é a mais importante. Um modelo de linguagem completa texto de forma plausível; se o visitante pergunta por um artigo que não existe, o caminho mais provável para o modelo é descrever um artigo que parece existir. A instrução explícita reduz muito esse risco, mas não zera. O artigo sobre fontes confiáveis para IA corporativa trata do caso em que a resposta precisa ser comprovável.
Histórico: a API não lembra de nada
Cada chamada é independente. Para o modelo saber o que foi dito antes, o histórico inteiro vai em toda requisição. Isso tem duas consequências práticas. Primeiro, o custo de uma conversa cresce a cada mensagem, porque o texto anterior é reenviado. Segundo, quem guarda o histórico é você: no Porto, ele fica no navegador do visitante e vem junto em cada pedido.
Como o histórico vem do cliente, ele precisa de validação como qualquer entrada externa. O Porto aceita no máximo 20 mensagens por pedido, cada uma com até 2.000 caracteres, e o papel de cada mensagem só pode ser user ou assistant:
public record PortoChatRequest(
@NotEmpty @Size(max = 20) List<@Valid PortoChatMessageDto> messages
) {}
public record PortoChatMessageDto(
@NotBlank @Pattern(regexp = "user|assistant") String role,
@NotBlank @Size(max = 2000) String content
) {}
Sem esses limites, qualquer pessoa poderia mandar um histórico enorme e transformar o seu endpoint num jeito barato de gastar a sua cota. A validação de entrada em API REST vale aqui exatamente como em qualquer outro endpoint.
Limite de uso por visitante
Um endpoint público que chama uma API paga precisa de limite. O Porto usa uma janela deslizante simples em memória: no máximo 10 mensagens por minuto por IP; acima disso, a resposta é HTTP 429. É uma solução honesta para um único servidor. Com várias instâncias, o contador precisa ir para um lugar compartilhado, como o Redis, ou para o proxy.
Cache de prompt: pagar menos pelo que se repete
O system prompt é o mesmo em todas as conversas. A API permite marcar um bloco como cacheável, e então as chamadas seguintes que começam com o mesmo bloco são cobradas a uma fração do preço normal por essa parte. No Porto, o bloco do system prompt leva a marcação:
systemBlock.put("type", "text");
systemBlock.put("text", SYSTEM_PROMPT);
systemBlock.putObject("cache_control").put("type", "ephemeral");
Vale saber o limite: o cache só entra em ação acima de um tamanho mínimo de prompt, que varia por modelo. Um system prompt curto não é cacheado, e a marcação simplesmente não tem efeito. É o caso do próprio Porto: o system prompt dele tem algumas centenas de tokens, abaixo do mínimo, então a marcação está no código mas provavelmente não economiza nada hoje. Ela passa a valer quando o prompt crescer, por exemplo com um índice dos artigos do site. Confira na documentação do provedor o mínimo do modelo que você usa, e confirme pelo campo de uso da resposta se houve leitura de cache.
Quando dá errado
A API pode falhar por vários motivos: rede, chave inválida, limite de taxa do provedor, sobrecarga. O Porto captura qualquer exceção, registra no log e devolve ao visitante uma mensagem fixa pedindo para tentar de novo ou usar a página de contato. Também há uma resposta fixa para quando a chave não está configurada, que é o caso do ambiente de desenvolvimento de quem não tem chave.
O que melhoraríamos, olhando o código hoje com calma:
- Tentar de novo nos erros temporários. Os erros de sobrecarga e de limite de taxa costumam passar em segundos. Uma ou duas novas tentativas com espera crescente resolveriam parte das falhas sem o visitante perceber.
- Timeout explícito. Sem timeout configurado, uma chamada lenta segura a thread do servidor pelo tempo padrão do cliente HTTP.
- Ler a resposta com menos suposições. O código pega o primeiro bloco de conteúdo e lê o texto. Funciona para este uso, mas a resposta pode ter mais de um bloco; ler procurando o bloco de texto é mais robusto.
- Registrar o consumo. A resposta informa quantos tokens entraram e saíram. Guardar esse número por dia é o que permite saber quanto o assistente custa antes de a fatura chegar.
Quanto custa
O preço é por token, separado entre entrada e saída, e muda com o modelo. Para estimar, some o tamanho do system prompt, o histórico médio e a resposta média, multiplique pelo número de mensagens por dia e aplique a tabela do provedor. Dois fatores dominam a conta: o modelo escolhido e o tamanho do histórico reenviado. Por isso o limite de 20 mensagens não é só segurança, é orçamento.
Checklist antes de colocar no ar
- Chave da API em variável de ambiente, fora do repositório e fora do frontend.
- Chamada feita pelo backend, nunca direto do navegador.
- Modelo escolhido pela tarefa, não pelo mais capaz disponível.
- Teto de resposta definido em max_tokens.
- Histórico validado: número de mensagens, tamanho e papel.
- Limite de uso por visitante.
- Resposta de erro amigável e log da falha real.
- System prompt com regras explícitas sobre o que não inventar.
- Timeout e nova tentativa para erros temporários.
- Consumo de tokens registrado.
Para continuar
Este assistente responde só com o que sabe do system prompt. O passo seguinte na trilha é dar ferramentas ao modelo, para ele consultar dados reais em vez de depender só das instruções. Antes disso, se a IA vai gerar código para o seu sistema, leia como revisar código gerado por IA antes do deploy.
Termos tecnicos desta leitura
Alguns conceitos aparecem com frequencia neste tema. Abrir o glossario ajuda a comparar definicoes, exemplos e leituras relacionadas sem sair do contexto do artigo.