Pular para o conteúdo
Fantástico Mundo de Jon
RSS

Dissecando o Modelfile do Qwen3 para ele parar de pensar em voz alta no OpenCode

O Qwen3 alterna entre pensar e responder direto, e um agente esperando um diff não quer o raciocínio. Disseco o Modelfile parâmetro por parâmetro, gravo um derivado sem pensamento e ligo no OpenCode pelo provedor local novo.

Com pressa? Peça o TL;DR ao Claude — ele lê a página e resume.

O Qwen3 saiu no fim de abril e a primeira coisa que fiz foi puxar a tag de 14B pra ligar num agente de terminal. Pedi uma correção de três linhas. O modelo começou a raciocinar: sobre a arquitetura do projeto, sobre alternativas que ninguém pediu, sobre o que faria se o requisito fosse outro. O patch veio no fim disso tudo.

Não é bug nem peso ruim. É o modelo fazendo o que foi treinado pra fazer. O Qwen3 é híbrido, alterna entre pensar antes de responder e responder direto, e o comportamento de partida é pensar.

A receita que eu usava no Qwen2.5-Coder era curta: os padrões do Ollama são padrões de conversa, então derrube a temperatura e siga a vida. Com o Qwen3 ela quebra em dois pontos que só apareceram quando li o arquivo linha por linha. Vou pelo caminho todo: extraio o Modelfile da tag publicada, disseco parâmetro por parâmetro, gravo um derivado e ligo no OpenCode.

O que a tag do qwen3 já traz gravada

O primeiro passo é o que quase ninguém faz antes de mexer em parâmetro: ler o de fábrica.

ollama show qwen3:14b --modelfile > Modelfile

(sem saída no terminal — o conteúdo foi para o arquivo Modelfile)

Extrai a receita da tag instalada para o diretório corrente

O que sai tem esta cara, com o FROM apontando pro blob local:

# Modelfile generated by "ollama show"
# To build a new Modelfile based on this, replace FROM with:
# FROM qwen3:14b

FROM /usr/share/ollama/.ollama/models/blobs/sha256-...
TEMPLATE """{{- ... turnos <|im_start|> / <|im_end|> ... -}}"""
PARAMETER stop "<|im_start|>"
PARAMETER stop "<|im_end|>"
PARAMETER temperature 0.6
PARAMETER top_k 20
PARAMETER top_p 0.95
PARAMETER repeat_penalty 1

Aqui está a primeira surpresa, e ela derruba metade da receita antiga. Esses valores não são padrão de conversa do Ollama: são os que a Qwen publica no cartão do modelo para o modo de raciocínio. Quem empacotou leu o cartão e gravou o que estava lá.

A segunda surpresa é o que não está no arquivo: a tag do qwen3 não traz SYSTEM nenhum, enquanto o Qwen2.5-Coder vinha com um “You are Qwen, created by Alibaba Cloud” de assistente prestativo. O espaço está livre.

O trabalho mudou de natureza: não é mais desfazer preset de chat, é decidir onde eu discordo da própria Qwen.

temperature, e por que zero está fora de cogitação

temperature reescala os logits antes do softmax. Em zero o modelo sempre pega o token mais provável; conforme sobe, a distribuição achata e tokens menos prováveis entram no jogo. O padrão global do Ollama é 0.8, conferível no fonte, em DefaultOptions(). A tag do qwen3 sobrescreve com 0.6, e vale a camada de cima.

Meu reflexo antigo era descer pra 0.1, e pra diff estável ir a 0.0. Acontece que o cartão do Qwen3 tem um aviso seco contra isso: “DO NOT use greedy decoding, as it can lead to performance degradation and endless repetitions”. Gulosa é a decodificação de temperatura 0.

A Qwen recomenda 0,6 no modo de raciocínio e 0,7 no direto. Eu paro em 0.3, abaixo dos 0,7 porque agente de código quer convergência, e longe do zero que o cartão desaconselha. É julgamento de uso, não medição: não rodei tarefas variando temperatura e contando acerto.

Os dois cortes de cauda que já vêm apertados

top_p, ou nucleus sampling, restringe a escolha aos tokens mais prováveis até somar a probabilidade p. top_k corta parecido, mas por contagem.

Os padrões globais do Ollama são 0.9 e 40. A tag do qwen3 grava 0.95 e 20, e repare que os dois andaram em direções opostas. O top_p mais alto deixa passar mais massa de probabilidade, o que faz sentido num modelo que precisa explorar durante o raciocínio. O top_k na metade é o freio: por mais que a massa esteja liberada, no máximo vinte candidatos disputam cada posição.

Deixo os dois como vieram. Já vi gente empilhar top_p 0.8 por cima da temperatura baixa e depois não saber qual filtro causou o quê.

min_p e repeat_penalty: dois em que eu mudei de ideia

min_p descarta tokens cuja probabilidade fica abaixo de min_p vezes a do token mais provável, e o padrão do Ollama é 0.0, desligado. Eu tinha o hábito de ligar em 0.05 como cinto e suspensório, mas o cartão do Qwen3 recomenda MinP=0 nos dois modos, escrito com o número.

Deixo desligado. Não porque tenha medido que 0.05 piora, mas porque não medi que melhora, e o ônus da prova é meu.

repeat_penalty acima de 1 penaliza tokens que já apareceram, e o padrão global do Ollama é 1.1. Em prosa isso reduz loop de parágrafo. Em código, repetir não é defeito: i, err, ctx, return, o mesmo nome de função três vezes no arquivo. Penalizar repetição empurra o modelo a inventar sinônimo onde sinônimo quebra, e o sintoma clássico é o userId virar user_id no meio de um patch.

A tag do qwen3 já grava 1, efeito neutro, e eu não mexo.

num_ctx: a documentação e o código discordam

num_ctx é o tamanho da janela de contexto, e é o que mais pega gente de surpresa. Fui conferir e achei uma discrepância: a tabela do docs/modelfile.md do Ollama diz 2048, mas em envconfig/config.go a variável OLLAMA_CONTEXT_LENGTH tem padrão 4096 desde a versão 0.6.7, de 26 de abril. A documentação ficou pra trás. Se você leu 2048 num tutorial e não bate com a sua máquina, é isso.

E 4096 continua pequeno demais. Num agente o contexto some rápido: instrução de sistema, arquivo atual, trechos vizinhos, histórico, diff. Com o Qwen3 tem um consumidor a mais na fila, porque o raciocínio também ocupa contexto.

O cartão do Qwen3-14B declara 32.768 tokens de contexto nativo e 131.072 com YaRN. O teto útil não é o número do anúncio: é o que a sua VRAM aguenta, porque subir contexto sobe KV cache na hora, e quando a placa enche o Ollama empurra camada pra CPU e a latência despenca.

Gravo 32768 no derivado, o contexto nativo. É onde eu paro de brigar com truncamento silencioso sem entrar em YaRN.

num_predict fica sem teto, e a culpa é do raciocínio

num_predict é o teto de tokens na saída, e o padrão do Ollama é -1, sem limite além do contexto.

No Qwen2.5-Coder eu cortava em 2048 ou 4096 sem pensar, porque saída sem teto em prompt vago vira monólogo. Com o Qwen3 o corte fica perigoso, por uma razão do modelo híbrido: o raciocínio consome o orçamento de saída antes de a resposta começar. Se o teto é baixo e o modelo resolve pensar bastante naquele turno, a geração termina com o raciocínio pela metade e código nenhum. Não é uma resposta curta, é uma resposta que nunca chegou.

A Qwen sugere folga generosa por causa disso, e o cartão fala em 32.768 tokens de saída pra uso normal. Deixo em -1 e resolvo o monólogo pela raiz: desligando o raciocínio.

O modo híbrido é o parâmetro que não é um parâmetro

Aqui está o que separa um Modelfile de Qwen3 de qualquer outro. Todo o resto é sampling, e sampling se ajusta com PARAMETER. O modo de raciocínio, não.

No template oficial do Qwen3, no tokenizer_config.json da Qwen, existe um argumento chamado enable_thinking. Quando é falso, o template emite isto logo depois do cabeçalho do turno do assistente:

<think>

</think>

O template não desliga nada por dentro do modelo: abre e fecha o bloco de raciocínio já vazio, e o modelo segue daí com o pensamento dado como concluído. É truque de prompt, não chave de arquitetura.

Acontece que enable_thinking é argumento do template Jinja da Hugging Face, e o Ollama não usa Jinja, converte pro formato Go dele. Procurei o campo equivalente nas opções da API do Ollama nesta versão e não existe. Você não desliga o raciocínio declarando parâmetro, porque o parâmetro não está lá.

O que existe, e funciona em qualquer runtime, é o interruptor treinado no peso. A Qwen documenta assim: dá pra incluir /think e /no_think em prompts de usuário ou em mensagens de sistema. Não passa por template nenhum. E é por isso que o Modelfile parece o lugar certo: o SYSTEM de um derivado é uma mensagem de sistema.

O que muda na ferramenta quando o raciocínio fica ligado

Três efeitos, e eles se acumulam.

O primeiro é latência antes do primeiro token útil: o agente manda o pedido e espera, porque o pensamento vem antes de qualquer coisa aproveitável. Num loop de editar-rodar-corrigir, cada turno paga o pedágio.

O segundo é orçamento: tokens de raciocínio ocupam contexto e saída, espaço disputado com o que interessa.

O terceiro é o mais chato de diagnosticar: o agente pediu um patch e recebeu um <think> antes dele. Cliente que entende o bloco separa e mostra à parte; cliente que não entende trata tudo como conteúdo, e o raciocínio vai parar dentro do arquivo. Se você viu texto solto num arquivo editado, olhe isso antes de culpar o modelo.

Não é que raciocínio seja ruim: pra um refactor grande ele ajuda. Pra “renomeie essa função”, é imposto puro.

O SYSTEM seco, e por que ele começa com /no_think

Pra desligar o pensamento existem dois caminhos. O primeiro é o marcador numa mensagem de sistema, e o SYSTEM do Modelfile é uma. O segundo é editar o TEMPLATE pra sempre emitir o bloco vazio, imitando o Jinja oficial: vale para qualquer cliente, vantagem que só fica clara lá na frente, mas te congela numa versão do template.

Fico com o primeiro, e aproveito que o espaço estava vazio de fábrica pra escrever o resto junto. Em inglês, porque esta família obedece melhor instrução de sistema em inglês do que em português longo, e seco, porque quem vai ler é um programa esperando um patch:

SYSTEM """/no_think
You are a senior software engineer working inside a terminal coding agent.
Prefer correct, minimal changes over rewrites.
Match the style, naming, and abstractions already present in the repository.
Output only what was asked: no preamble, no tutorial, no recap of the request.
Do not restate the plan before the code. If code was asked for, write code.
Do not invent files, APIs, or config keys that were not given.
If requirements are ambiguous, state the assumption in one line and proceed.
Never wrap a whole multi-file answer in a single markdown fence.
Use placeholders for secrets and credentials; never guess them.
"""

Por que cada linha está ali:

  • /no_think na primeira linha: o interruptor, em cima pra ficar óbvio ao conferir com ollama show.
  • terminal coding agent: ancora o contexto de uso. Não é chat, é ferramenta.
  • minimal changes over rewrites: o vício número um é reescrever o arquivo inteiro quando pediram seis linhas.
  • no preamble, no recap: corta o “Claro! Aqui está uma implementação…”.
  • Do not restate the plan before the code: essa é por causa do modo híbrido. Mesmo com o raciocínio desligado, o Qwen3 tende a narrar o plano antes de escrever.
  • Do not invent files, APIs…: alucinação de módulo vizinho é o modo de falha mais caro em agente com repositório real.

O que eu não escrevo: lista de linguagens, “you are world-class”, regra que eu não quero valendo de verdade. SYSTEM comprido demais vira ruído, e aí você perde as linhas que importavam.

O marcador continua reversível no turno: se num pedido eu quero raciocínio, mando /think na mensagem. Guarde a ressalva do SYSTEM, porque ela volta no OpenCode.

O Modelfile fechado e o ollama create

Juntando tudo num arquivo que parte da tag oficial, mais portátil que o blob:

FROM qwen3:14b

PARAMETER temperature 0.3
PARAMETER top_p 0.95
PARAMETER top_k 20
PARAMETER min_p 0
PARAMETER repeat_penalty 1
PARAMETER num_ctx 32768

SYSTEM """/no_think
You are a senior software engineer working inside a terminal coding agent.
Prefer correct, minimal changes over rewrites.
Match the style, naming, and abstractions already present in the repository.
Output only what was asked: no preamble, no tutorial, no recap of the request.
Do not restate the plan before the code. If code was asked for, write code.
Do not invent files, APIs, or config keys that were not given.
If requirements are ambiguous, state the assumption in one line and proceed.
Never wrap a whole multi-file answer in a single markdown fence.
Use placeholders for secrets and credentials; never guess them.
"""

Repare no que não está aqui: não redeclarei TEMPLATE, nem os stop, nem num_predict. Os dois primeiros vêm herdados do base, e o que eu não mexo não quebro. Crio a tag derivada:

ollama create qwen3-dev -f Modelfile

transferring model data using existing layer sha256-… creating new layer sha256-… writing manifest success

Grava o modelo derivado

E confiro o que ficou gravado, o passo que a maioria pula antes de debugar a tag errada por uma tarde:

ollama show qwen3-dev --modelfile

FROM qwen3:14b TEMPLATE “”“…”“” PARAMETER stop “<|im_start|>” PARAMETER stop “<|im_end|>” PARAMETER temperature 0.3 … SYSTEM “”“/no_think You are a senior software engineer working inside a terminal coding agent. …”“”

Conferir a receita da tag derivada

TEMPLATE e os dois stop continuam lá, herdados do base, que é o que eu queria. Se mesmo assim o modelo vier pensando em voz alta, são duas hipóteses: ou o SYSTEM não pegou, ou o cliente mandou a própria mensagem de sistema por cima da sua. A segunda é a que me pegou.

Apontar o OpenCode pro modelo derivado

O OpenCode é um agente de código que roda no terminal, escrito em Go, com repositório público desde meados de março. Até duas semanas atrás ele não falava com modelo local: li o fonte de uma tag do começo do mês e a estrutura de provedor tinha só uma chave de API e um booleano de desligado, sem campo de URL. Isso mudou na versão 0.0.49, que trouxe o arquivo internal/llm/models/local.go e, com ele, um provedor chamado local.

A primeira coisa a saber é que esse provedor não se liga pela configuração, e sim por variável de ambiente:

export LOCAL_ENDPOINT=http://localhost:11434/v1
opencode

Na subida, o OpenCode consulta o endpoint pra descobrir que modelos existem. Tenta primeiro o caminho do LM Studio, api/v0/models, e se não vier nada cai no compatível com a OpenAI, v1/models, que é o que o Ollama serve. Cada modelo entra no catálogo com o prefixo local., então a minha tag aparece como local.qwen3-dev:latest.

Descobertos os modelos, o OpenCode escolhe um sozinho pros agentes internos: o primeiro da lista, na ordem do Ollama. Prefiro fixar no .opencode.json do projeto:

{
  "agents": {
    "coder": { "model": "local.qwen3-dev:latest", "maxTokens": 4096 }
  }
}

Chave de API não precisa: o provedor grava dummy como padrão em providers.local.apiKey.

O que o OpenCode assume sobre um modelo local

Duas suposições que vale conhecer antes de culpar o modelo por algo do adaptador.

A primeira é o tamanho da janela. O código lê o contexto do campo loaded_context_length da resposta de /models, que é coisa do LM Studio, e cai num padrão de 4096 quando o campo não vem. A resposta do Ollama tem quatro campos: id, object, created e owned_by. Ou seja, não importa que eu tenha gravado num_ctx 32768, o OpenCode segue achando que a janela é de 4096 tokens, e o resumo automático dele dispara com base nesse número. O modelo aguenta mais do que a ferramenta acredita.

A segunda é que todo modelo local entra marcado como capaz de raciocinar. Isso muda o corpo da requisição: em vez de max_tokens, o cliente manda max_completion_tokens e um reasoning_effort. O endpoint compatível do Ollama nesta versão não tem nenhum dos dois na struct de requisição, então ambos são descartados em silêncio. Não quebra nada, mas o teto de tokens do agente não chega ao servidor.

Onde o /no_think se perde

Liguei tudo, mandei o primeiro pedido e o modelo pensou em voz alta. O SYSTEM estava gravado na tag, conferido com ollama show, e mesmo assim o raciocínio voltou.

A resposta está no ChatHandler do Ollama. Ele só põe o SYSTEM do modelo na frente se a primeira mensagem do cliente não for uma mensagem de sistema. E o OpenCode monta toda requisição começando pela dele. O SYSTEM do meu Modelfile nunca chega ao modelo, e o /no_think vai junto.

Repare no que isso não afeta. Os PARAMETER continuam valendo, porque são opções do modelo e o cliente não sobrescreve nenhuma: temperatura, top-p, top-k e contexto são os que eu gravei. O que se perde é só a camada de texto.

A saída é entregar o marcador por um caminho que o OpenCode respeite. Ele lê arquivos de contexto do projeto e cola o conteúdo no fim do próprio prompt de sistema, e a lista padrão inclui opencode.md e CLAUDE.md na raiz do repo. Então o /no_think mora ali:

cat opencode.md

/no_think

Prefer minimal diffs. Do not restate the plan before the code.

Arquivo de contexto na raiz do repositório

Fica torto ter a instrução em dois lugares, o Modelfile pro uso direto e o arquivo de contexto pro agente. Mas é honesto com o mecanismo: quem manda na mensagem de sistema é o cliente.

O conjunto, lado a lado

A história cabe em três camadas: o padrão do runtime, o que a tag publicada grava por cima e o que eu deixo no derivado.

Parâmetro Padrão global do Ollama Gravado na tag qwen3 No meu derivado
temperature 0.8 0.6 0.3
top_p 0.9 0.95 0.95
top_k 40 20 20
min_p 0.0 não declarado 0
repeat_penalty 1.1 1 1
num_predict -1 não declarado -1
num_ctx 4096 não declarado 32768
Coluna 2: valores de DefaultOptions() no fonte do Ollama, versão 0.7.1, de 21 de maio de 2025 — o num_ctx vem de OLLAMA_CONTEXT_LENGTH, que passou de 2048 para 4096 na 0.6.7 e ainda aparece como 2048 na documentação. Coluna 3: o que a tag qwen3 publicada declara, conferível com ollama show. Coluna 4: julgamento de uso meu, não medição — não rodei o mesmo conjunto de tarefas variando um parâmetro por vez.

Duas das três colunas de valor são fato conferível. A terceira é opinião minha com cara de receita.

Onde esse ajuste não compensa

Quatro situações me fizeram desfazer o trabalho.

Modelo pequeno demais. Abaixo de 8B, o problema deixa de ser pensar em voz alta e passa a ser não seguir instrução comprida: o SYSTEM que eu mostrei tem quase dez linhas, e num 4B elas competem com a tarefa.

Existe modelo mais adequado. O Qwen3 é generalista com raciocínio. Se o que você quer é um peso treinado pra loop de agente, a Mistral soltou o Devstral com a All Hands em 21 de maio, 24B e Apache 2.0. Não medi os dois lado a lado, mas afinar Modelfile sem olhar o que saiu na semana passada é otimizar a coisa errada.

Tarefa que não é código. Pra documentação ou arquitetura, a tag com /no_think e temperatura baixa é a errada: o raciocínio ajuda ali, e um sistema que proíbe preâmbulo atrapalha um texto que precisa de preâmbulo. É caso de ter duas tags, não meio-termo.

Contexto que não cabe. num_ctx em 32768 só é honesto se a placa segura os pesos mais o KV cache desse tamanho. Se não segura, o ajuste certo não é no Modelfile: é quantização menor ou modelo menor.

O que eu levo daqui

A lição que eu não esperava não estava em parâmetro de sampling nenhum: estava em ler o arquivo antes de escrever por cima dele. A tag do qwen3 já vinha com a recomendação da Qwen gravada, e metade dos meus ajustes de reflexo, herdados do Qwen2.5-Coder, eram piora com cara de otimização.

A segunda lição é sobre camadas. Um Modelfile guarda duas coisas muito diferentes: números, que sobrevivem a qualquer cliente, e texto, que o cliente sobrescreve sem avisar. Meus PARAMETER chegaram inteiros ao OpenCode; meu SYSTEM morreu na porta. Quando uma instrução parecer ignorada, pergunte primeiro quem escreveu a mensagem de sistema daquela requisição.

Extraia o Modelfile antes de editar, e leia o cartão do modelo antes de confiar no seu hábito. Fica faltando medir se temperatura 0,3 acerta mais que 0,6 num conjunto fixo de tarefas de patch. É o próximo post, com número.