CodeGym /행동 /ChatGPT Apps /system‑prompt, tool...

system‑prompt, tool descriptions, follow‑ups의 연계: 할루시네이션 억제

ChatGPT Apps
레벨 5 , 레슨 2
사용 가능

1. 소개

프롬프트 엔지니어링 과정에서는 가끔 이런 ‘마법의 주문’을 보여줍니다:

“사실을 만들어내지 말고, 정보가 없으면 ‘모르겠습니다’라고 말하세요.”

하지만 안타깝게도(혹은 다행히도) ChatGPT App에서는 이것이 만능 해결책이 아닙니다. 이유는 간단합니다. 모델은 당신의 system‑prompt만 보는 것이 아니라 다음도 함께 보기 때문입니다:

  • 도구 목록과 각 도구의 설명 및 스키마;
  • 그 도구들의 실행 결과;
  • 이전 follow‑up까지 포함한 대화 이력.

이 세 층 — system‑prompt, 도구 설명, follow‑up 패턴 — 이 서로 모순되거나 허술하게 연결되어 있으면, 모델은 성실하지만 상상으로 메우기 쉬운 전형적인 주니어처럼 행동하기 시작합니다.

이 강의에서는 defense in depth라는 ‘3단계 할루시네이션 방어’ 아이디어를 사용합니다:

  • 레벨 1system‑prompt에 전역 규칙 정의;
  • 레벨 2 — 각 도구 정의에 지역적 제한을 명시;
  • 레벨 3 — 오류, 빈 결과 처리 및 follow‑ups.

이번 강의에서는 교육용 앱 GiftGenius에 대해 세 가지 레이어를 하나로 묶은 일관된 계약을 완성합니다.

Insight

새로운 ChatGPT Apps 아키텍처에서는 system‑prompt를 위한 필드가 사라졌습니다 — 형식적으로는 더 이상 존재하지 않습니다. 그러나 이것이 모델에 대한 전역 지침을 포기해야 한다는 뜻은 아닙니다. 요령은 이렇습니다: ChatGPT에서 도구 설명은 클래식한 system 텍스트와 동일한 프롬프트로 간주됩니다. 바로 여기에 앱의 ‘두뇌 펌웨어’를 되살려 넣을 수 있습니다.

about 또는 about_app 같은 서비스용 도구를 하나 만드세요. 이 도구는 자주 호출될 필요는 없지만, 그 description은 다른 도구들과 동일하게 모델이 읽습니다. 따라서 그 설명 속에 전체 system‑prompt를 삽입할 수 있습니다. 도구 자체에 대한 짧은 기술 설명 뒤에 배치하는 것이 좋습니다. 결과적으로 모델은 시작 시 당신의 system‑prompt를 있는 그대로 받아들이고, 이후의 대화와 도구 호출 전반에 적용합니다.

유지보수를 쉽게 하려면 system‑prompt 맨 앞에 명시적인 버전을 추가하는 것을 권장합니다. 예: SYSTEM_PROMPT_VERSION: v3. 이렇게 하면 모델이 왜 다르게 행동하는지 추적하기 쉬워집니다. 어떤 지침 세트에서 동작 중인지 즉시 보이기 때문이죠. 프로덕션에서 이상한 행동이 발생하면, 문제가 이전 프롬프트 버전인지 최신 버전인지 빠르게 판단할 수 있습니다.

예시:

tool description 

### Global assistant behavior for the entire GiftGenius App (ver 3.01)
*(system-level guidelines, not user-facing text)*

system prompt text

2. ChatGPT App에서 나타나는 할루시네이션 유형(선물 카탈로그 예시)

치료법을 알기 위해서는 병명을 아는 것이 유용합니다. 카탈로그(선물, 상품, 요금제) 맥락에서 자주 보이는 할루시네이션 유형은 다음과 같습니다.

첫째, 존재하지 않는 카탈로그 아이템을 만들어내는 경우입니다. 사용자가 “특정 서비스의 1년 구독용 디지털 기프트 카드”나 당신의 피드에 없는 아주 이색적인 선물을 요청하면, 모델은 도움이 되고 싶다는 마음에 GiftGenius DB에 전혀 없는 “Super Space Flight 3000 — 우주 비행” 같은 것을 신나게 지어낼 수 있습니다.

둘째, 존재하는 아이템에 가짜 속성을 덧붙이는 경우입니다. 카탈로그에 선물은 있지만, 모델이 현실을 ‘꾸밈’니다: 가격을 바꾸거나, 선물 유형(디지털 vs 실물), 사용자 국가로의 배송 가능 여부, 바우처 유효기간 등을 “그게 더 논리적이거나 들리기 좋아서”라는 이유로 바꿔 버립니다.

셋째, 할루시네이션 기반의 행동입니다. 모델이 “이미 이 선물을 구매했고 코드가 당신의 e‑mail로 발송되었으며, 카드에서 $49가 청구되었습니다”라고 말하지만, 실제로 GiftGenius 백엔드는 아무것도 구매하지 않았고 어떤 ACP/Stripe 플로우도 시작되지 않았던 경우입니다.

마지막으로, 도구가 빈 리스트를 반환했는데(해당 필터/예산에 맞는 선물이 없음), 사용자를 실망시키지 않으려고 모델이 “대략적인 옵션” 몇 개를 지어내고 카탈로그에는 없다는 사실을 설명하지 않는 복합 사례도 있습니다.

우리의 목표는 이런 상황에서 모델이 다음을 수행하도록 만드는 것입니다:

  • 카탈로그에 정확한 일치 항목이 없다는 사실을 솔직히 인정하기;
  • 새 선물을 만들거나 필드 값을 임의로 바꾸지 않기;
  • 무슨 일이 일어났는지 사용자가 이해하도록 설명하고, 다음 단계 제안을 명확히 하기.

그리고 이것을 임의의 위치에 ‘주문’을 넣는 방식이 아니라, system‑prompttoolsfollow‑ups로 연결된 계약을 통해 달성해야 합니다.

3. 레벨 1: 할루시네이션에 강한 system‑prompt 만들기

앞 절에서 본 가짜 선물/속성/행동을 멈추려면, 먼저 상위 레이어인 system‑prompt를 강화해 카탈로그에서의 전반적 ‘행동 철학’을 모델에 규정해야 합니다.

이 모듈의 첫 강의에서 이미 기본 system‑prompt를 작성했습니다. 예: “당신은 GiftGenius이며, 선물 고르기를 돕습니다…” 그리고 어시스턴트의 책임 범위와 도구 사용 방식을 고정했죠.

이제 여기에 명시적인 반‑할루시네이션 규칙을 추가합니다.

논리는 다음과 같습니다. system‑prompt행동의 전반적 철학을 정합니다. 각 도구의 세부는 모르지만 다음을 할 수 있습니다:

  • 카탈로그 밖의 선물/가격/재고를 만들어내는 것을 금지;
  • 도구가 빈 결과나 오류를 반환했을 때의 행동을 명시;
  • 선물 카탈로그 데이터와 모델의 ‘일반 지식’을 명확히 구분하도록 요구.

예시 system‑prompt 조각(우리 Next.js 프로젝트의 TypeScript 상수, 예: config/systemPrompt.ts):

// config/systemPrompt.ts
export const SYSTEM_PROMPT = `
# 역할
당신은 우리 애플리케이션의 선물 카탈로그를 기반으로
선물 선택을 돕는 GiftGenius 어시스턴트입니다.

# 데이터와 제한
- 카탈로그나 도구의 응답에 없는 선물을 지어내지 마세요.
- 가격, 재고 여부, 선물 유형(디지털/실물),
  배송 지역 같은 속성을 꾸며내지 마세요.
- 도구가 필요한 데이터를 반환하지 않거나 오류가 발생하면,
  이를 솔직하게 알리고 값을 추측하려 하지 마세요.

# 도구 사용
- 사실 데이터(선물 목록, 가격, 유형, 재고, SKU 등)가 필요하면
  항상 선물 카탈로그 관련 도구를 사용하세요.
- 도구가 빈 결과를 반환하면, 현재 조건에서는
  적합한 선물이 없다고 말하고 다음을 제안하세요:
  예산/카테고리/선물 유형 등 필터 완화.
`;

여기서 중요한 점이 몇 가지 있습니다.

첫째, 앱 데이터와 모델의 ‘일반 지식’을 분리합니다. 모델은 디지털 선물과 실물 선물의 차이나 보편적 기념일 같은 이론을 설명할 수는 있지만, 구체적인 선물/가격/SKU는 오직 GiftGenius 도구의 결과에서만 가져와야 합니다.

둘째, 오류/빈 응답 시 해야 할 일을 명확히 적습니다. 침묵하거나 ‘창의적’으로 채우는 대신, 아무것도 없다는 사실을 솔직히 말하고 파라미터 변경을 제안하도록 합니다.

셋째, 추상적인 “할루시네이션 금지”가 아니라 우리 도메인(선물 카탈로그와 디지털/실물 SKU 구매)에 맞춘 구체적 행동 규칙에 초점을 둡니다.

이 레이어만으로도 큰 도움이 되지만, 부주의한 도구 설명 하나로 쉽게 무력화될 수 있습니다. 이제 도구 설명으로 넘어가 봅시다.

4. 레벨 2: 도구 설명(tool descriptions)과 스키마를 계약의 형식적 일부로

모델은 기본적으로 다음을 보고 언제/어떻게 도구를 호출할지 결정합니다:

  • 도구 이름;
  • 도구의 description;
  • inputSchema / outputSchema (필드 정의를 담은 JSON Schema).

즉, 도구 설명(tool description)은 ‘사람을 위한 문서’가 아니라, 형식을 갖춘 또 하나의 프롬프트입니다. 많은 할루시네이션이 바로 여기에서 탄생합니다.

백엔드가 GiftGenius 카탈로그에서 선물을 추천하는 recommend_gifts 도구를 상상해 봅시다.

좋지 않은 설명은 다음처럼 보일 수 있습니다:

// 나쁨: 지나치게 모호함
const recommendGiftsTool = {
  name: "recommend_gifts",
  description: "사용자에게 선물을 추천합니다",
  inputSchema: {
    type: "object",
    properties: {
      profile: { type: "string" }
    }
  }
};

형식상 문제가 없어 보여도, 모델은 여기서 다음을 이해하지 못합니다:

  • 도구의 경계가 어디인지;
  • 선물을 찾지 못했을 때 무엇을 해야 하는지;
  • 카탈로그 밖의 선물과 가격을 지어내면 안 된다는 점.

좋은 설명은 여러 일을 동시에 수행합니다. 도메인을 명확히 규정하고, 언제 도구를 호출해야 하는지 설명하며, 결과를 지어내면 안 된다는 점을 강하게 고정합니다.

예시(우리 GiftGenius 계약의 segments, budget, locale, occasion에 맞춰 작성):

// config/tools.ts
export const recommendGiftsTool = {
  name: "recommend_gifts",
  description: `
GiftGenius 카탈로그에서 선물을 추천합니다.

수신자 프로필 세그먼트, 예산, 로캘, 기념일 정보를 바탕으로
실제 선물 목록이 필요할 때 이 도구를 사용하세요.
이 도구는 GiftGenius 카탈로그에 실제로 존재하는 선물만 반환합니다.

이 도구의 결과 밖에서 선물이나 속성을 지어내지 마세요.
도구가 빈 리스트를 반환하면 임의의 대안을 만들지 말고,
대화로 전환해 예산/선물 유형/기념일 등의 조건 변경을 제안하세요.
  `.trim(),
  inputSchema: {
    type: "object",
    properties: {
      segments: {
        type: "array",
        description:
          "수신자 프로필 세그먼트, 예: ['tech', 'fitness'].",
        items: { type: "string" }
      },
      budget: {
        type: "object",
        description: "선물 예산 범위.",
        properties: {
          min: {
            type: "number",
            description: "최소 금액(음수가 될 수 없음).",
            minimum: 0
          },
          max: {
            type: "number",
            description: "최대 금액(0보다 커야 함).",
            exclusiveMinimum: 0
          },
          currency: {
            type: "string",
            description: "통화의 3글자 코드, 예: 'USD' 또는 'RUB'.",
            minLength: 3,
            maxLength: 3
          }
        },
        required: ["min", "max", "currency"]
      },
      locale: {
        type: "string",
        description:
          "사용자 로캘(언어/지역 형식), 예: 'ru-RU' 또는 'en-US'.",
        minLength: 2
      },
      occasion: {
        type: "string",
        description:
          "선물의 용도/기념일: 예 'birthday', 'anniversary', 'new_year'."
      }
    },
    required: ["segments", "budget", "locale", "occasion"]
  }
};

이 description은 다음과 같은 유용한 점이 있습니다.

도구가 GiftGenius 선물 카탈로그만 다룬다는 것을 명확히 하고, 구체적인 선물과 가격은 반드시 도구 결과에서만 가져와야 함을 강조합니다.

또한 언제 도구를 사용해야 하는지도 설명합니다. 수신자 조건에 맞춘 구체적인 선물 목록이 필요할 때(이론이 아니라 실전) 사용한다는 점입니다.

빈 결과일 때의 행동도 정의합니다. 지어내지 말고 대화로 전환하라는 것입니다(이는 이후 follow‑ups에서 더 고정합니다).

그리고 inputSchema는 사용자 요청에서 엔터티를 더 안정적으로 추출하도록 돕습니다. 명확한 구조와 필드 제한(min, max, 길이가 고정된 currency)을 지정하면 이상한 조합이나 파싱 오류 가능성을 낮출 수 있습니다.

또한 언제 사용하지 말아야 하는지도 명시할 수 있습니다. 예를 들어, 요청이 명확히 이론적인 경우:

description: `
...
사용자가 특정인을 위한 추천이 아닌
일반적인 선물 이론 질문만 하는 경우에는 이 도구를 사용하지 마세요
(예: "전반적으로 새해에 인기 있는 선물은 무엇인가요?").
그럴 때는 채팅에서 직접 답해 주세요.
`.trim()

이렇게 하면 system‑prompt의 ‘이론적 vs 실무적’ 요청 규칙과 도구 설명이 일치하게 됩니다.

5. 레벨 3: follow‑ups를 UX 및 안전 레이어로

아무리 system‑prompt와 도구 설명을 잘 써도, 현실은 늘 완벽하지 않습니다:

  • 백엔드에서 오류가 날 수 있고;
  • 카탈로그가 빈 리스트를 줄 수 있으며;
  • 결과가 모호하거나 너무 많을 수 있습니다.

도구 호출 이후 무엇을 말할지를 정해 두지 않으면, 모델은 즉흥적으로 대응합니다. 가끔은 좋지만, 가끔은 지어낸 사실이 섞입니다.

2강에서 이미 기본 UX 지침(앱 시작 알림, 시나리오 종료, “출력” 시 사용자에게 무엇을 말할지)을 봤습니다. 여기에 할루시네이션을 줄여 주는 follow‑up 패턴을 추가해 봅시다.

이 패턴은 보통 system‑prompt 안에 “도구 작업 이후의 대화” 같은 별도 블록으로 기술합니다.

예시 조각:

// SYSTEM_PROMPT 계속
export const SYSTEM_PROMPT = `
# ... 이전 섹션 ...

# 도구 작업 이후의 대화

- 선물 추천 도구가 빈 리스트를 반환했을 때:
  1) 현재 필터로는 적합한 선물을 찾지 못했다고 솔직히 말하세요;
  2) 사용자가 1–2개의 핵심 파라미터
     (예산, 선물 유형, 수신자의 관심사, 기념일)를 바꾸도록 제안하세요.

- 결과가 너무 많을 때:
  1) 가장 관련성이 높은 3–7개를 고르세요;
  2) 어떤 기준으로 선별했는지 명시하세요
     (관심사 일치, 예산 내 적합, 평점 등).

- 도구 오류가 발생했을 때:
  1) 데이터를 지어내지 마세요;
  2) 기술적 오류가 있었다고 알리고,
     나중에 다시 시도하거나 요청을 단순화하자고 제안하세요.
`.trim();

이렇게 하면 모델에 전형적인 follow‑up 문장이 ‘주입’됩니다. 모델은 자신의 말로 표현하겠지만, 구조는 유지됩니다:

  • 사실 진술(빈 결과 / 너무 많음 / 오류);
  • 제한에 대한 솔직한 인정;
  • 다음 단계에 대한 부드러운 제안.

선물 카탈로그에서는 이것이 특히 중요합니다. 빈 결과임에도 “다 됐어요, 선물 3개 드립니다”라고 말하는 대신, 다음처럼 말하게 됩니다:

“현재 조건(우주 관련 디지털 선물, 예산 $5, 배송은 미국만)으로는 카탈로그에 해당 항목이 없습니다. 예산을 늘리거나 다른 카테고리를 제안해 드려도 될까요?”

이것은 도구 코드가 아니라 프롬프트의 지침으로, 기대하는 UX를 정의합니다.

6. 모두 연결하기: 우리 GiftGenius의 진화

우리 앱의 미니 진화를 보면서, 할루시네이션 수준을 단계적으로 낮춰 봅시다.

초기 버전: 어디서 깨지는가

아주 미니멀한 system‑prompt가 있었다고 가정해 봅시다:

export const SYSTEM_PROMPT = `
당신은 선물 추천을 돕는 어시스턴트입니다.
사용자가 적합한 아이디어를 찾도록 도와주세요.
`;

도구 설명은 다음과 같았습니다:

export const recommendGiftsTool = {
  name: "recommend_gifts",
  description: "사용자에게 선물을 추천합니다",
  inputSchema: { type: "object" }
};

Follow‑up 지침은 없습니다.

실제로 어떤 일이 일어날까요:

  • 사용자가 “게이머 친구에게 줄 디지털 선물, 예산 $10” 같은 요청을 했는데, DB에 해당 조건으로는 아무것도 없을 때 모델은:
    • 아예 도구를 호출하지 않고 머릿속에서 선물을 지어내거나;
    • 도구를 호출해 빈 리스트를 받았지만, 그 사실은 말하지 않고 임의로 몇 가지를 제시할 수 있습니다;
  • 백엔드가 오류를 반환하면, “뭔가 있긴 하겠지”라며 추측을 시작할 수도 있습니다.

이렇게 해서 채팅에는 그럴듯한 답이 있지만, 백엔드에는 아무 기록도 없는 고전적인 상황이 생깁니다.

새로운 system‑prompt

3단계 방어를 고려해 system‑prompt를 다시 작성해 보겠습니다. 일부는 위에서 봤으니, 전체를 모아 보죠:

// config/systemPrompt.ts
export const SYSTEM_PROMPT = `
# 역할
당신은 우리 애플리케이션의 선물 카탈로그를 기반으로
선물 선택을 돕는 GiftGenius 어시스턴트입니다.

# 책임 범위
- 당신의 임무는 카탈로그에서 적합한 선물을 고르도록 돕고,
  각 옵션의 장단점을 설명하는 것입니다.
- 실제 구매나 선물 발송을 약속하지 마세요 —
  당신은 어디까지나 추천과 비교를 돕는 역할입니다.
  구매 및 코드/링크 발급은 사용자의 명시적 동의 후 백엔드가 진행합니다.

# 데이터와 제한
- 카탈로그나 도구 응답에 없는 선물을 지어내지 마세요.
- 가격, 선물 유형, 재고, 배송 지역을 꾸며내지 마세요.
- 도구가 데이터를 반환하지 않거나 오류가 발생한 경우,
  추측하지 말고 그 사실을 알리세요.

# 도구 사용
- 선물 카탈로그 관련 도구(예: profile_to_segments,
  recommend_gifts, get_gift)를 사용해
  모든 사실 데이터(선물 목록, 가격, 유형, SKU, 설명)를 얻으세요.
- 특정 선물 추천이 필요 없는 이론적 질문에는
  도구 없이 직접 답하세요.

# 도구 작업 이후의 대화
- 빈 결과: 현재 조건에서는 없다고 솔직히 설명하고,
  1–2개의 파라미터를 바꾸자고 제안하세요.
- 결과 과다: 3–7개로 추려 제시하고,
  선별 기준을 설명하세요.
- 도구 오류: 데이터를 지어내지 말고 사과한 뒤,
  다시 시도하거나 요청을 단순화하자고 제안하세요.
`.trim();

이제 모델은 다음을 분명히 알게 됩니다:

  • 어디까지가 선물 컨설턴트의 영역이고, ‘백오피스’(도구 영역)가 어디인지;
  • 어떤 데이터를 지어내면 절대 안 되는지;
  • 전형적인 비이상적 상황에서 어떻게 행동해야 하는지.

recommend_gifts를 위한 새로운 description과 스키마

시스템 프롬프트를 강화했으니, 이제 도구를 다듬고 4장에서의 아이디어로 설명의 최종 버전을 구성합니다.

// config/tools.ts
export const recommendGiftsTool = {
  name: "recommend_gifts",
  description: `
GiftGenius 카탈로그에서 선물을 추천합니다.

이 도구를 사용하세요:
- 실제 선물 목록과 최신 가격, 유형
  (digital/physical), 태그가 필요할 때;
- 관심사 세그먼트, 예산, 로캘, 기념일로 선택을 좁힐 때.

이 도구를 사용하지 마세요:
- 특정인을 위한 추천이 아닌 일반적인 이론 질문일 때;
- 카탈로그에 없는 선물을 만들어내기 위해.

결과가 비어 있으면 선물을 임의로 만들지 말고,
대화로 제어를 돌려주세요(system‑prompt의 지침을 따르세요).
  `.trim(),
  inputSchema: {
    type: "object",
    properties: {
      segments: {
        type: "array",
        description:
          "수신자 프로필 세그먼트: 예 'tech', 'sport', 'books'.",
        items: { type: "string" }
      },
      budget: {
        type: "object",
        description:
          "사용자 통화 기준(min/max)의 선물 예산 범위.",
        properties: {
          min: { type: "number", minimum: 0 },
          max: { type: "number", exclusiveMinimum: 0 },
          currency: {
            type: "string",
            minLength: 3,
            maxLength: 3,
            description: "ISO 4217 통화 코드, 예: 'USD' 또는 'RUB'."
          }
        },
        required: ["min", "max", "currency"]
      },
      locale: {
        type: "string",
        description: "사용자 로캘, 예: 'ru-RU' 또는 'en-US'."
      },
      occasion: {
        type: "string",
        description:
          "선물 용도/기념일: 'birthday', 'anniversary', 'new_year' 등."
      }
    },
    required: ["segments", "budget", "locale", "occasion"]
  }
};

여기에는 몇 가지 포인트가 있습니다.

system‑prompt와 도구 행동을 명시적으로 연결했습니다. “system‑prompt의 지침을 따르세요”라는 문구는 모델에게 “빈 결과 = 솔직한 설명, 창작 금지”를 상기시킵니다.

또한 부정 조건(“사용하지 마세요…”, “지어내지 마세요…”)을 명확히 적었습니다. 실무적으로 긍정 설명 못지않게 중요합니다.

inputSchema를 의미 있게 구성했습니다. 제대로 된 설명과 제한은 모델이 요청을 필드에 올바르게 매핑하도록 돕고, 도구 호출 전부터 ‘실수’를 줄입니다.

위젯의 follow‑up 패턴과 응답 포맷

텍스트 지침 외에도 한 가지 레버리지가 더 있습니다 — 도구의 응답 포맷 자체입니다. 이를 통해 모델에 무슨 일이 일어났는지 힌트를 주고, 상상력을 줄일 수 있습니다.

형식적으로 follow‑up은 system‑prompt의 텍스트로 정의되지만, Next.js 위젯에서 ToolOutput을 정규화해 모델의 삶을 더 쉽게 만들고 상상 공간을 좁힐 수 있습니다.

예를 들어, 백엔드의 recommend_gifts는 항상 다음을 반환한다고 약속합니다:

// 백엔드의 도구 응답 타입
export type RecommendGiftsResult = {
  items: Array<{
    id: string;
    title: string;
    price: number;
    currency: "USD" | "EUR" | "RUB";
    tags: ("digital" | "physical" | "education" | "fitness" | "tech")[];
  }>;
  // 백엔드가 채워, 모델에게 상황을 명시적으로 전달하는 필드
  status: "ok" | "empty" | "error";
  errorMessage?: string;
};

위젯은 이를 보기 좋게 렌더링하고, 모델은 응답을 구성할 때 status를 근거로 삼을 수 있습니다. Apps SDK에서는 ToolOutput을 종종 JSON 객체로 모델에게 전달하며, 모델은 이 필드를 볼 수 있습니다.

system‑prompt에 작은 블록을 추가할 수 있습니다:

# 도구 status 해석

- status = "empty"인 경우: “도구 작업 이후의 대화” 섹션을 따르고
  선물을 지어내지 마세요.
- status = "error"인 경우: 기술적 오류를 알리고
  카탈로그 내용을 추측하지 마세요.

물론 모델이 이 없이도 눈치챌 수 있지만, 명시적 지침은 ‘추측’ 가능성을 더 낮춰 줍니다.

7. 실습: 내 App 다듬기

“슬라이드에서는 멋져 보이는데…”라는 느낌이 들지 않도록, 지금 사용 중인 App(우리의 경우 GiftGenius)으로 바로 해볼 수 있는 연습을 정리해 보겠습니다. system‑prompt, 도구 설명, 결과 처리 — 이 세 레이어를 소폭 리팩터링해 봅시다.

첫째, system‑prompt 레벨에서, 1강의 system‑prompt를 열고 책임 범위와 도구 사용을 설명하는 부분을 찾으세요. 여기에 다음을 추가합니다:

  • 카탈로그/DB 밖의 엔터티(선물, SKU, 가격)를 지어내는 행위 금지;
  • 빈 결과/도구 오류 시 행동 규칙;
  • “도구 작업 이후의 대화” 섹션에 2–3개의 시나리오(빈 결과, 과다 결과, 오류).

둘째, 도구 설명 레벨에서, 핵심 도구의 설명을 여세요(예: recommend_gifts, search_gifts, search_tariffs, calculate_quote 등). description을 다음을 만족하도록 다시 작성하세요:

  • 도구가 오직 당신의 데이터 소스(선물/요금제 카탈로그 등)만 다룬다는 점을 명확히;
  • 언제 필요하고, 언제 필요하지 않은지 설명;
  • 명시적 부정 제한 포함: “지어내지 마세요…”, “이럴 때 사용하지 마세요…”.

셋째, 도구 응답 구조 레벨에서, 아직 결과 상태를 명시하는 필드(status, resultType, hasMore)가 없다면 백엔드 타입과 ToolOutput에 추가하세요. 그다음 system‑prompt에 모델이 그 status를 대화에서 어떻게 해석해야 하는지를 적어 둡니다.

마지막으로 Dev Mode에서 몇 가지 요청을 돌려보세요. 특히 결과가 의도적으로 비거나 경계 조건인 경우를 포함해 테스트하고, 모델이 더는 지어내지 않는지, 그리고 제한을 사용자에게 얼마나 솔직하게 전달하는지 확인하세요.

다음 강의에서는 이런 요청들을 golden prompt set으로 형식화해 반복 가능한 테스트 아티팩트로 만들 겁니다. 그전까지는 손으로 차이를 체감해 보는 것이 중요합니다.

8. 프롬프트와 도구로 할루시네이션을 다룰 때의 흔한 실수

오류 №1: system‑prompt에 “할루시네이션 금지” 한 줄만.
프롬프트 끝에 “정보를 지어내지 마라”라고 쓰고 끝내는 경우가 있습니다. 실전에서는 모델이 여전히 지어냅니다. 도구 설명과 follow‑up이 대안 행동을 제시하지 않기 때문입니다. “지어내는 대신 무엇을 할지”(빈 결과 인정, 필터 변경 제안, 오류 알림)를 구체적으로 명시하지 않으면, 이런 문구는 거의 도움이 되지 않습니다.

오류 №2: system‑prompt와 도구 description 간의 모순.
system‑prompt에서는 “카탈로그에 없는 선물을 지어내지 마라”고 해놓고, 도구 설명에서는 “적합한 선물을 찾지 못하면 비슷한 걸 임의로 제안할 수 있다”고 적어둔다면, 모델은 두 ‘진실’ 사이에서 흔들립니다. 일반적으로 더 구체적인 쪽(보통 도구 description)이 이깁니다. 두 레이어가 동일한 메시지를 말해야 합니다. 유사 대안이 허용된다면, 그것도 형식화하고(정확한 일치가 아님을 사용자에게 반드시 설명) 일관되게 기술해야 합니다.

오류 №3: 지나치게 모호한 도구 설명.
“사용자 과제를 돕는 도구” 같은 설명은 도구의 경계를 거의 알려주지 않습니다. 이 경우 모델은 아예 도구를 사용하지 않거나, 아무 때나 호출했다가 결과가 적거나 없을 때 데이터를 ‘메꾸는’ 쪽으로 흐르기 쉽습니다. 좋은 description은 식별력이 있어야 합니다. 도구가 무엇을 하는지와 언제 호출하면 안 되는지를 분명히 말해야 합니다.

오류 №4: 빈/오류 결과에 대한 전략 부재.
백엔드가 { items: [], status: "empty" }를 친절히 반환해도, 모델이 그것의 의미를 어디에서도 배우지 못하면 문제입니다. 모델은 빈 배열을 보고 “그럼 일반 지식에서 뭔가 제시하자”라고 판단할 수 있습니다. system‑prompt에 이런 상태를 어떻게 해석하고 사용자에게 무엇을 말할지 설명하는 섹션이 필요합니다. 빈/오류 결과에 대한 간단한 규칙 몇 개만 추가해도 품질이 크게 향상됩니다.

오류 №5: 위젯 코드 레벨에서만 ‘치료’하려는 시도.
“리스트가 비면 플레이스홀더를 보여주고 모델의 텍스트 답은 숨기자”처럼 모든 것을 프론트엔드에 떠넘기고 싶을 때가 있습니다. UX를 조금 누그러뜨릴 수는 있지만, 모델은 여전히 허구의 엔터티를 믿고 이후 발화에서도 같은 행동을 할 가능성이 큽니다. 올바른 접근은 먼저 지침(system‑prompt, 도구 설명, follow‑ups)을 변경하고, 그 다음 UI 보호장치를 보완하는 것입니다.

오류 №6: 메타데이터와 스키마가 모델 행동에 미치는 영향 무시.
일부 개발자는 JSON Schema와 필드 설명을 “폼 검증용”으로만 봅니다. 실제로 ChatGPT에게 이는 중요한 프롬프트의 일부입니다. 이 설명을 통해 모델은 요청에서 어떤 파라미터를 추출해야 하는지, 올바른 응답이 어떤 형태인지 이해합니다. 빈약하거나 일관되지 않은 필드 설명(description, enum)은 오류 가능성을 높이고, 간접적으로 할루시네이션을 부추깁니다.

오류 №7: 대안 없이 금지만 나열.
프롬프트가 온통 “이건 하지 마, 저건 하지 마”로만 가득 차 있는데, 어려운 상황에서 무엇을 해야 하는지 쓰여 있지 않은 경우가 있습니다. 예를 들어, 선물 지어내기는 금지하고 카탈로그만 허용했지만, 이론적 질문에 대해서는 아무 말도 하지 않는 식입니다. 그러면 모델은 유용한 일반 원칙을 설명할 수 있음에도 “모르겠습니다”라고만 답하기도 합니다. 항상 금지와 함께 허용된 경로를 열어 주세요. 예: “카탈로그에서 찾지 못하면 솔직히 말하고 요청 변경 방법을 제안하라” 또는 “이론 질문이면 도구 없이 직접 답하라.”

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION