콘텐츠로 이동

11. responses 와 fallback 선언 정리

노드 responses 에서 fallback 선언을 전부 걷어냈고, 남은 키가 실제로 어떻게 쓰이는지 경계를 문서·코드·테스트에 고정했다.


A. fallback 선언을 전부 제거했다

출발점

3단계까지의 결과, 6개 노드 중 6개가 no_input / not_understood 를 선언하고 있었다. 문구가 서로 비슷했다.

잘 안 들리셨을까요? 천천히 말씀해 주셔도 괜찮아요.          ← greeting (공통값과 완전히 동일)
잘 안 들리셨을까요? 요즘 어떻게 지내시는지만 편하게…        ← daily_life_check
잘 안 들리셨을까요? 오늘 식사하셨는지만 편하게…             ← meal_check
잘 안 들리셨을까요? 몸은 좀 어떠신지만 편하게…              ← health_check

FallbackPolicy선언이 없으면 이미 공통 기본값으로 내려간다.

declared = _node_response_template(node_info, response_key)
default_message = declared or DEFAULT_MESSAGES.get(message_kind, {}).get(lang, "")

즉 모든 노드가 같은 것을 복사해 둘 이유가 없었다.

세 번에 걸쳐 기준을 좁혔다

1차 — "주제로 되짚어 줄 때만 선언"

공통값과 완전히 같은 선언 4개(greeting, general_conversation)를 제거했다. 세 수집 노드는 주제를 되짚어 주므로 남긴다고 판단했다.

2차 — 실측하니 세 노드도 불필요했다

선언을 지우고 같은 조건으로 재측정했다.

노드 선언 있음 선언 제거 후
daily_life_check 혹시 잘 안 들리셨을까요? 요즘 어떻게 지내시는지 편하게… 혹시 잘 안 들리셨을까요? 요즘 어떻게 지내시는지 간단히…
meal_check 오늘 식사하셨는지 궁금해요. 편하게… 식사는 잘 하셨는지 궁금해요. 혹시 드신 게 있으시면…
health_check 몸 상태가 어떠신지 궁금해요. 편하게… 혹시 잘 안 들리셨을까요? 몸 상태가 어떠신지 궁금해서요…

차이가 없다. 주제 유도는 이미 세 가지가 하고 있었다.

  • 노드 description — 이 노드가 무엇을 묻는지
  • FALLBACK_NO_INPUT 규칙 — "직전에 한 질문을 더 짧고 쉬운 표현으로 바꿔 물어보세요"
  • 대화 히스토리 — 직전 봇 질문

노드 선언은 같은 정보를 네 번째로 반복하고 있었다.

3차 — 규칙이 잘못된 전제를 갖고 있었다

conversation_closure 만은 남겨야 한다고 판단했다. 마무리 노드에서 무응답은 "다시 물을 상황"이 아니라 "조용히 닫을 상황"이므로 공통값(되묻기)을 쓰면 안 된다고 본 것이다.

그런데 문제는 노드가 아니라 규칙에 있었다.

FALLBACK_NO_INPUT       "…더 짧고 쉬운 표현으로 바꿔 물어보세요."
FALLBACK_NOT_UNDERSTOOD "…없으면 더 쉬운 말로 한 가지만 다시 여쭤보세요."

두 문장 모두 "물어볼 것이 있다"를 전제한다. 마무리 노드에는 틀린 전제다. closure 선언은 그 잘못된 전제를 덮고 있던 것이었다.

규칙을 역할 중립으로 고쳤다.

**이 노드의 목적에 맞게 대응하세요.** 아직 들어야 할 것이 있는 노드라면 직전 질문을
글자 그대로 반복하지 말고 더 짧고 쉬운 말로 바꿔 물으세요.
물어볼 것이 없거나 마무리하는 노드라면 억지로 질문을 만들지 말고 자연스럽게 마무리하세요.

규칙은 태도만 정하고(재촉 금지, 탓하지 않기, 한두 문장), 무엇을 할지는 노드가 정한다.

결과

세 노드가 같은 규칙에서 각자 다르게 반응한다. 선언은 하나도 없다.

meal_check           무응답 → 혹시 오늘 식사하셨는지 궁금해요. 드셨다면 어떤 걸…   [되묻기]
general_conversation 무응답 → 혹시 무슨 말씀을 하시려다 멈추셨을까요? 천천히…      [열어두기]
conversation_closure 무응답 → 네, 알겠습니다.                                  [닫기]

no_input / not_understood 선언 12개가 전부 사라졌다.

원칙

fallback 문구는 런타임이 준다. 노드는 역할만 description 에 적으면 된다.

노드가 fallback 을 선언해야 한다고 느껴지면, 먼저 규칙이 잘못된 전제를 갖고 있는지 의심할 것.

두 번 다 선언으로 덮으려 했고 규칙을 의심하지 않았다. 근본 원인은 규칙에 있었다.


B. responses 의 두 역할

정리 과정에서 각 키가 실제로 어떻게 쓰이는지 확인했고, 예상과 다른 부분이 있었다.

역할 1 — 프롬프트의 기준 문장

LLM 프롬프트에 "이 뜻을 전달하되 그대로 쓰지 마세요"와 함께 한 개만 실린다.

기준 문장이 되는 키
무응답 no_input (없으면 공통 기본값)
이해 불가 not_understood (없으면 공통 기본값)
그 외 모든 턴 default
직전 노드의 전환 턴 default — 이 노드를 여는 질문으로 지목된다 (12)

역할 2 — LLM 호출이 실패했을 때만

generate_natural_response() 가 예외를 만나면 outcome 에 맞는 키를 그대로 출력한다.

outcome
missing_slots, turn_limit_blocking initial
continue_conversation follow_upempathy
slots_complete, optional_abandoned confirmation
dialog act 기반 answer, acknowledge_request, … → repeat_question
턴 상한 turn_limitfarewell

확인된 사실: 역할 2 키는 프롬프트에 들어가지 않는다

continue_conversation 턴에 executor 가 follow_up 을 고르지만, 그 값은 generate_natural_response(fallback_template=...) 로만 전달되고 except 블록에서만 쓰인다. _build_llm_context()fallback_template 을 context 에 넣지 않기 때문이다.

FallbackPolicyextra_context={"fallback_template": ...} 로 넣어 기준 문장이 된다.

실측:

turn1  outcome=continue_conversation
   프롬프트 기준 문장 : 요즘 어떻게 지내고 계세요?      ← default
   follow_up 포함?    : False
   empathy   포함?    : False

follow_up 을 고쳐도 LLM 응답은 달라지지 않는다.

선택: 동작을 바꾸지 않고 경계를 명시한다

세 가지 안을 두고 (A) 를 택했다.

내용 판단
(A) 예외용으로 두고 문서화 채택. 현재 동작에 문제가 없다
(B) outcome 별 템플릿을 기준 문장으로 승격 상황별 문구가 의도대로 동작하지만 프롬프트가 다시 커진다
(C) follow_up / empathy 제거 일관되지만 안전망을 잃는다

동작은 그대로 두되, "고쳐도 아무 일이 없다"는 사실을 발견 가능하게 만드는 것이 핵심이다. 세 군데에 남겼다.

  1. 문서docs/scenario/guideline.md Responses 절을 두 역할로 다시 씀
  2. 코드_build_reference_utterance() docstring, _DIALOG_ACT_TEMPLATE_KEYS 주석
  3. 테스트 — 경계를 고정
test_only_default_reaches_the_prompt_on_ordinary_turns
    # follow_up·empathy·initial·confirmation 이 프롬프트에 없음을 확인
test_fallback_policy_choice_overrides_default_as_the_reference
    # FallbackPolicy 가 고른 문구만 default 를 대체
test_exception_path_still_uses_the_outcome_template
    # LLM 을 죽이고 follow_up 이 그대로 출력되는지 — 역할 2가 살아 있음을 증명

세 번째가 중요하다. 예외를 던지는 LLM 스텁으로 follow_up 이 반환되는 것을 확인했다. 죽은 코드가 아니라 안전망으로 동작 중이다.


C. CLI — 엔터로 무응답 테스트

빈 입력을 continue 로 버리고 있어 CLI 에서 무응답 경로를 테스트할 수 없었다.

# 이전
if not user_input:
    continue

무응답은 FallbackPolicy 가 처리하는 실제 대화 상황이므로 그대로 턴으로 보낸다. 실수와 구분되도록 표시를 넣었다.

You >       (무응답 입력)
Mago > 몸 상태가 어떠신지 궁금해서 여쭤봤는데, 혹시 잘 안 들리셨을까요?

시작 배너에 안내를 추가했다.


최종 상태

prompts.bot  579자 (원래 2449자)

greeting_introduction    ['default']
daily_life_check         ['default', 'follow_up', 'empathy']
meal_check               ['default', 'follow_up', 'empathy']
health_check             ['default', 'follow_up', 'empathy']
general_conversation     ['default', 'empathy']
conversation_closure     ['default', 'farewell']

기준 문장으로 쓰이는 것은 default 뿐이고, 나머지는 LLM 실패 시 안전망이다.

python -m pytest    # 171 passed

남은 것

  • follow_up / empathy 를 (B) 로 승격할지 — 상황별 기준 문장이 필요하다고 판단되면
  • ~~이중 질문~~ → 12 에서 해결
  • 무응답 재요청 턴의 허락 표현ASK_DIRECTLY 가 금지하는 "말씀해 주실 수 있을까요?" 가 fallback 턴에 나온다. 되묻기 맥락에서는 직설이 어색할 수 있어 예외를 둘지 판단 필요