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).
자유 대화 노드. **정보를 수집하는 노드가 아닙니다.** 새 질문 주제를 열지 말고,
앞 대화에서 사용자가 꺼낸 이야기를 이어받아 사용자가 하고 싶은 말을 하도록 두세요.
**다시 묻지 말 것: 안부·일상, 식사, 건강·통증.** 앞 단계에서 이미 다 물었습니다.
...
responses 에 no_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 예외 절
검증¶
실제 LLM(gpt-4o / gpt-4o-mini)으로 두 시나리오를 재현해 확인했다.
| 확인 항목 | 결과 |
|---|---|
health_check → general_conversation 전환에서 일상 재질문 |
해소 |
| 전환 턴 이중 질문 | 해소 |
| "없었어" 후 봇의 자발적 작별 | 해소 |
| "아니 가지마" 로 세션 종료 | 해소 |
남은 것¶
daily_life_check전환 턴은 아직 질문이 두 개다. 원인은 1-3단계에서 쓴 description 의"한 걸음 더 들어가세요"예시가 후속 질문을 유도하기 때문이다. 2단계에서 다룬다.- 슬롯 오배정. 노드가 한 칸씩 밀려
'응 먹었지'가daily_life로,'건강해'가meal_status로 들어간다. 전환 턴에 다음 노드 질문을 미리 하는 구조와 NLU 의 주제 판정이 어긋난다. 2단계 핵심 과제. - 자유 대화가 실제로 짧다.
max_turns: 6이지만 앞 노드들이 슬롯 하나에 즉시 넘어가 general_conversation 에 도달할 때는 대화가 이미 마무리 국면이다. 노드별 최소 체류 턴 개념이 다시 필요할 수 있다(이전에min_turns로 제안했다가 되돌린 항목).