02. FallbackPolicy¶
이 문서는 도입 당시의 설계 기록입니다. 이후
ConfidenceGatingPolicy가 제거되고 fallback counter 초기화·소진 처리가 변경되었습니다. 현재 계약은 Policy를 참고하세요.
dm/core/dialog/policy/global_policies.py
배경¶
fallback 처리가 두 가지 문제를 안고 있었다.
- 무응답과 이해 불가를 구분하지 않았다. 둘 다
ConfidenceGatingPolicy의 low confidence 한 갈래로 들어가 같은 문장을 냈다. - 문구가 Python 에 있었다. 시나리오가 상황별 화법을 정할 수 없었다.
게다가 인사 노드는 greeting gate 예외 때문에 ConfidenceGatingPolicy 자체가 우회되어 fallback 이 아예 없었다.
설계¶
판정은 dialog act 로 한다. intent 가 아니다.
| 상황 | 판정 | 사용하는 응답 |
|---|---|---|
| 무응답 / 빈 입력 | dialogue_act = silence 또는 intent = empty |
노드의 responses.no_input |
| 의미 파악 실패 | dialogue_act = null |
노드의 responses.not_understood |
원칙¶
- 문구는 시나리오가 소유한다. 노드가
responses에 선언한 문장을 쓴다. 선언이 없거나 LLM 생성이 실패했을 때만dm/core/executors/messages.py의 공통 기본 메시지로 내려간다. - 세션을 끊지 않는다. 재시도 한도를 넘겨도 종료하지 않는다.
- entity 가 있으면 fallback 이 아니다. NLU 가 무언가를 추출했다면 대화는 진행되고 있다.
- 세션 첫 턴은 예외다.
start_session_with_greeting()이 빈 입력으로 첫 턴을 실행하므로, 이 빈 입력을 무응답으로 처리하면 인사말 대신 재질문이 나간다.
파이프라인 위치¶
# global_pipeline.py
self._policies = (
SafetyPolicy(),
EndIntentPolicy(),
FallbackPolicy(), # 추가
ConfidenceGatingPolicy(),
TopicGuardPolicy(),
)
안전·종료 다음, 신뢰도 게이팅 앞이다. 무응답·이해 불가는 신뢰도 문제보다 구체적인 신호이므로 먼저 잡는다.
동작¶
입력
├─ 세션 첫 턴인가? → 통과 (봇이 말을 여는 턴)
├─ entity 가 추출됐나? → 통과 (대화 진행 중)
├─ dialog act 가 silence/null? → 아니면 통과
│
├─ 재시도 횟수 >= max_fallback_retries?
│ → fallback_exhausted 만 기록하고 통과 (진행은 max_turns/routing 담당)
│
└─ 응답 생성
템플릿 = 노드 responses[key] or 공통 기본 메시지
LLM 에 fallback_kind / fallback_retry_index 전달
STAY_CURRENT + 카운터 증가
컨텍스트 키¶
| 키 | 의미 |
|---|---|
fallback_no_input_count |
무응답 재시도 횟수 |
fallback_not_understood_count |
이해 불가 재시도 횟수 |
last_fallback_kind |
마지막 fallback 종류 |
fallback_exhausted |
한도 초과 기록 (no_input / not_understood) |
노드 파라미터¶
| 파라미터 | 타입 | 기본값 | 의미 |
|---|---|---|---|
params.max_fallback_retries |
integer |
2 |
같은 종류의 fallback 재시도 한도 |
도입 당시 기본값은 constants.DEFAULT_MAX_FALLBACK_RETRIES = 2였고 종류별로 따로
셌다. 현재 기본값은 3이며 정상 발화에서 counter를 초기화하고, 소진 시
fallback_exhausted 전역 전이로 종료 노드에 이동한다.
NLG 연동¶
domain_rules.py 가 fallback_kind 를 보고 화법 제약을 준다. 두 경우의 화법이 다르다.
무응답 — 사용자를 탓하지 않고, 못 들었을 수 있다는 전제로 말한다. 직전 질문을 글자 그대로 반복하지 않고 더 짧고 쉬운 표현으로 바꾼다.
이해 불가 — 사용자의 말을 틀렸다고 하지 않고 "제가 못 알아들었다"는 쪽으로 표현한다. 알아들은 부분이 있으면 그것부터 확인한다(혹시 ~ 말씀이실까요?).
두 경우 모두 suppress_turn_notes = True 로 두어 턴 상한 안내 같은 다른 지침이 겹치지 않게 한다.
예시¶
[1] '' → [no_input] "잘 안 들리셨을까요? 천천히 말씀해 주셔도 괜찮아요."
[2] 'ㅁㄴㅇㄹ' → [not_understood] "제가 잘 못 알아들었어요. 한 번만 다시 말씀해 주시겠어요?"
[3] '네 괜찮아요' → 정상 진행
테스트¶
dm/tests/test_fallback_policy.py
| 테스트 | 확인하는 것 |
|---|---|
test_empty_input_uses_declared_no_input_response |
빈 입력 → 선언된 no_input |
test_unintelligible_input_uses_declared_not_understood_response |
null act → 선언된 not_understood |
test_informative_input_is_not_a_fallback |
inform 은 통과 |
test_session_opening_turn_is_not_treated_as_no_input |
첫 턴 빈 입력은 무응답이 아님 |
test_fallback_stops_holding_the_node_after_max_retries |
한도 초과 시 통과, 세션 유지 |
test_fallback_falls_back_to_code_default_without_declared_response |
선언 없고 LLM 실패 시 공통 기본 메시지 |
test_extracted_entities_are_never_a_fallback |
entity 있으면 통과 |
알려진 한계¶
- NLU 의
dialogue_act정확도에 의존한다. 인사말을null로 라벨하면 되묻게 된다. 프롬프트로 지시했다. → 04. 프롬프트 변경 ConfidenceGatingPolicy와 역할이 일부 겹친다. 현재는 낮은 confidence 이면서 act 가silence/null이 아닌 경우를ConfidenceGatingPolicy가 계속 담당한다. 두 정책을 합칠지는 다음 단계에서 판단한다.- 한도 초과 후 동작이 노드 설정에 달려 있다.
fallback_exhausted후에는 executor 로 흘러가므로,max_turns가 없거나 크면 같은 노드에 오래 머물 수 있다.