시나리오 생성기¶
만들고 싶은 대화를 말로 설명하면, LLM이 실행 가능한 Scenario v2 JSON을 만듭니다. 손으로 JSON을 처음부터 쓰지 않아도 됩니다.
스키마와 필드 의미의 기준은 Scenario 작성 가이드입니다. 이 문서는 생성 → 검증 → 대화 확인만 다룹니다.
한 장으로 이해하기¶
서비스 설명 (자연어)
→ build_messages()
→ LLM (JSON 한 개)
→ GraphBuilder 검증
→ 오류가 있으면 build_revision_messages()로 고침
→ dm/scenario/<slug>.json 저장
→ CLI로 대화 테스트
생성기는 런타임이 실제로 읽는 필드만 가르칩니다. 허용 계약에 없는 설정이나 의도를 임의로 만들지 않습니다.
준비¶
프로젝트 루트에서 가상환경을 켜고 .env에 LLM 키를 둡니다. 설치는
프로젝트 README를 따릅니다.
Azure OpenAI만 있으면 AZURE_OPENAI_ENDPOINT / AZURE_OPENAI_KEY /
AZURE_OPENAI_VERSION을 사용합니다.
1. 서비스를 짧게 적기¶
단계 번호와 끝내는 방식을 적어 주세요. 빠진 부분은 생성기가 일반 흐름으로 채우지만, 결제 승인·재고 조회처럼 실제 연동이 없는 일을 완료했다고 말하지는 않습니다.
좋은 설명 예:
병원 예약을 받는 챗봇입니다.
1. 인사하고 예약을 도와준다고 안내
2. 진료과 선택
3. 희망 날짜와 오전/오후 선택
4. 이름과 연락처 확인
5. 예약 내용을 읽고 접수할지 확인
사용자가 그만두겠다고 하면 예약을 취소하고 끝냅니다.
봇이 먼저 인사합니다.
적을 것:
- 대화가 끝나면 무엇이 모여 있어야 하는가
- 정상 완료 / 사용자 취소가 어떻게 다른가
- 한 번에 물을 선택지 (메뉴, 카드 종류 등)
- bot first가 필요한지 (
graph.bot_first: true)
적지 말 것: 주민등록번호, 비밀번호, CVC 같은 민감정보. 생성기도 이런 슬롯은 만들지 않도록 되어 있습니다.
2. JSON 만들기¶
프로젝트 루트에서 Python을 실행합니다.
import json
from openai import OpenAI
from dm.generator import build_messages
description = """
병원 예약을 받는 챗봇입니다.
1. 인사하고 예약을 도와준다고 안내
2. 진료과 선택
3. 희망 날짜와 오전/오후 선택
4. 이름과 연락처 확인
5. 예약 내용을 읽고 접수할지 확인
사용자가 그만두겠다고 하면 예약을 취소하고 끝냅니다.
"""
messages = build_messages(
description,
slug="clinic_booking",
language="ko",
extra_requirements=[
"bot first 로 봇이 먼저 인사한다",
"진료과는 내과, 정형외과, 치과 세 가지만",
],
)
client = OpenAI() # OPENAI_API_KEY
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.3,
max_tokens=8192,
response_format={"type": "json_object"},
)
scenario = json.loads(response.choices[0].message.content)
Azure OpenAI를 쓰면 프로젝트 클라이언트를 그대로 쓸 수 있습니다.
from dm.core.llm.clients.openai_client import OpenAIClient
llm = OpenAIClient()
response = llm.client.chat.completions.create(
model=llm.default_model,
messages=messages,
temperature=0.3,
max_tokens=8192,
response_format={"type": "json_object"},
)
temperature는 낮게 둡니다. 스키마를 지켜야 하므로 창의성보다 계약 준수가
중요합니다. 시나리오 JSON은 보통 수 KB라 max_tokens=8192를 권합니다.
build_messages 옵션¶
| 인자 | 기본값 | 언제 쓰나 |
|---|---|---|
slug |
my_scenario |
파일명·metadata.slug |
language |
ko |
ko 또는 ja |
extra_requirements |
없음 | bot first, 존댓말, 선택지 고정 등 한 줄씩 |
few_shot |
True |
카페 주문 정답 예시. 끄면 토큰이 줄지만 스키마 위반이 늘 수 있음 |
seed_assistant |
True |
설계 절차(목표 → 단계 → 슬롯 → 연결)를 먼저 밟게 함 |
reference_scenarios |
없음 | 기존 운영 시나리오를 참고 자료로 첨부 |
기존 SeniMate 구조를 닮은 케어 대화를 만들 때:
from pathlib import Path
senimate = json.loads(
Path("dm/scenario/senimate_weather.json").read_text(encoding="utf-8")
)
messages = build_messages(
description,
slug="my_care_bot",
reference_scenarios={"senimate": senimate},
)
내용을 복사하지 말고 구조와 문장 밀도만 참고하라고 프롬프트에 적혀 있습니다.
3. 검증하고 고치기¶
생성 결과는 반드시 GraphBuilder로 확인합니다. 오류 문구를 그대로 되먹이면
됩니다.
from dm.graph.graph_builder import GraphBuilder
from dm.generator import build_revision_messages
def validate(scenario: dict) -> list[str]:
builder = GraphBuilder()
load = builder.load_from_dict(scenario)
if not load.ok:
return [load.error]
report = builder.build_graph()
return list(report.errors) + list(report.warnings)
for _ in range(3):
issues = validate(scenario)
if not issues:
break
messages = build_revision_messages(
scenario,
issues,
service_description=description,
slug="clinic_booking",
)
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.2,
max_tokens=8192,
response_format={"type": "json_object"},
)
scenario = json.loads(response.choices[0].message.content)
else:
raise RuntimeError("검증을 통과하지 못했습니다:\n" + "\n".join(issues))
통과하면 저장합니다.
from pathlib import Path
out = Path("dm/scenario/clinic_booking.json")
out.write_text(
json.dumps(scenario, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print("저장:", out)
4. CLI로 대화 확인¶
--validate-only는 그래프 형태만 봅니다. 실제 말은 CLI로 확인합니다.
python -m cli.chatbot \
--scenario dm/scenario/clinic_booking.json \
--validate-only
python -m cli.chatbot \
--scenario dm/scenario/clinic_booking.json \
--output-level detailed
대화에서 확인할 것:
- 정상 경로가 목표(접수·안내)까지 가는가
- 무응답·이해 불가에서 같은 질문만 반복하지 않는가
- 명시적인 종료 의도에서만 끝나는가
- 취소·거절이 취소 terminal 노드로 가는가
노드를 고칠 때는 JSON을 직접 수정해도 되고, 지적 사항을 issues에 넣어
build_revision_messages를 다시 호출해도 됩니다.
생성기가 지키는 현재 계약¶
런타임과 어긋나면 대화가 멈추거나 설정이 무시됩니다.
| 항목 | 현재 동작 |
|---|---|
| 최상위 | metadata, prompts, slots, graph만 |
| bot first | entry가 슬롯을 수집하면 첫 발화 후 자동 체류. 슬롯이 없으면 일반 전이 적용 |
| prompts | 최상위 prompts.bot / prompts.nlu만. metadata.system_prompt는 읽지 않음 |
| executor | collect_slots가 있으면 Task, 없으면 General. 이름을 적지 않음 |
| 체류 | action: "stay". 자기 자신을 target/default_next로 두지 않음 |
| 종료 | terminal: true 노드 + 보통 user_wants_to_end global transition |
| 응답 기준 | responses.default는 NLG 참고 문장이며 LLM 실패 fallback이 될 수도 있음 |
| 실패 안전망 | no_input·not_understood와 outcome 템플릿은 보통 LLM 예외 때 사용 |
| params | ALLOWED_NODE_PARAMS에 없는 키는 Validation에서 거부 |
허용 params의 단일 기준은
dm/graph/scenario_contract.py::ALLOWED_NODE_PARAMS입니다. GraphBuilder Validation과
생성기 프롬프트가 이 목록을 함께 사용하므로, 미지원 키는 생성하지 않고 입력
시나리오에서도 즉시 거부합니다. responses 키와 조건 연산자 역시 테스트가 Runtime
계약과 일치하도록 잠급니다.
자주 하는 추가 요청¶
extra_requirements에 한 줄씩 넣습니다.
extra_requirements = [
"bot first 로 연다",
"prompts.bot 과 노드 문구는 일본어",
"슬롯은 optional 위주로 두고 사용자가 건너뛸 수 있게 한다",
"자유 대화 노드에는 collect_slots 와 responses 를 두지 않는다",
"turn_limit_enabled 는 false 로 두어 턴 수로 끝내지 않는다",
]
Skill(날씨·절기)이 필요하면 생성 JSON을 만든 뒤
Skill 사용 가이드대로 params.skill을 손보는 편이
안전합니다. 생성기는 HTTP Skill 계약까지 보장하지 않습니다.
코드 위치¶
| 파일 | 역할 |
|---|---|
dm/generator/prompts.py |
시스템·설계·요청·수정 프롬프트와 build_messages |
dm/generator/__init__.py |
공개 API |
dm/tests/test_generator_prompts.py |
허용 목록 ↔ 런타임, 1-shot 예시 validator |
프롬프트 구성:
| 이름 | 역할 |
|---|---|
SYSTEM_PROMPT |
스키마 계약 |
ASSISTANT_PROMPT |
8단계 설계 절차와 출력 전 점검 |
USER_PROMPT_TEMPLATE |
서비스 설명 틀 |
| 카페 주문 1-shot | 로더·validator를 통과하는 정답 예시 |
REVISION_PROMPT_TEMPLATE |
오류를 되먹여 고침 |
런타임이 읽는 필드가 바뀌면 prompts.py와 이 문서, Scenario 가이드를
함께 고칩니다.
관련 문서¶
- Scenario v2 작성 가이드 — 필드를 손으로 다듬을 때
- Transition — 노드가 왜 이동했는지
- Testing — CLI 출력과 회귀 확인
- SeniMate 예제 — 참고용 운영 시나리오