포스트

2026-03-25 강의 정리

프롬프트 엔지니어링 - Zero-shot, Few-shot

2026-03-25 강의 정리

목차

  1. 문제와 접근
  2. 환경 설정 & 라이브러리 import
  3. 출력 형식 제어 (JSON / Markdown / 직접 지정)
  4. 구조화된 출력 - 회의록 자동 변환
  5. ConstrainedPrompt 클래스 - 제약 조건 빌더
  6. 실습: 광고 카피 & 상세 제품 설명 생성
  7. 컨텍스트 기반 응답 제어 (Context-Grounded QA)
  8. answer_with_context 함수
  9. Zero-Shot vs Few-Shot 프롬프팅
  10. Few-Shot 감정 분석
  11. Few-Shot 비즈니스 문체 변환
  12. 핵심 개념 총정리
  13. 자주 나오는 실수/주의사항
  14. 출력 형식과 내용 검증을 나누기

1. 문제와 접근

LLM 출력을 다음 프로그램의 입력으로 쓰려면 보기 좋은 문장만으로는 부족하다. 출력 형식, 필수 필드, 근거 범위를 정하고 파싱 이후의 검증까지 생각해야 한다. Few-Shot 예제도 개수를 늘리는 데 앞서 어떤 경계 사례를 전달할지 기준을 세워 살펴본다.

핵심 키워드: 출력 형식 제어, 제약 조건, Few-Shot

전체 학습 흐름

1
2
3
4
5
6
7
8
9
10
11
출력 형식 제어 (JSON, Markdown, 직접 지정)
        ↓
구조화된 데이터 추출 (회의록 → JSON)
        ↓
ConstrainedPrompt 클래스 (제약 조건 체이닝)
        ↓
컨텍스트 기반 응답 제어 (hallucination 방지)
        ↓
Zero-Shot vs Few-Shot 비교
        ↓
Few-Shot 실전 응용 (감정 분석, 문체 변환)

프롬프트를 잘 짜는 것 = LLM을 코딩 없이 “프로그래밍”하는 것 오늘 배우는 기법들은 실제 프로덕션 LLM 앱에서 가장 많이 쓰이는 패턴들이다


2. 환경 설정 & 라이브러리 import

코드

1
2
3
4
5
6
7
8
9
10
11
12
import os
from dotenv import load_dotenv
import pandas as pd
import json
import time

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage

load_dotenv()

llm = ChatOpenAI(model="gpt-4o-mini")

3. 출력 형식 제어 (JSON / Markdown / 직접 지정)

LLM이 출력하는 형식을 정밀하게 제어하는 방법 3가지를 비교한다.

형식 비교 표

1
2
3
방법 1: Plain 텍스트에 "json만 출력" 지시
방법 2: Markdown 형식의 프롬프트로 작성
방법 3: json 구조를 템플릿으로 직접 제시

방법 1: 일반 텍스트 + 단순 지시

1
2
3
4
5
6
plain_prompt = """다음 제품 리뷰를 분석해줘.
전체 감정(긍정/부정/혼합), 1~5점 점수, 장점 목록, 단점 목록, 핵심 키워드 3개를 알려줘.
리뷰 : "이 노트북 정말 가벼워서 좋아요! 다만 키보드 타건감이 아쉽네요"
유효한 json만 출력하세요"""

print(llm.invoke([HumanMessage(content=plain_prompt)]).content)

방법 1 응답 예시 (코드 블록 포함됨 - 파싱 주의)

1
2
3
4
5
6
7
8
```json
{
  "전체_감정": "혼합",
  "점수": 4,
  "장점": ["가벼운 무게"],
  "단점": ["타건감 아쉬움"],
  "핵심_키워드": ["가벼움", "키보드", "타건감"]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> 방법 1의 문제점: 응답에 ```json ... ``` 코드 블록이 포함되어 직접 json.loads() 불가
> 파이썬에서 파싱할 때 코드 블록 마커를 제거하는 전처리 필요

### 방법 2: Markdown 형식 프롬프트

```python
markdown_prompt = """# 제품 리뷰 감정 분석

## 입력 리뷰
> "이 노트북 정말 가벼워서 좋아요! 다만 키보드 타건감이 아쉽네요"

## 분석 항목
- **overall_sentiment** : 긍정/부정/혼합 중 하나
- **score** : 1~5점수
- **pros** : 장점 리스트
- **cons** : 단점 리스트
- **keywords** : 핵심 키워드 3개

## 출력 형식
유효한 json만 출력하세요. 다른 텍스트를 포함하지 마세요
"""

print(llm.invoke([HumanMessage(content=markdown_prompt)]).content)

방법 2 응답 예시 (순수 JSON - 바로 파싱 가능)

1
2
3
4
5
6
7
{
  "overall_sentiment": "혼합",
  "score": 3,
  "pros": ["가벼움"],
  "cons": ["타건감 아쉬움"],
  "keywords": ["노트북", "가벼워서", "키보드"]
}

방법 2의 장점: 프롬프트 자체가 Markdown으로 구조화되어 있어 LLM이 형식을 더 잘 따름 “다른 텍스트를 포함하지 마세요” 명시 → 코드 블록 없이 순수 JSON 반환

방법 3: JSON 템플릿 직접 제시

1
2
3
4
5
6
7
8
9
10
11
12
13
json_prompt = """다음 제품 리뷰를 분석해서 json 형식으로 출력하세요

리뷰 : "이 노트북 정말 가벼워서 좋아요! 다만 키보드 타건감이 아쉽네요"

출력 형식
{
    "overall_sentiment" : 긍정/부정/혼합,
    "score" : 1~5점수,
    "pros" : [장점],
    "cons" : [단점],
    "keywords" : [키워드1, 키워드2, 키워드3]
}
"""

방법별 특징 비교

1
2
3
방법 1 (plain)    : 코드 블록 포함 가능성 있음, 파싱 전처리 필요
방법 2 (markdown) : 구조화된 지시로 형식 준수율 높음, 순수 JSON 반환
방법 3 (template) : 빈 칸 채우기 방식, 키 이름 고정 가능

4. 구조화된 출력 - 회의록 자동 변환

회의록을 구조화할 때는 JSON 문법이 유효한지와 추출한 내용이 원문에 있는지를 분리해서 확인한다. 특히 담당자나 기한이 원문에 없는 경우를 어떤 값으로 표시할지 정해야, 누락된 정보가 그럴듯한 값으로 채워지는 것을 발견할 수 있다.

비정형 텍스트(회의 내용)를 정형 JSON으로 변환하는 실전 예시.

JSON 구조 정의

1
2
3
4
5
6
7
8
9
json_structure = {
    'date'         : 'YYYY-MM-DD',
    'attendees'    : ['이름1', '이름2'],
    'agenda'       : ['안건1', '안건2'],
    'decisions'    : ['결정1'],
    'action_items' : [
        {'assignee': '담당자', 'task': '작업', 'deadline': '기한'}
    ]
}

generate_meeting_minute 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
text = '3월 15일 마케팅팀 주간 회의. 참석: 김팀장, 이대리, 박사원. '
       '신규 SNS 캠페인 예산 5000만원 확정. '
       '이대리가 3월 22일까지 시안 준비. '
       '박사원은 경쟁사 분석 보고서 3월 20일까지.'

def generate_meeting_minute(raw_text):
    messages = [
        SystemMessage(content='당신은 회의록 전문가입니다. '
                               '인사말이나 부연설명 없이 순수한 json 데이터만 반환하세요'),
        HumanMessage(content=f"""다음 회의 내용을 지정된 json 구조로 변환하세요.

        회의내용 : {raw_text}

        json구조:
        {{
            'date'         : 'YYYY-MM-DD',
            'title'        : '회의 제목',
            'attendees'    : ['이름1', '이름2'],
            'agenda'       : ['안건1', '안건2'],
            'decisions'    : ['결정1'],
            'action_items' : [
                    {{'assignee' : '담당자', 'task' : '작업', 'deadline' : '기한'}}]
        }}
        """)
    ]

    response = llm.invoke(messages).content
    return response

print(generate_meeting_minute(text))

변환 결과 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "date": "2023-03-15",
  "title": "마케팅팀 주간 회의",
  "attendees": ["김팀장", "이대리", "박사원"],
  "agenda": ["신규 SNS 캠페인 예산 확정", "시안 준비", "경쟁사 분석 보고서"],
  "decisions": ["신규 SNS 캠페인 예산 5000만원 확정"],
  "action_items": [
    { "assignee": "이대리", "task": "시안 준비", "deadline": "2023-03-22" },
    {
      "assignee": "박사원",
      "task": "경쟁사 분석 보고서",
      "deadline": "2023-03-20"
    }
  ]
}

전체 변환 흐름

1
2
3
4
5
6
7
8
비정형 텍스트 (자연어 회의 내용)
        ↓
SystemMessage: "json만 반환"
HumanMessage: 내용 + json 구조 템플릿
        ↓
LLM 처리
        ↓
구조화된 JSON (DB 저장, API 연동 가능)

SystemMessage에 “인사말이나 부연설명 없이 순수한 json 데이터만 반환하세요”를 명시하면 코드 블록이나 설명 텍스트 없이 바로 json.loads()가 가능한 형태로 나온다


5. ConstrainedPrompt 클래스 - 제약 조건 빌더

제약 조건을 메서드 체이닝으로 추가하고, 최종 프롬프트를 생성하는 빌더 패턴 클래스.

클래스 구조 도식

1
2
3
4
5
6
7
ConstrainedPrompt(base_instruction)
    ├─ add_length(description)   → [길이] 제약 추가
    ├─ add_content(description)  → [내용] 제약 추가
    ├─ add_format(description)   → [형식] 제약 추가
    ├─ add_style(description)    → [스타일] 제약 추가
    ├─ build()                   → 완성된 프롬프트 문자열 반환
    └─ execute(temperature, max_tokens) → llm 호출 후 결과 반환

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ConstrainedPrompt:
    def __init__(self, base_instruction):
        self.instruction = base_instruction
        self.constraints = []

    def add_length(self, description):
        self.constraints.append(f"[길이] {description}")
        return self

    def add_content(self, description):
        self.constraints.append(f"[내용] {description}")
        return self

    def add_format(self, description):
        self.constraints.append(f"[형식] {description}")
        return self

    def add_style(self, description):
        self.constraints.append(f"[스타일] {description}")
        return self

    def build(self):
        parts = [self.instruction, "\n제약조건:"]
        for c in self.constraints:
            parts.append(f" - {c}")
        return '\n'.join(parts)

    def execute(self, temperature=0.7, max_tokens=None):
        prompt = self.build()
        llm_instance = ChatOpenAI(
            model="gpt-4o-mini",
            temperature=temperature,
            **({"max_tokens": max_tokens} if max_tokens else {})
        )
        return llm_instance.invoke([HumanMessage(content=prompt)]).content

return self가 핵심 - 각 add_* 메서드가 자기 자신을 반환하므로 체이닝이 가능하다 이 패턴을 “Builder Pattern” 또는 “Method Chaining”이라고 한다

사용 예시 - 클라우드 컴퓨팅 설명

1
2
3
4
5
6
7
8
9
result = (
    ConstrainedPrompt('클라우드 컴퓨팅의 장점을 설명해주세요')
    .add_length("5개의 불릿포인트")
    .add_content('비용, 확장성, 보안 관점을 반드시 포함')
    .add_format('각 포인트는 한줄로, 이모지로 시작')
    .add_style('IT 비전공 경영진을 대상, 전문 용어에 괄호로 설명 추가')
    .execute(temperature=0.3, max_tokens=200)
)
print(result)

build() 실행 시 생성되는 프롬프트 구조

1
2
3
4
5
6
7
클라우드 컴퓨팅의 장점을 설명해주세요

제약조건:
 - [길이] 5개의 불릿포인트
 - [내용] 비용, 확장성, 보안 관점을 반드시 포함
 - [형식] 각 포인트는 한줄로, 이모지로 시작
 - [스타일] IT 비전공 경영진을 대상, 전문 용어에 괄호로 설명 추가

응답 예시

1
2
3
4
5
💰 비용 효율성: 클라우드 서비스는 초기 투자 비용을 줄이고, 사용한 만큼만 지불하여 재무 부담을 최소화합니다.
📈 확장성: 필요에 따라 신속하게 리소스를 추가하거나 감소시킬 수 있어, 사업 성장에 유연하게 대응합니다.
🔒 보안 강화: 클라우드 제공업체는 높은 수준의 보안을 제공하며, 데이터 보호를 위해 최신 기술을 업데이트합니다.
🚀 신속한 배포: 새로운 서비스를 간편하게 배포할 수 있어, 시장 변화에 빠르게 적응합니다.
🌍 접근성 향상: 인터넷만 있으면 언제 어디서나 데이터에 접근 가능하여 글로벌 비즈니스 운영에 유리합니다.

6. 실습: 광고 카피 & 상세 제품 설명 생성

ConstrainedPrompt로 같은 제품에 대해 서로 다른 톤/목적의 텍스트를 생성하는 실습.

제품 정보 준비

1
2
product  = "에어프로 맥스 무선 헤드폰"
features = ["40시간 배터리", "멀티포인트 연결", "30dB 노이즈캔슬링", "300g 경량 설계"]

광고 카피 생성

1
2
3
4
5
6
7
8
9
10
ad_result = (
    ConstrainedPrompt(f"{product} 제품 광고 카피를 작성해주세요. 특징 : {', '.join(features)}")
    .add_length("한 줄, 30자 이내")
    .add_content('핵심 특징 1가지만 강조')
    .add_format('슬로건 형태, 마침표 없이')
    .add_style('2030 타겟, 감성적이고 트렌디한 톤')
    .execute()
)
print(ad_result)
# → 40시간의 음악 여정, 자유롭게 즐겨요

상세 제품 설명 생성

1
2
3
4
5
6
7
8
9
detail_result = (
    ConstrainedPrompt(f"{product} 상세 제품 설명을 작성해주세요. 특징 : {', '.join(features)}")
    .add_length("200자 내외, 3단락")
    .add_content('모든 특징을 빠짐없이 설명, 사용 시나리오 1개 포함')
    .add_format('단락 구분을 명확히, 마지막에 가격 정보 위치 표시')
    .add_style('공식 제품 페이지 톤, 신뢰감 있는 문체')
    .execute()
)
print(detail_result)

상세 설명 응답 예시

1
2
3
4
5
6
7
8
9
에어프로 맥스 무선 헤드폰은 최대 40시간 지속 재생이 가능한 배터리 성능을 자랑합니다.
한 번의 완충으로 오랜 시간 음악을 감상하거나 회의에 참여할 수 있어, 바쁜 일상에서도 쾌적한 청취 환경을 제공합니다.

멀티포인트 연결 기능으로 두 개의 기기를 동시에 연결하여 편리하게 전환할 수 있으며,
30dB의 강력한 노이즈 캔슬링 기술로 외부 소음을 차단하고 집중력을 극대화합니다.
300g의 경량 설계로 장시간 착용에도 귀의 피로를 최소화합니다.

출퇴근 시 지하철에서도 노이즈 캔슬링으로 완벽한 음악 감상이 가능합니다.
[가격 정보]

두 결과물 비교

1
2
광고 카피    : 짧고 감성적, 특정 세대 타겟, 임팩트 있는 한 줄
상세 설명    : 길고 신뢰감 있는 문체, 모든 특징 나열, 사용 시나리오 포함

같은 데이터(features)에서 제약 조건만 바꿔서 완전히 다른 텍스트를 생성할 수 있다 ConstrainedPrompt는 콘텐츠 마케팅, 제품 설명, 이메일 등 다양한 텍스트 생성에 재사용 가능


7. 컨텍스트 기반 응답 제어 (Context-Grounded QA)

LLM이 외부 지식 대신 제공된 문서만을 근거로 답변하도록 강제하는 기법. RAG 시스템의 핵심 원리와 동일하다. 할루시네이션(Hallucination) 방지에 필수적이다.

할루시네이션 문제

1
2
3
LLM은 학습 데이터 기반으로 그럴듯한 답변을 생성하기 때문에
문서에 없는 내용도 자신 있게 답변하는 문제가 발생한다
→ 이를 "Hallucination(환각)" 이라고 함

컨텍스트 문서 예시

1
2
3
4
5
6
7
8
9
10
company_policy = """
[모두컴퍼니 재택근무 정책 v2.3]
- 주 3일 재택, 2일 출근 (화/목 필수 출근)
- 재택근무 시 오전 9시까지 Slack 상태 '업무중' 설정 필수
- 해외 원격근무는 최대 연속 2주까지 가능 (사전 승인 필요)
- 야간근무(22시 이후) 시 익일 오후 출근 가능
- 재택근무 장비 지원금: 연 100만원 (영수증 제출)
"""

question = '해외에서 한 달 동안 원격 근무할 수 있나요?'

기본 컨텍스트 제어

1
2
3
4
5
6
7
8
9
10
with_context = f"""아래 회사 정책 문서를 참고하여 질문에 답하세요.
문서에 없는 내용은 "해당 정책 문서에 명시되어 있지 않습니다"라고 답하세요.

정책 문서:
\"\"\"{company_policy}\"\"\"

질문 : {question}"""

print(llm.invoke([HumanMessage(content=with_context)]).content)
# → 해당 정책 문서에 명시되어 있지 않습니다.

실제 정책: “최대 연속 2주” → 한 달은 불가 → LLM이 정확히 판단 이처럼 문서를 제공하면 LLM이 외부 지식이 아닌 문서만 참조한다


8. answer_with_context 함수

컨텍스트 기반 응답을 재사용 가능하게 함수화하고, 답변과 근거를 함께 출력하는 형식을 추가한 버전.

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def answer_with_context(context, question):
    clause = """
    중요 : 반드시 제공된 문서 내용만을 근거로 답변하세요.
    문서에 없는 내용은 "제공된 문서에 해당 정보가 없습니다"라고 답하세요.
    추측하거나 외부 지식을 사용하지 마세요."""

    prompt = f"""아래 참고 문서를 기반으로 질문에 답하세요.
    {clause}

    참고 문서:
    \"\"\"{context}\"\"\"

    질문 : {question}

    답변 형식:
    - 답변 : [핵심 답변]
    - 근거 : [문서에서 관련 부분 인용]"""

    return llm.invoke([HumanMessage(content=prompt)]).content

응답 예시 1 - 정보 있는 경우

1
answer_with_context(company_policy, '해외에서 한 달 동안 원격 근무할 수 있나요?')
1
2
- 답변 : 해외에서 한 달 동안 원격 근무할 수 없습니다.
- 근거 : "해외 원격근무는 최대 연속 2주까지 가능 (사전 승인 필요)"

응답 예시 2 - 정보 없는 경우

1
answer_with_context(company_policy, '식대 지원금은 얼마인가요?')
1
2
- 답변 : 제공된 문서에 해당 정보가 없습니다
- 근거 : 제공된 문서에는 식대 지원금에 대한 정보가 포함되어 있지 않습니다.

전체 흐름

1
2
3
4
5
6
7
context (문서) + question (질문)
        ↓
"문서만 근거로", "없으면 없다고" 제약 조건 포함 프롬프트 생성
        ↓
LLM 호출
        ↓
답변 + 근거 (문서 인용) 형식으로 반환

답변 형식에 “근거 : [문서에서 관련 부분 인용]”을 포함하면 LLM이 어떤 정보를 참조했는지 추적할 수 있어 신뢰도가 높아진다


9. Zero-Shot vs Few-Shot 프롬프팅

Few-Shot 예제는 원하는 말투뿐 아니라 판단 기준을 전달하는 입력이다. 서로 비슷한 예제만 추가하기보다 분류가 애매한 입력이나 정보가 부족한 입력을 포함하고, 예제에 사용하지 않은 데이터에서 결과를 비교한다.

Zero-Shot

1
2
예시 없이 지시만으로 LLM이 작업을 수행하는 방식
→ 범용적인 작업에는 잘 동작하지만, 특수한 형식/스타일 요구 시 불안정

Few-Shot

1
2
몇 가지 예시(입력-출력 쌍)를 함께 제공해서 LLM이 패턴을 학습하게 하는 방식
→ LLM이 원하는 형식이나 스타일을 더 정확히 따름

비교 도식

1
2
3
4
5
6
7
8
9
10
11
12
Zero-Shot:
[SystemMessage] + [HumanMessage(질문)]
                        ↓
              LLM이 학습 지식만으로 답변

Few-Shot:
[SystemMessage]
+ [HumanMessage(예시1)] + [AIMessage(예시1 답변)]
+ [HumanMessage(예시2)] + [AIMessage(예시2 답변)]
+ [HumanMessage(실제 질문)]
                        ↓
         LLM이 예시 패턴을 보고 동일한 형식으로 답변

Few-Shot의 핵심은 LangChain의 AIMessage를 활용해서 “이미 일어난 대화인 척” 예시를 대화 히스토리에 심어두는 것이다


10. Few-Shot 감정 분석

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
few_shot_messages = [
    SystemMessage(content='주어진 리뷰의 감정을 분석하세요'),

    # 예시 1 (긍정)
    HumanMessage(content="리뷰: '이 제품 정말 최고입니다! 강력 추천해요'"),
    AIMessage(content='{"sentiment" : "긍정", "score" : 0.95, "keywords" : ["최고", "강력 추천"]}'),

    # 예시 2 (부정)
    HumanMessage(content="리뷰: '배송도 느리고 제품 품질도 형편없네요'"),
    AIMessage(content='{"sentiment" : "부정", "score" : 0.15, "keywords" : ["느리고", "형편없네요"]}'),

    # 예시 3 (혼합)
    HumanMessage(content="리뷰: '가격 대비 괜찮지만, 기대했던 것보다는 아쉬워요'"),
    AIMessage(content='{"sentiment" : "혼합", "score" : 0.50, "keywords" : ["괜찮지만", "아쉬워요"]}'),

    # 실제 쿼리
    HumanMessage(content="리뷰: '디자인은 예쁜데, 배터리가 빨리 닳아요. 전체적으로 보통입니다'")
]

print(llm.invoke(few_shot_messages).content)

응답 예시

1
2
3
4
5
{
  "sentiment": "혼합",
  "score": 0.45,
  "keywords": ["예쁜데", "빨리 닳아요", "보통"]
}

Few-Shot을 쓰지 않으면 응답이 자유로운 자연어 형태로 나올 수 있다 Few-Shot으로 json 예시를 3개 보여주면 LLM이 동일한 json 구조로 일관되게 답변한다

Few-Shot 예시 수에 관한 팁

1
2
3
1-2개  : 약한 힌트, 형식이 가끔 무너질 수 있음
3-5개  : 적정 수준, 패턴 인식이 확실해짐
6개 이상: 토큰 낭비 가능, 3-5개와 품질 차이 거의 없음

11. Few-Shot 비즈니스 문체 변환

비공식 메시지를 정중한 비즈니스 이메일 톤으로 변환하는 실습.

예시 데이터 준비

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
examples = [
    {
        "informal": "내일 미팅 좀 미룰 수 있을까? 갑자기 일이 생겼어.",
        "formal"  : "안녕하세요. 내일 예정된 미팅 일정 변경을 요청드립니다. "
                    "긴급한 업무가 발생하여 조율이 필요합니다. "
                    "가능한 대체 일정을 알려주시면 감사하겠습니다."
    },
    {
        "informal": "그 보고서 다 했어? 빨리 보내줘.",
        "formal"  : "안녕하세요. 요청드렸던 보고서 진행 상황을 확인드립니다. "
                    "완료되셨다면 전달 부탁드리며, 추가 시간이 필요하시면 말씀해 주세요."
    },
    {
        "informal": "이번 프로젝트 결과 별로인데 어떻게 할까?",
        "formal"  : "안녕하세요. 이번 프로젝트 결과에 대해 논의가 필요합니다. "
                    "개선 방안을 함께 검토하기 위해 미팅을 잡는 것이 어떨까요?"
    }
]

test_messages = [
    "다음 주 워크숍 참석 못 할 것 같아. 다른 사람 보내도 돼?",
    "예산 좀 더 받을 수 있을까? 지금 부족해."
]

Few-Shot 메시지 구성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
messages = [
    SystemMessage(content='당신은 비즈니스 커뮤니케이션 전문가입니다. '
                           '비공식적인 메시지를 정중한 비즈니스 이메일 톤으로 변환하세요. '
                           '인사말, 존칭, 구체적 표현을 포함하세요')
]

# Few-Shot 예시 추가
for ex in examples:
    messages.append(HumanMessage(content=ex['informal']))
    messages.append(AIMessage(content=ex['formal']))

# 실제 변환할 메시지 추가
messages.append(HumanMessage(content=test_messages[0]))

print(llm.invoke(messages).content)

응답 예시 1

1
2
3
4
입력 : "다음 주 워크숍 참석 못 할 것 같아. 다른 사람 보내도 돼?"

출력 : "안녕하세요. 다음 주 예정된 워크숍에 참석하기 어려운 상황입니다.
         대신 다른 분을 참석시켜도 괜찮으신지 확인 부탁드립니다. 감사합니다."

응답 예시 2

1
2
3
4
5
입력 : "예산 좀 더 받을 수 있을까? 지금 부족해."

출력 : "안녕하세요. 현재 예산에 대한 상황을 말씀드리고자 합니다.
         현재 예산이 부족하여 추가 지원을 요청드릴 수 있는지 검토해 주시면 감사하겠습니다.
         필요한 추가 자료가 있다면 말씀해 주세요."

매번 새로운 쿼리를 테스트할 때 메시지 구성 방법

1
2
3
4
5
6
7
8
9
10
11
def transform_to_formal(informal_text):
    """examples를 활용한 few-shot 문체 변환"""
    messages = [
        SystemMessage(content='당신은 비즈니스 커뮤니케이션 전문가입니다. '
                               '비공식적인 메시지를 정중한 비즈니스 이메일 톤으로 변환하세요.')
    ]
    for ex in examples:
        messages.append(HumanMessage(content=ex['informal']))
        messages.append(AIMessage(content=ex['formal']))
    messages.append(HumanMessage(content=informal_text))
    return llm.invoke(messages).content

Few-Shot은 매번 동일한 예시 메시지를 앞에 붙이는 방식이므로 예시가 많을수록 토큰이 증가한다 3개 예시 정도가 품질 대비 토큰 비용이 적절한 균형점이다


12. 핵심 개념 총정리

오늘 배운 기법 정리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1. 출력 형식 제어
   - Markdown 형식 프롬프트: 형식 준수율 높음
   - "다른 텍스트 포함하지 마세요": 코드 블록 방지
   - JSON 템플릿 제시: 키 이름과 구조 고정

2. 구조화된 출력 (회의록 변환)
   - SystemMessage에 "순수한 json만 반환" 명시
   - json 구조 템플릿을 프롬프트에 직접 포함

3. ConstrainedPrompt (빌더 패턴)
   - add_length / add_content / add_format / add_style
   - return self로 메서드 체이닝 가능
   - execute()로 직접 LLM 호출

4. 컨텍스트 기반 응답 (Grounded QA)
   - "문서만 근거로", "없으면 없다고" 제약
   - 답변 + 근거 형식으로 출처 추적 가능
   - Hallucination 방지의 기본 원리

5. Zero-Shot vs Few-Shot
   - Zero-Shot: 예시 없이 지시만
   - Few-Shot: HumanMessage + AIMessage 쌍으로 예시 제공
   - 3-5개 예시가 적정 수준

6. 뉴스 분류 (Zero-Shot)
   - SystemMessage에 카테고리 목록 명시
   - "반드시 카테고리 이름만 출력" 제약

7. Few-Shot 문체 변환
   - examples 리스트로 예시 관리
   - 매번 SystemMessage + examples + 새 쿼리 조합

기법 선택 가이드

1
2
3
4
5
원하는 출력이 JSON인 경우          → 방법 2 (Markdown 구조 프롬프트)
특수한 형식/스타일이 있는 경우     → Few-Shot
환각 방지가 필요한 경우            → 컨텍스트 기반 응답
다양한 제약 조건 조합이 필요한 경우 → ConstrainedPrompt 빌더 패턴
단순 분류/추출인 경우              → Zero-Shot + 출력 형식 명시

13. 자주 나오는 실수/주의사항

주의사항

  1. JSON 파싱 시 코드 블록 확인
    • LLM이 응답에 json ... 블록을 붙일 수 있음
    • 직접 json.loads()하면 오류 발생
    • “다른 텍스트를 포함하지 마세요” 명시하거나 파싱 전 전처리 필요
  2. Few-Shot 예시의 일관성
    • 예시들의 형식이 제각각이면 LLM이 혼란스러워함
    • 모든 예시는 동일한 입력-출력 형식을 유지할 것
  3. 컨텍스트 기반 응답의 제약 조건
    • “추측하거나 외부 지식을 사용하지 마세요” 명시 필수
    • 없는 내용에 대한 응답 형식도 지정 (“해당 정보가 없습니다”)
    • 부정 제약(“거짓말하지 마세요”) 대신 긍정 제약(“문서 내용만 답하세요”) 사용
  4. ConstrainedPrompt execute() 사용 시
    • temperature=0.3 등 낮은 값 → 더 일관된 출력
    • max_tokens 지정 시 응답이 잘릴 수 있음 (충분히 설정할 것)
  5. Few-Shot 토큰 비용
    • 예시가 많을수록 매 요청마다 토큰 소모 증가
    • 3-5개 예시가 실용적인 균형점
    • 예시는 간결하고 대표적인 케이스로 선별
  6. 카테고리 분류 시 오분류 방지
    • categories 리스트를 SystemMessage에 명확히 나열
    • “반드시 카테고리 이름만 출력하세요” 명시
    • 카테고리 이름은 모호하지 않게 정의할 것

14. 출력 형식과 내용 검증을 나누기

구조화된 출력은 후속 코드가 결과를 다루기 쉽게 하지만, 내용의 정확성까지 대신 확인해 주지는 않는다. 파싱 성공, 스키마 적합성, 원문 일치 여부를 구분하면 실패 시 재시도할 범위도 좁힐 수 있다.

형식을 강하게 제한한 뒤에는 빈 입력, 상충하는 요구, 원문에 없는 항목을 테스트 입력으로 남긴다. 이런 경우의 반환 규칙이 정해져 있어야 정상 응답뿐 아니라 예외적인 결과도 후속 처리에 연결할 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그