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 만은 남겨야 한다고 판단했다. 마무리 노드에서 무응답은 "다시 물을 상황"이 아니라 "조용히 닫을 상황"이므로 공통값(되묻기)을 쓰면 안 된다고 본 것이다.
그런데 문제는 노드가 아니라 규칙에 있었다.
두 문장 모두 "물어볼 것이 있다"를 전제한다. 마무리 노드에는 틀린 전제다. 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_up → empathy |
slots_complete, optional_abandoned |
confirmation |
| dialog act 기반 | answer, acknowledge_request, … → repeat_question |
| 턴 상한 | turn_limit → farewell |
확인된 사실: 역할 2 키는 프롬프트에 들어가지 않는다¶
continue_conversation 턴에 executor 가 follow_up 을 고르지만, 그 값은 generate_natural_response(fallback_template=...) 로만 전달되고 except 블록에서만 쓰인다. _build_llm_context() 가 fallback_template 을 context 에 넣지 않기 때문이다.
FallbackPolicy 만 extra_context={"fallback_template": ...} 로 넣어 기준 문장이 된다.
실측:
turn1 outcome=continue_conversation
프롬프트 기준 문장 : 요즘 어떻게 지내고 계세요? ← default
follow_up 포함? : False
empathy 포함? : False
즉 follow_up 을 고쳐도 LLM 응답은 달라지지 않는다.
선택: 동작을 바꾸지 않고 경계를 명시한다¶
세 가지 안을 두고 (A) 를 택했다.
| 안 | 내용 | 판단 |
|---|---|---|
| (A) | 예외용으로 두고 문서화 | 채택. 현재 동작에 문제가 없다 |
| (B) | outcome 별 템플릿을 기준 문장으로 승격 | 상황별 문구가 의도대로 동작하지만 프롬프트가 다시 커진다 |
| (C) | follow_up / empathy 제거 |
일관되지만 안전망을 잃는다 |
동작은 그대로 두되, "고쳐도 아무 일이 없다"는 사실을 발견 가능하게 만드는 것이 핵심이다. 세 군데에 남겼다.
- 문서 —
docs/scenario/guideline.mdResponses 절을 두 역할로 다시 씀 - 코드 —
_build_reference_utterance()docstring,_DIALOG_ACT_TEMPLATE_KEYS주석 - 테스트 — 경계를 고정
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 에서 무응답 경로를 테스트할 수 없었다.
무응답은 FallbackPolicy 가 처리하는 실제 대화 상황이므로 그대로 턴으로 보낸다. 실수와 구분되도록 표시를 넣었다.
시작 배너에 안내를 추가했다.
최종 상태¶
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 실패 시 안전망이다.
남은 것¶
follow_up/empathy를 (B) 로 승격할지 — 상황별 기준 문장이 필요하다고 판단되면- ~~이중 질문~~ → 12 에서 해결
- 무응답 재요청 턴의 허락 표현 —
ASK_DIRECTLY가 금지하는"말씀해 주실 수 있을까요?"가 fallback 턴에 나온다. 되묻기 맥락에서는 직설이 어색할 수 있어 예외를 둘지 판단 필요