Transformers documentation

응답 파싱

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.2).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

응답 파싱

채팅 모델이 단일 응답 문자열이 아닌 구조화된 출력을 생성하는 경우가 점점 더 흔해지고 있습니다. 예를 들어, 추론 모델은 추론 과정을 포함하는 사고 체인을 출력할 수 있고, 도구 호출 모델은 함수 이름과 인수를 출력할 수 있습니다.

구조화된 출력의 문제는 LLM의 출력이 본질적으로 구조화되어 있지 않다는 점입니다. LLM API는 일반적으로 role, content, reasoning_content와 같은 키를 가진 메시지 딕셔너리를 받아들이고 반환하지만, 내부적으로 LLM은 실제로 단일 토큰 시퀀스를 계속 생성할 뿐입니다. 우리는 사용자 대상 API와 모델의 실제 토큰 스트림을 연결하기 위해 접착 계층(glue layer)을 사용합니다. 입력을 토큰 스트림으로 변환하기 위해 chat_templates를 사용하며, 이는 다른 문서에서 다루고 있습니다. 이 문서는 해당 접착 계층의 다른 절반, 즉 모델이 생성한 토큰을 다시 구조화된 응답 딕셔너리로 변환하는 시스템인 응답 템플릿(Response templates)에 관한 것입니다.

많은 면에서 response templates는 chat templates와 반대되는 작업을 수행합니다. chat templates에서는 메시지 목록을 입력하면 모델에 입력할 준비가 된 토큰을 얻습니다. 반면 response templates에서는 모델의 원시 출력 토큰을 입력하면 구조화된 메시지를 얻습니다. chat templates와 마찬가지로 response templates는 사용자가 모델이 기대하는 특정 형식 및 제어 토큰의 복잡한 세부 사항을 무시하고, 모든 모델과 호환되는 메시지 딕셔너리의 범용 API를 사용할 수 있도록 합니다.

응답 템플릿을 이해하는 가장 좋은 방법은 실제 동작을 살펴보는 것입니다. 주요 진입점은 단일 시퀀스 또는 배치 중 하나를 받는 parse_response() 메서드입니다:

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": "..."}

토크나이저에 response_template가 있으면, parse_response 메서드가 출력 메시지를 깔끔하게 구조화된 dict로 변환하여 채팅에 바로 추가할 수 있는 상태로 만들어 줍니다. 이 메서드에는 prefix(프롬프트 토큰)도 함께 전달해야 합니다. 많은 채팅 템플릿은 모델이 응답을 시작하기 전에 메시지를 시작하거나 사고 블록을 열기 때문에, 파서가 메시지를 이해하려면 프롬프트를 확인해야 합니다. 마지막 턴 이전의 모든 prefix는 폐기되며, 한 번에 하나의 메시지만 파싱합니다. prefix는 마지막 메시지의 전체를 보고 미리 채워진 필드를 놓치지 않도록 확인하는 데만 필요합니다.

누락된 prefix가 미리 채워진 메시지를 조용히 잘못 파싱할 수 있으므로 prefix는 필수입니다. 생략하면 오류가 발생합니다. 생성 결과가 이미 완전한 메시지를 포함하고 prefix 컨텍스트가 필요하지 않다고 확신하는 드문 경우, prefix=""(또는 빈 토큰 ID 목록)을 전달하여 명시적으로 제외할 수 있습니다.

토크나이저에 응답 템플릿이 설정되어 있지 않으면 parse_response가 오류를 발생시킵니다. 가능한 한 빠르게 더 많은 모델에 템플릿을 추가하는 작업을 진행 중입니다!

스트리밍 응답 파싱

위의 예시에서는 생성이 완료된 후 모델 응답을 한 번에 파싱합니다. 그러나 부분 메시지가 생성되는 동안 이를 파싱하는 경우가 많습니다. 특히 모델이 완료될 때까지 1~2분 동안 정적인 페이지만 표시하는 것을 원하지 않는 사용자 대상 앱에서 그렇습니다.

스트리밍 파싱이 필요할 때는 tokenizer.get_response_parser()를 호출하여 ResponseParser를 반환받습니다. parse_response와 마찬가지로, 채팅 프롬프트를 prefix=로 전달하여 채팅 템플릿에 의해 미리 채워진 메시지 부분을 파서가 인식할 수 있도록 합니다. 반환된 객체는 모델이 텍스트를 생성하는 동안 해당 텍스트를 입력할 수 있는 상태ful 파서입니다:

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)

요청에 tools가 포함된 경우, 이들도 함께 전달합니다(get_response_parser(..., tools=tools)). 각 region이 닫힐 때 tool-call 인수는 호출된 tool의 JSON Schema에 따라 타입이 지정되므로, 스트리밍 소비자는 finalize() 이후가 아니라 region_close 시점에 스키마 타입이 지정된 인수를 확인할 수 있습니다. 자세한 내용은 tool-call 인수 타이핑을 참조하세요.

파서는 생성 프로세스에서 텍스트가 입력되는 동안 이벤트를 출력합니다. 이는 현재 생성 중인 영역을 나타냅니다. 영역이 완료되면 완전히 파싱된 내용과 함께 별도의 이벤트로 출력됩니다. 생성이 끝나면 finalize() 메서드가 남은 텍스트를 플러시하고 최종 이벤트 및 전체 메시지 딕을 출력합니다.

parse_response는 배치(batch)를 받을 수 있지만, 스트리밍 파싱은 항상 단일 시퀀스입니다. 각 ResponseParser는 하나의 생성(generation) 상태를 추적합니다. 여러 생성을 동시에 스트리밍하려면 시퀀스마다 ResponseParser를 하나씩 생성하세요.

스트리밍 이벤트

각 스트림된 파싱 이벤트는 type 키를 가진 dict입니다. 세 가지 종류가 있습니다:

유형설명목차
region_opencontent 또는 reasoning_content와 같이 모델이 새로운 영역을 시작했음을 나타냅니다.field (str): 필드 이름.
region_chunk현재 영역에 대한 텍스트 청크.field (str): 필드 이름. text (str): 새로운 청크. dirty (bool): 청크가 파싱이 필요한 원시 텍스트인 경우 True.
region_close영역이 완료되었음을 나타내며, 해당 키는 이제 확정됩니다.field (str): 필드 이름. value (any): 해당 영역에 대한 완전히 파싱된 값

region_chunk 이벤트는 바이트가 도착할 때마다 모든 region에 대해 발생하므로, 구조화된 region의 경우에도 스트리밍 UI가 진행 상황을 렌더링할 수 있습니다. 텍스트와 유사한 region(text, int, float, bool)의 경우 청크는 dirty=False로 표시됩니다. 각 청크는 이미 최종 값의 일부이며(닫힐 때 제거되는 끝 공백 제외)입니다. JSON 형식의 도구 호출과 같은 구조화된 region의 경우 청크는 dirty=True로 표시됩니다. 이는 텍스트가 파싱되지 않은 원본 본문임을 의미하며, 점진적으로 표시하는 것은 안전하지만 parsed 값(딕트, 리스트 등)은 해당 region_close 이벤트에서만 도착합니다. 어느 경우든 region의 최종화된 값은 항상 region_close에 의해 전달되므로, 중간 렌더링에 관심이 없는 사용자는 단순히 region_chunk 이벤트를 무시하면 됩니다.

채팅 prefix가 메시지에 아무것도 작성하지 않았다면(예: 템플릿이 thinking 블록을 열었거나, 모델로 제어권을 넘기기 전에 어시스턴트 prefill이 응답을 시작했거나), 파서는 이러한 이벤트를 parser.initial_events로 노출합니다. 이는 모델 출력을 렌더러에 공급하기 전에 렌더러에 재생할 수 있는 목록입니다. prefix 내에서 열리고 닫힌 영역은 region_open / region_chunk / region_close의 완전한 시퀀스를 생성하며, 그 파싱된 값은 모델이 직접 작성한 것과 정확히 동일한 방식으로 출력 dict에 저장됩니다.

전형적인 이벤트 스트림은 다음과 같은 형태를 가질 수 있습니다:

{"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!"}}}}

reasoning_content가 dirty=False로 출력되는 방식을 주목해 보세요. reasoning_content와 content와 같은 필드는 일반적으로 단순한 원시 텍스트이기 때문입니다. 이는 각 청크를 유효한 “부분 출력”으로 취급할 수 있음을 의미합니다. 반면, tool_calls는 dirty로 표시됩니다. 원시 텍스트에 상당한 정리가 필요하기 때문이죠. 도구 호출은 JSON 또는 다른 형식으로 파싱한 후 재구성하여 최종 도구 호출 딕셔너리를 생성해야 하는 경우가 많습니다. 그 결과, 이러한 영역의 최종 출력은 원시 텍스트와 매우, 매우 다르게 보일 수 있습니다. 이 최종 파싱은 region_close에 도달할 때만 수행됩니다. 그때까지 dirty 청크를 어떻게 처리할지는 사용자의 선택입니다. 사용자에게 “원시” 출력을 보여주기 위해 그대로 표시하거나, 표시할 수 있는 깨끗한 결과가 나올 때까지 기다리는 것도 가능합니다.

이로써 response template을 사용하는 데 필요한 대부분의 내용을 다뤘습니다. 이 문서의 나머지 부분은 파싱 시스템의 내부 구조와 response template 작성법에 초점을 맞춥니다. 이는 주로 개발자와 모델 작성자에게 관련이 있습니다. 대부분의 사용자는 여기서 안전하게 멈추셔도 됩니다!

고급: 응답 템플릿 작성

응답 템플릿 작성 방법을 이해하는 가장 좋은 방법은 구체적인 예시를 선택하는 것입니다. SmolLM의 원시 응답은 다음과 같은 형태일 수 있습니다:

<think>
I should greet the user
</think>

<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>

이 출력을 표준 메시지 딕트 형식으로 파싱하면 다음과 같이 보일 것입니다:

{
    "role": "assistant",
    "reasoning_content": "I should greet the user",
    "tool_calls": [
        {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}
    ]
}

그리고 이를 파싱하는 템플릿은 다음과 같습니다. 겁먹지 마세요 - 대부분은 스스로 설명이 됩니다!

{
    "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",
        },
    },
}

본질적으로 템플릿은 필드와 구분자를 정의합니다. 각 필드는 출력 딕셔너리의 키에 대응합니다. 필드에는 해당 구분자 안의 텍스트를 파싱하기 위한 정보도 포함됩니다. 한 가지 세부 사항이 있습니다. content 필드는 open이 없습니다. SmolLM(그리고 몇몇 다른 모델)에서는 특수 토큰으로 표시되지 않기 때문입니다. 대신 content는 다른 영역의 공간 이후, 시퀀스 종료 이전에 저장됩니다. 우리의 템플릿에서는 이를 다른 영역에 의해 주장되지 않는 모든 텍스트를 가져오는 암시적 / 나머지 필드로 표현합니다.

필드 이름은 모델의 채팅 템플릿이 읽는 키와 일치해야 하며, 이를 통해 파싱된 메시지를 채팅에 추가하고 다시 렌더링할 수 있습니다. 특히 추론 필드 이름은 reasoning_content로 지정해야 합니다. 이는 메시지 딕셔너리에서 추론을 위한 표준 키이며, 거의 모든 채팅 템플릿이 추론을 읽는 데 사용하는 키입니다.

fields 외에도 템플릿은 두 개의 최상위 키를 지원합니다:

  • defaults (선택) — 출력에 미리 채워 넣을 값의 dict입니다 (예: {"role": "assistant"}). 여기에 포함된 키는 해당 필드가 아무것도 기록하지 않았더라도 파싱된 출력에 항상 유지됩니다. 다른 키는 해당 필드가 아무것도 기록하지 않은 경우 제거됩니다.
  • start_anchor (str) / start_anchor_pattern (str regex) — 채팅 프롬프트 내에서 현재 어시스턴트 메시지가 시작되는 위치를 표시합니다. parse_response 또는 get_response_parser에 prefix=를 전달하면 파서는 처리하기 전에 이 앵커의 마지막 발생 위치 이후의 prefix를 오른쪽에서 잘라냅니다. 이를 통해 다중 턴 대화에서 이전 턴이 현재 메시지의 상태를 오염시키지 않습니다. 앵커는 prefix에만 적용되며, 파싱하는 응답/생성 결과에는 절대 적용되지 않습니다. 일부 형식은 메시지의 중간에서 이 앵커를 정당하게 다시 출력합니다(gpt-oss harmony 출력은 모든 채널을 <|start|>assistant로 시작합니다). 따라서 앵커 이후의 응답을 제거하면 모델 자체의 추론과 도구 호출이 누락됩니다. 이것이 생성 결과만으로는 히스토리 누출을 방지할 수 없는 이유이며, 프롬프트를 prefix=로 전달해야 합니다. ChatML 스타일 모델의 경우 앵커는 일반적으로 "<|im_start|>assistant\n"입니다. start_anchor 또는 start_anchor_pattern 중 정확히 하나만 설정해야 합니다.

예를 들어, 이 다중 턴 접두사가 주어졌을 때(두 개의 어시스턴트 턴에 주목):

<|im_start|>user
Hi<|im_end|>
<|im_start|>assistant
Hello!<|im_end|>
<|im_start|>user
Again?<|im_end|>
<|im_start|>assistant

파서는 마지막 <|im_start|>assistant\n까지의 모든 내용을 잘라내며, 이전의 "Hello!" 턴은 삭제됩니다. 규칙은 마지막 어시스턴트 턴을 제외한 모든 것이 항상 삭제된다는 것입니다.

채팅 템플릿과 마찬가지로, 응답 템플릿은 토크나이저 속성으로 저장되며 토크나이저와 함께 저장됩니다. 채팅 템플릿과 달리, 응답 템플릿은 tokenizer_config.json 안에 저장되며 별도의 파일로 저장되지 않습니다. 이는 채팅 템플릿의 Jinja 스크립트와 달리 응답 템플릿의 형식이 JSON에 자연스럽게 적합하기 때문입니다.

tokenizer.response_template = template
tokenizer.save_pretrained(...)  # Written as a key in tokenizer_config.json

고급: 필드 API 참조

각 필드는 여러 키를 지원합니다. 이 키들은 두 가지 유형으로 나눌 수 있습니다. 첫째, 필드가 어떻게 캡처되어야 하는지를 정의하는 키가 있습니다:

키유형목적
openstr 또는 list[str]이 영역을 시작하는 리터럴 문자열입니다. 문자열 목록은 "이 중 하나와 일치"를 의미합니다.
open_patternstr (정규식)open의 정규식 대안입니다. 이름이 지정된 그룹은 transform에서 사용할 수 있는 캡처 변수가 됩니다.
closestr 또는 list[str]이 영역을 닫는 리터럴 문자열(또는 문자열 목록). 생략하면 스트림의 끝까지 실행됩니다.
close_patternstr (정규식)close의 정규식 대안입니다. 이름이 지정된 그룹은 transform에서 사용할 수 있는 캡처 변수가 됩니다.
repeatsbooltrue인 경우, 필드는 리스트이며 각 매치는 추가됩니다. 기본값은 false입니다.
joinstrrepeats를 사용하면 매칭 결과를 리스트 대신 이 구분자를 사용하여 하나의 문자열로 연결합니다.
optionalboolfalse이고 region이 절대 일치하지 않으면 오류를 발생시킵니다. 기본값은 true입니다.

필드는 open 또는 open_pattern 중 하나만 가져야 하며, close와 close_pattern도 마찬가지입니다.

필드는 close/close_pattern을 완전히 생략할 수 있으며, 이 경우 해당 영역은 생성된 텍스트의 끝까지 열려 있는 상태로 유지됩니다. 이는 메시지의 끝까지 이어지는 마지막 필드에 유용합니다.

neither open도 open_pattern도 없는 필드는 implicit 필드입니다. 명시적인 region이 열려 있지 않을 때 항상 활성화되므로, 나머지 텍스트를 캡처합니다. implicit 필드는 최대 1개만 사용할 수 있습니다. 이는 content에 special token 태그가 없는 경우, 즉 다른 필드 뒤에 plain text로만 작성되는 경우에 가장 자주 사용됩니다.

개방 및 종결 구분자 외에도 repeats를 지정할 수 있으며, 이는 해당 필드가 목록임을 나타내고 구분자가 여러 번 일치할 수 있음을 의미합니다. 이는 모델이 여러 도구 호출을 동시에 출력하는 병렬 도구 호출(parallel tool calling)에서 가장 흔합니다:

'<tool_call>{"name": "a", ...}</tool_call><tool_call>{"name": "b", ...}</tool_call>'
# Returns `"tool_calls": [{... "a" ...}, {... "b" ...}]` in a template with repeats: true

다만, 모든 반복 필드를 리스트로 변환해야 하는 것은 아닙니다. 일부 모델은 단일 메시지에서 여러 reasoning_content 또는 content 블록을 출력하는데, 이러한 경우 자연스러운 출력은 하나의 연결된 문자열입니다. join(분리 문자열, 보통 ""만 사용)를 설정하면 repeats 필드가 리스트를 수집하는 대신 일치하는 항목들을 연결하도록 전환됩니다:

field = {"reasoning_content": {"open": "<think>", "close": "</think>", "repeats": True, "join": "\n"}}
input = "<think>first</think>...<think>second</think>"
# Returns: {"reasoning_content": "first\nsecond"}

join 필드의 각 매치는 문자열로 파싱되어야 합니다. 스트리밍 중에는 region_close가 여전히 각 블록의 자체 값을 포함하며, 구분자는 최종 메시지 dict에만 나타납니다.

마지막으로, 반드시 존재해야 하는 필드에 대해 optional: false를 지정할 수 있습니다. 이러한 필드가 누락된 경우, 해당 필드가 없는 메시지 딕셔너리를 반환하는 대신 오류를 발생시킵니다.

생성 종료 시 닫히지 않은 모든 영역을 닫고 최종 처리합니다. 닫는 구분자가 확인되지 않은 경우에도 마찬가지입니다.

필드 내용 파싱

필드를 캡처하는 방법을 정의한 후에는 해당 캡처 내부의 원시 텍스트를 파싱하는 방법도 지정해야 합니다. 이를 제어하는 키는 네 가지입니다:

키유형목적
contentstr이 영역의 콘텐츠 유형입니다. 기본값은 "text"입니다. 각 유형에는 고유한 파서가 있습니다.
content_argsdict이 영역의 콘텐츠 파서에 전달할 인자입니다.
transformdict/list파싱된 본문을 재구성하는 선택적 후처리 템플릿(자세한 내용은 Transform을 참조).
transform_eachbooltrue인 경우, 파싱된 콘텐츠는 리스트여야 하며 transform이 요소별로 적용됩니다.

첫 번째(그리고 가장 중요한) 키는 content입니다. 이는 필드의 콘텐츠 유형을 나타내며, 필드에 캡처된 원시 텍스트를 최종 출력으로 변환하는 데 사용할 파서를 결정합니다. content_args는 파서를 구성하는 데 사용되며, 커스텀 코드 없이 다양한 형식의 특이사항을 지원할 수 있게 합니다. 각 파서 유형과 그 인수를 차례로 살펴보겠습니다.

기본 유형

text, int, float, bool은 기본 타입입니다. 이러한 콘텐츠 타입은 모두 공백을 제거한 후 필요 시 간단한 타입 변환을 수행합니다. text를 제외하고는 content_args가 없으며, text는 캡처된 텍스트의 앞뒤 공백을 제거하는 strip 인수를 지원하며 기본값은 true입니다.

field = {"count": {"open": "<n>", "close": "</n>", "content": "int"}}
input = "<n> 42 </n>"
# Returns: {"count": 42}

json

json 파서는 캡처된 텍스트를 JSON으로 파싱합니다. 도구 호출 인자와 기타 중첩 구조가 있는 데이터의 핵심 역할을 수행합니다. 실제 환경에서 모델이 JSON을 어떻게 변형시키는지 다양한 경우를 처리하기 위해 몇 가지 선택적 content_args를 지원합니다:

  • unquoted_keys (bool, 기본값 false): 키 이름이 따옴표로 감싸지지 않은 원시(raw) 형태일 때 활성화합니다 (예: {name: "foo"}). 엄격한 JSON 대신 Javascript 스타일의 객체 리터럴을 출력하는 모델에 유용합니다.
  • string_delims ([open, close] 쌍의 목록, 선택 사항): 문자열 값을 "..." 대신 사용자 정의 구분자로 감싸는 모델에 사용됩니다. 각 쌍은 시작 및 종료 마커를 지정합니다.
  • allow_non_json (bool, 기본값 false): 파싱이 실패하면 예외를 발생시키지 않고 줄바꿈을 제거한 원시 텍스트를 반환합니다. 모델이 보통 JSON을 출력하지만 가끔 일반 텍스트로 출력하는 필드에 대한 폴백으로 유용합니다.

unquoted_keys와 string_delims는 모두 비표준적이고, JSON에 거의 가깝지만 완전히 JSON이 아닌 출력을 내보내는 모델을 처리하기 위해 존재하므로, 소수의 모델에서만 필요할 것입니다.

field = {"args": {"open": "<args>", "close": "</args>", "content": "json", "content_args": {"unquoted_keys": True}}}
input = '<args>{city: "London"}</args>'
# Returns: {"args": {"city": "London"}}

xml-inline

xml-inline 파서는 평탄한 순서의 XML 유사 태그로 구성된 영역에 사용되며, 각 태그가 dict의 하나의 항목이 됩니다. JSON blob이 아닌 각 인수를 개별 태그로 출력하는 모델의 tool_calls 필드 내에서 가장 자주 사용됩니다:

  • tag_pattern (str, 필수): 단일 태그와 일치하는 정규식입니다. 결과 딕셔너리의 키가 되는 key와 딕셔너리 값이 되는 원시 텍스트를 나타내는 value라는 이름이 붙은 그룹을 포함해야 합니다.
  • value_parser (dict, optional): 각 캡처된 value에 적용되는 중첩 콘텐츠 파서. name(파서, 예: "json", "int")과 선택적 args(해당 content_args)를 가진 dict입니다. 생략하면 값은 원시 문자열로 유지됩니다.
  • merge_duplicates (bool, 기본값 false): 동일한 키가 여러 번 나타나는 경우, 후속 매칭이 이전 값을 덮어쓰지 않도록 값을 리스트로 수집합니다.

예를 들어, Qwen3은 각 tool-call 인수를 <parameter> 태그로 출력하며, 이를 다음과 같이 파싱합니다:

"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}"}},
}

중첩된 value_parser에 유의하세요. 각 매개변수 값은 json 파서(allow_non_json로 일반 문자열도 통과하도록 설정)를 다시 거칩니다. 위의 tool_calls 필드에 이 입력을 전달하면:

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

kv-lines 파서는 줄 구분 key: value 쌍을 처리합니다(YAML 스타일의 메타데이터나 .env 파일처럼). 각 줄은 결과 딕셔너리의 하나의 항목이 됩니다. 모든 인수는 선택 사항입니다:

  • line_sep (str, 기본값 "\n"): 쌍 사이의 구분자.
  • kv_sep (str, 기본값 ":"): 한 줄 안에서 키와 값 사이의 구분자입니다. 첫 번째 발생 위치만 분리점으로 사용되므로 값 자체에 구분자가 포함될 수 있습니다.
  • strip (bool, 기본값 true): 각 키와 값의 앞뒤 공백을 제거합니다.
  • value_parser (dict, optional): 각 값에 적용되는 중첩 콘텐츠 파서로, xml-inline와 동일한 {"name": ..., "args": ...} 형식입니다. 생략하면 값은 원시 문자열로 유지됩니다.

빈 줄이거나 kv_sep를 포함하지 않는 줄은 조용히 건너뛰므로, 캡처된 영역에 있는 불필요한 빈 줄은 허용됩니다.

field = {"metadata": {"open": "<meta>", "close": "</meta>", "content": "kv-lines"}}
input = "<meta>name: alice\nage: 30</meta>"
# Returns: {"metadata": {"name": "alice", "age": "30"}}

Note age는 "30"을 문자열로 유지합니다. 30으로 파싱하려면 value_parser에 {"name": "int"}를 추가하세요.

Typing tool-call arguments

가끔 응답 파서가 잘못된 타입으로 모델 출력을 파싱할 수 있습니다. 예를 들어, 1.5라는 float를 “1.50”으로 파싱할 수 있습니다. 도구가 특정 타입의 인수를 기대하지만 다른 타입의 인수를 받는 경우, 이것은 도구 호출에 문제를 일으킬 수 있습니다. 이를 방지하려면, 요청의 tools를 parse_response() 또는 ResponseParser에 전달하여 각 도구의 JSON Schema parameters를 사용해 타입 캐스팅을 수행할 수 있습니다. 도구는 apply_chat_template()와 동일한 형식으로 허용됩니다: JSON 스키마, 또는 스키마로 자동 변환되는 타입 힌트와 docstring이 있는 Python 함수.

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 키가 필요하지 않습니다. 파싱된 본문을 최종 출력으로 재구성해야 하거나, 구분자에서 가져온 정보를 결과에 병합해야 할 때 사용됩니다. 복잡한 구조를 가지는 경우가 많기 때문에 tool_calls 필드에서 가장 자주 나타납니다.

transform은 템플릿입니다. 출력 형상을 설명하는 dict(또는 list)이며, "{name}" 형태의 문자열은 해당 값으로 대체됩니다. 값은 content(해당 영역의 파싱된 본문)와 open_pattern / close_pattern으로 캡처된 모든 이름 지정 그룹에서 접근할 수 있습니다. 매우 일반적인 사용 사례는 도구 호출 dict를 function 키를 가진 외부 dict로 감싸는 것입니다. 이는 표준 도구 호출 형식의 일부이기 때문입니다:

"tool_calls": {
    "open": "<tool_call>",
    "close": "</tool_call>",
    "repeats": True,
    "content": "json",
    "transform": {"type": "function", "function": "{content}"},
},

따라서 이 원시 출력은:

<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>

다음과 같이 변경됩니다(repeats: True는 tool_calls가 리스트가 되도록 만듦):

[{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}]

"{content}"과 같은 전체 문자열 플레이스홀더는 조회된 값을 타입을 유지한 채 반환합니다. 따라서 위 예시에서 파싱된 JSON dict가 function의 값으로 직접 삽입됩니다. 플레이스홀더는 문자열 전체를 차지해야 하며, 텍스트와 플레이스홀더를 혼합하는 것("abc {name} def")은 허용되지 않습니다. f-string이 아닙니다!

Placeholders는 "{content.args}"와 같은 점 경로(dotted path)를 사용할 수도 있으며, 이는 content를 조회한 후 파싱된 dict의 args 키로 내려갑니다. 이는 키 이름이 표준 형식과 일치하지 않는 tool-call 본문을 재구성하는 방법입니다. 예를 들어, {"name": ..., "args": ...}를 출력하는 모델의 경우 args를 arguments로 변경해야 합니다:

"tool_calls": {
    "open": "<tool>",
    "close": "</tool>",
    "repeats": True,
    "content": "json",
    "transform": {"type": "function", "function": {"name": "{content.name}", "arguments": "{content.args}"}},
},

해결되지 않는 경로(여기서는 args가 없는 도구 호출 본문)는 잘못된 형식의 도구 호출을 출력하는 대신 오류를 발생시킵니다.

transform은 매우 다재다능하며, 모델 출력이 표준 API와 완전히 다른 형식을 가질 때 필요합니다. GPT-OSS가 좋은 예시입니다. 이 모델은 함수 이름을 JSON 본문이 아닌 채널 헤더에 포함하므로, open_pattern의 명명된 그룹으로 이를 캡처한 뒤 transform 내에서 content와 병합해야 합니다. open_pattern과 close_pattern의 모든 명명된 그룹은 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}"}},
},

함수 이름은 JSON 본문이 아닌 채널 헤더에 있으므로, 이 경우:

<|channel|>commentary to=functions.get_current_weather <|constrain|>json<|message|>{"location": "San Francisco, CA"}<|call|>

다음과 같이 변경됩니다:

[{"type": "function", "function": {"name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}}}]

때로는 필드의 파싱된 내용이 그 자체로 레코드 목록이며 각 요소를 재구성하고 싶을 수 있습니다. Cohere 템플릿이 좋은 예시입니다. 이 템플릿은 모든 도구 호출을 단일 JSON 배열 안에 출력하므로, transform_each: True를 설정하여 각 요소별로 변환을 적용합니다. 각 배열 요소의 키는 템플릿 스코프로 풀리므로, "{tool_name}"는 현재 요소에서 tool_name을 조회합니다:

"tool_calls": {
    "open": "<|START_ACTION|>",
    "close": "<|END_ACTION|>",
    "content": "json",
    "transform_each": True,
    "transform": {"type": "function", "function": {"name": "{tool_name}", "arguments": "{parameters}"}},
},

이것은 다음과 같은 출력을 변환합니다:

[
    {"tool_name": "greet_user", "parameters": {"greeting": "Hi!"}},
    {"tool_name": "search", "parameters": {"query": "weather tomorrow"}}
]

다음과 같은 표준 API에 맞는 출력으로 변환합니다:

[
    {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}},
    {"type": "function", "function": {"name": "search", "arguments": {"query": "weather tomorrow"}}}
]

transform_each 플래그는 content가 이미 리스트인 경우에만 필요합니다. 각 매치가 하나의 요소를 기여하는 더 일반적인 경우(repeats: True가 이를 누적)에는 기본적으로 각 요소에 변환이 적용됩니다.

프레임워크 개발자: 정규식 이식성

open_pattern, close_pattern, start_anchor_pattern은 정규식 문자열입니다. 대부분의 사용자, 심지어 대부분의 모델 작성자에게도 이는 문제가 되지 않지만, 다른 언어로 응답 파싱 구현을 작성하는 개발자라면 우리의 구현 세부 사항을 알아두는 것이 좋습니다. 이 섹션은 Python이 아닌 chat templating을 작동시키기 위해 Jinja 파서를 통째로 구현해야 했던 모든 분을 위해 마련되었습니다. 아래 간단한 가이드라인을 따르시면 응답 템플릿 작업이 훨씬 수월해질 것입니다:

  • 채팅 파싱에 사용되는 모든 정규식은 Python의 regex 모듈을 사용합니다. 모든 Python3 문자열이 unicode이므로, 모든 정규식 매치는 unicode를 인식합니다. 이는 특히 \w와 같은 일반 문자에 영향을 미칩니다. 엔진에서 관련 unicode 플래그를 설정했는지 확인하세요.
  • 모든 정규식을 re.DOTALL를 활성화하고 re.MULTILINE를 비활성화한 상태로 컴파일하므로, .는 \n와도 일치하지만 ^와 $는 줄바꿈이 아닌 전체 입력의 시작/끝만 일치합니다.
  • 이름 지정 그룹에는 (?P<name>...) 구문을 사용합니다. 다른 정규식 구현체들은 이름 지정 캡처 그룹 구문이 매우 다르므로, 정규식에서 이 패턴을 찾아 해당 로컬 구현체에 맞게 다시 작성해야 할 수 있습니다.
  • 데이터를 출력할 수 있는 시점을 결정하기 위해 부분 정규식 매칭을 사용합니다. 이는 영역의 끝이 여러 토큰으로 구성될 수 있으며, 영역 끝 구분자를 출력하고 싶지 않으므로, 해당 토큰이 영역의 경계 일부가 아니라 영역 내부에 속하는지 확신할 때까지 토큰을 보류해야 하기 때문입니다. 즉, 영역 끝 정규식이 부분 매칭되면, 정규식이 완전히 매칭되거나 실패할 때까지 데이터를 보류합니다. 정규식 엔진이 부분 매칭을 지원하지 않더라도 응답 템플릿을 구현할 수는 있지만, 이 문제를 해결하기 위해 다른 방법을 찾아야 할 수 있습니다. 하나의 간단한 접근법은 끝 구분자를 명확히 확인할 때까지 해당 영역을 보류하거나 dirty로 출력하는 것입니다.
  • 정규식 엔진이 (?!...)와 같은 lookaround를 지원하지 않을 수 있습니다(드물게). 이러한 기능은 응답 템플릿에서 흔히 사용되지는 않지만 나타날 수 있으며, 저희는 이를 지원합니다! 해당 경우 오류를 발생시키거나, 정규식 엔진이 가능한 매치를 찾았을 때 lookaround를 수동으로 추출하여 코드에서 이를 강제해야 할 수 있습니다.
  • 백레퍼런스, 원자 그룹, 소유적 양화사, 재귀 등 다른 고급 기능은 일반적으로 응답 템플릿에서 사용되지 않습니다. 모델 작성자가 이러한 기능을 사용하지 않도록 유도할 것이므로, 안전하게 무시하셔도 됩니다.
Update on GitHub