콘텐츠로 이동

05. general_conversation 재정의

이 문서는 당시 문제 분석과 변경 과정입니다. 이후 종료 권한은 policy에서 routing으로 이동했고 closure 재개 경로가 추가되었습니다. 현재 흐름은 SeniMate 시나리오를 참고하세요.

배경

1-3단계에서 노드 전환 문장에 다음 노드의 지침을 싣도록 고쳤다. 그런데 실제 LLM 으로 확인하니 반복이 그대로였다.

turn 2  ... 그럼 요즘 일상은 어떻게 보내고 계세요?      ← daily_life_check 로 넘어가며
turn 5  ... 그럼 요즘 일상에서 특별히 즐기시는 일이 있으신가요?   ← general_conversation 으로 넘어가며

프롬프트를 캡처해 보니 지침은 정상적으로 들어가 있었다. LLM 이 무시한 것이다.

원인: 두 지시가 싸우고 있었다

① 다음 주제인 '일반대화'에 대해 자연스럽게 질문하세요.      ← domain_rules
② (다음 노드 지침) 새 질문 주제를 열지 말고 ...              ← general_conversation description

①이 더 구체적이라 이겼다. 문제의 뿌리는 물어볼 것이 없는 노드에게 "질문하라"고 시킨 것이다.

general_conversation 의 description 첫 문장도 이를 부추겼다.

"일반적인 대화 진행. 이 노드의 목적도 정보 수집입니다."

노드 이름은 일반대화, 목적은 "정보 수집"이다. LLM 이 여기에 맞는 질문을 만들면 "요즘 일상은 어떠세요?"가 된다.

고친 방식: 프롬프트가 아니라 구조로

collect_slots 유무를 신호로 쓴다. executor 선택에 이미 쓰는 것과 같은 구조다.

다음 노드 전환 턴 지시
collect_slots 있음 "다음 주제 '{이름}'에 대해 자연스럽게 질문하세요"
collect_slots 없음 "새 질문을 만들지 마세요. 여지만 남기고 문장을 끝내세요"
알 수 없음 기존 문구 유지

세 번째 줄이 중요하다. _next_node 주입이 없는 경로(레거시 호출)에서는 판단 근거가 없다. 모른다는 이유로 더 제한적인 분기를 타면 안 된다. _next_node_collects_slots()True / False / None 세 상태를 돌려준다.

전환 턴에 "질문은 최대 한 개만" 제약도 함께 넣었다.

결과

이전 → 건강이 괜찮으시다니 다행이에요. 그럼 요즘 일상에서 특별히 즐기시는 일이 있으신가요?
이후 → 건강에 문제가 없으시다니 다행이에요. 혹시 더 이야기 나누고 싶으신 게 있으실까요?
        언제든지 편하게 말씀해 주세요.

turn 4 의 이중 질문도 함께 사라졌다.

노드 재정의

description 이 1800자짜리 누적 패치였다. 상당수는 이제 다른 계층이 담당한다.

기존 description 항목 지금 담당하는 곳
"'위로해줘' 요청 시 질문 덧붙이지 말 것" request dialog act 규칙
"할 수 없는 요청도 무시하지 말 것" request dialog act 규칙
"화제를 무시하고 바꾸지 마세요" [경청] 공통 규칙
"직전 턴의 공감·제안을 반복하지 마세요" prompts.bot 18번
"이 대화가 신경쓰여 등에 맞춰 반응" prompts.bot 11번

1800자 → 556자로 줄이고, 핵심 금지사항을 맨 앞에 놓았다. 전환 턴에는 앞 400자만 전달되기 때문이다(_NEXT_NODE_GUIDE_MAX_CHARS).

자유 대화 노드. **정보를 수집하는 노드가 아닙니다.** 새 질문 주제를 열지 말고,
앞 대화에서 사용자가 꺼낸 이야기를 이어받아 사용자가 하고 싶은 말을 하도록 두세요.
**다시 묻지 말 것: 안부·일상, 식사, 건강·통증.** 앞 단계에서 이미 다 물었습니다.
...

responsesno_input / not_understood 를 추가하고, 코드가 읽지 않는 conversation_topics / empathy_triggers 를 제거했다.


재테스트에서 드러난 조기 종료

노드를 고친 뒤 다시 돌리니 다른 문제가 나왔다.

turn 6  '그냥 심심해'  → 심심하실 때는 ... 오늘 이야기 나눠주셔서 감사해요, 다음에 또 뵐게요.
turn 7  '없었어'      → intent=user_wants_to_end → conversation_closure
turn 8  '아니 가지마'  → intent=user_wants_to_end → 세션 종료

사용자가 "가지마"라고 하는데도 대화가 끊겼다. 원인이 셋이었고 하나는 이 단계에서 스스로 만든 것이다.

원인 ①: 스스로 만든 회귀 — 조건부 전이를 다음 노드로 단정

이 단계에서 _next_node_brief() 에 "조건부라도 목적지가 하나면 다음 노드로 본다"는 규칙을 넣었다. general_conversation 의 조건부 전이는 전부 conversation_closure 를 가리키므로 후보로 잡혔다.

그 결과 머무는 턴에도 closure 의 description 이 매번 주입됐다.

주입된 내용: "...'오늘 이야기 나눠주셔서 감사해요, 다음에 또 뵐게요'처럼 부드럽게 인사를..."
봇 출력:    "...오늘 이야기 나눠주셔서 감사해요, 다음에 또 뵐게요."          ← 그대로

조건부 전이는 대개 예외 경로(종료 의도, 턴 상한)이고 평소에는 노드에 머문다. 이걸 다음 노드로 보면 안 된다. 되돌렸고 test_next_node_brief_ignores_conditional_only_branches 에 이유를 남겼다.

원인 ②: description 이 봇에게 스스로 끝내라고 시킴

재정의할 때 넣은 문장이 문제였다.

"사용자가 '없어'라고 하면 다시 묻지 말고 마무리로 넘어가세요."

기존 description 은 "두 번 연속" 이었는데 한 번으로 줄여 버렸다. 교체했다.

**대화를 먼저 끝내려 하지 마세요.** '오늘 이야기 나눠주셔서 감사해요' 같은 작별 인사는
**사용자가 그만하자고 명시적으로 말했을 때만** 하세요. '없어', '딱히'는 그 질문에 대한
답일 뿐 대화를 끝내자는 뜻이 아닙니다.

종료 판단은 봇이 아니라 라우팅이 한다. 종료 의도 또는 max_turns 다.

원인 ③: 봇이 한 번 작별하면 NLU 가 이후 전부 종료로 읽음

prompts.nlu 에 예외 없는 규칙이 있었다.

"봇이 이미 인사를 했으면 대화가 끝난 것이므로, 이후 사용자의 짧은 수긍을 affirm 으로 분류하지 마세요."

그래서 "아니 가지마"까지 user_wants_to_end 가 됐다.

프롬프트 수정

공통 NLU 프롬프트(도메인 중립)에 원칙을 넣었다.

사용자가 대화를 계속하겠다는 의사를 밝히면 intent 를 절대 user_wants_to_end 로 두지 마세요. 봇이 이미 작별 인사를 한 뒤라도 마찬가지입니다.

senimate prompts.nlu 의 해당 규칙에도 예외 절을 붙였다.

런타임 보정 — 프롬프트에만 맡기지 않는다

dst_nlu.drop_contradicted_end_intent()

intent == "user_wants_to_end"  AND
act  {deny, negate}           AND
종료 키워드 없음
     intent  "user_wants_to_continue"  바꾼다

"아니 그만하자"처럼 실제 종료 의사에는 키워드가 함께 오므로 그대로 통과한다.

보정 위치가 중요하다. NLU 단계에서 한 번만 한다. 이번 케이스는 EndIntentPolicy 가 아니라 global_transitions 로 끊겼다general_conversation 이 자체 종료 라우팅을 가져서 _node_has_end_routing() 때문에 EndIntentPolicy 가 물러났기 때문이다. 두 경로가 같은 nlu.intent 를 보므로 NLU 결과 자체를 고쳐야 양쪽에 적용된다.

결과

이전 → ['아니 가지마'] intent=user_wants_to_end → conversation_closure  *** 세션 종료
이후 → ['아니 가지마'] intent=deny              → general_conversation  (대화 계속)

봇도 작별 대신 대화를 이어간다.

심심하신 것 같네요. 혹시 요즘 관심 있는 것들이나 해보고 싶은 것들이 있으신가요? 듣고 싶어요.

변경 파일

dm/core/dialog/dst/dst_execution.py   조건부 전이 해석 되돌림, _next_node.collects_slots 추가
dm/core/dialog/dst/dst_nlu.py         drop_contradicted_end_intent() 추가
dm/core/llm/prompts/domain_rules.py   _next_node_collects_slots() 3상태, 전환 턴 질문 수 제한
dm/core/llm/prompts/prompts_nlu.py    대화 지속 의사 원칙 추가
dm/scenario/senimate.json             general_conversation 재정의, prompts.nlu 예외 절

검증

python -m pytest    # 131 passed

실제 LLM(gpt-4o / gpt-4o-mini)으로 두 시나리오를 재현해 확인했다.

확인 항목 결과
health_checkgeneral_conversation 전환에서 일상 재질문 해소
전환 턴 이중 질문 해소
"없었어" 후 봇의 자발적 작별 해소
"아니 가지마" 로 세션 종료 해소

남은 것

  • daily_life_check 전환 턴은 아직 질문이 두 개다. 원인은 1-3단계에서 쓴 description 의 "한 걸음 더 들어가세요" 예시가 후속 질문을 유도하기 때문이다. 2단계에서 다룬다.
  • 슬롯 오배정. 노드가 한 칸씩 밀려 '응 먹었지'daily_life 로, '건강해'meal_status 로 들어간다. 전환 턴에 다음 노드 질문을 미리 하는 구조와 NLU 의 주제 판정이 어긋난다. 2단계 핵심 과제.
  • 자유 대화가 실제로 짧다. max_turns: 6 이지만 앞 노드들이 슬롯 하나에 즉시 넘어가 general_conversation 에 도달할 때는 대화가 이미 마무리 국면이다. 노드별 최소 체류 턴 개념이 다시 필요할 수 있다(이전에 min_turns 로 제안했다가 되돌린 항목).