Transformers documentation
Análise de Respostas
Análise de Respostas
É cada vez mais comum que modelos de chat gerem saídas estruturadas, em vez de apenas uma única string de resposta. Por exemplo, um modelo de raciocínio pode emitir uma cadeia de pensamento contendo seu rastro de raciocínio, enquanto um modelo de chamada de ferramentas pode emitir nomes de funções e argumentos.
O problema com saídas estruturadas, no entanto, é que as saídas dos LLMs não são estruturadas por natureza. As APIs de LLMs geralmente aceitam e retornam dicionários de mensagens, com chaves como role e content e reasoning_content, mas internamente, os LLMs na verdade apenas continuam uma única sequência de tokens. Usamos uma camada de conexão para ligar a API voltada ao usuário ao fluxo real de tokens do modelo. Para transformar as entradas em um fluxo de tokens, usamos chat_templates, que são abordados em outros documentos. Este documento trata da outra metade dessa camada de conexão: Response templates, o sistema para transformar os tokens gerados pela saída do modelo de volta em um dicionário de resposta estruturado.
Em muitos aspectos, os templates de resposta executam a operação inversa dos templates de chat. Com os templates de chat, você fornece uma lista de mensagens e obtém tokens prontos para serem inseridos no modelo. Com os templates de resposta, você fornece os tokens de saída bruta do modelo e obtém uma mensagem estruturada. Assim como os templates de chat, os templates de resposta permitem que os usuários ignorem os detalhes complexos dos formatos específicos e dos tokens de controle que um modelo espera, e utilizem uma API universal de dicionários de mensagens que funciona com qualquer modelo.
A melhor forma de entender os modelos de resposta é vê-los em ação. O ponto de entrada principal é o método parse_response(), que aceita uma única sequência ou um lote:
from transformers import AutoModelForCausalLM, AutoTokenizer
checkpoint = "HuggingFaceTB/SmolLM3-3B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint, dtype="auto", device_map="auto")
messages = [{"role": "user", "content": "Summarize the end of the Cold War, very briefly."}]
input_ids = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt")["input_ids"].to(model.device)
outputs = model.generate(input_ids, max_new_tokens=1024)[0, input_ids.shape[1]:]
out_text = tokenizer.decode(outputs)
print(tokenizer.parse_response(out_text, prefix=input_ids[0]))
# Outputs a structured dict: {"role": "assistant", "reasoning_content": "...", "content": "..."}Quando um tokenizer possui um response_template, o método parse_response converte limpa e diretamente uma mensagem de saída em um dicionário estruturado, pronto para ser adicionado ao chat. Observe que precisamos passar o prefix (os tokens do prompt) para este método também. Isso ocorre porque muitos modelos de chat iniciam mensagens ou abrem blocos de raciocínio antes de permitir que o modelo comece sua resposta, e, portanto, nosso parser precisa ver o prompt para entender a mensagem. Todo o prefixo antes do turno final é descartado; analisamos apenas uma mensagem por vez. Precisamos do prefixo apenas para garantir que estamos vendo a mensagem final inteira, sem perder nenhum campo pré-preenchido!
Como um prefixo ausente pode fazer com que uma mensagem pré-preenchida seja interpretada incorretamente de forma silenciosa, prefix é obrigatório. Omiti-lo gera um erro. No raro caso em que você tenha certeza de que a geração já contém a mensagem completa e nenhum contexto de prefixo é necessário, passe prefix="" (ou uma lista vazia de ids de tokens) para desativar explicitamente.
Se o tokenizer não tiver um template de resposta definido, parse_response lançará um erro. Estamos trabalhando para adicionar templates a mais modelos o mais rápido possível!
Análise de respostas em streaming
No exemplo acima, analisamos a resposta do modelo de uma só vez, após o término da geração. No entanto, muitas vezes podemos querer analisar mensagens parciais conforme são geradas, especialmente em aplicativos voltados ao usuário, em que não queremos apenas exibir uma página estática por um ou dois minutos até que o modelo termine.
Quando você deseja análise em streaming, chame tokenizer.get_response_parser(), que retorna um ResponseParser.
Assim como em parse_response, passe o prompt de chat como prefix= para que o parser saiba sobre as partes da mensagem que
foram pré-preenchidas pelo modelo de chat. O objeto retornado é um
parser com estado que você pode alimentar com texto à medida que o modelo o gera:
parser = tokenizer.get_response_parser(prefix=input_ids[0])
for event in parser.initial_events:
render(event) # Display the partial message to the user however you want to
for chunk in model_output:
for event in parser.feed(chunk):
render(event)
message, final_events = parser.finalize()
for event in final_events:
render(event)Se a solicitação incluir ferramentas, transmita-as também (get_response_parser(..., tools=tools)).
Os argumentos de chamada de ferramenta são então tipados a partir do JSON Schema da ferramenta chamada à medida que cada região é fechada, de modo que
os consumidores de streaming vejam argumentos tipados pelo schema em region_close em vez de apenas após finalize().
Veja Tipagem de argumentos de chamada de ferramenta para mais detalhes.
O parser emitirá eventos conforme o texto do processo de geração é fornecido. Isso indica qual região está sendo gerada no momento. Quando a região é concluída, ela será emitida em um evento separado com o conteúdo totalmente analisado. No final da geração, o método finalize() descarta qualquer texto restante e emite os eventos finais, bem como o dicionário completo da mensagem.
Observe que, embora parse_response possa aceitar lotes, o processamento em streaming é sempre de sequência única: cada ResponseParser rastreia o estado de uma geração.
Se você deseja transmitir várias gerações ao mesmo tempo, crie um ResponseParser por sequência.
Eventos de streaming
Cada evento de análise transmitido é um dicionário com a chave type. Existem três tipos:
| Tipo | Descrição | Conteúdos |
|---|---|---|
region_open | Indica que o modelo iniciou uma nova região, como content ou reasoning_content. | field (str): o nome do campo. |
region_chunk | Um trecho de texto para a região atual. | field (str): o nome do campo. text (str): o novo fragmento. dirty (bool): True se o fragmento for texto bruto que precisa de análise. |
region_close | Indica que uma região foi concluída e que a chave agora está finalizada. | field (str): o nome do campo. value (any): o valor totalmente analisado para a região |
Os eventos region_chunk são emitidos para cada região conforme os bytes chegam, permitindo que uma interface de streaming renderize o progresso mesmo para regiões estruturadas. Para regiões semelhantes a texto (text, int, float, bool), os blocos são marcados como dirty=False: cada bloco já faz parte do valor final (exceto por espaços em branco finais removidos no fechamento). Para regiões estruturadas, como chamadas de ferramenta em formato JSON, os blocos são marcados como dirty=True. Isso significa que o texto é o corpo bruto, não analisado; é seguro exibi-lo incrementalmente, mas o valor parsed (um dict, list, etc.) só chega no evento correspondente de region_close. De qualquer forma, o valor finalizado de uma região é sempre transportado pelo region_close, de modo que consumidores que não se importam com a renderização intermediária podem simplesmente ignorar os eventos region_chunk.
Se o prefix do chat escreveu algo na mensagem (por exemplo, o template abriu um bloco de raciocínio, ou um prefill do assistente iniciou uma resposta antes de passar o controle para o modelo), o parser expõe esses eventos como parser.initial_events, uma lista que você pode reproduzir no seu renderer antes de alimentar qualquer saída do modelo. Regiões que foram abertas e fechadas dentro do prefixo produzem uma sequência completa de region_open / region_chunk / region_close e seu valor analisado é colocado no dicionário de saída, exatamente como se o próprio modelo as tivesse escrito.
Um fluxo de eventos típico pode ser parecido com o seguinte:
{"type": "region_open", "field": "reasoning_content"}
{"type": "region_chunk", "field": "reasoning_content", "text": "I should ", "dirty": False}
{"type": "region_chunk", "field": "reasoning_content", "text": "greet the user", "dirty": False}
{"type": "region_close", "field": "reasoning_content", "value": "I should greet the user"}
{"type": "region_open", "field": "tool_calls"}
{"type": "region_chunk", "field": "tool_calls", "text": '{"name": "greet_user", ', "dirty": True}
{"type": "region_chunk", "field": "tool_calls", "text": '"arguments": {"greeting": "Hi!"}}', "dirty": True}
{"type": "region_close", "field": "tool_calls", "value": {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}}Observe como reasoning_content é emitido com dirty=False, pois campos como reasoning_content e content são geralmente apenas texto bruto. Isso significa que você pode tratar os chunks como uma “saída parcial” válida. No entanto, tool_calls é marcado como dirty porque o texto bruto precisa de uma limpeza significativa - as chamadas de ferramenta frequentemente precisam ser analisadas como JSON ou outro formato e, em seguida, reestruturadas para gerar o dicionário final da chamada de ferramenta. Como resultado, a saída final para essas regiões geralmente se parece muito, muito diferente do texto bruto. Essa análise final só ocorrerá quando region_close for atingido. Cabe a você decidir o que fazer com os chunks dirty até lá - você pode exibi-los como estão para mostrar ao usuário a saída “bruta”, ou simplesmente esperar até ter algo limpo para exibir.
Isso conclui a maior parte do que você precisa saber para usar modelos de resposta. O restante deste documento é focado nos detalhes internos do sistema de análise e em como escrever modelos de resposta. Isso é relevante principalmente para desenvolvedores e autores de modelos. A maioria das pessoas pode parar com segurança por aqui!
Avançado: Criando um modelo de resposta
A melhor maneira de entender como escrever um modelo de resposta é escolher um exemplo concreto. Veja como uma resposta bruta do SmolLM pode ser:
<think>
I should greet the user
</think>
<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>Ao analisarmos essa saída no formato padrão de dicionário de mensagens, ela deve ficar assim:
{
"role": "assistant",
"reasoning_content": "I should greet the user",
"tool_calls": [
{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}
]
}E aqui está o modelo que faz o parsing dele. Não se intimide - grande parte dele é bastante autoexplicativo!
{
"defaults": {"role": "assistant"},
"start_anchor": "<|im_start|>assistant\n",
"fields": {
"reasoning_content": {"open": "<think>", "close": "</think>", "content": "text"},
"tool_calls": {
"open": "<tool_call>",
"close": "</tool_call>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": "{content}"},
},
"content": {
"close": "<|im_end|>",
"content": "text",
},
},
}Em essência, o template define campos e delimitadores. Cada campo corresponde a uma chave no
dicionário de saída. Os campos também incluem informações para analisar o texto dentro de seus delimitadores. Há uma sutileza: o campo content não possui open, porque no SmolLM (e em vários outros modelos), ele não é marcado por um token especial. Em vez disso, content é armazenado no espaço após as outras regiões, mas antes do fim da sequência. No nosso template,
representamos isso como um campo implícito / residual que captura qualquer texto não reivindicado por outra região.
Os nomes dos campos devem corresponder às chaves que o modelo de chat template lê, para que uma mensagem analisada possa ser adicionada ao chat e renderizada novamente. Em particular, nomeie o campo de raciocínio como reasoning_content. Esta é a chave padrão para raciocínio em dicionários de mensagens e a que quase todos os chat templates leem o raciocínio.
Além dos fields, o modelo suporta duas chaves de nível superior:
defaults(opcional) — Um dicionário de valores pré-preenchidos na saída (por exemplo,{"role": "assistant"}). As chaves aqui são sempre mantidas na saída analisada, mesmo que nenhum campo tenha gravado nelas; as outras chaves são descartadas quando seu campo não capturou nada.start_anchor(str) /start_anchor_pattern(str regex) — Indica onde a mensagem atual do assistente começa dentro de um prompt de chat. Ao passarprefix=paraparse_responseouget_response_parser, o parser realiza o truncamento à direita do prefixo a partir da última ocorrência desta âncora antes de processá-lo, de modo que as rodadas anteriores em uma conversa de múltiplas rodadas não contaminem o estado da mensagem atual. A âncora é aplicada apenas aoprefix, nunca à resposta/geração que você analisa — alguns formatos reemitem legítimamente a âncora no meio da mensagem (a saída harmony do gpt-oss inicia cada canal com<|start|>assistant), portanto, remover a resposta a partir da âncora descartaria o próprio raciocínio e as chamadas de ferramentas do modelo. É por isso que apenas a geração nunca é suficiente para se proteger contra o vazamento de histórico: passe o prompt comoprefix=. Para modelos no estilo ChatML, a âncora é tipicamente"<|im_start|>assistant\n". Exatamente um destart_anchoroustart_anchor_patterndeve ser definido.
Por exemplo, dado este prefixo de múltiplas rodadas (note as duas turnos do assistente):
<|im_start|>user Hi<|im_end|> <|im_start|>assistant Hello!<|im_end|> <|im_start|>user Again?<|im_end|> <|im_start|>assistant
o parser truncará tudo até o último <|im_start|>assistant\n, descartando a rodada anterior "Hello!". A regra é que tudo, exceto a última rodada do assistente, é sempre descartado.
Assim como os modelos de chat, os modelos de resposta são armazenados como atributos do tokenizer e salvos junto com ele. Diferentemente dos modelos de chat, salvamos os modelos de resposta dentro de tokenizer_config.json e não como um arquivo separado, pois seu formato se encaixa naturalmente em JSON, ao contrário de um script Jinja de modelo de chat.
tokenizer.response_template = template
tokenizer.save_pretrained(...) # Written as a key in tokenizer_config.jsonAvançado: Referência da API de Field
Cada campo suporta várias chaves. Podemos dividi-las em dois tipos. Primeiro, existem as chaves que definem como o campo deve ser capturado:
| Chave | Tipo | Propósito |
|---|---|---|
open | str ou list[str] | String literal que abre esta região. Uma lista de strings significa “corresponder a qualquer uma destas”. |
open_pattern | str (regex) | Alternativa por regex para open. Os grupos nomeados se tornam variáveis de captura disponíveis para transform. |
close | str ou list[str] | String literal (ou lista de strings) que fecha esta região. Omita para executar até o fim do stream. |
close_pattern | str (regex) | Alternativa por regex para close. Os grupos nomeados se tornam variáveis de captura disponíveis para transform. |
repeats | bool | Se verdadeiro, o campo é uma lista e cada correspondência é adicionada. Padrão false. |
join | str | Com repeats, concatene as correspondências em uma única string com este separador, em vez de uma lista. |
optional | bool | Se for false e a região nunca corresponder, geramos um erro. Padrão true. |
Um campo deve ter ou open ou open_pattern, mas não ambos, e o mesmo vale para close e close_pattern.
Um campo pode omitir completamente close/close_pattern, caso em que a região permanece aberta até o final do texto gerado. Isso é útil para um campo final que se estende até o fim da mensagem.
Um campo que não possui nem open nem open_pattern é o campo implícito: ele fica ativo sempre que nenhuma região explícita estiver aberta, capturando assim o texto restante. No máximo, um campo pode ser implícito. Isso é usado com mais frequência quando content não possui tags de tokens especiais, sendo apenas escrito como texto simples após os outros campos.
Além dos delimitadores de abertura e fechamento, você também pode especificar repeats, o que indica que o campo é uma lista e que os delimitadores podem corresponder várias vezes. Isso é mais comum para chamadas de ferramentas paralelas, quando um modelo emite várias chamadas de ferramentas simultaneamente:
'<tool_call>{"name": "a", ...}</tool_call><tool_call>{"name": "b", ...}</tool_call>'
# Returns `"tool_calls": [{... "a" ...}, {... "b" ...}]` in a template with repeats: trueNem todo campo repetido deve se tornar uma lista, no entanto. Alguns modelos emitem vários blocos de reasoning_content ou content em uma única mensagem, e a saída natural para esses casos é uma única string concatenada. Definir join (uma string separadora, muitas vezes apenas "") altera um campo repeats de coletar uma lista para concatenar suas correspondências:
field = {"reasoning_content": {"open": "<think>", "close": "</think>", "repeats": True, "join": "\n"}}
input = "<think>first</think>...<think>second</think>"
# Returns: {"reasoning_content": "first\nsecond"}Cada correspondência de um campo join deve ser analisada como uma string. Durante o streaming, region_close ainda carrega o valor próprio de cada bloco; o separador só aparece no dicionário da mensagem final.
Por fim, você pode especificar optional: false para campos que devem estar presentes. Se esse campo estiver ausente,
geramos um erro em vez de apenas retornar um dicionário de mensagem sem ele.
O fim da geração encerrará e finalizará qualquer região aberta, mesmo que seu delimitador de fechamento não tenha sido visto.
Analisando o conteúdo de um campo
Depois de definir como capturar um campo, também precisamos especificar como analisar o texto bruto dentro dessa captura. Existem quatro chaves que controlam isso:
| Chave | Tipo | Propósito |
|---|---|---|
content | str | O tipo de conteúdo dentro desta região. O padrão é "text". Cada tipo possui seu próprio parser. |
content_args | dict | Argumentos a serem passados ao parser de conteúdo para esta região. |
transform | dict/list | Template opcional pós-análise que reorganiza o corpo analisado (veja Transform). |
transform_each | bool | Se for verdadeiro, o conteúdo analisado deve ser uma lista e transform é aplicado elemento a elemento. |
A primeira (e mais importante) chave é content. Ela indica o tipo de conteúdo
do campo, o que determina o parser que será usado para converter o texto bruto capturado no campo para a saída final. content_args são usados para configurar o parser e nos permitem suportar diversas peculiaridades de formato sem a necessidade de código personalizado.
Vamos examinar cada tipo de parser e seus argumentos em sequência.
Tipos básicos
text, int, float e bool são os tipos básicos. Todos esses tipos de conteúdo apenas removem espaços em branco e, se necessário, realizam uma conversão de tipo simples. Eles não possuem content_args, exceto text, que suporta o argumento strip, que remove espaços em branco do início e do fim do texto capturado, com valor padrão true.
field = {"count": {"open": "<n>", "close": "</n>", "content": "int"}}
input = "<n> 42 </n>"
# Returns: {"count": 42}json
O parser json interpreta o texto capturado como JSON. Ele é a ferramenta principal para argumentos de chamadas de ferramentas e para qualquer outra coisa com estrutura aninhada. Ele aceita alguns content_args opcionais para lidar com as diversas formas como os modelos corrompem o JSON no mundo real:
unquoted_keys(bool, padrãofalse): Ative quando os nomes das chaves não estão entre aspas (por exemplo,{name: "foo"}). Útil para modelos que emitem literais de objeto no estilo Javascript em vez de JSON estrito.string_delims(lista de pares[open, close], opcional): para modelos que envolvem valores de string em delimitadores personalizados em vez de"...". Cada par fornece um marcador de abertura e um de fechamento.allow_non_json(bool, padrãofalse): se a análise falhar, retorna o texto bruto removido em vez de lançar uma exceção. Útil como alternativa para campos em que o modelo geralmente emite JSON, mas ocasionalmente passa para texto simples.
unquoted_keys e string_delims existem para lidar com modelos que emitem saídas não padrão, quase, mas não totalmente, em JSON, então você deve precisar deles apenas para alguns modelos.
field = {"args": {"open": "<args>", "close": "</args>", "content": "json", "content_args": {"unquoted_keys": True}}}
input = '<args>{city: "London"}</args>'
# Returns: {"args": {"city": "London"}}xml-inline
O parser xml-inline é destinado a regiões compostas por uma sequência plana de tags semelhantes a XML, em que cada tag se torna uma entrada em um dict. Ele é mais comumente usado dentro de um campo tool_calls para modelos que emitem cada argumento como sua própria tag, em vez de como um blob JSON:
tag_pattern(str, obrigatório): expressão regular que corresponde a uma única tag. Deve conter os grupos nomeadoskey(a chave resultante no dicionário) evalue(o texto bruto que se torna o valor no dicionário).value_parser(dict, opcional): parser de conteúdo aninhado aplicado a cadavaluecapturado. Um dict comname(o parser, por exemplo,"json","int") eargsopcional (seuscontent_args). Se omitido, os valores permanecem como strings brutas.merge_duplicates(bool, padrãofalse): quando a mesma chave aparece várias vezes, os valores são reunidos em uma lista, em vez de permitir que correspondências posteriores sobrescrevam as anteriores.
Por exemplo, o Qwen3 emite cada argumento de chamada de ferramenta como sua própria tag <parameter>, e fazemos o parsing da seguinte forma:
"tool_calls": {
"open_pattern": r"<tool_call>\s*<function=(?P<name>\w+)>",
"close": "</tool_call>",
"repeats": True,
"content": "xml-inline",
"content_args": {
"tag_pattern": r"<parameter=(?P<key>\w+)>\s*(?P<value>.*?)\s*</parameter>",
"value_parser": {"name": "json", "args": {"allow_non_json": True}},
},
"transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
}Observe o value_parser aninhado: cada valor de parâmetro é processado pelo parser json (com allow_non_json para que strings simples ainda passem). Alimentando o campo tool_calls acima com esta entrada:
input = "<tool_call><function=get_weather><parameter=city>London</parameter><parameter=units>celsius</parameter></function></tool_call>"
# Returns: {"tool_calls": [{"type": "function", "function": {"name": "get_weather", "arguments": {"city": "London", "units": "celsius"}}}]}kv-lines
O parser kv-lines processa pares key: value delimitados por linha (pense em metadados no estilo YAML ou arquivos .env). Cada linha se torna uma entrada no dicionário resultante. Todos os argumentos são opcionais:
line_sep(str, padrão"\n"): separador entre pares.kv_sep(str, padrão":"): separador entre uma chave e seu valor dentro de uma única linha. Apenas a primeira ocorrência é usada como ponto de divisão, portanto, os valores podem conter o separador.strip(bool, padrãotrue): remove espaços em branco ao redor de cada chave e valor.value_parser(dict, opcional): parser de conteúdo aninhado aplicado a cada valor, no mesmo formato{"name": ..., "args": ...}doxml-inline. Se omitido, os valores permanecem como strings brutas.
Linhas que estão vazias ou não contêm kv_sep são ignoradas silenciosamente, de modo que linhas em branco acidentais na região capturada são toleradas.
field = {"metadata": {"open": "<meta>", "close": "</meta>", "content": "kv-lines"}}
input = "<meta>name: alice\nage: 30</meta>"
# Returns: {"metadata": {"name": "alice", "age": "30"}}Observe que age mantém "30" como string; adicione um value_parser de {"name": "int"} para convertê-lo em 30.
Digitação dos argumentos de chamada de ferramenta
Às vezes, o parser de respostas pode interpretar as saídas do modelo com o tipo incorreto. Por exemplo, ele pode interpretar o float 1.5 como “1.50”. Isso pode causar problemas na chamada de ferramentas, se as ferramentas esperarem um argumento em um tipo, mas o receberem em outro. Para evitar isso, você pode passar as tools da requisição para parse_response() ou ResponseParser para convertê-las usando o parameters do JSON Schema de cada ferramenta. As ferramentas são aceitas no mesmo formato de apply_chat_template(): schemas JSON, ou funções Python com type hints e docstrings que são convertidas automaticamente em schemas.
tools = [{
"type": "function",
"function": {
"name": "set_alarm",
"parameters": {
"type": "object",
"properties": {
"hour": {"type": "integer"},
"enabled": {"type": "boolean"},
"label": {"type": "string"},
},
},
},
}]
message = parse_response(model_out, template, prefix="", tools=tools)
# message["tool_calls"][0]["function"]["arguments"] ==
# {"hour": 7, "enabled": True, "label": "wake up"}Transform
Na maioria dos campos, a chave transform é desnecessária. Ela é usada quando o corpo analisado precisa ser reformatado para a saída final, ou quando informações dos delimitadores precisam ser mescladas no resultado. Ela aparece com mais frequência em campos tool_calls, pois esses costumam ter uma estrutura complexa.
transform é um template: um dict (ou list) que descreve a forma de saída, onde qualquer string na
forma "{name}" é substituída pelo valor correspondente. Os valores podem ser acessados a partir de content (o corpo
analisado desta região) e de quaisquer grupos nomeados capturados por open_pattern / close_pattern. Um caso de uso muito comum é envolver um dict de chamada de ferramenta em um dict externo com uma chave function, pois estes fazem parte do nosso formato padrão de chamada de ferramenta:
"tool_calls": {
"open": "<tool_call>",
"close": "</tool_call>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": "{content}"},
},Então, esta saída bruta:
<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>torna-se (observe que repeats: True faz tool_calls se tornar uma lista):
[{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}]Um placeholder de string inteira, como "{content}", retorna o valor consultado com seu tipo preservado — portanto, acima, o dicionário JSON analisado é inserido diretamente como o valor de function. O placeholder deve ser a string inteira: misturar texto e placeholders ("abc {name} def") não é permitido. Eles não são f-strings!
Os placeholders também podem aceitar um caminho pontilhado, como "{content.args}", que localiza content e, em seguida, desce para a chave args do dicionário analisado. É assim que você reestrutura corpos de chamadas de ferramenta cujos nomes de chaves não correspondem ao nosso formato padrão. Por exemplo, um modelo que emite {"name": ..., "args": ...} precisa que args seja renomeado para arguments:
"tool_calls": {
"open": "<tool>",
"close": "</tool>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": {"name": "{content.name}", "arguments": "{content.args}"}},
},Um caminho que não é resolvido (aqui, uma chamada de ferramenta cujo corpo não possui args) gera um erro em vez de emitir uma chamada de ferramenta malformada.
transform é bastante versátil, o que se torna necessário quando a saída do modelo tem um formato muito diferente da nossa API padrão. O GPT-OSS é um bom exemplo: ele embute o nome da função no cabeçalho do canal, em vez de no corpo JSON, então precisamos capturá-lo com um grupo nomeado em open_pattern e mesclá-lo com content dentro do transform. Todos os grupos nomeados em open_pattern e close_pattern ficam disponíveis como variáveis ao lado de content:
"tool_calls": {
"open_pattern": r"<\|channel\|>commentary to=functions\.(?P<name>\w+).*?<\|message\|>",
"close": "<|call|>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
},O nome da função fica no cabeçalho do canal, não no corpo JSON, então isso:
<|channel|>commentary to=functions.get_current_weather <|constrain|>json<|message|>{"location": "San Francisco, CA"}<|call|>torna-se:
[{"type": "function", "function": {"name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}}}]Às vezes, o conteúdo analisado de um campo é ele mesmo uma lista de registros e você deseja reformatar cada um deles. O template da Cohere é um bom exemplo: ele emite todas as chamadas de ferramentas dentro de um único array JSON, então definimos transform_each: True para aplicar a transformação por elemento. As chaves de cada elemento do array são descompactadas no escopo do template, de modo que "{tool_name}" consulta tool_name no elemento atual:
"tool_calls": {
"open": "<|START_ACTION|>",
"close": "<|END_ACTION|>",
"content": "json",
"transform_each": True,
"transform": {"type": "function", "function": {"name": "{tool_name}", "arguments": "{parameters}"}},
},Isso converterá uma saída como esta:
[
{"tool_name": "greet_user", "parameters": {"greeting": "Hi!"}},
{"tool_name": "search", "parameters": {"query": "weather tomorrow"}}
]Para uma saída como esta, que se encaixa na nossa API padrão:
[
{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}},
{"type": "function", "function": {"name": "search", "arguments": {"query": "weather tomorrow"}}}
]A flag transform_each é necessária apenas quando content já é uma lista; para o caso mais comum em que cada correspondência contribui com um elemento (e repeats: True os acumula), a transformação será aplicada a cada elemento por padrão.
Desenvolvedores de frameworks: Portabilidade de expressões regulares
open_pattern, close_pattern e start_anchor_pattern são strings de regex. Para a maioria dos usuários e até mesmo para a maioria dos autores de modelos, isso não deve ser um problema, mas, se você é um desenvolvedor escrevendo uma implementação de análise de resposta em outra linguagem, deve estar ciente dos detalhes da nossa implementação. Esta seção é dedicada a todos que tiveram que implementar um parser Jinja completo para fazer o chat templating não em Python funcionar — esperamos que, se você seguir as diretrizes simples abaixo, os templates de resposta sejam muito menos dolorosos:
- Usamos o módulo
regexdo Python para todas as expressões regulares usadas na análise de chat. Como todas as strings do Python3 são unicode, todas as nossas correspondências de expressões regulares são conscientes de unicode. Isso afeta particularmente caracteres comuns como\w. Certifique-se de definir as flags de unicode relevantes no seu mecanismo. - Compilamos todas as expressões regulares com
re.DOTALLhabilitado ere.MULTILINEdesabilitado, de modo que.corresponda a\n, mas^e$correspondam apenas ao início/fim de toda a entrada, e não a quebras de linha. - Usamos a sintaxe
(?P<name>...)para grupos nomeados. Outras implementações de regex possuem sintaxes de grupos de captura nomeados muito diferentes, portanto, você pode precisar procurar esse padrão em regexes e reescrevê-lo para corresponder à sua implementação local. - Usamos correspondência parcial de regex para decidir quando podemos emitir dados. Isso é necessário porque o final de uma região pode consistir em múltiplos tokens, e não queremos emitir esse delimitador de fim de região, então seguramos os tokens até termos certeza de que estão dentro da região e não fazem parte da fronteira. Isso significa que, sempre que a regex de fim de região tem uma correspondência parcial, seguramos os dados até que a regex corresponda ou falhe. Se o seu mecanismo de regex não suporta correspondência parcial, você ainda pode implementar modelos de resposta, mas pode precisar encontrar outra solução para esse problema. Uma abordagem simples é apenas segurar essas regiões / emití-las como
dirtyaté que você veja definitivamente o delimitador de término. - Seu mecanismo de expressões regulares pode (raramente) não suportar lookarounds como
(?!...). Embora esses não sejam comumente usados em modelos de resposta, eles podem aparecer e nós os suportamos! Você pode precisar lançar um erro nesses casos, ou extrair manualmente os lookarounds e aplicá-los no seu código quando o mecanismo de expressões regulares encontrar uma possível correspondência. - Outros recursos avançados, como backreferences, grupos atômicos, quantificadores possessivos, recursão e assim por diante, geralmente não são usados em modelos de resposta. Tentaremos dissuadir os autores de modelos de usá-los, para que você possa, com segurança, ignorá-los.