콘텐츠로 이동

02. FallbackPolicy

이 문서는 도입 당시의 설계 기록입니다. 이후 ConfidenceGatingPolicy가 제거되고 fallback counter 초기화·소진 처리가 변경되었습니다. 현재 계약은 Policy를 참고하세요.

dm/core/dialog/policy/global_policies.py

배경

fallback 처리가 두 가지 문제를 안고 있었다.

  1. 무응답과 이해 불가를 구분하지 않았다. 둘 다 ConfidenceGatingPolicy 의 low confidence 한 갈래로 들어가 같은 문장을 냈다.
  2. 문구가 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.pyfallback_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 가 없거나 크면 같은 노드에 오래 머물 수 있다.