콘텐츠로 이동

Scenario v2 작성 가이드

이 문서는 Scenario 작성자가 사용하는 schema와 작성 규칙의 기준 문서입니다. 런타임 내부 실행 순서는 Graph Builder Runtime, 실제 Scenario별 의미는 각 예제 문서에서 설명합니다.

대화를 말로 설명해서 JSON을 먼저 만들려면 시나리오 생성기를 사용하세요. 생성 결과를 다듬을 때 이 가이드의 필드 계약을 따릅니다.

1. 핵심 원칙

  1. V2 구조만 사용합니다. 최상위 영역은 metadata, prompts, slots, graph로 분리합니다.
  2. 노드는 역할을 선언합니다. description, responses, collect_slots, params, 전이를 사용해 목적을 표현합니다.
  3. 상태 판정과 말하기를 분리합니다. 슬롯 업데이트와 라우팅은 policy/runtime이, 자연어 표현은 NLG가 담당합니다.
  4. 수집 대상은 명시합니다. 전역 slots는 registry이고, 실제 수집 대상은 각 노드의 collect_slots입니다.
  5. 종료는 라우팅으로 처리합니다. 사용자의 종료 의도는 terminal 노드로 이동시킵니다.
  6. 런타임이 읽지 않는 필드는 만들지 않습니다. 선언만 있고 소비되지 않는 설정은 아무 효과가 없습니다.

2. 전체 구조

{
  "metadata": {
    "slug": "sample_scenario",
    "name": "샘플 시나리오",
    "description": "시나리오 설명",
    "language": "ko",
    "version": "1.0.0"
  },
  "prompts": {
    "bot": "봇 응답 생성용 시스템 지침",
    "nlu": "의도와 엔티티 추출용 도메인 지침"
  },
  "slots": {},
  "graph": {
    "entry": "greeting",
    "global_transitions": [],
    "nodes": []
  }
}

최상위 영역의 책임

필드 타입 필수 의미
metadata object 식별자와 표시 정보
prompts.bot string 권장 시나리오 전체의 정체성, 말투, 도메인 제약
prompts.nlu string 권장 도메인 특화 intent/entity/slot 추출 기준
slots object 아니오 전체 시나리오에서 참조 가능한 슬롯 정의
graph.entry string 최초 진입 노드 ID
graph.global_transitions array 아니오 어느 노드에서나 우선 적용할 전이
graph.nodes array 노드 정의 배열

3. Metadata 작성

"metadata": {
  "slug": "senimate",
  "name": "시니메이트",
  "description": "어르신의 건강과 일상을 살피는 대화",
  "language": "ko",
  "version": "1.0.0"
}
필드 타입 필수 의미
slug string 파일명과 독립적인 내부 식별자, 영문 snake_case 권장
name string 사용자 또는 운영 화면에 표시할 이름
description string 시나리오 전체 목적
language string ko, ja 등 언어 코드
version string 변경 추적용 버전
id string 아니오 외부 저장소에서 사용할 안정적인 고유 ID

4. Prompt 작성

prompts.bot

시나리오 전체에 적용할 내용만 작성합니다.

  • 챗봇의 역할과 정체성
  • 대상 사용자
  • 언어와 말투
  • 도메인 안전 제약
  • 시나리오 전반에 필요한 금지사항

노드별 질문 순서나 라우팅 조건은 넣지 않습니다. 지나치게 많은 긍정·부정 지시를 섞으면 LLM이 핵심을 놓칠 수 있으므로 짧고 실행 가능하게 작성합니다.

"prompts": {
  "bot": "당신은 한국어 생활 지원 대화 봇입니다. 사용자의 질문에 직접 답하고, 의학적 진단이나 처방은 하지 마세요.",
  "nlu": "직전 봇 질문과 현재 노드의 collect_slots를 기준으로 intent와 entities를 추출하세요."
}

prompts.nlu

공통 NLU 출력 형식은 런타임이 제공합니다. 여기에는 도메인에서만 필요한 해석 기준을 추가합니다.

  • 축약형·구어체 해석 방법
  • 도메인 값 정규화 기준
  • 혼동하기 쉬운 intent 구분
  • 특정 슬롯을 채워도 되는 문맥
  • 종료 의도와 단순 부정 응답의 차이

5. Slot 설계

전역 slots는 정의 registry입니다.

"slots": {
  "meal_status": {
    "policy": "optional",
    "value_type": "enum",
    "values": ["adequate", "skipped"],
    "description": "최근 식사 섭취 상태"
  },
  "symptom": {
    "policy": "optional",
    "value_type": "string",
    "description": "사용자가 표현한 증상"
  }
}

필드

필드 타입 필수 의미
policy required \| optional 슬롯 수집 정책
value_type string enum, string, number, boolean 등 값 타입
values array enum일 때 enum의 허용 값
description string 권장 NLU가 값을 해석할 수 있는 의미 설명

설계 기준

  • required: 값이 없으면 다음 단계 진행이 불가능한 정보에만 사용
  • optional: 수집하지 못해도 흐름을 계속할 수 있는 정보
  • enum 값은 모호하지 않은 작은 집합으로 유지
  • 슬롯 ID는 영문 snake_case 사용
  • 한 슬롯에 서로 다른 개념을 섞지 않음

6. Node 작성

{
  "id": "meal_check",
  "name": "식사 확인",
  "description": "최근 식사 여부를 확인하는 노드입니다.",
  "responses": {
    "default": "오늘 식사는 하셨어요?"
  },
  "params": {
    "min_turns": 1,
    "max_turns": 3
  },
  "collect_slots": ["meal_status"],
  "default_next": "health_check"
}

주요 필드

필드 타입 필수 의미
id string 노드 고유 ID
name string 아니오 표시 이름
description string 노드 목적과 해당 노드에서만 필요한 응답 지침
responses object 아니오 NLG 기준 문장과 LLM 실패 시 안전 문구
params object 아니오 실행 제어값
collect_slots array[string] 아니오 현재 노드가 수집할 슬롯 ID와 순서
next_nodes array[object] 아니오 조건부 전이 또는 action
default_next string 아니오 조건이 모두 불일치할 때 이동할 노드
terminal boolean 아니오 명시적 종료 노드 여부

Executor 선택

Executor는 별도 필드로 지정하지 않습니다.

  • 유효한 collect_slots가 있으면 TaskOrientedExecutor
  • 없으면 GeneralResponseExecutor

인사, 안내, 자유 대화, 마무리를 위한 전용 executor를 새로 만들지 않고 노드 설정으로 표현합니다.

graph.bot_first: true이고 entry 노드에 collect_slots가 있으면 Runtime은 첫 봇 발화 후 자동으로 해당 노드에 머물러 사용자의 첫 답변을 받습니다. 이 경우 첫 턴 전용 action: "stay"를 반복해서 선언할 필요가 없습니다. 슬롯을 수집하지 않는 entry 노드는 기존 전이 규칙을 따릅니다.

7. description 작성 기준

좋은 description은 다음을 짧게 설명합니다.

  • 이 노드가 무엇을 달성해야 하는가
  • 어떤 정보를 수집하는가
  • 어떤 질문을 먼저 해야 하는가
  • 이 노드에서 하지 말아야 할 행동은 무엇인가
"description": "몸 상태를 확인합니다. 먼저 현재 불편한 곳이 있는지 한 가지 질문만 하세요. 불편하지 않다고 하면 추가 증상을 캐묻지 마세요."

피해야 할 형태:

  • 전체 시나리오 prompt를 노드마다 반복
  • 라우팅 로직을 자연어로만 설명
  • 서로 모순되는 지시를 나열
  • 코드가 읽지 않는 임의 설정을 description 대신 추가

8. Responses 사용

default: NLG 기준 문장과 최종 fallback

일반 응답 노드에서는 responses.default가 NLG prompt의 기준 문장으로 전달됩니다. 모델은 문장을 그대로 복사하지 않고 의미와 말투를 참고합니다. 같은 문장은 LLM 호출이 실패했을 때 최종 fallback으로도 쓰일 수 있습니다. Task 노드의 outcome별 템플릿은 이와 달리 대체로 prompt에 들어가지 않습니다.

같은 default를 모든 턴에 넣으면 반복적인 답변을 유도할 수 있습니다. 자유 대화처럼 정형 문장이 필요 없는 노드는 responses를 생략할 수 있습니다.

실패 안전망과 outcome 템플릿

no_inputnot_understood도 고정 응답이 아닙니다. 무응답·이해 불가 상황에서도 FallbackPolicy가 먼저 LLM으로 자연스러운 응답을 만들고, 노드 문구나 공통 기본 문구는 그 호출이 실패했을 때만 사용합니다.

사용 시점
no_input 무응답 fallback의 LLM 실패 안전망
not_understood 이해 불가 fallback의 LLM 실패 안전망
initial 첫 질문 또는 required 슬롯 재요청 fallback
follow_up 같은 주제 대화 유지 fallback
confirmation 슬롯 완료·전환 확인 fallback
repeat_question 질문 반복 요청 fallback
turn_limit 노드 턴 제한 도달 fallback
farewell 종료 응답 fallback

answer, acknowledge_request, alternative, correction, help 같은 dialog-act outcome 템플릿도 일반적으로 LLM 예외 때만 사용됩니다. 이 값들을 바꿔도 정상 응답의 화법은 달라지지 않을 수 있습니다. 정상 화법은 노드 description, scenario prompt, 공통 domain rule이 담당합니다.

9. Params

현재 주요 설정:

필드 타입 기본값 의미
min_turns integer runtime 기본값 슬롯을 채워도 현재 노드에 머물 최소 턴 수
max_turns integer executor 기본값 현재 노드에 머물 최대 턴 수
max_fallback_retries integer 3 같은 종류의 무응답·이해 불가 재시도 한도
completion_slots array[string] [] 완료 판단에 참고할 슬롯
turn_limit_enabled boolean true false이면 executor turn limit과 NLG turn-budget 안내 비활성화
nlg_opening boolean false true이면 bot first 첫 발화를 responses.default 복사가 아니라 NLG로 생성
"params": {
  "turn_limit_enabled": false
}

10. Routing

선형 이동

"default_next": "health_check"

조건부 이동

"next_nodes": [
  {
    "target": "conversation_closure",
    "condition": {
      "field": "nlu.intent",
      "operator": "equals",
      "value": "user_wants_to_end"
    }
  }
]

현재 노드 유지

{
  "action": "stay",
  "condition": {
    "field": "nlu.intent",
    "operator": "not_equals",
    "value": "user_wants_to_end"
  }
}

조건 field 권장 형태

field 형태 의미
slots.slot_id 저장된 슬롯 값 누적 도메인 상태
nlu.intent string 현재 턴 intent
nlu.dialogue_act string 현재 턴 dialog act
nlu.entities.key Any 현재 턴 추출 entity
nlu.confidence float NLU confidence
system.current_node string 현재 노드
system.node_turns integer 현재 노드 턴 수
system.no_input_count integer 연속 무응답 수
system.end_reason Optional[string] 현재 턴 종료·전환 사유
system.conversation_resuming boolean 마무리 노드에서 대화를 다시 이어가는지
context.key Any DialogueState.context

라우팅 우선순위

  1. global_transitions
  2. 현재 노드 next_nodes 배열 순서
  3. default_next
  4. terminal 판정
  5. 정적 graph successor fallback

조건부 예외 전이를 default_next로 선언하지 않습니다. default_next는 평상시 이동 경로에만 사용합니다.

11. Global Transition과 종료

사용자가 어느 노드에서든 대화를 끝낼 수 있어야 하면 전역 종료 전이를 선언합니다.

"global_transitions": [
  {
    "name": "user_wants_to_end",
    "target": "conversation_closure",
    "condition": {
      "field": "nlu.intent",
      "operator": "equals",
      "value": "user_wants_to_end"
    }
  }
]

마무리 노드는 terminal: true를 권장합니다.

{
  "id": "conversation_closure",
  "name": "대화 마무리",
  "description": "사용자의 말을 짧게 받은 뒤 한 번만 마무리합니다.",
  "terminal": true
}

terminal: true는 나가는 조건부 edge가 있어도 이 노드를 종료 지점으로 유지합니다. 사용자가 마무리를 거두었을 때 이전 대화로 복귀시키는 구조에 필요합니다.

12. 자유 대화 시나리오

목적 대화와 슬롯 수집이 필요 없다면 단일 자유 대화 노드와 종료 노드만 사용합니다.

{
  "metadata": {
    "slug": "general_conversation",
    "name": "자유 대화",
    "description": "목적과 슬롯 수집이 없는 자유 대화",
    "language": "ko",
    "version": "1.0.0"
  },
  "prompts": {
    "bot": "사용자의 최신 입력에 직접 답하세요.",
    "nlu": "사용자가 명시적으로 대화를 끝내는지 정확히 구분하세요."
  },
  "graph": {
    "entry": "conversation",
    "global_transitions": [
      {
        "name": "user_wants_to_end",
        "target": "conversation_closure",
        "condition": {
          "field": "nlu.intent",
          "operator": "equals",
          "value": "user_wants_to_end"
        }
      }
    ],
    "nodes": [
      {
        "id": "conversation",
        "name": "자유 대화",
        "description": "사용자의 최신 발화에 자연스럽게 답합니다.",
        "params": {
          "turn_limit_enabled": false
        },
        "next_nodes": [
          {
            "target": "conversation_closure",
            "condition": {
              "field": "nlu.intent",
              "operator": "equals",
              "value": "user_wants_to_end"
            }
          },
          {
            "action": "stay",
            "condition": {
              "field": "nlu.intent",
              "operator": "not_equals",
              "value": "user_wants_to_end"
            }
          }
        ]
      },
      {
        "id": "conversation_closure",
        "name": "대화 종료",
        "description": "종료 의사를 짧게 받아들이고 대화를 마칩니다.",
        "terminal": true
      }
    ]
  }
}

자기 자신을 default_next로 지정하면 self-loop 때문에 Graph validator가 시작 노드를 찾지 못할 수 있습니다. 정적 graph에는 종료 노드 방향 edge를 두고, 런타임 체류는 action: stay로 표현합니다.

현재 예제의 상세 설계와 runtime 동작은 자유 대화 시나리오 문서를 참고합니다.

13. 작성 절차

  1. 시나리오 목적과 종료 조건 정의
  2. 필요한 노드와 기본 순서 작성
  3. 각 노드가 수집할 정보 식별
  4. 전역 slots 정의
  5. 각 노드 collect_slots 연결
  6. requiredoptional 결정
  7. 조건부 전이와 default_next 작성
  8. terminal 노드와 전역 종료 전이 작성
  9. 노드 description과 필요한 responses 작성
  10. scenario prompt는 마지막에 공통 지침만 추가
  11. validator와 CLI로 검증

14. 검증 방법

python -m cli.chatbot \
  --scenario dm/scenario/my_scenario.json \
  --validate-only

대화 테스트:

python -m cli.chatbot \
  --scenario dm/scenario/my_scenario.json \
  --llm-choice gpt-4o \
  --nlu-llm-choice gpt-4o-mini \
  --output-level detailed

확인 항목:

  • JSON 문법이 올바른가
  • graph.entry가 실제 노드 ID인가
  • 시작 노드의 정적 in-degree가 0인가
  • 모든 target/default_next가 실제 노드인가
  • 수집 슬롯이 전역 slots에 정의되어 있는가
  • 모든 params 키가 Runtime 허용 목록에 포함되는가
  • 종료 노드가 존재하는가
  • 일반 턴에서 예상치 못한 closure 이동이 없는가
  • action: stay가 필요한 경로에 있는가
  • 같은 질문과 기준 문장이 반복되지 않는가
  • 명시적인 종료 의도에서만 종료되는가

15. 자주 발생하는 오류

시작 노드가 없다고 나옴

원인: 시작 노드가 자기 자신을 향하는 self-loop를 가져 in-degree가 0이 아님.

해결: self default_next를 제거하고 조건부 target + action: stay 사용.

Prompt를 작성했는데 적용되지 않음

원인: metadata.prompts.system_prompt처럼 지원하지 않는 위치에 작성.

해결: 최상위 prompts.bot, prompts.nlu 사용.

params에 Runtime이 지원하지 않는 키가 있다고 나옴

원인: Runtime 코드가 소비하지 않는 설정을 선언.

해결: 해당 키를 제거합니다. 허용 키는 dm/graph/scenario_contract.py::ALLOWED_NODE_PARAMS에서 확인합니다. 새 키가 실제로 필요하면 Runtime 소비 코드를 먼저 구현한 뒤 공용 계약에 추가합니다.

슬롯 없는 노드가 다음 단계로 이동하려 함

원인: 슬롯이 없는데 all_slots_filled를 전환 신호처럼 취급하거나 불필요한 수집 설정을 선언.

해결: collect_slots를 생략하고 일반 응답 노드로 유지. 턴 수로 떠나지 않으려면 turn_limit_enabled: false를 둔다.

조건부 종료 노드의 말투가 평소 응답에 섞임

원인: 조건부 종료 target을 평상시 다음 노드로 추정하거나 default_next로 선언.

해결: 종료는 조건부 next_nodes 또는 global_transitions에만 둠.

Responses를 수정했는데 정상 응답이 바뀌지 않음

원인: 수정한 키가 LLM 실패 경로 전용.

해결: 정상 NLG의 기준 문장은 default; 상황별 공통 화법은 domain rule을 확인.

16. 최종 체크리스트

  • [ ] V2 최상위 구조를 사용했다.
  • [ ] metadata와 prompts를 분리했다.
  • [ ] node ID와 slot ID는 snake_case다.
  • [ ] 모든 collect_slots가 전역 registry에 존재한다.
  • [ ] required 슬롯은 반드시 필요한 정보에만 사용했다.
  • [ ] 조건부 전이와 평상시 default 경로를 구분했다.
  • [ ] actiontarget을 한 항목에 함께 넣지 않았다.
  • [ ] terminal 노드를 정의했다.
  • [ ] 전역 종료 전이를 검토했다.
  • [ ] 런타임이 읽지 않는 params를 넣지 않았다.
  • [ ] 자유 대화에 불필요한 responses와 turn limit을 두지 않았다.
  • [ ] validator를 통과했다.
  • [ ] 실제 CLI에서 정상·무응답·이해 불가·종료 경로를 테스트했다.

관리 원칙: 코드 동작이 바뀌면 scenario schema, Graph Builder 문서, 대표 시나리오 예제와 이 가이드라인을 함께 업데이트합니다.