antonio leandro

claude, na prática

Streaming Messages

tutorial · 01 · o básico · Anthropic · · ~21 min de leitura do original

a tese

streaming troca uma resposta pronta por uma máquina de estados: blocos indexados que você remonta no cliente — e, com max_tokens alto, remontar deixa de ser escolha, porque a chamada síncrona estoura o timeout

o que fica

  1. Os tokens do campo `usage` no evento `message_delta` são cumulativos, não incrementais: somar os eventos infla a contagem.
  2. O input de um bloco `tool_use` chega como pedaços de JSON inválido, e só depois do `content_block_stop` dá para parsear com segurança.
  3. Os modelos atuais emitem uma chave e valor completos do input por vez, então a pausa entre eventos durante tool use é o comportamento esperado, não travamento.
  4. Para `max_tokens` grande os SDKs exigem streaming para não estourar o timeout HTTP, mesmo quando você só quer o `Message` final acumulado.
  5. Blocos de thinking e de tool_use não são parcialmente recuperáveis: depois de uma queda, a retomada só funciona a partir do último bloco de texto.
  6. A receita de retomada mudou com o modelo: até Claude 4.5 o trecho parcial volta como início de uma mensagem do assistant, e do 4.6 em diante volta como mensagem do usuário pedindo continuação.

o problema

Uma chamada comum à Messages API é uma pergunta e uma espera. Você abre a conexão, manda o prompt e fica parado até o modelo terminar de gerar tudo. Trezentos tokens ou trinta mil dá no mesmo do ponto de vista do cliente: o primeiro byte útil chega junto com o último. Numa interface de chat, isso é a diferença entre um cursor piscando por vinte segundos e um texto que começa a aparecer quase imediatamente.

O segundo problema é menos estético. Geração longa segura a conexão aberta por muito tempo, e a infraestrutura HTTP no meio do caminho não gosta disso. Com max_tokens alto, a requisição não-streaming simplesmente não sobrevive ao timeout — os SDKs oficiais tratam isso como erro de uso e exigem streaming, mesmo quando você não tem nenhum interesse em processar a resposta incrementalmente. É por isso que o modo streaming não é um recurso de interface: é o caminho normal para qualquer geração grande.

a ideia

"stream": true transforma a resposta em um fluxo de server-sent events. Em vez de um objeto Message completo, você recebe uma sequência de eventos nomeados que descrevem a construção desse objeto: começou uma mensagem, abriu um bloco de conteúdo, chegou um pedaço, fechou o bloco, acabou. O contrato de destino é o mesmo — se você acumular tudo, tem exatamente o Message que .create() devolveria.

A troca é onde mora a montagem. No modo síncrono, o servidor monta. No streaming, quem monta é você. Os SDKs escondem isso atrás de helpers (get_final_message() em Python, finalMessage() em TypeScript, message.Accumulate(event) em Go, MessageAccumulator em Java, .accumulated_message em Ruby), mas numa integração direta com a API o acumulador é código seu.

como funciona

O fluxo é estável. Primeiro um message_start, com um Message de content vazio. Depois uma série de blocos de conteúdo, cada um com content_block_start, um ou mais content_block_delta e um content_block_stop. Cada bloco carrega um index que corresponde à posição dele no array content final — é por esse índice que você reconstrói, não pela ordem de chegada. Em seguida um ou mais message_delta com mudanças de topo (stop_reason, usage) e, por fim, message_stop. Eventos ping podem aparecer em qualquer ponto.

O delta muda de tipo conforme o bloco. Texto vem em text_delta, com o campo text. Ferramentas vêm em input_json_delta, com partial_json: strings parciais que só formam um objeto válido quando concatenadas até o fim do bloco. Thinking vem em thinking_delta, e logo antes do content_block_stop chega um signature_delta — a assinatura que verifica a integridade do bloco de raciocínio. Com display: "omitted", o bloco de thinking abre, recebe só a assinatura e fecha, sem nenhum thinking_delta.

Em Go, o laço mínimo separa evento e delta em dois switches de tipo:

for stream.Next() {
	event := stream.Current()
	switch v := event.AsAny().(type) {
	case anthropic.ContentBlockDeltaEvent:
		switch d := v.Delta.AsAny().(type) {
		case anthropic.TextDelta:
			fmt.Print(d.Text)
		}
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}

Ferramentas de servidor aparecem no mesmo stream como blocos próprios: uma busca na web produz um server_tool_use com o query em JSON parcial e, depois, um web_search_tool_result que chega inteiro, sem deltas.

o que isso custou

Três armadilhas de contabilidade e estado. A primeira: o usage do message_delta é cumulativo — quem soma os eventos conta errado. A segunda: tool_use.input é sempre um objeto no fim, mas nunca durante; parsear cedo quebra. E a granularidade prometida é parcial — os modelos atuais só emitem uma chave e valor completos por vez, então o stream de uma ferramenta tem silêncios longos enquanto o modelo trabalha. O eager_input_streaming existe justamente para tirar esse buffer do servidor, por ferramenta.

A terceira é a falha. Erros chegam dentro do stream: um overloaded_error que num contexto não-streaming seria um HTTP 529 aqui vira um evento numa conexão que já respondeu 200. E a recuperação é incompleta por construção — a documentação afirma que blocos de thinking e de tool_use não podem ser parcialmente recuperados, e que a retomada só é possível a partir do último bloco de texto. Pior: a forma de retomar depende do modelo. Até Claude 4.5, você devolve o parcial como início de uma mensagem do assistant; do 4.6 em diante, devolve como mensagem do usuário pedindo continuação. O mesmo cliente falando com duas gerações de modelo precisa de dois caminhos.

Some a isso a política de versionamento: novos tipos de evento podem ser adicionados, e o código tem que tolerar o desconhecido sem estourar. Um switch exaustivo com default: panic é uma bomba-relógio.

onde isso aparece hoje

Streaming é o substrato de quase tudo que veio depois na plataforma. Extended thinking só é observável porque existe thinking_delta, e a assinatura que o acompanha é o que permite devolver o raciocínio em turnos seguintes. Tool use depende do acumulador de JSON parcial, e o uso avançado de ferramentas empurra a granularidade desse acumulador para baixo. O Agent SDK herda o loop inteiro: um agente é um cliente que lê deltas, decide num content_block_stop e reabre outro stream.

O contraponto está do outro lado da mesa. Quando não há ninguém esperando na frente da tela, batch processing resolve o mesmo problema de geração longa sem manter conexão nenhuma aberta.

lido na íntegra por pipeline de llm, revisado por antonio leandro antes de publicar ·