콘텐츠로 이동

08. 수집 노드 세 개 재설계

이 문서는 수집 노드 재설계 당시의 기록입니다. 현재 node params와 optional slot 동작은 SeniMate 시나리오Policy를 참고하세요.

daily_life_check · meal_check · health_check. 안부 → 식사 → 건강으로 이어지는 같은 맥락의 노드들이다.

무엇이 문제였나

각 노드가 한 턴 만에 넘어갔다.

사용자 답 → 슬롯 채워짐 → slots_complete → 봇이 받아주며 다음 노드 질문 → 전이

노드마다 봇 턴이 정확히 하나뿐이고, 그 한 턴이 "직전 답 받아주기 + 다음 주제 묻기"로 쪼개진다. 사용자가 한 말에 반응할 자리가 없어 설문처럼 느껴진다.

여기에 노드별 문제가 겹쳐 있었다.

노드 문제
daily_life_check description 이 얇았다가, 고치면서 넣은 예시가 이중 질문을 유발
meal_check 미사용 param 2종, fallback 응답 없음
health_check description 1562자 누적 패치. 슬롯 4개인데 수집 순서가 불분명

설계

1. 같은 틀로 통일

세 노드가 같은 맥락이므로 description 을 동일한 4단 구조로 다시 썼다.

[목적 + 채우는 슬롯] → [인정 범위 / 판정 기준] → [대화 방식] → [채우면 안 되는 슬롯]

전환 턴에는 다음 노드 description 의 앞 400자만 전달되므로(_NEXT_NODE_GUIDE_MAX_CHARS), 목적과 핵심 금지를 앞쪽에 배치했다.

노드 이전 이후
daily_life_check 458자 425자
meal_check 487자 447자
health_check 1562자 466자

health_check 의 1562자는 대부분 이미 다른 계층이 담당하는 내용이었다.

기존 description 항목 지금 담당하는 곳
"위로해줘 요청 시 질문 덧붙이지 말 것" request dialog act 규칙
"화제를 무시하고 바꾸지 마세요" [경청] 공통 규칙
"직전 턴의 공감·제안을 반복하지 마세요" anti-repeat + prompts.bot 18번
"허락+실제 질문을 복수로 하지 말 것" handoff 의 "질문은 최대 한 개만"

대신 수집 순서를 명시했다. 슬롯이 4개인 노드에는 이게 필요하다.

먼저 몸이 어떤지 한 문장으로만 묻는다
  → 괜찮다고 하면 더 캐묻지 말고 짧게 받아준 뒤 이어간다
  → 불편하다고 하면 어디가 어떻게 불편한지 한 가지만 듣고,
     그 증상 이름을 그대로 넣어 정도를 묻는다

2. min_turns — 이번 설계의 핵심

노드가 한 턴 만에 넘어가지 않도록 최소 체류 턴을 도입했다.

# MinTurnPolicy
if not context.min_turns_satisfied and not context.turn_limit_reached:
    return TaskPolicyResult(outcome="continue_conversation", ...)

continue_conversation 턴은 같은 질문을 반복하지도, 다음 주제를 꺼내지도 않는다. 사용자가 방금 한 말을 이어받는다.

[대화 이어가기] 이 노드에서 필요한 답은 이미 들었습니다. **다시 묻지 마세요.**
그리고 **다음 주제로 넘어가지도 마세요.** 사용자가 방금 한 말에서 한 조각을 골라
구체적으로 반응하고, 그 이야기 안에서 자연스럽게 한 걸음만 더 들어가세요.
질문은 하지 않아도 됩니다. 공감이나 짧은 맞장구로 끝내도 좋습니다.
노드 min_turns max_turns 이유
daily_life_check 2 3
meal_check 2 3
health_check 2 4 슬롯 4개라 한 턴 더

상한이 항상 이긴다. 두 값이 충돌하면 노드를 떠난다.

min_turns 는 이전에 되돌린 항목이다. 이번에는 근본 원인(off_topic 오판정, 전환 지침 누락)을 먼저 고친 뒤 적용했고, 단독 테스트로 동작을 확인했다. → 06

3. 죽은 스키마 제거

코드에서 참조 0건이던 param 4종을 뺐다.

노드 제거한 param 상태
meal_check allowed_slot_values, nutrition_advice 런타임 참조 없음
health_check health_advice, empathy_levels 런타임 참조 없음

completion_slotsdst_nlu 가 NLU 힌트로, dst_logging 이 기대 슬롯 계산에 실제로 읽으므로 유지했다.

4. responses 를 세트로 맞췄다

당시 response schema:

노드 공통 키 추가 키
daily_life_check default, follow_up, empathy, no_input, not_understood
meal_check default, follow_up, empathy, no_input, not_understood
health_check default, follow_up, empathy, no_input, not_understood medical_advice

fallback 문구는 노드 성격에 맞췄다. responses 는 이제 "그대로 쓸 문장"이 아니라 전달할 뜻의 기준이므로(07), 그 노드가 지금 알고 싶어 하는 것을 더 짧고 쉬운 말로 담는다.

daily_life_check  요즘 어떻게 지내시는지만 편하게 말씀해 주세요.
meal_check        오늘 식사하셨는지만 편하게 말씀해 주세요.
health_check      몸은 좀 어떠신지만 편하게 말씀해 주세요.

검증

python -m pytest    # 156 passed
python -m cli.chatbot --scenario dm/scenario/senimate.json --validate-only

실제 LLM(gpt-4o / gpt-4o-mini) 전체 흐름:

'오늘 심심해'     daily_life_check  continue_conversation  daily_life='심심해'
                  → 심심하셨군요. 가끔은 그런 날도 있죠. 혹시 오늘 식사는 잘 챙겨 드셨나요?
'TV만 봐'        daily_life_check  slots_complete         daily_life='TV만 봐'
                  → TV를 보며 시간을 보내셨군요...
'응 먹었지'       meal_check        continue_conversation  meal_status='adequate'
'김치찌개 먹었어'  meal_check        slots_complete         meal_status='adequate'
                  → 김치찌개를 드셨군요. 맛있게 드셨다니 좋네요...
'허리가 좀 아파'   health_check      continue_conversation  health_status='complaint', symptom='허리 통증'
                  → 허리가 아프시군요. 혹시 허리 통증이 어느 정도인지 말씀해주실 수 있을까요?
'이틀 됐어'       health_check      slots_complete

확인된 것:

  • 각 노드가 2턴씩 쓴다. continue_conversationslots_complete
  • 슬롯이 제자리에 들어간다. {daily_life, meal_status, health_status, symptom}
  • health_check 가 증상 이름을 그대로 넣어 되물었다 ("허리 통증이 어느 정도인지") — 설계 의도대로

회귀 테스트

dm/tests/test_task_policy_collect_slots.py 5건 추가.

테스트 고정하는 동작
test_min_turns_holds_the_node_even_when_slots_are_filled 슬롯이 차도 최소 턴 전에는 머문다
test_node_advances_once_min_turns_is_met 채우면 넘어간다
test_max_turns_beats_min_turns 상한이 하한을 이긴다
test_without_min_turns_the_node_advances_immediately 지정 안 하면 종전 동작
test_invalid_min_turns_falls_back_to_default 잘못된 값은 기본값으로

테스트 스텁의 결함도 함께 고쳤다

DummyExecutor.get_max_turns()노드 params 를 무시하고 항상 기본값을 반환하고 있었다. 실제 executor 와 달라서 max_turns 가 걸리는 경로를 검증할 수 없었다. 실제 동작에 맞췄다.


남은 것

  • prompts.bot 정리가 필요하다. 17번이 명시적으로 금지하는 허락 질문("여쭤봐도 될까요?")이 계속 나온다. 18개 항목이 서로 겹치고 길어서 개별 항목이 묻힌다.
  • health_checkpain_level 을 못 채운다. "이틀 됐어" 는 기간이지 정도가 아닌데 slots_complete 로 넘어갔다. 전 슬롯이 optional 이라 진행을 막지 않는다. pain_levelrequired 로 올릴지는 제품 판단이 필요하다.
  • min_turns 값이 경험적이다. 2턴이 적절한지는 실사용 관찰이 필요하다. 노드마다 다르게 둘 수도 있다.