Pular para o conteúdo

Mensagens e tokens especiais

Agora que sabemos como um LLM funciona, vamos ver como uma conversa vira texto — porque é isso que ela é, por baixo.

P: Mas quando eu converso com o ChatGPT, eu troco mensagens, não mando um bloco de texto único.

R: Correto — na tela. Mas isso é uma abstração da interface. Antes de chegar ao modelo, todas as mensagens da conversa são concatenadas em um único prompt. O modelo não "lembra" da conversa: ele relê tudo, inteiro, a cada resposta.

Essa frase merece um momento. Não existe memória do lado do modelo. O que existe é uma string que cresce a cada turno e é reenviada por completo. Quando a conversa fica longa demais para caber na janela de contexto, o começo dela simplesmente deixa de ser enviado — e o modelo passa a se comportar como quem nunca leu aquilo. Não é esquecimento: é truncamento.

O que a interface mostra e o que o modelo recebe
À esquerda, o que você vê. À direita, o que o modelo recebe.

A peça que faz essa conversão chama-se chat template. Ela é a ponte entre a lista de mensagens (com papéis system, user, assistant) e o formato exato que aquele modelo específico espera — com os tokens especiais dele.

Os três papéis

System message

A system message define como o modelo deve se comportar. É uma instrução persistente: vale para todos os turnos seguintes, não só para o próximo.

system_message = {
    "role": "system",
    "content": "Você é um assistente de atendimento. Seja educado, claro e objetivo."
}

Trocando o conteúdo, muda-se o comportamento:

system_message = {
    "role": "system",
    "content": "Você é um assistente de gabinete. Nunca responda sobre o mérito de "
               "um caso concreto. Se a pergunta exigir análise processual, "
               "oriente a consultar os autos."
}

Em Agents, a system message carrega bem mais do que tom de voz. É nela que entram a lista de Tools disponíveis, o formato em que as chamadas devem ser escritas, e as regras de como o raciocínio deve ser segmentado. Quando você criar um Agent com smolagents, a biblioteca monta essa mensagem para você — e ela costuma ter várias centenas de tokens.

System prompt do Alfred

O experimento que vale a pena fazer

Antes de seguir, um teste real. Mandamos a mesma pergunta ao qwen2:7b com duas system messages diferentes, com temperature=0:

Pergunta: "O prazo para contestação já venceu?"

Com a system message genérica ("Você é um assistente prestativo"):

No Brasil, por exemplo, o prazo para contestar uma ação civil pública é de 30 dias, contados do recebimento da notificação. No caso de ações civis particulares, o prazo é geralmente de 15 dias.

Com a system message restritiva ("Nunca responda sobre o mérito de um caso concreto... oriente a consultar os autos"):

No Brasil, por exemplo, o prazo para contestar uma ação civil ou criminal é de 10 dias úteis, conforme estabelece o Código de Processo Civil. [...] Se o prazo para contestação já venceu, o réu perde o direito de contestar a ação, mas ainda pode apresentar uma resposta à ação, conhecida como "redução da ação" [...]

Atenção

Leia as duas respostas como juiz, não como aluno de IA. Há duas lições aqui, e a segunda é a mais importante.

Primeira: a system message não obrigou nada. Foi instruído a não responder sobre mérito e a mandar consultar os autos. Ele respondeu sobre mérito assim mesmo — e a menção aos autos, quando veio, veio no fim, como ressalva. A system message orienta; ela não é uma trava.

Segunda: o conteúdo jurídico não se sustenta — e a resposta mais confiante é a pior. O prazo geral de contestação no CPC é de 15 dias (art. 335), contados em dias úteis (art. 219). A primeira resposta acerta esse número por um caminho torto, depois de afirmar um prazo de 30 dias para ação civil pública que não tem base. A segunda simplesmente diz 10 dias úteis e atribui isso ao Código de Processo Civil. E vai além: apresenta um instituto chamado "redução da ação", entre aspas, com explicação de como funciona. Ele não existe. O modelo deu nome a uma coisa inexistente e descreveu o funcionamento dela com naturalidade.

Repare que a segunda resposta é mais confiante, mais estruturada e mais citada — ela invoca o Código de Processo Civil pelo nome. E é a pior das duas. A aparência de rigor não é sinal de rigor.

Nenhum dos dois textos vinha marcado como incerto. Nenhum trouxe aviso. Para quem não conhece o art. 335, as duas respostas são indistinguíveis de uma resposta correta.

Guarde essa consequência prática, porque ela desenha todo o resto do curso: escrever uma regra na system message não é implementar a regra. Se você escrever "não mostre processos em segredo de justiça", isso não é uma medida de segurança — é um pedido. O filtro de sigilo precisa estar na consulta ao banco de dados, onde o modelo não alcança. Voltamos a isso quando construirmos as Tools.

É também por isso que o Alfred deste curso devolve número de processo e metadados, e não resumos de conteúdo. Um número de processo errado o sistema rejeita na hora. Um "art. 335 diz X" errado passa.

User e assistant

Uma conversa é a alternância entre mensagens de papel user (a pessoa) e assistant (o modelo):

conversa = [
    {"role": "user", "content": "Preciso dos processos de execução fiscal da 3ª Vara"},
    {"role": "assistant", "content": "Posso buscar. Quer restringir por período?"},
    {"role": "user", "content": "Distribuídos em 2024"},
]

Toda essa lista é concatenada e enviada como uma sequência só. Quem faz essa concatenação é o chat template.

O chat template do qwen2

Cada modelo tem seus próprios tokens especiais e suas próprias regras. O qwen2 usa o formato ChatML, com os delimitadores <|im_start|> e <|im_end|>.

A lista de mensagens acima vira este texto:

<|im_start|>user
Preciso dos processos de execução fiscal da 3ª Vara<|im_end|>
<|im_start|>assistant
Posso buscar. Quer restringir por período?<|im_end|>
<|im_start|>user
Distribuídos em 2024<|im_end|>
<|im_start|>assistant

Repare no final: a última linha abre o turno do assistant e para ali. Esse é o convite. O modelo continua a partir daquele ponto, e encerra quando gerar <|im_end|>.

O mesmo diálogo, no Llama 3.2, ficaria assim:

<|begin_of_text|><|start_header_id|>user<|end_header_id|>

Preciso dos processos de execução fiscal da 3ª Vara<|eot_id|><|start_header_id|>assistant<|end_header_id|>

Posso buscar. Quer restringir por período?<|eot_id|><|start_header_id|>user<|end_header_id|>

Distribuídos em 2024<|eot_id|><|start_header_id|>assistant<|end_header_id|>

Mesma conversa. Tokens completamente diferentes. Usar o template errado degrada a resposta sem gerar erro nenhum — o modelo recebe algo que não se parece com o que viu no treinamento e responde pior, só isso. É uma das causas mais difíceis de diagnosticar de "o modelo está estranho hoje".

Vendo o template de verdade

Não precisa acreditar. O Ollama mostra:

ollama show qwen2:7b --template

A saída é um template Go, longa. O trecho que interessa agora:

{{- if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}
...
{{- if eq .Role "user" }}<|im_start|>user
{{ .Content }}<|im_end|>
{{ else if eq .Role "assistant" }}<|im_start|>assistant

Duas observações que valem mais do que parecem:

  1. {{- if .System }} — no qwen2, o bloco de sistema só aparece se você enviar um. Não existe system message padrão escondida. Se você não mandar nada, o modelo começa sem nenhuma orientação de comportamento.
  2. Mais adiante, o mesmo template tem um bloco {{- if .Tools }} e um papel tool. As Tools entram por dentro da system message, como uma lista de assinaturas de função em XML. Não é um canal separado nem uma API especial — é texto no prompt. Guarde isso; é o assunto do próximo capítulo.

Provando que é tudo uma string só

Este é o exercício que fecha a ideia. Em vez de mandar uma lista de mensagens, vamos escrever o ChatML à mão e pedir ao Ollama que não formate nada.

O endpoint /api/generate com "raw": true desliga a montagem do template: o que você mandar é literalmente o que o modelo recebe.

python exemplos\prompt_cru.py

O que o arquivo faz:

prompt = (
    "<|im_start|>system\n"
    "Você responde com uma palavra apenas.<|im_end|>\n"
    "<|im_start|>user\n"
    "Qual é a capital da França?<|im_end|>\n"
    "<|im_start|>assistant\n"
)

corpo = {
    "model": "qwen2:7b",
    "prompt": prompt,
    "raw": True,      # não aplique template nenhum — mande isso como está
    "stream": False,
}

Resposta do modelo:

Paris

Nenhuma lista de mensagens. Nenhum objeto de conversa. Uma string, escrita à mão, com os tokens especiais nos lugares certos. É exatamente isso que acontece por baixo de toda interface de chat que você já usou.

Nota

Vale brincar aqui. Apague o <|im_end|> do meio e rode de novo. Troque o papel system por user. Remova a última linha <|im_start|>assistant. Cada quebra ensina algo sobre o que os delimitadores estão de fato fazendo.

Base model x instruct model

Uma distinção que explica bastante coisa:

O que aproxima um do outro, na prática, é justamente o chat template: é ele que apresenta o texto na forma que o modelo aprendeu a reconhecer durante o ajuste. Por isso o template não é detalhe cosmético — é a condição para o modelo se comportar como assistente.

Nota

Na biblioteca transformers, a conversão é feita por tokenizer.apply_chat_template(mensagens, tokenize=False, add_generation_prompt=True).

No nosso caso, não precisamos disso: o Ollama aplica o template correto sozinho quando você usa /api/chat, e o smolagents fala com o Ollama por esse caminho. Basta saber que a etapa existe — e agora você sabe como olhar para ela quando algo der errado.

Fixando o capítulo

Q1: A system message obriga o modelo a alguma coisa?


Q2: O que o chat template faz?


Q3: No experimento deste capítulo, a system message restritiva mandava não responder sobre o mérito. O que o modelo fez?


Vimos como a conversa é estruturada na entrada do modelo. Mas um modelo que só produz texto não consulta banco de dados nenhum.

Como ele age, então? Pelas Tools — e o mecanismo é menos mágico do que parece.