Lidando com JSON Parcial em Chamadas de Ferramentas Transmitidas
Quando Claude decide chamar uma ferramenta durante a transmissão, os argumentos da ferramenta não chegam como um único objeto JSON limpo.
Eles chegam como uma sequência de eventos input_json_delta, cada um sendo um pequeno fragmento de string que só se torna JSON válido quando todos os fragmentos foram concatenados em ordem.
Analisar muito cedo - ou analisar cada fragmento individualmente - é uma fonte comum de travamentos em código de uso de ferramentas transmitido. Esta página mostra o padrão correto de acumular e depois analisar.
Receita
import json
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "Obtém o clima atual de uma cidade.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
json_buffer = ""
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=512,
tools=tools,
messages=[{"role": "user", "content": "Qual é o clima em Lisboa?"}],
) as stream:
for event in stream:
if event.type == "content_block_delta" and event.delta.type == "input_json_delta":
json_buffer += event.delta.partial_json
elif event.type == "content_block_stop":
if json_buffer:
tool_input = json.loads(json_buffer)
print(tool_input) # {"city": "Lisbon"}
json_buffer = ""Quando usar isso:
- Qualquer solicitação de transmissão que inclua
tools, pois os argumentos de uma chamada de ferramenta sempre são transmitidos como fragmentos, nunca como um único evento. - Loops de agente que precisam saber o momento exato em que a entrada de uma ferramenta está completa e pronta para ser executada.
- Interfaces de usuário que desejam mostrar "Claude está chamando
get_weather..." antes que os argumentos sejam totalmente conhecidos. - Respostas de múltiplas ferramentas, onde vários blocos
tool_usesão transmitidos em sequência e cada um precisa de seu próprio buffer.
Exemplo Funcional
Um exemplo completo e executável que lida com várias chamadas de ferramentas em uma única resposta, indexado pelo índice do bloco para que o JSON de cada ferramenta seja acumulado independentemente.
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Obtém o clima atual de uma cidade.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
{
"name": "convert_currency",
"description": "Converte um valor de uma moeda para outra.",
"input_schema": {
"type": "object",
"properties": {
"amount": {"type": "number"},
"from_currency": {"type": "string"},
"to_currency": {"type": "string"},
},
"required": ["amount", "from_currency", "to_currency"],
},
},
]
def stream_tool_calls(prompt: str) -> list[dict]:
completed_calls = []
# Indexado pelo índice do bloco de conteúdo, pois vários blocos tool_use podem ser transmitidos em uma única resposta.
buffers: dict[int, dict] = {}
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": prompt}],
) as stream:
for event in stream:
if event.type == "content_block_start" and event.content_block.type == "tool_use":
buffers[event.index] = {
"name": event.content_block.name,
"id": event.content_block.id,
"json_parts": [],
}
elif event.type == "content_block_delta" and event.delta.type == "input_json_delta":
if event.index in buffers:
buffers[event.index]["json_parts"].append(event.delta.partial_json)
elif event.type == "content_block_stop" and event.index in buffers:
entry = buffers.pop(event.index)
raw_json = "".join(entry["json_parts"]) or "{}"
completed_calls.append({
"id": entry["id"],
"name": entry["name"],
"input": json.loads(raw_json),
})
return completed_calls
calls = stream_tool_calls("Qual é o clima em Tóquio e converta 50 USD para EUR.")
for call in calls:
print(f"{call['name']}({call['input']})")O que isso demonstra:
- O buffer é indexado por
event.index, não por uma única string global, para que blocostool_useconcorrentes na mesma resposta não corrompam o JSON uns dos outros. - A leitura do
nameeidda ferramenta a partir decontent_block_start, pois os eventosinput_json_deltacarregam apenas fragmentos JSON, não metadados de identificação. - O adiamento de
json.loads(...)atécontent_block_stop, o único ponto em que a string acumulada tem garantia de estar completa e analisável. - O fallback para
"{}"quando uma chamada de ferramenta não tem argumentos (uma ferramenta sem entrada necessária ainda produz um objeto JSON vazio).
Mergulho Profundo
Como Funciona
- O campo
delta.partial_jsonde cada eventocontent_block_deltaé um fragmento de uma string JSON - não um fragmento de um objeto analisado - portanto, os fragmentos devem ser unidos como strings antes que qualquer análise ocorra. content_block_startdispara uma vez por blocotool_usee carrega onamee umidúnico da ferramenta, ambos necessários posteriormente para construir otool_resultque você envia de volta para Claude.content_block_stopé o sinal de que o JSON de um determinado bloco está completo; analisar antes deste evento falhará na maioria dos fragmentos, pois eles são, individualmente, JSON inválido.- Múltiplos blocos
tool_useem uma resposta são distinguidos porindex- o mesmo campo usado para blocos de texto e de pensamento - portanto, qualquer manipulador de fluxo de propósito geral que rastreia o estado por bloco deve usar o índice como chave, não apenas pelo tipo de conteúdo.
partial_json em Resumo
| Campo | Tipo | Significado |
|---|---|---|
event.delta.partial_json | str | O próximo fragmento da string JSON de entrada da ferramenta - anexe, nunca substitua. |
event.content_block.name | str | A ferramenta que está sendo chamada - disponível em content_block_start, não em cada delta. |
event.content_block.id | str | O tool_use_id que você deve ecoar de volta no bloco tool_result correspondente. |
event.index | int | A qual bloco de conteúdo este evento pertence - a chave de acumulação. |
Notas Python
# Uma classe acumuladora pequena e reutilizável em vez de dicionários inline - útil quando você tiver
# mais de alguns locais de chamada fazendo esse padrão.
class ToolCallAccumulator:
def __init__(self) -> None:
self._parts: dict[int, list[str]] = {}
self._meta: dict[int, dict] = {}
def start(self, index: int, name: str, tool_id: str) -> None:
self._parts[index] = []
self._meta[index] = {"name": name, "id": tool_id}
def add_delta(self, index: int, fragment: str) -> None:
self._parts.setdefault(index, []).append(fragment)
def finish(self, index: int) -> dict:
import json
raw = "".join(self._parts.pop(index, [])) or "{}"
meta = self._meta.pop(index)
return {**meta, "input": json.loads(raw)}Armadilhas
- Chamar
json.loads()em cadainput_json_deltaindividual - quase todos os fragmentos são JSON inválido por si só, então isso gerajson.JSONDecodeErrorconstantemente. Correção: acumule fragmentos como strings e analise exatamente uma vez, emcontent_block_stop. - Usar um único buffer de string global para todas as chamadas de ferramentas em uma resposta - os deltas de um segundo bloco
tool_usecorrompem silenciosamente o JSON da primeira ferramenta. Correção: use oevent.indexcomo chave do buffer, como mostrado acima. - Ler
content_block.namede um evento delta em vez decontent_block_start- os deltas não carregam o nome da ferramenta, apenascontent_block_starto faz. Correção: capturenameeidquando o bloco começar, armazene-os junto com o buffer. - Assumir que uma chamada de ferramenta sem argumentos ainda transmite pelo menos um delta - uma ferramenta com um esquema de entrada vazio pode produzir zero eventos
input_json_delta. Correção: defina o buffer como"{}"antes de analisar se nenhum delta chegou. - Esquecer de ecoar o
idda ferramenta de volta comotool_use_idnotool_result- Claude não consegue corresponder seu resultado à chamada correta sem ele. Correção: passe ocontent_block.idatravés do seu acumulador para o resultado que você envia de volta. - Tentar "renderizar parcialmente" os argumentos da ferramenta para um usuário enquanto eles são transmitidos - ao contrário do texto, mostrar uma string JSON meio formada raramente é útil e muitas vezes confuso. Correção: mostre um indicador genérico "chamando
tool_name..." em vez do JSON parcial bruto.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
Uso de ferramenta sem transmissão (client.messages.create) | A chamada completa da ferramenta, não o processo de chegar a ela, é tudo o que seu aplicativo precisa | O usuário está assistindo à resposta ao vivo e a latência de seleção da ferramenta importa |
| Um analisador "JSON parcial" tolerante (por exemplo, tentando fechar chaves abertas) | Você realmente precisa mostrar argumentos parciais atualizados ao vivo em uma interface de depuração | Caminhos de código de produção - uma análise parcial malformada pode produzir silenciosamente uma entrada de ferramenta incorreta |
| Armazenar toda a lista de eventos em buffer e processar após o fechamento do fluxo | Código mais simples, sem necessidade de uma interface de usuário ao vivo | Você deseja agir em uma chamada de ferramenta no momento em que ela estiver pronta, antes que o restante da resposta seja concluído |
FAQs
O que um único evento `input_json_delta` realmente contém?
Um campo de string partial_json contendo o próximo fragmento do JSON de entrada da ferramenta - alguns caracteres a algumas dezenas, sem garantia de que se alinhe com qualquer limite de sintaxe JSON.
Quando é seguro chamar `json.loads()` na string acumulada?
Somente após content_block_stop disparar para o índice desse bloco. Antes disso, a string acumulada pode ser (e geralmente é) JSON incompleto.
Como sei qual ferramenta está sendo chamada antes que seus argumentos terminem de serem transmitidos?
content_block_start inclui content_block.name e content_block.id imediatamente, antes que quaisquer eventos input_json_delta cheguem - capture esses primeiro.
O que acontece se duas ferramentas forem chamadas na mesma resposta?
Dois blocos de conteúdo tool_use separados são transmitidos, cada um com seu próprio index. Mantenha um buffer por índice para que seus fragmentos JSON não sejam intercalados em uma única string corrompida.
Posso mostrar argumentos de "digitação" ao vivo ao usuário como faria com texto?
Não de forma útil - uma string JSON meio formada não significa nada para um leitor. Mostre um indicador estático "chamando tool_name..." em vez disso e revele os argumentos assim que forem analisados.
E se `json.loads()` gerar um erro mesmo após `content_block_stop`?
Isso indica um bug na sua lógica de acumulação (por exemplo, falta de um delta, ordem incorreta ou mistura de buffers entre índices) em vez de um problema na API Claude - a API garante que a string acumulada completa seja JSON válido no fechamento do bloco.
Preciso do `tool_use_id` para algo além de registrar?
Sim - ele deve ser ecoado exatamente no bloco de conteúdo tool_result que você envia na próxima vez, para que Claude possa corresponder seu resultado à chamada que fez.
Isso é diferente de analisar JSON de uma chamada de ferramenta sem transmissão?
O resultado final (um dicionário Python de json.loads) é idêntico. A diferença está apenas em como você chega lá: sem transmissão fornece a string JSON completa de uma vez; a transmissão requer acumulá-la primeiro.
E se uma ferramenta não tiver argumentos obrigatórios?
Seu bloco tool_use pode não produzir nenhum evento input_json_delta. Defina seu buffer como um objeto JSON vazio ("{}") antes de analisar para não chamar json.loads(""), o que gera um erro.
Como isso interage com o loop mais amplo de uso de ferramentas de agente?
Uma vez que você tenha o input analisado, a execução e a próxima etapa de tool_result funcionam exatamente da mesma forma que no loop de uso de ferramentas sem transmissão - veja Combinando Transmissão com o Loop de Uso de Ferramentas para continuar a conversa.
Relacionados
- Como os Eventos Enviados pelo Servidor Potencializam as Respostas Transmitidas do Claude - onde
input_json_deltase encaixa no modelo geral de eventos. - Combinando Transmissão com o Loop de Uso de Ferramentas - o que fazer com uma chamada de ferramenta analisada assim que você a tiver.
- Referência de Tipos de Eventos de Transmissão - a referência completa de eventos/campos.
- Definindo Esquemas de Ferramentas com Nome, Descrição e Esquema de Entrada - como o parâmetro
toolsusado acima é estruturado. - Lidando com Transmissão de Uso de Ferramentas em Fluxos de Trabalho Python Assíncronos - o mesmo padrão em código
asyncio.
Versões da Pilha: Escrito contra a linha de modelos Claude atual em ~junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão), e Claude Haiku 4.5 - e o SDK oficial
anthropicpara Python (última versão 0.x). Nomes de modelos, versões de SDK e preços mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs antes de confiar neles.