LLMOps 12

LLM 응답, 예측 불가능에서 재현 가능으로: Prompt Spec의 역할

프롬프트는 코드를 짜다 보면서 대충 고치면 되는 문자열로 보일 수 있다.

근거 · 교안 p30

LLM 응답, 예측 불가능에서 재현 가능으로: Prompt Spec의 역할 대표 이미지

1장: 프롬프트는 ‘문자열’이 아니다: LLM 응답의 놀라운 변덕

솔라의 손가락이 키보드 위에서 잠시 멈칫했다. 노트북 화면 한쪽에는 코드가, 다른 한쪽에는 방금 실행한 결과가 떠 있었다. 솔라는 미간을 찌푸리며 다시 한번 실행 버튼을 눌렀다. 결과는 또 달랐다.

“아, 진짜!”

나지막한 탄식과 함께 솔라가 의자 등받이에 몸을 기댔다. 방금 전까지 자신만만하게 고치던 코드의 문제가 아니었다. المشكلة كانت في طلب بسيط إلى LLM.

“또 뭐랑 싸우고 있어?”

어느새 다가온 루나가 솔라의 어깨너머로 화면을 들여다보며 물었다. 솔라는 기다렸다는 듯이 화면을 가리켰다.

“언니, 이것 좀 봐. LLM한테 간단한 텍스트를 한 문장으로 요약해달라고 요청하는 중이야. 그런데 실행할 때마다 결과가 제멋대로야. 분명히 똑같은 프롬프트를 보냈는데도 말이야.”

솔라의 말대로 화면 속 출력 결과는 일관성이 없었다. 어떤 결과는 군더더기 없이 요약문만 담겨 있었지만, 다른 결과는 “요약해 드리겠습니다:” 같은 불필요한 안내 문구로 시작했다.

“그냥 코드 짤 때 쓰는 문자열 변수 같은 건 줄 알았는데… 이건 변수라기보다 변덕에 가까운걸.”

솔라는 투덜거리며 마우스를 움직여 최근 다섯 개의 출력 결과를 한곳에 모아 보여주었다.

1. 이 글은 Prompt Spec의 중요성을 설명합니다.
2. 요약: 이 글은 Prompt Spec의 중요성을 설명합니다.
3. 네, 알겠습니다. 해당 텍스트는 Prompt Spec의 중요성에 대해 설명하고 있습니다.
4. Prompt Spec의 중요성을 다루는 글입니다.
5. 물론이죠! 글을 요약해 드릴게요. 이 글은 Prompt Spec의 중요성을 설명합니다.

루나는 잠시 화면을 응시하더니, 고개를 끄덕였다. “네가 보낸 요청은 전부 같았고?”

“응. "{text}"를 한 문장으로 요약해줘. 딱 이 한 줄. 단 한 글자도 바꾸지 않았어.”

솔라의 목소리에는 당혹감이 묻어났다. 개발자로서 같은 입력에 다른 출력이 나온다는 사실 자체가 낯설고 불편했다. 버그라면 원인을 찾아 고치면 되지만, 이것은 버그라기엔 너무나 변덕스러운 현상이었다.

“프롬프트를 단순한 ‘명령어 문자열’로 생각하면 그렇게 보일 거야.”

루나가 조용히 입을 열었다.

“같은 명령을 내렸는데 매번 다른 행동을 하니 혼란스럽지. 관점을 조금 바꿔보면 어떨까? 프롬프트가 LLM에게 보내는 ‘지시’가 아니라, 최소한의 정보만 담긴 ‘요청서’라고 생각해보는 거야.”

“요청서?”

“응. 솔라 네가 쓴 요청서는 ‘이 텍스트를 한 문장으로 요약해달라’는 핵심 내용만 담고 있어. 하지만 인사말을 붙일지, ‘요약’이라는 머리글을 달지, 어떤 말투를 사용할지에 대한 구체적인 규칙은 전혀 없지. 그러니 요청서를 받은 LLM은 매번 재량껏 세부 사항을 결정해서 응답하는 거야.”

루나의 말에 솔라는 나란히 정렬된 다섯 개의 결과물을 다시 쳐다보았다. 전에는 그저 ‘제멋대로인 결과’로 보였던 것들이 이제는 조금 다르게 보이기 시작했다. 핵심 내용은 모두 같았다. 다른 것은 내용이 아니라 그것을 감싸는 ‘형식’과 ‘말투’였다. 변동성이 어디에서 발생하는지 눈에 들어오기 시작했다.

그 순간, 솔라의 머릿속에 며칠 전 기술 문서에서 무심코 지나쳤던 문장이 떠올랐다.

‘Prompt Spec은 LLM의 페르소나, 작업 지침, 출력 형식 등을 정의하여 출력 품질을 통제한다.’

“아…!”

작은 탄성이 터져 나왔다. 그때는 그저 복잡하고 거창하게만 들렸던 말이었다. 프롬프트는 그냥 필요한 말을 잘 적어 보내면 되는 것 아니었던가? 하지만 눈앞의 변덕스러운 결과들은 그 생각이 얼마나 순진했는지 보여주고 있었다.

“그 ‘통제한다’는 말이… 바로 이런 변동성을 줄인다는 뜻이었구나. 내가 원하는 결과가 나올 때까지 프롬프트의 단어를 바꾸며 시도하는 게 아니라, 처음부터 결과물이 어떤 모양이어야 하는지 명확한 구조를 가진 요청서를 보내야 한다는 거네.”

솔라는 자신이 프롬프트라는 것을 완전히 잘못 이해하고 있었음을 깨달았다. 그것은 단순한 문자열이 아니었다. LLM이라는 예측 불가능한 상대를 일관되게 움직이게 만들어야 하는, 훨씬 더 정교한 설계의 대상이었다. 이제 솔라는 LLM의 응답이 예상과 다를 때, 그저 ‘이상하다’고 불평하는 대신 그 변동성 자체를 해결해야 할 문제로 인식하게 되었다.

고개가 끄덕여졌다. 하지만 새로운 의문이 꼬리를 물고 이어졌다.

“알겠어. 이제 프롬프트가 그냥 고쳐 쓸 수 있는 문자열이 아니라는 건 확실히 알겠네. 이건 일종의 계약서 초안 같은 거구나. 그런데 궁금한 게 생겼어. 애초에 LLM은 왜 이렇게 매번 다른 답을 내놓는 거야? 기계니까 똑같은 입력엔 똑같은 출력이 나와야 하는 거 아니야? 그리고 이런 변덕이 그냥 좀 귀찮은 수준이 아니라, 실제 서비스를 만들 땐 왜 더 심각한 문제가 되는 거지?“

2장: 비결정성을 넘어선 일관성: 운영 환경의 LLM이 가져야 할 덕목

솔라의 질문이 채 끝나기도 전에, 루나는 마우스를 움직여 화면에 떠 있던 코드와 결과 창을 한쪽으로 밀어냈다. 그리고는 텅 빈 디지털 화이트보드 앱을 열어 화면 중앙에 커다란 사각형 두 개를 나란히 그렸다. 익숙한 손놀림이었다. 왼쪽 사각형 위에는 내 노트북: 개발 환경이라고 적었고, 오른쪽에는 사용자 서비스: 운영 환경이라고 적었다.

물질적인 공간이 재편되자, 방금 전까지 솔라의 머릿속을 맴돌던 질문의 무게중심도 함께 이동했다. ‘왜 변덕스러울까?’라는 기술적 호기심에서 ‘이 변덕이 어떤 문제를 일으킬까?’라는 실용적인 고민으로.

“네 질문에 대한 답은 이 두 세계의 차이를 이해하는 데서 시작돼.”

루나는 이전 장에서 솔라가 보여줬던 다섯 가지 다른 요약 결과를 복사해 운영 환경이라고 적힌 오른쪽 사각형 안에 붙여넣었다.

[사용자 서비스: 운영 환경]

1. 이 글은 Prompt Spec의 중요성을 설명합니다.
2. 요약: 이 글은 Prompt Spec의 중요성을 설명합니다.
3. 네, 알겠습니다. 해당 텍스트는 Prompt Spec의 중요성에 대해 설명하고 있습니다.
4. Prompt Spec의 중요성을 다루는 글입니다.
5. 물론이죠! 글을 요약해 드릴게요. 이 글은 Prompt Spec의 중요성을 설명합니다.

“개발 환경에서 이 결과들은 그냥 ‘다르게 나온 결과’일 뿐이야. 하지만 수백, 수천 명의 사용자가 쓰는 서비스에서 이런 결과들이 무작위로 나타난다면 어떨까?”

솔라는 잠시 오른쪽 사각형을 응시했다. 처음에는 그저 사소한 차이라고 생각했다. 하지만 수많은 사용자가 각기 다른 형태의 응답을 받는다고 상상하자, 문제가 다르게 보이기 시작했다.

“음… 어떤 사람은 간결한 요약만 받고, 어떤 사람은 친절한 인사말을 받게 되겠네. 서비스가 좀… 프로답지 못해 보일 것 같아. 신뢰도도 떨어지고. 마치 매번 다른 직원이 제멋대로 응대하는 가게처럼.”

“정확해. 그게 사용자가 겪는 첫 번째 문제야. 서비스의 목소리가 하나로 정해져 있지 않으니, 경험의 일관성이 깨지는 거지.”

루나는 일관성이라는 단어에 잠시 멈췄다가 말을 이었다. “더 심각한 문제도 있어. 자, 가정을 하나 해보자. 우리 서비스에 버그가 생겼는데, LLM의 응답이 네, 알겠습니다.로 시작할 때만 앱이 멈춘다고 쳐봐.”

솔라의 표정이 굳었다. 개발자의 악몽과도 같은 시나리오였다.

“내 노트북에서 테스트할 땐 그 문구가 안 나타날 수도 있겠네. 그럼 난 버그를 재현할 수가 없어. 운영 서버에서는 사용자들이 계속 앱이 멈춘다고 아우성인데, 내 개발 환경에서는 아무리 테스트해도 멀쩡한 거야. 원인을 찾을 수가 없게 돼.”

“바로 그거야.” 루나가 말했다. “결과를 똑같이 다시 만들어낼 수 없는 것. 우리는 이걸 재현 가능성이 없다고 말해. 재현이 불가능한 버그는 잡을 수 없어. 이건 그냥 귀찮은 수준이 아니라, 서비스의 안정성을 뿌리부터 흔드는 문제야.”

솔라는 고개를 끄덕였다. LLM의 변덕이 단순한 ‘자연스러움’의 표현이 아니라, 운영 환경에서는 재앙을 초래할 수 있는 치명적인 약점임을 깨달았다. 자연스럽다는 LLM의 특성이 오히려 서비스의 신뢰성을 깎아 먹는 모순이었다.

루나는 마지막으로 질문을 던졌다. “그럼 마지막으로, 우리가 더 나은 요약 결과를 얻기 위해 프롬프트를 개선했다고 해보자. "{text}"를 전문적인 톤으로, 핵심만 담아 한 문장으로 요약해. 이렇게 바꿨어. 이 새로운 프롬프트가 기존 것보다 정말로 더 나은지 어떻게 비교하고 증명할 수 있을까?”

솔라는 즉시 대답할 수 있었다. “비교할 수가 없지. 이전 프롬프트의 결과 자체가 매번 달랐으니까. 새로운 프롬프트의 결과가 좋아진 게 정말 프롬프트 덕분인지, 아니면 그냥 그 순간 LLM이 우연히 좋은 답변을 생성한 건지 알 수 없잖아.”

그 말과 함께, 솔라의 머릿속에 흩어져 있던 조각들이 맞춰졌다. 개발 중에는 사소해 보였던 변동성이 운영 환경에서는 사용자 경험, 버그 수정, 성능 개선이라는 세 가지 핵심 영역 모두를 망가뜨리고 있었다. ‘각 Agent의 요청이 일관되고 재현 가능해야 운영 중 비교와 개선이 가능하다.’ 기술 문서의 그 건조했던 문장이 이제는 생생한 현실의 문제로 다가왔다.

“LLM은 본질적으로 다음에 올 단어를 확률적으로 예측해서 문장을 만들어. 그래서 같은 요청에도 다른 결과가 나올 수 있는 게 당연해. 결정론적으로 움직이는 일반적인 프로그램과는 다르지.” 루나가 덧붙였다. “중요한 건 그 특성을 없애려는 게 아니라, 우리가 원하는 결과의 구조를 명확히 정의해서 그 변덕스러움을 통제하는 거야.”

이제 솔라는 LLM 기반의 서비스를 평가할 때, 단순히 결과가 ‘그럴듯한가’를 넘어 ‘일관적인가’와 ‘재현 가능한가’를 핵심적인 운영 품질 기준으로 삼아야 한다는 것을 이해했다. 변덕스러운 천재를 동료로 두는 게 아니라, 안정적인 전문가와 함께 일하는 방법을 찾아야 했다.

“알겠어, 언니. 운영 환경에선 이런 변덕이 그냥 변덕이 아니라 재앙이 될 수 있구나. 일관성과 재현 가능성이 핵심 품질 기준이라는 것도. 그럼 이제 질문은 이거네. 어떻게?”

솔라는 이제 문제의 심각성을 완전히 이해했다. 자연스럽게 그녀의 관심은 해결책으로 향했다.

“이 확률 덩어리인 LLM을 어떻게 붙잡아서 우리가 원하는 대로, 언제나 일관된 결과를 만들게 할 수 있어? 언니가 전에 말했던 그 ‘Prompt Spec’이라는 게, 바로 그 방법을 알려주는 설계도 같은 거야?“

3장: Prompt Spec, 불확실성을 구조로 잡다: 3가지 핵심 구성요소

“설계도 맞아. 아주 정확한 표현이야.”

루나의 대답과 함께, 디지털 화이트보드의 화면이 전환됐다. 이전 장에서 사용했던 ‘개발 환경’과 ‘운영 환경’ 비교 다이어그램이 사라지고, 그 자리에는 마치 서류 양식처럼 보이는 깨끗한 템플릿이 나타났다.

--- Prompt Spec 초안 ---

1. 역할 (Persona):
   -

2. 작업 지침 (Instructions):
   -

3. 출력 형식 (Output Format):
   -
-------------------------

솔라는 화면을 빤히 쳐다보았다. “어떻게?”라는 그녀의 질문에 대한 답이 눈앞에 펼쳐진 것 같았다. 하지만 동시에 약간의 의구심이 들었다. 프롬프트 하나를 보내기 위해 이렇게까지 해야 한다니. 이건 마치 간단한 이메일 한 통을 보내기 전에 품의서를 작성하는 기분이었다.

“그냥 ‘좋은 프롬프트’를 쓰기 위한 체크리스트 같은 거야?” 솔라가 물었다. 그녀의 목소리에는 이 모든 것이 그저 권장 사항일 뿐, 필수적인 구조라고는 생각되지 않는다는 뉘앙스가 섞여 있었다.

루나는 말없이 솔라의 원래 프롬프트("{text}"를 한 문장으로 요약해줘.)를 다시 실행했다. 결과는 여전히 제멋대로였다. 결과: Prompt Spec의 중요성을 설명하는 글입니다.라는 퉁명스러운 답변이 돌아왔다.

“자, 이 양식을 채워가면서 결과가 어떻게 바뀌는지 직접 보자.”

루나는 템플릿의 첫 번째 항목, 역할 (Persona) 아래에 커서를 옮겨 한 문장을 타이핑했다.

1. 역할 (Persona): 당신은 기술 블로그의 수석 편집자입니다.

“먼저 LLM에게 정체성을 부여하는 거야.” 루나가 말했다. “어떤 역할을 맡아야 할지 알려주는 거지.”

솔라는 고개를 갸웃하며 말했다. “이게 그렇게 큰 차이를 만들어? 어차피 해야 할 일은 요약인데.”

루나는 프롬프트의 다른 부분은 그대로 둔 채, 방금 추가한 역할 정의만 포함해서 다시 실행했다.

저는 기술 블로그의 수석 편집자로서, 해당 텍스트의 핵심을 'Prompt Spec은 LLM 응답의 일관성을 확보하는 핵심 설계'라고 요약할 수 있겠습니다.

결과가 달라졌다. 이전의 무작위적인 말투와 달리, 훨씬 전문적이고 정제된 톤이 느껴졌다. 하지만 솔라는 즉시 문제를 발견했다.

“오, 말투는 확실히 좋아졌네. 그런데 여전히 ‘저는 ~라고 요약할 수 있겠습니다’ 같은 불필요한 말이 붙어있어. 우리가 원하는 건 딱 요약 문장 그 자체인데.”

역할을 부여하는 것이 LLM의 정체성을 바꿔 톤 앤 매너에 영향을 주었지만, 구체적인 행동 방식까지 통제하지는 못한다는 점이 명확해졌다. 첫 번째 실험은 문제를 부분적으로만 해결했을 뿐, 새로운 문제를 드러냈다.

“좋은 지적이야.” 루나는 다음 단계로 넘어갔다. 두 번째 항목인 작업 지침 (Instructions)에 새로운 규칙들을 추가했다.

2. 작업 지침 (Instructions): 주어진 텍스트의 핵심 내용을 정확히 한 문장으로 요약하세요. 어떤 경우에도 인사말, 서론, 부연 설명을 포함하지 마세요. 오직 최종 요약 문장만 생성해야 합니다.

이제 프롬프트는 역할과 함께 구체적인 행동 규칙까지 갖추게 되었다. 루나가 다시 실행 버튼을 눌렀다. 화면에 나타난 결과는 이전과 확연히 달랐다.

Prompt Spec은 LLM 응답의 일관성과 재현 가능성을 보장하는 구조화된 설계 명세입니다.

군더더기가 사라졌다. 솔라는 눈을 동그랗게 떴다. “와. 이제 정말 우리가 원하는 결과만 딱 나오네. 다섯 번, 여섯 번을 실행해도 계속 똑같은 결과가 나올까?”

“거의 그럴 거야. 일관성은 확보됐지. 하지만 아직 완벽하진 않아.”

루나는 마지막 항목인 출력 형식 (Output Format)을 가리켰다. “지금 이 결과는 사람이 보기엔 완벽해. 하지만 이 결과를 받아서 자동으로 처리해야 하는 다른 프로그램 입장에선 어떨까? 이건 여전히 그냥 하나의 긴 문자열일 뿐이야. 만약 우리가 요약된 문장만 쏙 뽑아서 데이터베이스에 저장하거나 화면의 특정 위치에 보여주고 싶다면?”

그제야 솔라는 마지막 퍼즐 조각을 깨달았다. 개발자로서 그녀는 수없이 많은 데이터를 파싱(parsing)해왔다. 예측 불가능한 문자열에서 원하는 정보를 추출하는 작업이 얼마나 번거롭고 오류에 취약한지 잘 알고 있었다. 재현 가능성은 단순히 같은 결과가 반복되는 것을 넘어, 기계가 안정적으로 해석할 수 있는 형태여야 했다.

루나는 마지막 항목을 채워 넣었다.

3. 출력 형식 (Output Format): 결과는 반드시 다음 JSON 형식에 맞춰 'summary' 키의 값으로 제공하세요. 예시: {"summary": "요약문"}

세 가지 구성 요소가 모두 채워진 완전한 ‘Prompt Spec’이 완성되었다. 루나가 마지막으로 실행하자, 결과는 더 이상 평범한 텍스트가 아니었다.

{ "summary": "Prompt Spec은 LLM 응답의 일관성과 재현 가능성을 보장하는 구조화된 설계 명세입니다." }

솔라는 자기도 모르게 작게 탄성을 내뱉었다. 이것은 단순한 ‘좋은 결과’가 아니었다. 이것은 기계가 언제나 동일한 방식으로 해석하고 사용할 수 있는, 예측 가능하고 구조화된 데이터였다. 프롬프트의 변덕은 사라졌고, 그 자리에는 신뢰할 수 있는 부품처럼 작동하는 결과만이 남았다. 이제야 비로소 기술 문서에서 읽었던 ‘Prompt Spec은 출력 품질을 통제한다’ 는 문장의 의미가 뼈저리게 와 닿았다. 그것은 단순한 제안이 아니라, 불확실성을 제어하는 필수적인 설계 원칙이었던 것이다.

“알겠다….” 솔라가 중얼거렸다. “역할, 지침, 형식. 이 세 가지가 LLM이라는 확률 덩어리에 채우는 족쇄 같은 거구나.”

이제 솔라는 새로운 LLM 요청을 마주할 때, 그저 자연어로 문장을 쓰는 게 아니라 이 세 가지 구조를 먼저 떠올리게 될 터였다. 그녀는 이제 단순한 프롬프트 작성자가 아니라, LLM 응답의 품질 편차를 사전에 방지하는 구조화 검증자가 된 것이다.

루나는 만족스러운 미소를 지으며 화이트보드의 내용을 모두 지웠다. 그리고 깨끗해진 화면에 새로운 시나리오를 제시했다.

“좋아, 솔라. 그럼 이제 네가 직접 설계도를 그려볼 차례야. 우리 앱에 ‘사용자 리뷰에서 핵심 불만사항 3가지를 키워드로 추출하는 기능’을 만든다고 상상해봐. 이 기능을 위한 Prompt Spec을 저 템플릿에 맞춰서 작성해볼래?”

솔라는 잠시 생각에 잠기더니, 키보드를 자기 앞으로 끌어당겼다. 더 이상 막막함이나 의구심은 없었다. 그녀의 손가락은 망설임 없이 움직이기 시작했다.

--- Prompt Spec: 리뷰 불만사항 추출 ---

1. 역할 (Persona):
   - 당신은 수많은 고객 피드백에서 핵심 인사이트를 정확히 찾아내는 고객 경험 분석 전문가입니다.

2. 작업 지침 (Instructions):
   - 주어진 사용자 리뷰 텍스트에서 사용자가 겪는 가장 핵심적인 불만사항을 나타내는 키워드를 정확히 3개만 추출하세요.
   - 키워드는 명사형으로 간결해야 합니다.
   - 긍정적인 내용이나 칭찬은 완전히 무시하고, 오직 부정적인 경험과 관련된 내용에만 집중하세요.
   - 결과는 3개를 초과하거나 미달해서는 안 됩니다.

3. 출력 형식 (Output Format):
   - 결과는 반드시 `{"keywords": ["키워드1", "키워드2", "키워드3"]}` 형식의 JSON 배열로 출력하세요.

-----------------------------------------

자신이 완성한 구조화된 요청서를 보며, 솔라는 조용히 고개를 끄덕였다. 이것이라면, LLM은 더 이상 변덕스러운 동료가 아니라, 자신이 설계한 대로 정확하게 움직이는 든든한 파트너가 되어줄 것이 분명했다.