콘텐츠로 이동

09. conversation_closure 와 스키마 위생

이 문서는 초기 closure 정리 기록입니다. 이후 EndIntentPolicy가 제거되고 self-target 전이 무시·closure 재개·연속 무응답 routing이 추가되었습니다. 현재 동작은 SeniMate 시나리오Transition을 참고하세요.

A. conversation_closure 재설계

이 노드가 실제로 하는 일

터미널 노드다. next_nodesdefault_next 도 없어 도착하면 대화가 끝난다.

flowchart LR
    G["general_conversation"] -->|"user_wants_to_end<br>또는 turn limit"| C["conversation_closure"]
    C -->|"successor 없음<br>set_complete(True)"| E(["세션 종료"])

중요한 것은 이 노드가 쓰는 턴이 사실상 한 번뿐이라는 점이다.

  • 전환을 일으킨 턴의 응답은 general_conversation 에서 생성된다 (전환 문장은 현재 노드에서 만들어진다)
  • 사용자가 그 뒤에 한 번 더 말하면 그 턴이 conversation_closure 에서 처리된다
  • 그마저도 EndIntentPolicy 가 먼저 잡는 경우가 많다. 이미 end node 에 있으므로 종료 의도·키워드·goodbye act 중 하나만 있으면 정책이 응답을 만든다

즉 이 노드의 description 이 실제로 영향을 주는 범위는 좁다. 거기에 맞게 줄였다.

무엇을 바꿨나

항목 이전 이후
description 447자 190자
max_turns 3 2 (마무리 노드가 오래 머물 이유가 없다)
responses 7개 4개
미사용 params 3종 제거

description 에서 빼낸 것은 이미 다른 계층이 담당한다.

기존 문구 지금 담당
"직전 봇 턴에서 이미 한 공감·제안을 다시 말하지 마세요" anti-repeat
"두 번째 이후 종료 턴에서는 새 인사 문장을 만들지 말고…" domain_rules.END_INTENT

남긴 것은 이 노드 고유의 제약이다.

대화를 마무리하는 노드. **새 질문을 하지 않습니다.**
사용자가 방금 한 말을 한 마디로 받아준 뒤, 마무리 인사를 **한 번만** 하세요.
전체 1~2문장을 넘기지 마세요.
이미 마무리 인사를 했다면 다시 만들지 말고 '네, 알겠습니다.'처럼 아주 짧게만 받아주세요.
건강·식사·일상 등 앞에서 다룬 주제를 여기서 다시 꺼내지 마세요.

B. 죽은 responses

발견

responses 키 중 코드가 한 번도 읽지 않는 것이 있었다.

노드 죽은 response 키 처리
health_check medical_advice 제거
general_conversation listening, encouragement 제거
conversation_closure future_meeting, availability, apology_farewell 제거

responses 를 읽는 경로는 get_response_template(node_config, "<key>") 뿐이고, 코드에 등장하는 키는 정해져 있다.

소비 지점
일반 응답 default
슬롯 수집 initial, confirmation
대화 이어가기 follow_up, empathy
턴 상한 turn_limit, farewell
fallback no_input, not_understood
dialog act answer, acknowledge_request, alternative, help, correction, acknowledge_thanks, repeat_question

선언되지 않은 키를 쓰면 None 이 되고 조용히 다음 폴백으로 내려간다. 반대로 선언만 하고 코드가 읽지 않으면 아무 일도 일어나지 않는다.

responses 를 노드 설정 덤프에서 제외한 뒤(07) 이 키들은 프롬프트에도 나가지 않는다. 이전에는 few-shot 예시로라도 들어갔으나 지금은 완전히 죽은 값이다.

재발 방지

죽은 스키마가 이번까지 세 번 나왔다.

단계 내용
1 params: service_name, conversation_type
2-2 params: allowed_slot_values, nutrition_advice, health_advice, empathy_levels
3 responses 6종 + params 3종

조용히 무시되므로 리뷰에서 계속 놓친다. dm/tests/test_scenario_schema_hygiene.py 로 고정했다.

# 코드에서 소비되는 키를 정규식으로 추출해 시나리오와 대조한다
keys |= set(re.findall(r'get_response_template\([^)]*?"([a-z_]+)"', source))
keys |= set(_DIALOG_ACT_TEMPLATE_KEYS.values())
테스트 검사
test_node_params_are_all_consumed_by_the_runtime params 키가 런타임이 읽는 목록에 있는지
test_node_responses_are_all_consumed_by_the_runtime responses 키가 코드에서 소비되는지
test_senimate_every_node_declares_fallback_responses 모든 노드에 no_input / not_understood 가 있는지

dm/scenario/*.json 전체에 적용된다(현재 4개).

테스트가 실제로 잡는지 확인했다. 가짜 키를 주입하니 2건이 실패하며 키 이름까지 출력했다.

런타임이 읽지 않는 params: ['greeting_introduction.bogus_param'].
제거하거나, 새로 소비하는 코드를 넣고 CONSUMED_PARAMS 에 추가할 것.

검증

python -m pytest    # 165 passed

실제 LLM 전체 흐름 11턴, 종료까지 정상.

greeting_introduction    안녕하세요, 저는 SeniMate이에요…
daily_life_check    ×2   심심하셨군요… / TV를 보면서 시간을 보내셨군요…
meal_check          ×2   식사를 잘 챙기셨다니… / 김치찌개를 드셨군요…
health_check        ×2   허리가 아프시군요… 어느 정도인지…
general_conversation×2   혹시 요즘 TV에서 재미있게 본 프로그램이…
conversation_closure     네, 알겠습니다.          *** 종료

pain_level 까지 채워졌다("많이 아프진 않아"none). 2-2단계에서 미수집으로 남겼던 항목이다.

관찰

  • general_conversation 이 앞에서 나온 TV 이야기를 다시 꺼냈다. 새 주제를 열지 않고 사용자가 꺼낸 소재를 이어받으라는 1-4단계 설계대로 동작한다.
  • 다른 시나리오 3개는 params·responses 가 아예 없다. card_issuance, call_for_fire, dental_caries_case1 은 v2 스키마를 거의 활용하지 않는다. senimate 에서 정리한 기준을 옮길 때 참고할 것.