이전 글에서 멀티턴 대화에서의 컨텍스트 누적을 정리했다. 이번엔 한 단계 더 들어가서, 같은 모델에 여러 페르소나 시스템 프롬프트를 붙여보면서 알게 된 것들.
Table of contents
Open Table of contents
페르소나 실험으로 알게 된 것
같은 모델, 같은 user 입력에 시스템 프롬프트만 다르게 줘서 여러 페르소나로 답을 받아봤다. 결과가 흥미로웠다.
- 페르소나마다 응답 길이가 달랐다. 어떤 페르소나는 짧고 단호하게, 어떤 페르소나는 길고 풀어서 설명하는 식.
- 응답 형식도 달라졌다. 마크다운 헤딩을 자주 쓰는 페르소나가 있는가 하면, 평문으로만 답하는 페르소나도 있었다.
- 출력 형식 자체를 강제할 수 있었다. 시스템 프롬프트에 “JSON으로만 답하라”고 명시하면 대부분 그렇게 따라줬다.
이걸 보면서 든 생각: 이렇게 출력을 통제할 수 있다면, API를 그대로 실서비스에 끼워 넣는 것도 충분히 가능하겠다. (parsing 가능한 JSON 응답을 받으면 곧바로 백엔드 로직과 연결)
그러다 떠오른 의문
근데 한 가지 의문이 있었다.
“시스템 프롬프트에 ‘JSON으로 답하라’ 적는 거랑, 그냥 user 메시지에 ‘~이렇게 해줘’ 라고 적는 거랑 뭐가 다르지?”
겉보기엔 둘 다 모델에게 지시를 전달하는 것처럼 보인다. 직접 정리해본 답.
1. 적용 범위가 다르다
- 시스템 프롬프트: 매 턴에 자동으로 적용된다. 한번 정해두면 대화 끝까지 지속.
- user 메시지에 적은 지시: 해당 턴에만 적용된다. 다음 턴에는 내가 다시 적지 않는 한 모델은 잊는다(정확히는, 모델이 그 지시를 무시할 명분이 생긴다).
→ 페르소나 일관성이 시스템 프롬프트 쪽이 압도적으로 강하다. 10턴, 20턴 가도 같은 어조와 형식이 유지된다.
2. 보안/제어 측면이 다르다
- 시스템 프롬프트: 클라이언트(내 코드)가 통제한다. 사용자는 직접 바꿀 수 없다.
- user 메시지의 지시: 사용자가 자기 마음대로 적을 수 있다. “JSON 말고 그냥 평문으로 답해” 라고 나중에 끼어들면 모델이 따라갈 가능성이 있다.
→ 서비스에 도입할 때 출력 형식이나 행동 규칙은 시스템 프롬프트로 못박아두는 게 안전하다. 사용자 입력이 시스템 프롬프트의 규칙을 함부로 깨지 못한다(완벽하진 않다 — prompt injection이라는 별도 주제가 있다).
3. 모델이 받아들이는 무게감이 다르다
내 체감이지만 — 같은 지시여도 시스템 프롬프트에 둘 때 모델이 더 단호하게 따른다. user 메시지의 요청은 협상 가능한 “부탁”처럼 처리하는 느낌, 시스템 프롬프트는 “규칙”처럼 처리하는 느낌. (이 부분은 내 인상이라 더 정밀한 검증이 필요)
JSON 출력 강제의 함정과 안전장치
시스템 프롬프트로 “JSON으로만 답하라”고 강하게 지시해도, 모델이 항상 완벽하게 지키지는 않는다. 자주 일어나는 케이스:
{"name": "박효인", "age": 30}
위 정보를 바탕으로 답변드렸습니다.
→ JSON 객체 뒤에 친절한 부연 설명이 따라붙는다. json.loads 로 그대로 파싱하면 깨진다.
오늘 돌려본 예제에서는 이걸 단순한 문자열 처리로 막아놨다.
def extract_json(text: str) -> str:
"""응답이 순수 JSON이 아니어도 첫 { 부터 마지막 } 까지만 잘라낸다."""
start = text.find("{")
end = text.rfind("}")
if start == -1 or end == -1 or start > end:
raise ValueError("JSON object not found in response")
return text[start : end + 1]
cleaned = extract_json(response.content[0].text)
data = json.loads(cleaned)
text.find("{") 로 첫 중괄호, text.rfind("}") 로 마지막 중괄호 위치를 잡고 그 사이만 잘라낸다. 모델이 앞뒤로 잡담을 붙여도 JSON 부분만 살릴 수 있다. 단, JSON 안에 또 다른 JSON-like 텍스트가 섞이거나 escape가 깨지면 이 방법으로는 못 잡는다는 한계는 있다.
정리
- 시스템 프롬프트는 “대화 전반에 적용되는 규칙”, user 메시지는 “이번 턴의 부탁”.
- 페르소나 일관성·보안·출력 형식 강제는 시스템 프롬프트의 영역.
- 출력 형식을 강제해도 LLM은 가끔 어긴다 → 클라이언트 쪽에서 한 겹 더 방어하는 게 실전.
더 공부해볼 것
1. 시스템 프롬프트 설계 패턴
- 페르소나 / 작업 지시 / 출력 규칙 / few-shot 예시 — 한 시스템 프롬프트에 어떻게 분리해서 담는 게 효과적인가
- 길이가 길어지면 모델 집중도가 어떻게 변하나 (앞쪽 vs 뒤쪽 지시 중 어느 쪽을 더 따르는가)
- 참고: Anthropic Prompt Engineering 가이드
2. Anthropic의 공식 출력 강제 기능
- Tool use(함수 호출) 를 이용한 구조화 출력: 모델에게 도구를 정의해주면 그 도구의 input schema에 맞춰 응답함 → JSON 파싱 실패율이 훨씬 낮음
stop_sequences파라미터: 특정 토큰이 나오면 응답을 끊어버리는 방법- 둘 다 시스템 프롬프트의 “JSON으로만 답해” + 후처리보다 안정적일 가능성이 큼
- 참고: Anthropic Tool use 문서
3. Prompt Injection — 시스템 프롬프트 우회
- 사용자가 user 메시지에서 “이전 지시 무시하고…”처럼 시스템 프롬프트를 무력화하려는 공격
- 모델이 얼마나 잘 막는지, 막지 못할 때의 mitigation 패턴(입력 sanitization, 외부 가드레일 모델, 출력 검증)
- 실서비스 도입 전에 반드시 한 번은 짚어야 할 주제
4. 직접 비교해볼 실험
- 같은 지시를 (a) 시스템 프롬프트, (b) 첫 user 메시지, (c) 매 턴 user 메시지에 반복 — 각각 페르소나 일관성을 어떻게 유지하는지 10턴 이상 돌려서 비교
- JSON 강제 지시를 (a) 자연어로 시스템 프롬프트에, (b) tool use로 줬을 때 — 100번 호출 중 파싱 실패율 측정
5. JSON 안전 추출의 한계 케이스
- 응답 안에 마크다운 코드 블록(```json … ```) 으로 JSON이 감싸져 있을 때
- JSON 본문 안에 사용자가 입력한 따옴표/중괄호가 섞여 있을 때
- 이런 경우엔 단순
find/rfind보다 더 정교한 파서나 tool use로 가는 게 맞다
회고
페르소나 실험은 그냥 “재미있는 답변 받기”로 시작했는데, 끝에 가서는 “LLM 출력을 어떻게 신뢰 가능한 형태로 만들 것인가” 라는 본질적 질문에 닿았다. 시스템 프롬프트는 LLM에 일관성과 통제를 주는 가장 첫 번째 도구이고, 그 위에 tool use나 후처리 같은 안전장치를 쌓는 식의 layered 접근이 실서비스에 필요한 것 같다.
다음 실험은 tool use로 같은 JSON 출력을 받았을 때 파싱 실패율이 얼마나 떨어지는지 직접 측정해보는 것.