Mago Graph Builder 개요¶
이 문서는 처음 읽는 사람을 위한 개념 문서입니다. 현재 코드의 함수별 실행 순서와 기본값은 Graph Builder Runtime, Scenario schema는 작성 가이드를 기준으로 확인합니다.
1. Graph Builder란 무엇인가¶
Graph Builder는 대화를 노드(Node)와 전이(Transition:Edge)로 표현합니다.
- 노드(Node): 인사, 안부 확인, 식사 확인, 건강 확인처럼 하나의 대화 목적
- 전이(Transition:Edge): 현재 노드에서 다음 노드로 이동하는 규칙
- 슬롯(Slot): 대화 중 수집하고 기억할 정보
- 조건(Condition): 사용자 의도, 슬롯 값, 시스템 상태에 따른 분기 기준
- 응답(Responses): 정상 NLG가 전달할 의미의 기준 문장과 LLM 실패 시 안전 문장
예를 들어 마고의 기본 대화는 다음과 같이 모델링할 수 있습니다.
flowchart LR
A["인사 및 소개"] --> B["일상 안부"]
B --> C["식사 확인"]
C --> D["건강 확인"]
D --> E["자유 대화"]
E --> F["마무리"]
A -. "종료 의도" .-> F
B -. "종료 의도" .-> F
C -. "종료 의도" .-> F
D -. "종료 의도" .-> F
E -. "종료 의도" .-> F
이 저장소에서 Graph Builder라는 이름은 두 범위를 가리킬 수 있습니다.
- 제품·아키텍처 관점: JSON으로 대화 흐름을 설계하고 실행하는 전체 시스템
- 코드 관점: JSON을 읽어
networkx.DiGraph와 런타임 설정으로 변환하는dm/graph/graph_builder.py의GraphBuilder클래스
현재 구현은 시각적인 드래그앤드롭 편집기가 아닙니다. 시나리오는 JSON으로 작성하며, Graph Builder는 이를 로드·정규화·그래프화·검증하는 역할을 합니다.
2. 마고에서는 왜 Graph Builder가 필요한가¶
2-1. 자연스러운 대화와 예측 가능한 서비스 흐름을 함께 얻기 위해¶
LLM에게 모든 판단을 맡기면 문장은 자연스럽지만 다음 문제가 생길 수 있습니다.
- 같은 질문을 반복한다.
- 필요한 정보를 빠뜨린다.
- 아직 확인하지 않은 단계로 넘어간다.
- 사용자가 대화를 끝내고 싶어도 계속 질문한다.
- 건강 확인 노드에서 관계없는 주제로 이동한다.
- 프롬프트 변경에 따라 전체 대화 순서가 흔들린다.
반대로 모든 문장과 분기를 Python 코드에 하드코딩하면 흐름은 통제할 수 있지만, 표현이 경직되고 시나리오 변경마다 코드 수정과 배포가 필요합니다.
Graph Builder는 두 방식을 결합합니다.
| 서비스가 통제하는 것 | LLM이 담당하는 것 |
|---|---|
| 대화 단계와 순서 | 자연스러운 문장 표현 |
| 수집할 정보와 필수 여부 | 사용자 발화의 의도·값 해석 |
| 분기와 종료 조건 | 맥락에 맞는 공감과 말투 |
| 노드별 역할과 금지 사항 | 같은 의미의 다양한 표현 |
| 최대 턴과 fallback 정책 | 앞선 대화를 반영한 응답 |
즉, 그래프는 대화의 뼈대이고 LLM은 그 뼈대를 따라 말하는 표현 계층입니다.
2-2. 마고의 대화 정책을 코드에서 분리하기 위해¶
마고에서는 서비스 대상, 대화 목적, 질문 순서, 말투가 시나리오마다 달라질 수 있습니다. 이런 도메인 정책을 executor나 라우팅 코드에 직접 넣으면 다음 문제가 생깁니다.
- 새 서비스마다 비슷한 executor를 다시 만들어야 한다.
- 문구 수정과 런타임 수정의 경계가 사라진다.
- 특정 시나리오의 예외가 공통 코드에 누적된다.
- 기획자가 대화 흐름을 검토하기 어렵다.
- 한 시나리오 변경이 다른 시나리오에 영향을 줄 수 있다.
Graph Builder를 사용하면 공통 런타임은 유지하고, 서비스별 차이는 시나리오 JSON으로 분리할 수 있습니다. 예를 들어 인사는 별도 GreetingExecutor가 아니라, 슬롯을 수집하지 않는 일반 노드와 시나리오의 응답·전이 규칙으로 표현합니다.
2-3. 운영 가능한 대화 시스템을 만들기 위해¶
그래프는 현재 대화가 어디에 있고 어디로 이동하는지를 명시합니다. 따라서 다음과 같은 운영상의 이점이 있습니다.
- 현재 노드, 이전 노드, 다음 노드를 추적할 수 있다.
- 어떤 슬롯이 수집되었고 무엇이 빠졌는지 확인할 수 있다.
- 특정 노드만 반복 테스트할 수 있다.
- 도달할 수 없는 노드나 종료점 없는 구조를 사전에 발견할 수 있다.
- 동일한 시나리오를 CLI와 API에서 함께 사용할 수 있다.
- 모델을 변경해도 대화의 핵심 흐름을 유지할 수 있다.
3. Graph Builder의 목적¶
Graph Builder의 핵심 목적은 다음과 같습니다.
-
대화 정책의 선언적 표현 대화 흐름을 Python 제어문이 아니라 JSON 데이터로 정의합니다.
-
도메인과 런타임의 분리 마고·카드 발급·상담 등 도메인별 내용은 시나리오가 소유하고, 상태 관리와 실행 로직은 공통 런타임이 담당합니다.
-
상태 기반 대화 진행 현재 노드, 슬롯, 사용자 의도, 노드별 턴 수를 바탕으로 다음 행동을 결정합니다.
-
필수 정보의 안정적인 수집 노드별
collect_slots와 슬롯의required/optional정책으로 수집 여부와 진행 조건을 관리합니다. -
공통 정책의 일관된 적용 안전 대응, 무응답, 이해 불가, 주제 이탈 같은 정책을 모든 시나리오에 일관되게 적용합니다. 종료 의도는 공통 routing 규칙과 scenario
global_transitions로 처리합니다. -
검증과 테스트 가능성 확보 시나리오를 실제 서비스에 연결하기 전에 구조를 검증하고 CLI에서 대화 흐름을 시험할 수 있습니다.
Graph Builder의 목적은 LLM을 대체하는 것이 아닙니다. 또한 그래프만으로 모든 자연어 의미를 표현하려는 것도 아닙니다. LLM의 자유도를 서비스가 허용한 범위 안에서 활용하도록 만드는 것이 목적입니다.
4. 핵심 구성 요소¶
4-1. 시나리오¶
시나리오는 Graph Builder의 입력입니다. 현재 기준은 scenario_v2 구조이며 네 영역으로 나뉩니다.
{
"metadata": {},
"prompts": {},
"slots": {},
"graph": {
"entry": "greeting_introduction",
"global_transitions": [],
"nodes": []
}
}
| 경로 | 타입 | 필수 | 역할 |
|---|---|---|---|
metadata |
object |
예 | 시나리오 이름, 언어, 버전 등 식별·표시 정보 |
prompts |
object |
권장 | 응답 생성과 NLU에 공통 적용할 지침 |
slots |
object |
아니오 | 시나리오 전체에서 사용할 정보 항목 registry |
graph.entry |
string |
예 | 최초 진입 노드 |
graph.global_transitions |
array |
아니오 | 모든 노드보다 우선하는 공통 전이 |
graph.nodes |
array |
예 | 노드와 연결 관계 |
4-2. 노드¶
노드는 한 번의 발화가 아니라 하나의 대화 목적을 수행하는 구간입니다. 필요한 경우 여러 턴 동안 같은 노드에 머물 수 있습니다.
{
"id": "meal_check",
"name": "식사 확인",
"description": "최근 식사 상태를 부담 없이 확인한다.",
"responses": {
"default": "오늘 식사는 잘 챙겨 드셨어요?"
},
"params": {
"max_turns": 3
},
"collect_slots": ["meal_status"],
"default_next": "health_check"
}
| 필드 | 타입 | 필수 | 역할 |
|---|---|---|---|
id |
string |
예 | 런타임이 사용하는 고유 식별자 |
name |
string |
아니오 | 로그와 CLI에 표시할 이름 |
description |
string |
예 | 노드가 달성할 목적과 지켜야 할 범위 |
responses.default |
string |
아니오 | 일반 NLG의 기준 문장 또는 bot-first 초기 응답 |
params |
object |
아니오 | 최대 턴 등 실행 설정 |
collect_slots |
array[string] |
아니오 | 이 노드에서 수집할 슬롯 |
next_nodes |
array[object] |
아니오 | 조건에 따른 전이 |
default_next |
string |
아니오 | 조건이 맞지 않을 때의 기본 전이 |
terminal |
boolean |
아니오 | 조건부 복귀 edge가 있어도 세션을 닫는 종료 node 선언 |
4-3. 슬롯¶
슬롯은 여러 턴에 걸쳐 기억해야 할 구조화된 정보입니다.
{
"slots": {
"meal_status": {
"policy": "optional",
"value_type": "enum",
"values": ["adequate", "skipped"],
"description": "식사 섭취 상태"
}
}
}
전역 slots는 정의 registry이고, 실제 수집 대상은 각 노드의 collect_slots가 결정합니다.
| 필드 | 타입 | 필수 | 역할 |
|---|---|---|---|
policy |
required \| optional |
예 | required는 미수집 시 진행을 막고 optional은 없어도 진행 |
value_type |
string |
예 | enum, string, number, boolean 등 값 타입 |
values |
array |
enum일 때 | enum 허용 값 |
description |
string |
권장 | NLU가 슬롯 값을 해석할 의미 |
4-4. 전이¶
가장 단순한 연결은 default_next입니다.
조건에 따라 분기하려면 next_nodes를 사용합니다.
| 필드 | 타입 | 필수 | 역할 |
|---|---|---|---|
target |
string |
target 전이 시 | 이동할 노드 ID |
action |
stay |
stay 동작 시 | 현재 노드 유지; target과 함께 쓰지 않음 |
condition |
object |
아니오 | 전이를 적용할 조건 |
condition.field |
string |
단일 조건 시 | 평가 context 경로 |
condition.operator |
string |
단일 조건 시 | 비교 연산자 |
condition.value |
Any |
operator에 따라 | 비교 기준값 |
{
"next_nodes": [
{
"target": "meal_support",
"condition": {
"field": "slots.meal_status",
"operator": "equals",
"value": "skipped"
}
}
],
"default_next": "health_check"
}
현재 노드에 계속 머무르는 동작은 가짜 노드 이름 대신 action: "stay"로 표현합니다.
{
"action": "stay",
"condition": {
"field": "system.node_turns",
"operator": "equals",
"value": 1
}
}
종료 node¶
단순히 후속 edge가 없는 node도 종료 node로 추론됩니다. 종료 node가 조건부 복귀 경로를 가져야 한다면 terminal: true를 명시합니다.
{
"id": "conversation_closure",
"terminal": true,
"next_nodes": [
{
"target": "general_conversation",
"condition": {
"field": "system.conversation_resuming",
"operator": "equals",
"value": true
}
}
]
}
명시적 terminal과 출력 차수 0에서 추론한 node를 합쳐 최종 end_nodes를 만듭니다.
4-5. 전역 전이¶
global_transitions는 어느 노드에 있든 먼저 확인해야 하는 공통 분기입니다. 사용자의 명시적인 종료 의도를 마무리 노드로 보내는 경우가 대표적입니다.
| 필드 | 타입 | 필수 | 역할 |
|---|---|---|---|
name |
string |
아니오 | 전이 표시·진단 이름 |
target |
string |
예 | 이동할 노드 ID |
condition |
object |
예 | 전역 전이를 적용할 조건 |
{
"name": "user_wants_to_end",
"target": "conversation_closure",
"condition": {
"field": "nlu.intent",
"operator": "equals",
"value": "user_wants_to_end"
}
}
5. 내부 동작 방식¶
5-1. 시나리오 로드와 그래프 생성¶
flowchart LR
JSON["Scenario JSON"] --> LOAD["구조 로드"]
LOAD --> NORMALIZE["노드·전이 정규화"]
NORMALIZE --> NX["networkx.DiGraph 생성"]
NX --> VALIDATE["구조 검증"]
VALIDATE --> INFO["GraphInfo 생성"]
INFO --> DST["DST 런타임"]
GraphBuilder.load_from_json()또는load_from_dict()가 시나리오를 읽습니다.graph.nodes[]를node_id → node_config형태로 변환합니다.next_nodes[].target과default_next를 방향성 edge로 정규화합니다.networkx.DiGraph를 생성합니다.- 시작 노드, 종료 노드, 고립 노드, 도달 불가능 노드 등을 검사합니다.
- 그래프와 prompts, slots, entry, global transitions를
GraphInfo로 묶어 DST 런타임에 전달합니다.
action: "stay"는 노드 간 이동이 아니므로 그래프 edge로 만들지 않습니다. global_transitions 역시 공통 런타임 규칙으로 별도 보관되며, 정적 DiGraph edge에는 포함되지 않습니다.
5-2. 세션 시작¶
DialogManager가 시나리오를 로드해 세션별 DSTManager를 만듭니다. DSTManager는 다음 우선순위로 시작 노드를 정합니다.
- 단일 노드 테스트용
focus_node - 시나리오의
graph.entry - 레거시 시작 노드 설정
- 그래프에서 계산된 시작 노드
bot first는 Session 요청의 bot_first override와 Scenario 설정으로 결정합니다.
활성화하면 빈 사용자 입력을 가짜 첫 턴으로 처리하지 않고 별도의 초기 응답 경로를
실행합니다. 비활성화한 Session은 실제 사용자 입력부터 첫 턴을 시작합니다.
5-3. 한 턴의 처리¶
flowchart LR
U["사용자 발화"] --> STATE["DialogueState 로드"]
STATE --> NLU["의도·슬롯 추출"]
NLU --> POLICY["정책 판단"]
POLICY --> EXEC["Executor 선택"]
EXEC --> NLG["응답 생성"]
NLG --> UPDATE["상태 갱신"]
UPDATE --> ROUTE["전이 평가"]
ROUTE --> SAVE["상태 저장"]
주요 흐름은 다음과 같습니다.
- 현재 세션의
DialogueState를 불러옵니다. - 현재 노드와 수집 대상 슬롯을 기준으로 사용자 발화를 해석합니다.
- Safety와 Fallback 전역 정책을 확인합니다.
- 전역 정책이 응답을 확정하지 않은 경우 현재 노드 구조에 따라 executor를 선택합니다.
- 노드 설명과 기준 응답을 이용해 자연스러운 문장을 생성합니다.
- 상태와 슬롯 변경을 반영한 뒤 전이 우선순위에 따라 다음 노드를 결정합니다.
- 슬롯·대화 기록·현재 노드를 저장하고 응답을 반환합니다.
Executor는 시나리오에 직접 적지 않습니다.
params.skill이 있으면SkillExecutor- 그 외에 유효한
collect_slots가 있으면TaskOrientedExecutor - 그 외에는
GeneralResponseExecutor
인사, 일반 대화, 확인, 마무리 노드를 이름으로 분류하지 않습니다. 노드의 구조와 목적을 기준으로 동작을 선택합니다.
5-4. 전이 우선순위¶
런타임은 대체로 다음 순서로 이동 여부를 판단합니다.
- executor가 세션 완료를 명시했는지
global_transitions조건이 일치하는지- executor 또는 policy가 특정 전이를 요청했는지
next_nodes[].condition이 일치하는지default_next가 있는지- 현재 node가 terminal인지
- 정적 그래프의 successor를 fallback으로 사용할 수 있는지
- 이동할 곳이 없으면 현재 노드를 유지할지
따라서 JSON에 적힌 edge만으로 실제 이동을 전부 설명할 수는 없습니다. global_transitions, executor 결과, action: "stay"도 최종 전이에 영향을 줍니다.
6. 활용 방법¶
6-1. 대화 시나리오 설계¶
시나리오는 다음 순서로 설계하는 것이 좋습니다.
-
대화의 최종 목적을 정합니다. 예: 어르신의 일상·식사·건강 상태를 확인하고 자연스럽게 대화를 마칩니다.
-
목적을 노드 단위로 나눕니다. 한 노드에는 한 가지 주된 목적만 둡니다.
-
기억해야 할 정보를 슬롯으로 정의합니다. 모든 대화 내용을 슬롯으로 만들지 말고, 분기나 후속 처리에 필요한 값만 구조화합니다.
-
각 슬롯을 수집할 노드를 지정합니다. 전역
slots에 정의한 뒤 해당 노드의collect_slots에서 참조합니다. -
정상 경로를
default_next로 연결합니다. -
예외 분기만
next_nodes와global_transitions로 추가합니다. 조건을 과도하게 늘리면 시나리오를 이해하고 테스트하기 어려워집니다. -
종료 노드를 명확히 둡니다. 단순 종료 노드는 후속 경로가 없어도 됩니다. 조건부 복귀 경로가 필요하면
terminal: true와next_nodes를 함께 사용합니다.
6-2. CLI에서 검증하고 테스트¶
프로젝트 루트에서 시나리오의 로드와 그래프 검증만 수행할 수 있습니다.
전체 대화를 실행합니다.
특정 노드에 머물면서 입력별 응답과 예상 전이를 반복 확인할 수 있습니다.
상세 상태를 확인하려면 다음 출력 단계를 사용합니다.
chat: 사용자에게 보이는 대화만 표시compact: 턴과 노드 이동을 간단히 표시detailed: 슬롯, 의도, 정책, 전이를 표시debug: 내부 로그까지 표시
6-3. API에서 사용¶
시나리오를 서비스에 연결하는 방법은 두 가지입니다.
POST /dm/v1/session: 서버에 등록된 시나리오 이름으로 세션 생성POST /dm/v1/session/json: 요청에 포함된 시나리오 JSON으로 세션 생성POST /dm/v1/message: 생성된 세션에서 사용자 메시지 처리POST /dm/v1/session/validate: 시나리오 JSON 검증
등록형 시나리오는 dm/service/scenario_map.py에서 이름과 파일을 연결합니다. 동적으로 작성한 그래프는 /session/json 또는 /session/validate를 통해 전달할 수 있습니다.
6-4. 변경 사항별 수정 위치¶
| 바꾸려는 내용 | 수정 위치 |
|---|---|
| 전체 페르소나·말투 | prompts.bot |
| 사용자 발화 해석 기준 | prompts.nlu |
| 특정 단계의 목적·금지 사항 | 노드 description |
| 기준 질문 또는 안전 문장 | 노드 responses |
| 수집할 정보의 타입·필수 여부 | 전역 slots |
| 특정 노드의 수집 대상 | 노드 collect_slots |
| 대화 순서 | default_next |
| 조건별 분기 | next_nodes[].condition |
| 어디서든 적용할 종료·안전 분기 | global_transitions 또는 공통 policy |
| 상태 추적·라우팅 알고리즘 | Python 런타임 |
도메인 문장이나 특정 서비스의 대화 순서를 Python에 넣지 않는 것이 기본 원칙입니다.
7. 마고 시나리오 적용 예¶
마고의 어르신 안부 대화에서 각 계층은 다음과 같이 역할을 나눌 수 있습니다.
| 요구사항 | 그래프 표현 |
|---|---|
| 먼저 자기소개하고 대화 가능 여부 확인 | greeting_introduction entry 노드 |
| 첫 인사 뒤 사용자의 답을 기다림 | 첫 턴의 action: "stay" |
| 일상 이야기를 충분히 듣기 | daily_life_check 노드의 목적과 체류 조건 |
| 식사 여부를 구조화해 기억 | meal_status 슬롯과 meal_check.collect_slots |
| 건강 문제를 별도 단계에서 확인 | health_check 노드 |
| 어느 단계에서든 종료 요청 존중 | 종료 의도를 처리하는 global_transitions |
| 마지막에 자연스럽게 마무리 | conversation_closure 종료 노드 |
이 구조의 장점은 문장 하나를 고치는 일과 대화 정책을 바꾸는 일을 구분할 수 있다는 점입니다. 예를 들어 인사말만 바꾸려면 responses.default를 수정하고, 식사 확인을 생략하려면 노드 연결을 수정합니다. 공통 DST 코드를 바꿀 필요가 없습니다.
8. 설계 원칙과 권장 사항¶
노드는 하나의 목적만 갖게 합니다¶
한 노드에서 안부, 식사, 건강을 모두 확인하려 하면 슬롯 수집과 전이 시점이 모호해집니다. 서로 독립적으로 테스트할 수 있는 목적 단위로 나눕니다.
description에는 목적과 경계를 씁니다¶
description은 단순한 주석이 아니라 LLM이 현재 노드에서 해야 할 일을 이해하는 지침입니다.
- 무엇을 확인해야 하는가
- 어떤 경우에 더 물어봐야 하는가
- 무엇을 이 노드에서 묻지 말아야 하는가
- 언제 다음 단계로 넘어가야 하는가
모든 노드에 공통인 말투는 description에 반복하지 말고 prompts.bot에 둡니다.
responses.default는 기준 문장으로 작성합니다¶
현재 정상 턴에서 responses.default는 LLM이 그대로 복사하는 고정 문구라기보다, 전달해야 할 의미를 보여주는 기준 문장입니다. LLM 호출이 실패한 경우에는 안전 응답으로 그대로 사용될 수 있으므로 단독으로 출력되어도 자연스러워야 합니다.
graph.bot_first=true인 시작 노드에서는 동작이 다릅니다. responses.default가 있으면 NLU와 LLM 호출 없이 해당 문구를 초기 응답으로 사용합니다. 선언하지 않으면 description과 시나리오 bot prompt를 바탕으로 LLM이 초기 응답을 생성하며, 생성 실패 또는 빈 응답에는 언어별 공통 인사말이 사용됩니다. 예측 가능한 첫 응답과 LLM 장애 시 시나리오별 문구가 필요하다면 responses.default를 선언하는 편이 좋습니다.
responses.no_input과 responses.not_understood도 정상 fallback에서 그대로 출력하는
문구가 아닙니다. Runtime은 fallback 종류, 연속 횟수, 직전 질문과 전체 history를
LLM에 전달해 새 응답을 만들고, LLM 호출이 실패할 때만 해당 response 또는 공통
메시지를 사용합니다. 재시도 예산이 소진되면 Policy가 직접 종료하지 않고
fallback_exhausted를 기록한 뒤 graph routing에 제어를 돌려줍니다.
슬롯을 과도하게 만들지 않습니다¶
슬롯은 다음 중 하나에 사용될 때 만드는 것이 좋습니다.
- 다음 노드 결정
- 필수 정보 수집 여부 판단
- 이후 응답 개인화
- 외부 시스템 전달
- 운영 분석
단순히 대화 기록에 남기기 위한 내용은 conversation history만으로 충분할 수 있습니다.
정상 경로는 단순하게 유지합니다¶
주요 진행은 default_next, 실제로 필요한 예외만 조건부 전이로 표현합니다. 같은 조건을 모든 노드에 복제해야 한다면 global_transitions나 공통 policy가 더 적합한지 검토합니다.
종료와 fallback을 반드시 시험합니다¶
정상 답변뿐 아니라 다음 입력을 각 노드에서 확인해야 합니다.
- 빈 입력
- 이해하기 어려운 입력
- 질문과 무관한 입력
- 짧은 긍정·부정
- 사용자의 종료 요청
- 필수 슬롯을 끝까지 제공하지 않는 경우
9. 현재 구현의 검증 범위와 한계¶
Graph Builder가 모든 시나리오 오류를 현재 자동으로 막는 것은 아닙니다.
현재 로드·빌드 단계에서 직접 확인하는 대표 항목:
- 입력이 비어 있지 않은 객체인지
graph.nodes가 배열인지- 각 노드가 객체이고 유효한 문자열
id를 갖는지 - 노드
id가 중복되지 않는지 - 제거된 v2 필드를 사용했는지
- 그래프에 시작 노드가 있는지
- 종료·고립·도달 불가 노드가 있는지
현재 엄격하게 검증되지 않는 대표 항목:
graph.entry가 실제 노드를 가리키는지collect_slots가 전역 슬롯에 정의되어 있는지- 모든
target과default_next가 존재하는 노드인지 - enum 슬롯에 허용값이 올바르게 선언되어 있는지
- 모든 condition의 구조와 field가 런타임에서 평가 가능한지
- 순환이 존재하는 그래프를 오류로 처리해야 하는지
순환 탐지 기능은 별도로 존재하지만 현재 build_graph()의 성공 조건에는 직접 포함되지 않습니다. 또한 존재하지 않는 target도 NetworkX가 암묵적인 노드로 추가할 수 있으므로, 그래프 빌드가 성공하더라도 해당 노드의 런타임 설정은 없을 수 있습니다. 종료·고립·도달 불가 노드는 warning이며 로드를 막지 않습니다.
따라서 --validate-only 성공만으로 대화 품질, 참조 무결성, 순환 안전성이 보장되지는 않습니다. CLI 대화 테스트와 자동화된 시나리오 테스트를 함께 수행해야 합니다.
또한 정적 그래프의 시작 노드는 진입 차수로도 계산되지만, 실제 v2 런타임의 시작점은 명시적인 graph.entry가 우선합니다. 시나리오를 설계할 때는 시작점을 추론에 맡기지 말고 항상 graph.entry를 지정해야 합니다.
10. 주요 코드 위치¶
| 위치 | 역할 |
|---|---|
dm/graph/graph_builder.py |
시나리오 로드, 노드 파싱, 그래프 생성·검증 조정 |
dm/graph/preprocess.py |
노드 설정을 GraphDef로 정규화 |
dm/graph/builder.py |
networkx.DiGraph 생성 |
dm/graph/validator.py |
시작·종료·고립·도달 불가 노드 검사 |
dm/core/runtime/graph_info.py |
그래프와 시나리오 설정을 런타임 객체로 묶음 |
dm/service/dialog_manager.py |
시나리오 로드와 세션 생성 |
dm/core/dialog/dst/dst_manager.py |
한 턴의 전체 처리와 대화 상태 관리 |
dm/core/executors/resolver.py |
노드 구조에 따른 executor 선택 |
dm/core/dialog/dst/dst_routing.py |
조건 평가와 다음 노드 결정 |
dm/scenario/senimate.json |
마고 계열 대화 시나리오 예시 |
11. 관련 문서¶
docs/definition/dialog_system.md: 대화 시스템의 NLU·intent·slot 개념docs/scenario/guideline.md: 현재 Scenario v2 schema와 작성 기준docs/graph_builder/architecture.md: 런타임의 로드 및 턴 처리 흐름docs/graph_builder/transition.md: graph 수정과 routing 기준docs/graph_builder/state.md: DialogueState와 context 구조
12. 요약¶
Mago Graph Builder는 대화를 단순한 프롬프트 호출이 아니라 상태를 가진 서비스 흐름으로 운영하기 위한 기반입니다.
- 그래프가 대화의 목적과 진행 순서를 통제합니다.
- 슬롯이 필요한 정보를 여러 턴에 걸쳐 기억합니다.
- 정책과 조건이 예외 상황과 분기를 처리합니다.
- LLM이 각 상황에 맞는 자연스러운 표현을 만듭니다.
- 공통 런타임과 도메인 시나리오를 분리해 변경과 확장을 쉽게 합니다.
결과적으로 Graph Builder의 가장 중요한 가치는 자연스러움은 유지하면서도, 마고가 의도한 대화 목적과 안전한 흐름을 잃지 않게 하는 것입니다.