-
같은 LLM인데 직접 실행하면 답이 달라지는 이유: chat templateAI Agent 2026. 9. 22. 08:37728x90반응형

대화의 역할과 경계를 정해 전달하는 AI 생성 개념 이미지입니다. 오픈 모델의 대화 결과가 이상하면 프롬프트 문장보다 먼저 해당 모델의 chat template와 특수 토큰 적용을 확인하세요.
같은 이름의 모델을 웹 데모와 로컬 코드에서 실행했는데 한쪽은 답변을 잘하고 다른 쪽은 사용자 말을 계속 이어 씁니다. 프롬프트를 길게 고치기 전에 실제로 모델에 전달한 토큰열이 같은지 볼 필요가 있습니다.
대화용 API에서 role과 content를 보냈더라도 모델이 그 JSON 구조를 그대로 읽는 것은 아닙니다. 실행 도구가 대화를 직렬화하는 과정이 사이에 있습니다. 이 글은 2026년 9월 13일 확인한 Transformers v5.17.0 문서를 기준으로 텍스트 대화의 입력 형식을 진단합니다. 실제 모델 답변을 생성하거나 품질 점수를 측정한 기록은 아닙니다.
대화 배열도 마지막에는 하나의 토큰열이 됩니다
인과적 언어 모델은 앞의 토큰열을 이어서 다음 토큰을 생성합니다. 대화 메시지 목록은 모델이 학습한 역할 표시, 메시지 구분, 종료 표시 등을 포함한 형식으로 변환됩니다. 이 변환을 맡는 것이 chat template입니다. 대화 모델과 템플릿
같은 기반 모델에서 파생된 대화 모델이라도 구분 형식은 다를 수 있습니다. 어떤 모델의 예시에서 본 역할 토큰을 다른 모델에도 그대로 붙이는 방식은 피해야 합니다. 사람이 읽기에 비슷한 대화라도 모델에 들어가는 제어 토큰은 달라질 수 있습니다.
또한 role 값 자체가 모델의 안전한 권한 경계를 자동으로 만들어 주는 것은 아닙니다. 이 글에서 다루는 것은 학습된 대화 형식에 맞게 입력하는 과정입니다. 도구 실행 권한이나 신뢰할 수 없는 자료를 처리하는 보안 설계까지 템플릿 하나로 해결했다고 해석하지 않습니다.
진단의 출발점은 질문 문장을 바꾸는 것이 아니라 “이 메시지 목록이 어떤 문자열과 토큰 ID로 바뀌었나”입니다. 사용자 발화 뒤에서 끝났는지, assistant 답변의 시작을 표시했는지 확인하면 이상한 이어쓰기의 원인을 좁힐 수 있습니다.
템플릿은 파일 이름보다 실제 로드된 값을 봅니다
Transformers에서는 토크나이저의 chat_template을 확인할 수 있습니다. v5.17.0 문서는 독립된 chat_template.jinja와 추가 이름별 템플릿, 과거 설정 파일 안에 저장된 형식 등을 설명합니다. 독립 Jinja 파일이 설정 안의 템플릿보다 우선하는 로딩 규칙도 있습니다. 템플릿 저장과 로딩 우선순위
따라서 tokenizer_config.json만 고쳤는데 동작이 같다면 다른 템플릿 파일이 우선했는지 확인할 수 있습니다. 반대로 파일 하나를 찾았다는 이유만으로 실제 실행에서 그 파일이 선택됐다고 단정하지 않습니다.
도구 사용용과 기본 대화용처럼 이름이 여러 개인 템플릿도 있습니다. tools 같은 추가 입력에 따라 선택이 달라질 수 있으므로 실행 환경에서 사용한 인자도 함께 기록합니다. 이 글의 예시는 도구 호출과 이미지가 없는 단순 텍스트 대화로 범위를 좁힙니다.
확인 자료에는 모델 파일의 리비전, 토크나이저 파일과 템플릿, Transformers 버전, 실제 메시지 목록을 남깁니다. 공개 질문을 올릴 때는 개인 대화나 인증 정보가 출력에 섞이지 않도록 작은 가상 메시지로 바꾸어 재현합니다.
두 메시지를 문자열과 토큰 ID로 확인해 봅니다
아래 tokenizer는 출처와 파일을 검토한 뒤 해당 모델과 같은 리비전으로 로컬에 준비한 텍스트용 토크나이저라고 가정합니다. 다운로드와 로딩 과정, 모델의 생성 실행은 포함하지 않습니다. system 역할을 지원하지 않는 템플릿이라면 모델 문서의 역할 제약부터 확인해야 합니다.
예시 질문은 “동기화와 백업의 차이를 설명해 주세요”입니다. 시간이나 외부 변수에 따라 렌더링 결과가 바뀌지 않는 템플릿을 사용하고, 패딩과 잘림은 적용하지 않는다는 조건입니다.
messages = [ {"role": "system", "content": "한국어로 명확하게 설명해 주세요."}, {"role": "user", "content": "동기화와 백업의 차이를 설명해 주세요."}, ] if not tokenizer.chat_template: raise ValueError("해당 모델의 대화 템플릿을 확인해야 합니다.") rendered = tokenizer.apply_chat_template( messages, tokenize=False, return_dict=False, add_generation_prompt=True, ) encoded = tokenizer.apply_chat_template( messages, tokenize=True, return_dict=True, add_generation_prompt=True, padding=False, truncation=False, ) manual_ids = tokenizer( rendered, add_special_tokens=False, padding=False, truncation=False, )["input_ids"] assert encoded["input_ids"] == manual_ids print(repr(rendered)) print(encoded["input_ids"])첫 결과는 사람이 볼 수 있는 형식화 문자열이고, 두 번째는 모델 입력에 사용할 토큰 ID입니다. repr로 문자열을 보면 줄바꿈과 공백도 확인할 수 있습니다. 출력 내용을 이 글에 임의로 채우지 않은 이유는 실제 템플릿에 따라 역할·종료 토큰과 ID가 달라지기 때문입니다.
마지막 assert는 같은 조건의 두 경로가 같은 ID를 만드는지 확인하는 진단입니다. 실패했다고 어느 쪽 값을 억지로 맞추기 전에 선택된 템플릿, 추가 인자, 토크나이저 옵션과 동적으로 들어간 값을 확인합니다. 템플릿이 날짜나 시간을 직접 넣는다면 두 번의 렌더링이 같은 입력 조건인지부터 다시 맞춰야 합니다. 템플릿의 추가 변수
v5.17.0의 apply_chat_template API는 return_dict 기본값이 True입니다. 이 예시에서는 문자열 경로는 False, 토큰 경로는 True로 명시해 반환 형식을 구분했습니다. 예전 코드의 리스트 반환 가정을 그대로 적용하기 전에 사용하는 버전의 시그니처를 확인합니다. apply_chat_template API
문자열을 다시 토큰화할 때 특수 토큰을 중복하지 않습니다
템플릿에는 보통 모델이 필요로 하는 특수 토큰이 이미 들어 있습니다. tokenize=False로 만든 문자열을 다시 토큰화하면서 add_special_tokens=True를 적용하면 시작·종료 토큰 등을 불필요하게 더할 수 있습니다. 공식 가이드가 재토큰화 때 False를 안내하는 이유입니다. 특수 토큰 중복 주의
앞의 예시에서는 직접 토큰을 받는 경로와 문자열을 거쳐 받는 경로를 비교했습니다. 보통은 apply_chat_template의 tokenize=True 경로를 사용해 중간 변환을 줄일 수 있습니다. 문자열은 진단을 위해 읽는 자료이지 언제나 다시 조립해야 하는 단계는 아닙니다.
모든 모델에 같은 시작 토큰을 수동으로 붙이거나, 이미 템플릿을 적용한 문자열을 다시 user 메시지 안에 넣어 템플릿을 한 번 더 적용하는 것도 확인할 항목입니다. 이런 이중 처리는 원래 의도한 대화 구조와 다른 입력을 만들 수 있습니다.
디코딩한 문자열을 확인할 때 특수 토큰을 숨기는 옵션만 사용하면 중복이나 누락이 보이지 않을 수 있습니다. 진단에서는 필요한 제어 토큰을 볼 수 있게 하고, 사용자에게 보여 줄 최종 답변의 후처리와 구분합니다.
새 답변 시작과 기존 답변 이어쓰기는 다릅니다
add_generation_prompt=True는 새 assistant 답변의 시작을 나타내는 토큰을 추가하도록 템플릿에 전달하는 옵션입니다. 사용자가 묻고 assistant가 새로 답하는 경우를 표현하는 데 쓰입니다. 다만 모든 모델이 같은 시작 표시를 요구하지 않으므로 모델에 따라 효과가 없을 수 있습니다. 생성 시작 표시
continue_final_message는 마지막 메시지를 닫지 않고 이어 쓰도록 하는 다른 목적의 옵션입니다. 예를 들어 assistant 답변의 앞부분을 미리 넣은 뒤 이어서 생성하려는 상황입니다. 새 메시지를 시작하는 옵션과 같은 의미가 아닙니다.
두 옵션을 동시에 켜면 안 됩니다. 또한 TextGenerationPipeline은 마지막 메시지 역할에 따라 새 답변 시작과 이어쓰기를 다르게 선택할 수 있으므로, 직접 generate를 호출한 코드와 비교할 때 그 차이도 확인합니다. 마지막 메시지 이어쓰기
질문을 user 역할로 끝냈는지, 빈 assistant 메시지를 덧붙였는지, assistant의 일부 답변을 넣었는지는 입력 계약의 차이입니다. “질문 문장은 같다”는 사실만으로 같은 모델 입력이라고 말할 수 없습니다.
토큰열을 맞춘 뒤에는 생성 조건을 비교합니다
템플릿이 올바르다고 모든 환경의 답변이 같아지는 것은 아닙니다. 모델 가중치와 양자화 방식, 토크나이저 리비전, 실행 엔진, 샘플링과 종료 조건도 결과에 영향을 줄 수 있습니다.
Transformers의 생성 설정에는 do_sample, temperature, top_p, 생성 토큰 한도와 종료 조건 등이 있습니다. 웹 데모의 실제 설정을 모른다면 로컬 실행과 같은 조건이라고 단정하지 않고 미확인 항목으로 남깁니다. GenerationConfig
실무에서는 먼저 같은 짧은 메시지를 형식화한 문자열과 토큰 ID를 비교하고, 차이가 없을 때 생성 설정으로 넘어가는 순서가 유용합니다. 한 번에 프롬프트·템플릿·온도·토큰 한도를 모두 바꾸면 무엇이 원인이었는지 알기 어렵습니다.
입력이 너무 길어 잘린 문제와 답변이 생성 한도 때문에 짧게 끝난 문제도 따로 봅니다. 템플릿이 추가한 제어 토큰까지 포함한 최종 입력 길이를 확인하되, 긴 문맥과 출력 예산의 설정 자체는 별도의 문제입니다.
템플릿을 수정할 때는 원본과 비교 근거를 남깁니다
지원하지 않는 역할 오류가 나거나 템플릿이 없다는 메시지를 봤다고 임의의 다른 모델 템플릿을 붙여 끝내지는 않습니다. 모델이 실제로 학습한 형식과 배포자가 제공하는 토크나이저·템플릿 조합을 먼저 확인합니다.
직접 수정해야 한다면 원본 파일과 수정한 부분, 같은 메시지의 전후 토큰열을 남깁니다. 모델 답변 한 개가 자연스러워졌다는 인상만으로 모든 입력에서 맞는 템플릿이라고 판단하지 않습니다. 여러 턴, 빈 내용, 지원 역할과 종료 조건을 각각 확인해야 합니다.
같은 LLM인데 결과가 달라졌다는 문제는 프롬프트 문장만의 문제가 아닐 수 있습니다. 대화가 어떤 토큰열로 변환됐는지부터 확인하고, 그다음 모델과 생성 조건을 맞춰 가면 원인을 분리할 수 있습니다. 템플릿 검수의 목적은 모델에 의도한 대화를 전달했는지 확인하는 것입니다. 입력 조건을 확인한 뒤에야 출력 차이의 다른 원인을 비교할 수 있습니다.
728x90반응형'AI Agent' 카테고리의 다른 글
이미지 읽는 AI에 영수증을 맡기기 전, 무엇을 대조해야 할까 (0) 2026.09.23 safetensors 파일이면 모델을 안심하고 내려받아도 될까 (0) 2026.09.23 임베딩 모델만 바꿨는데 검색이 이상해졌다면 (1) 2026.09.22 LLM의 KV 캐시를 CPU로 옮기면 어떤 대가가 생길까 (0) 2026.09.22 학습·테스트를 나눴는데도 점수가 과하게 좋은 이유 (0) 2026.09.21