Domando o Indomável - 4 formas de controlar o formato das mensagens geradas por IA e facilitar integrações
Adicionar funcionalidades com IA nas nossas aplicações é uma tarefa muito simples e muito difícil ao mesmo tempo.
É simples porque integrar qualquer treco a um modelo LLM é fácil demais hoje em dia: Basta criar uma conta em um provedor (OpenAI, Antropic, Google), escolher qual modelo usar (Fable 5, GPT 6, sei lá, tem tantos), programar uma integração simples com a API deles e voilá, tá feito.
Integrar é muito simples.
Agora conseguir tirar da IA o que tu precisa pra a aplicação funcionar, é outra história.
Qualquer modelo tem uma natureza aleatória indomável, e é praticamente impossível controlar O QUE ela vai escrever, mas podemos ao menos tentar controlar COMO ela vai escrever.
O Problema
Supondo que você é um 👨💻 dev 👩💻, é provável que queira integrar uma IA no seu sistema para performar ações junto ao usuário (famosos Agentes).
O caso mais clássico disso é aceitar uma solicitação do usuário em texto livre e utilizar uma IA para interpretar esse input e padronizar as ações que precisam ser executadas no sistema.
Dou um exemplo: Em um app de agenda, você pode permitir que o usuário escreva mensagens como ‘quero ver minha agenda da semana’ ou ‘marque uma reunião para 12h amanhã com fulaninho’ e utilizar uma IA para extrair (do texto livre) quais ações devem ser performadas pelo sistema (dentre um grupo de ações pré-definidos).
E aqui nós podemos cair em uma armadilha, pois geralmente a primeira ideia de padronização de mensagens envolve pedir para a IA codificar as solicitações com palavras chave e estruturas rudimentares, mais ou menos assim:
- Para consultar o banco de dados, escreva CONSULTA < sql > FIM_CONSULTA
- Para executar uma das funções disponíveis, escreva CALL nome_da_funcao(parametros)
Mas vai por mim, a última coisa que você quer se ver fazendo é escaneando uma resposta gerada por IA com Regex em busca de palavras chaves pra entender o que danado ela está querendo fazer.
Um JSON quentinho e um café, por favor (prompt engineering)
Pra evitar isso, uma técnica muito comum é solicitar ao modelo que gere respostas em um formato realmente padrão, normalmente JSON. É uma abordagem puramente de Prompt Engineering, a gente orienta o modelo a gerar respostas em um formato JSON seguindo o schema passado.
Exemplo de Prompt:
Você é um agente auxiliar bla bla bla ...
Para executar uma das funções disponíveis, escreva a ação em formato JSON
contendo os campos 'funcao' com o nome da função
e 'parametros' com os parâmetros que a função vai receber
Exemplos:
{
funcao: 'consultar_agenda_usuario',
parametros: ['João Pedro']
}
{
funcao: 'desmarcar_reuniao',
parametros: [0123]
}
Funções disponíveis:
- consultar_agenda_usuario(nome): Consulta a agenda do usuário pra o dia de hoje
- marcar_reuniao(data, titulo, convidados): Marca uma reunião
- desmarcar_reuniao(id): Desmarca uma reunião
Se o modelo obedecer, nossa aplicação consegue facilmente identificar quais ações ele deseja performar.
Mas orientação não é garantia - o modelo ainda pode falhar, gerar JSONs fora do schema ou sequer gerar um JSON.
Um código que se baseia somente nessa abordagem normalmente vai estar cheio de retries e fluxos de exceção contra diversos tipos de falha do modelo e pode acabar ficando bem complexo.
Dando um empurrãozinho (Prefix Filling)
Uma forma de dar um empurrãozinho no modelo para a direção certa é com o Prefix Filling.
Nele, além da mensagem do usuário/sistema, nos também damos o trecho inicial da resposta do modelo.
Ou seja, o LLM não vai gerar uma resposta totalmente do zero, mas sim continuar a partir do trecho inicial que nós fornecemos.
No exemplo abaixo, nós queremos que o modelo gere uma tabela HTML, então é fornecido como ‘prefixo’ o início das tags do cabeçalho:
from openai import OpenAI
client = OpenAI(
api_key="<your api key>",
base_url="https://api.deepseek.com/beta",
)
prefixo = """
<table>
<thead>
<tr>
<th>Nome</th>
<th>Data de Nascimento</th>
<th>Total de Gols</th>
"""
response = client.responses.create(
model="deepseek-v4-pro",
input=[
{
"role": "system",
"content": (
"Responda a pergunta do usuário em uma tabela HTML"
),
},
{
"role": "user",
"content": "Faça uma lista dos 10 maiores jogadores de Futebol"
},
{
"role": "assistant",
"content": prefixo,
"prefix": True
},
],
)
print("Continuação gerada:\n")
print(response.output_text)
Nesse outro exemplo, tentamos induzir o modelo a gerar um JSON válido
prefixo = """
{
'funcao':
"""
response = client.responses.create(
model="deepseek-v4-pro",
input=[
{
"role": "system",
"content": (
"""
Você é um agente auxiliar bla bla bla ...
Para executar uma das funções disponíveis,
escreva a ação em formato JSON
"""
),
},
{
"role": "user",
"content": "Quais meus compromissos pra hoje?"
},
{
"role": "assistant",
"content": prefixo,
"prefix": True
},
],
)
print("Continuação gerada:\n")
print(response.output_text)
E aqui dá pra ser bem criativo- unindo prompt e o trecho certo, conseguimos induzir o modelo a respeitar o padrão desejado.
Dando um empurrãozão (JSON Mode)
Mas garantia de formato mesmo a gente consegue com o JSON Mode, e esse é bem direto.
Alguns modelos de IA mais avançados suportam restrições de gramática, o que significa que eles NÃO CONSEGUEM gerar respostas fora do padrão esperado.
E é bem fácil de usar, na API do seu modelo favorito deve ter um parâmetro ‘json_mode’ ou ‘json_output’ que você pode ligar. É só ativar e pronto, todas as respostas agora serão em formato JSON.
prefixo = """
{
'funcao':
"""
response = client.responses.create(
model="deepseek-v4-pro",
input=[
{
"role": "system",
"content": (
"""
Você é um agente auxiliar bla bla bla ...
Para executar uma das funções disponíveis,
escreva a ação em formato JSON
"""
),
},
{
"role": "user",
"content": "Quais meus compromissos pra hoje?"
}
],
response_format={
'type': 'json_object'
}
)
print("Continuação gerada:\n")
print(response.output_text)
Mas isso ainda não é tudo, a gente consegue garantir que a resposta é um JSON, mas não COMO é esse JSON. Por isso, precisamos ir além.
Agora sim! (Constrained Decoding +)
O JSON Mode que a gente acabou de ver é um caso particular de Constrained Decoding, um conjunto de matemáticas e estatísticas mágicas que forçam o modelo a seguir um padrão de sintaxe.
Algumas implementações - como o Structured Outputs da OpenAI - dão um passo a mais e conseguem garantir não somente sintaxe, mas também que a resposta gerada vai seguir o SCHEMA fornecido.
Ele também é batata de usar, basta verificar na documentação do seu provedor se ele possui suporte a essa funcionalidade e como utilizar, daí é só fornecer o schema e consumir a resposta normalmente.
Aqui o exemplo tirado direto da documentação Oficial da OpenAI (usando schemas definidos pelo Pydantic diretamente no Python):
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
response = client.responses.parse(
model="gpt-5.6",
input=[
{"role": "system", "content": "Extract the event information."},
{
"role": "user",
"content": "Alice and Bob are going to a science fair on Friday.",
},
],
text_format=CalendarEvent,
)
event = response.output_parsed
Essa funcionalidade, embora muito poderosa, é mais rara e geralmente só está disponível em modelos mais avançados (e caros!).
Conclusão
Na minha opinião esses são macetes indispensáveis pra quem quer desenvolver sistemas integrados a LLM modernos. A IA é aleatória, mas nossa aplicação não é! O mínimo de garantia de estrutura é indispensável pra facilitar o desenvolvimento do nosso lado 😉
Referências
- OpenAI. Introducing Structured Outputs in the API.
- OpenAI. Structured Outputs. API Documentation.
- DeepSeek. JSON Output API Documentation.
- DeepSeek. Chat Prefix Completion (Beta) API Documentation.