Learn why replacing rule-based keyword matching with JSON extraction transforms a scripted bot into a real agent — grounded in the Internal Support Dashboard experience.
A primeira versão do Dashboard de Suporte Interno que construí usava correspondência de palavras-chave para rotear tickets de clientes. Um ticket contendo "reembolso" ia para a equipe de cobrança. Um com "erro" ou "crash" ia para o suporte técnico. Qualquer outra coisa caía em uma fila geral. Funcionou nos testes, onde eu mesmo escrevi os casos de teste.
Não funcionou com tráfego real.
Clientes reais não escrevem casos de teste. Eles escrevem coisas como:
"Não há erro na minha tela — só acho que o total da fatura está errado."
Meu comparador de palavras-chave leu "erro" e roteou para o suporte técnico. A equipe de cobrança nunca viu. O cliente recebeu uma resposta sobre limpar o cache do navegador.
Aqui está o comparador completo, para que a escala do problema fique clara:
Seis linhas. Toda a lógica de "entender um cliente" reduzida a busca de string. Adicionar mais palavras-chave piorou — cada nova regra criava um novo caso extremo, e os casos extremos eram os que importavam.
A solução não eram mais palavras-chave. A solução era parar de comparar palavras-chave completamente e pedir ao modelo para ler a intenção — e retornar um dict estruturado com o qual meu código pudesse agir.
Fator 1 em uma frase: Substitua a correspondência de palavras-chave por uma chamada de modelo que retorna JSON. O modelo lê a intenção; seu código age na estrutura.
Pense na extração JSON como pedir a um colega para preencher um formulário de triagem estruturado em vez de apenas ouvir um cliente e tentar adivinhar. Você poderia transcrever as palavras exatas do cliente e tentar analisar o significado você mesmo, ou poderia entregar ao seu colega um formulário com campos para tipo de problema, urgência e número do pedido — e receber de volta algo com que o sistema pode agir imediatamente.
É isso que client.messages.create() faz quando você instrui a retornar JSON. O modelo lê a
mensagem do cliente, raciocina sobre a intenção e preenche o formulário.
Aqui está a primeira chamada real, substituindo completamente o comparador de palavras-chave:
Execute isso e o ticket que quebrou o comparador de palavras-chave é roteado corretamente. O modelo leu "fatura" e inferiu cobrança, mesmo com a palavra "erro" presente.
Antes de continuar: se o modelo retornar "Claro! Aqui está o JSON: {\"category\": ..." em vez
de JSON puro, o que quebra e por quê?
A resposta é json.loads(). Ele espera que a string comece com {. Qualquer texto prefixado —
mesmo um preâmbulo educado — causa um JSONDecodeError. A instrução "Retorne apenas JSON — sem
outro texto" reduz esse modo de falha, mas não o elimina. Um fallback é sempre necessário:
Sempre capture JSONDecodeError. Mesmo um modelo bem instruído ocasionalmente adiciona um prefixo. O fallback garante que nenhum ticket fique sem roteamento. Registre a resposta ruim para poder melhorar o prompt ao longo do tempo — é um sinal, não apenas um incômodo.
Frameworks chamam isso de "uso de ferramentas." Esse nome faz muito trabalho para esconder algo simples.
Quando um framework diz que seu agente está "usando uma ferramenta para enviar um email", o que
está realmente acontecendo é: o modelo gera um dict JSON como {"action": "send_email", "to": "..."},
e seu código chama a função de email com esses parâmetros. O modelo não tocou na API de email.
Seu código tocou. O modelo escreveu instruções; seu código as seguiu.
O Fator 4 diz: faça isso você mesmo. Escreva o roteamento você mesmo. Uma função de roteamento é mais curta e mais limpa do que qualquer coisa que um framework gera, e é trivialmente testável sem executar o modelo.
A função de roteamento é completamente testável sem o modelo. Passe qualquer dict e assertive a saída. Essa separação — extração (LLM) vs roteamento (código) — é a recompensa do Fator 4.
Combinar extração e roteamento produz o primeiro agente de triagem real. Ele lida com qualquer tipo de ticket através do mesmo pipeline de dois passos: extrair a intenção, depois rotear.
Sem listas de palavras-chave. Sem cadeias de if que crescem cada vez que um novo tipo de ticket aparece. O modelo faz a classificação; a função de roteamento traduz essa classificação em ação.
As mesmas duas funções lidam com tickets de cobrança e tickets técnicos sem alterações no código de roteamento — apenas a categoria extraída muda.
Reserve um momento para esta pergunta: se o modelo retornar JSON inválido, quais são suas três opções realistas?
Três escolhas existem: lançar o erro e falhar rapidamente (bom em desenvolvimento, onde você quer ver o bug imediatamente); tentar novamente com o mesmo prompt ou um revisado (funciona para falhas probabilísticas); ou retornar um padrão seguro e registrar a resposta ruim (certo para produção, onde o ticket deve ser roteado mesmo que de forma imperfeita). A escolha depende do contexto. Módulos posteriores adicionam rastreamento de estado à sessão de triagem, o que cria uma quarta opção: fazer checkpoint do estado e retomar após a falha sem perder o histórico da conversa.
Application-level questions on JSON extraction and tool routing.
1. A keyword matcher routes tickets containing 'error' to the tech team. A customer writes: 'There's no error on my screen — the invoice total just looks wrong.' Which team receives this ticket, and why is that wrong?
Correct Answer: A
The word 'error' appears in the message, so the rule-based matcher sends it to the tech team — even though the actual issue is a billing discrepancy. This is the brittleness that JSON extraction fixes: the model reads intent, not keywords.
2. What does response.content[0].text hold after a client.messages.create() call that instructs the model to return JSON?
Correct Answer: B
The API returns a string. You must call json.loads() on it to get a Python dict. This is why wrapping the parse in a try/except json.JSONDecodeError is always the right move.
3. Your extractor calls the model and gets back: 'Sure! Here is the JSON: {"category": "billing"}'. What breaks next?
Correct Answer: B
json.loads() expects the string to begin with '{'. Any prefix text — even a polite preamble like 'Sure! Here is the JSON:' — causes JSONDecodeError. The fix: instruct the model to 'return JSON only — no other text', and still catch JSONDecodeError as a fallback.
4. Factor 1 says 'Natural Language → JSON'. Which best explains why this is the foundational primitive for agent systems?
Correct Answer: B
The key insight: once the model converts natural language into a structured dict, every downstream step (routing, validation, action dispatch) is ordinary, deterministic software. The model is scoped to exactly one transformation; everything else is code you control.
5. In route_ticket(), what is the recommended behavior when the extracted category does not match any key in the routes dict?
Correct Answer: C
Returning a safe default ensures no ticket is dropped. Logging the unexpected category creates a feedback loop: if 'refund_dispute' starts appearing frequently, you know to add it as an explicit category in the extraction prompt.
6. A framework says your agent 'uses tools autonomously'. Under the hood, what is most likely happening?
Correct Answer: B
Factor 4 in practice: 'tool use' is always the model writing JSON, your code reading it, and calling the matching function. The framework automates the dispatch loop. Understanding this makes every agent system debuggable — you can log the exact JSON and trace exactly which function was called.
7. If the model returns invalid JSON in production, what are your three realistic options in order of increasing resilience?
Correct Answer: A
Crash fast works in development where you need to see the bug. Retry works for probabilistic failures (the model sometimes prefixes). Safe default plus logging works in production where every ticket must be routed. The right choice depends on context — the next module adds state checkpointing as a fourth option.