09. conversation_closure 와 스키마 위생¶
이 문서는 초기 closure 정리 기록입니다. 이후
EndIntentPolicy가 제거되고 self-target 전이 무시·closure 재개·연속 무응답 routing이 추가되었습니다. 현재 동작은 SeniMate 시나리오와 Transition을 참고하세요.
A. conversation_closure 재설계¶
이 노드가 실제로 하는 일¶
터미널 노드다. next_nodes 도 default_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 에 있으므로 종료 의도·키워드·goodbyeact 중 하나만 있으면 정책이 응답을 만든다
즉 이 노드의 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 에 추가할 것.
검증¶
실제 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 에서 정리한 기준을 옮길 때 참고할 것.