모델이 지침을 무시하기 전: LLM 0.37.1의 system·messages 전달 누락 점검
시스템 지침에 “한국어로 답하라”라고 적었는데 응답이 어긋나면, 모델부터 바꾸고 싶어진다. 하지만 지침이 모델까지 도착하지 않았다면 모델 교체는 원인과 다른 곳을 고치는 일이 된다. Simon Willison의 Python 라이브러리 LLM 0.37.1은 바로 이 구분이 필요한 요청 구성 결함을 수정했다. 여기서 LLM은 대규모 언어 모델 일반을 뜻하는 말이 아니라, 해당 이름의 CLI·Python 라이브러리다.
핵심은 특정 조합이다. Python API의 prompt()나 chain()에 system=과 messages=를 함께 넘길 때, prompt.messages로 요청을 구성하는 모델에서 시스템 지침이 빠지던 문제가 수정됐다. 기본 OpenAI Chat 모델도 이 경로에 포함된다. 모든 지침 불이행을 설명하는 버그나 모델 자체의 지능 향상으로 읽어서는 안 된다. 공식 릴리스가 명시한 범위다.
1. 코드의 인자와 실제 전달 메시지를 나눠 본다
system=에 문자열이 있다는 사실과 실제 요청의 첫 메시지가 system이라는 사실은 다르다. 애플리케이션이 별도로 보관한 지침, 라이브러리 내부의 메시지 목록, 어댑터가 만든 요청 본문은 서로 다른 관찰 지점이다. 앞 단계의 값이 정상이라는 이유만으로 다음 단계도 정상이라고 결론 내릴 수 없다.
함수에 지침을 적은 사실만으로 실제 요청에 포함됐다고 단정할 수 없다. 릴리스 범위를 해석한 원본 도식이다. 출처: github.com/simonw/llm/releases/tag/0.37.1.
이번 수정에서 먼저 확인할 것은 답변의 문체가 아니라 요청 구성이다. 함수 인자를 기록한 로그만 보지 말고, 사용 중인 어댑터가 어떤 입력에서 요청을 만드는지 확인해야 한다. 그 어댑터가 prompt.messages를 사용한다면 실제 목록에 role: system이 포함되는지가 중요한 증거다. 제공자마다 표현 방식이 다를 수 있으므로 모든 API가 반드시 같은 JSON 구조를 가진다고 일반화하지 않는다.
실제 요청을 진단할 때는 개인정보나 비밀키가 섞인 전체 로그를 공개하지 않는 편이 좋다. 먼저 역할의 순서와 시스템 메시지의 존재 여부만 확인하고, 필요한 내용은 민감정보를 제거한 별도 환경에서 비교하는 방식이 안전하다. 이는 이번 릴리스가 제공하는 보안 보장이 아니라 이 글의 운영상 제안이다.
2. 빈 messages 목록도 분기 조건이 될 수 있다
이슈 #1849의 작성자는 system=을 지정한 비동기 모델의 chain() 호출을 비교했다. 한쪽에는 messages=[]가 있고 다른 쪽에는 그 인자가 없다. 이슈에 실린 수정 전 출력에서 전자는 사용자 메시지만 남고, 후자는 시스템 메시지와 사용자 메시지가 함께 나타난다. 모델이 답변을 생성한 결과가 아니라 build_messages가 구성한 메시지 목록의 비교다.
다음은 원문의 현상을 이해하기 위한 축약 도식이다. 실제 실행 결과를 새로 측정한 것이 아니며, content 표기도 읽기 쉽게 단순화했다.
입력: system="SYS PROMPT", messages=[], 새 사용자 입력="hello"
수정 전 이슈 예시의 메시지 모양:
[{role: user, content: hello}]
0.37.1 소스가 의도하는 메시지 모양:
[{role: system, content: SYS PROMPT},
{role: user, content: hello}]
왼쪽은 이슈의 수정 전 예시, 오른쪽은 0.37.1 소스에 근거한 의도된 구성이다. 이 글에서 API를 실행한 결과는 아니다. 출처: github.com/simonw/llm/issues/1849.
이 비교가 유용한 이유는 “아무 메시지도 추가하지 않았으니 영향이 없을 것”이라는 가정을 깨기 때문이다. 빈 목록을 명시하는 것과 인자를 생략하는 것은 라이브러리의 서로 다른 분기로 들어갈 수 있다. 대화 이력을 항상 messages=에 넣는 래퍼라면 이 조합을 우선 점검할 이유가 있다. 다만 자체 래퍼가 실제로 영향을 받았다는 증거는 그 래퍼의 요청 목록을 확인해야 얻을 수 있다.
3. 반례: 맨 앞에 system이 이미 있으면?
수정된 0.37.1의 _with_system은 전달된 목록을 복사한 뒤 시스템 텍스트를 준비한다. 추가할 텍스트가 비어 있지 않고, 기존 목록의 첫 메시지가 시스템 메시지가 아닐 때만 앞에 시스템 메시지를 삽입한다. 원본 목록을 그 자리에서 무조건 수정하는 방식도 아니고, 기존 첫 시스템 메시지를 덮어쓰는 방식도 아니다.
특히 messages=의 첫 항목이 이미 system이면 기존 목록의 앞부분을 유지한다. 따라서 “이제 system=이 언제나 최우선으로 합쳐진다”는 설명은 틀리다. _combine_system이 시스템 조각들과 system=을 결합하는 일과, 결합된 텍스트를 메시지 목록에 실제 삽입할지 결정하는 일도 구분해야 한다.
_with_system은 첫 메시지가 이미 system이면 추가 시스템 텍스트를 앞에 넣지 않는다. system=이 언제나 병합·덮어쓴다는 해석을 막는 반례다. 출처: github.com/simonw/llm/blob/0.37.1/llm/models.py#L714-L725.
실무에서는 이 반례가 중요하다. 대화 이력에 예전 시스템 지침이 들어 있는 상태에서 호출 인자의 system=만 바꿨다면, 이번 수정이 새 문자열로 자동 교체해 준다고 기대할 수 없다. 먼저 자신이 전달한 이력의 첫 항목이 무엇인지 확인해야 한다. 이 글은 시스템 메시지들 사이의 일반적인 우선순위 규칙을 새로 제시하는 것이 아니라 해당 함수의 조건문을 해석한다.
4. 응답이 어긋났을 때 쓰는 분기표
아래 표는 공식 설명에 근거해 작성한 진단 순서다. 무조건 업그레이드하거나 모든 시스템 지침을 중복 삽입하라는 처방이 아니다. 먼저 문제 경로에 해당하는지 좁히고, 그다음 버전과 메시지 구성의 일치를 확인하는 데 목적이 있다.
| 지금 확인한 상태 | 다음에 볼 증거 | 섣불리 내리면 안 되는 결론 |
|---|---|---|
| Python API에서 system=와 messages=를 함께 사용한다 | 설치된 LLM 버전, 해당 어댑터의 prompt.messages 사용 여부 | 모든 모델·모든 API에서 지침이 누락된다 |
| 빈 messages=[]만 추가했는데 동작이 달라졌다 | 어댑터가 구성한 시스템·사용자 역할의 순서 | 빈 목록은 항상 인자 생략과 같다 |
| messages의 첫 항목이 이미 system이다 | 기존 첫 시스템 메시지의 실제 내용 | 별도 system=이 기존 지침을 덮어썼다 |
| 실제 요청에 의도한 시스템 지침이 들어 있다 | 지침 간 충돌, 대화 맥락, 응답 평가 기준 | 이번 직렬화 버그가 곧 원인이다 |
| 버전만 바꾸고 요청은 아직 비교하지 않았다 | 같은 입력 조건의 메시지 구성 비교 | 응답 한 번이 좋아졌으니 원인이 확정됐다 |
공식 수정 범위를 바탕으로 작성한 진단 제안이다. 정상 전달과 전달 누락은 서로 다른 조사 경로이며 응답 한 번으로 원인이 확정되지는 않는다. 출처: github.com/simonw/llm/releases/tag/0.37.1.
어댑터 요청에 필요한 지침이 정상적으로 들어 있다면, 적어도 “이번 누락 경로”와 “모델이 받은 뒤 따르지 않은 상황”을 분리할 수 있다. 지침 충돌이나 모델의 응답 특성은 그다음 조사 대상이다. 이 순서가 모든 문제를 해결해 주지는 않지만, 모델의 행동을 탓하기 전에 전달 계층을 확인한다는 명확한 출발점을 준다.
팀에서 회귀 점검 항목을 만든다면 빈 이력, 시스템 메시지 없이 시작하는 이력, 기존 시스템 메시지로 시작하는 이력을 따로 다루는 편이 좋다. 기대 결과도 “답이 마음에 든다”가 아니라 “어떤 역할과 내용이 요청에 들어간다”로 적는 것이 이번 수정의 성격에 맞다. 이는 제안하는 테스트 설계이며, 이 글에서 해당 테스트를 실행하거나 제공자별 결과를 검증한 것은 아니다.
5. 날짜와 다른 수정 사항은 어디까지 읽을까
보존한 GitHub 릴리스 응답에서 0.37.1은 draft=false, prerelease=false로 표시된다. 게시 시각은 2026년 11월 3일 21:41:42 UTC, 한국 시각으로 2026년 11월 4일 06:41:42다. 이는 소스의 릴리스 게시 시각이지 버그가 처음 생긴 시점이 아니다. 또한 이 글의 예정 공개 시점과도 별개의 날짜다. 정확한 도입 버전이나 영향을 받은 모든 플러그인의 목록은 이 자료만으로 확정하지 않는다.
같은 릴리스에는 빈 목록이 대괄호 형태로 저장된 설정에 OpenAI 엔드포인트를 추가할 때 생기던 잘못된 YAML 문제, 비스트리밍 OpenAI 호환 응답에 usage가 없을 때 발생하던 오류의 수정도 포함된다. 그러나 이번 글의 진단 대상은 시스템 지침 전달 경로다. 주변 항목을 근거로 모든 제공자 호환성이나 보안이 검증됐다고 말할 수는 없다. 속도·정확도·비용 절감 수치를 측정했다는 주장도 하지 않는다.
6. 점검의 끝은 모델 교체가 아니라 관찰 지점 정리다
이번 사례의 교훈은 “프롬프트를 더 세게 쓰면 된다”가 아니다. 작성한 지침, 라이브러리가 보관한 메시지, 어댑터가 실제 구성한 요청을 구분해야 한다는 점이다. 지침이 전달되지 않았다면 전달 계층의 문제이고, 정상 전달됐다면 다음 원인을 조사할 수 있다. LLM 0.37.1은 그중 특정한 누락 경로를 바로잡은 라이브러리 수정이다.
아래 요청 모양과 점검표는 공식 릴리스·이슈·태그 소스를 읽고 만든 설명이며, 이 글에서 LLM 설치·모델 호출·수정 전후 재현 실험은 수행하지 않았다. 본문의 흐름도와 비교 그림은 이 해석을 돕기 위한 직접 제작 도식으로, 제품 화면이나 실측 성능 그래프가 아니다.
확인한 원문
- LLM 0.37.1 릴리스: Python API의 시스템 지침 누락 수정과 적용 범위.
- 이슈 #1849:
messages=[]유무에 따른 수정 전 메시지 구성 예시. - 0.37.1 태그의 models.py: 삽입 조건과 기존 선두 시스템 메시지의 처리. 같은 파일의 대화 경로 790–807행, 동기 경로 3365–3375행, 비동기 경로 3495–3504행도 함께 확인했다.




댓글
댓글 쓰기