Scenario v2 작성 가이드¶
이 문서는 Scenario 작성자가 사용하는 schema와 작성 규칙의 기준 문서입니다. 런타임 내부 실행 순서는 Graph Builder Runtime, 실제 Scenario별 의미는 각 예제 문서에서 설명합니다.
대화를 말로 설명해서 JSON을 먼저 만들려면 시나리오 생성기를 사용하세요. 생성 결과를 다듬을 때 이 가이드의 필드 계약을 따릅니다.
1. 핵심 원칙¶
- V2 구조만 사용합니다. 최상위 영역은
metadata,prompts,slots,graph로 분리합니다. - 노드는 역할을 선언합니다.
description,responses,collect_slots,params, 전이를 사용해 목적을 표현합니다. - 상태 판정과 말하기를 분리합니다. 슬롯 업데이트와 라우팅은 policy/runtime이, 자연어 표현은 NLG가 담당합니다.
- 수집 대상은 명시합니다. 전역
slots는 registry이고, 실제 수집 대상은 각 노드의collect_slots입니다. - 종료는 라우팅으로 처리합니다. 사용자의 종료 의도는 terminal 노드로 이동시킵니다.
- 런타임이 읽지 않는 필드는 만들지 않습니다. 선언만 있고 소비되지 않는 설정은 아무 효과가 없습니다.
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은 다음을 짧게 설명합니다.
- 이 노드가 무엇을 달성해야 하는가
- 어떤 정보를 수집하는가
- 어떤 질문을 먼저 해야 하는가
- 이 노드에서 하지 말아야 할 행동은 무엇인가
피해야 할 형태:
- 전체 시나리오 prompt를 노드마다 반복
- 라우팅 로직을 자연어로만 설명
- 서로 모순되는 지시를 나열
- 코드가 읽지 않는 임의 설정을 description 대신 추가
8. Responses 사용¶
default: NLG 기준 문장과 최종 fallback¶
일반 응답 노드에서는 responses.default가 NLG prompt의 기준 문장으로 전달됩니다. 모델은
문장을 그대로 복사하지 않고 의미와 말투를 참고합니다. 같은 문장은 LLM 호출이 실패했을
때 최종 fallback으로도 쓰일 수 있습니다. Task 노드의 outcome별 템플릿은 이와 달리
대체로 prompt에 들어가지 않습니다.
같은 default를 모든 턴에 넣으면 반복적인 답변을 유도할 수 있습니다. 자유 대화처럼 정형 문장이 필요 없는 노드는 responses를 생략할 수 있습니다.
실패 안전망과 outcome 템플릿¶
no_input과 not_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로 생성 |
10. Routing¶
선형 이동¶
조건부 이동¶
"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 값 |
라우팅 우선순위¶
global_transitions- 현재 노드
next_nodes배열 순서 default_next- terminal 판정
- 정적 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. 작성 절차¶
- 시나리오 목적과 종료 조건 정의
- 필요한 노드와 기본 순서 작성
- 각 노드가 수집할 정보 식별
- 전역
slots정의 - 각 노드
collect_slots연결 required와optional결정- 조건부 전이와
default_next작성 - terminal 노드와 전역 종료 전이 작성
- 노드 description과 필요한 responses 작성
- scenario prompt는 마지막에 공통 지침만 추가
- validator와 CLI로 검증
14. 검증 방법¶
대화 테스트:
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 경로를 구분했다.
- [ ]
action과target을 한 항목에 함께 넣지 않았다. - [ ] terminal 노드를 정의했다.
- [ ] 전역 종료 전이를 검토했다.
- [ ] 런타임이 읽지 않는 params를 넣지 않았다.
- [ ] 자유 대화에 불필요한 responses와 turn limit을 두지 않았다.
- [ ] validator를 통과했다.
- [ ] 실제 CLI에서 정상·무응답·이해 불가·종료 경로를 테스트했다.
관리 원칙: 코드 동작이 바뀌면 scenario schema, Graph Builder 문서, 대표 시나리오 예제와 이 가이드라인을 함께 업데이트합니다.