콘텐츠로 이동

13. min_turns 가 안 찬 턴이 다음 노드의 질문을 던지던 문제

meal_checkmin_turns=2 인데 슬롯이 한 턴 만에 차면 바로 다음 노드의 질문을 던지고 있었다. 노드에 머무는 의미가 없었다.


증상

[meal_check] B: 식사를 잘 챙겨 드셨다니 다행이에요. 몸은 좀 어떠세요?     ← health_check 의 질문
   U: 머리가 좀 아퍼
[meal_check] B: 머리가 아프시군요. … 혹시 몸 상태는 어떠신가요?          ← 또 health_check 의 질문

min_turns=2 라서 meal_check 에 한 턴 더 머물러야 하는데, 머무는 동안 식사 얘기를 하지 않았다.

원인 — 규칙끼리 모순이었다

TaskPolicyPipeline 은 제대로 동작하고 있었다.

if not context.min_turns_satisfied and not context.turn_limit_reached:
    ... outcome="continue_conversation"

문제는 그 턴에 생성되는 프롬프트였다. 실제로 찍어보니:

[대화 이어가기] … **다음 주제로 넘어가지도 마세요.** …
모든 필요한 정보가 수집되었습니다. … 다음 주제인 '건강챙기기'에 대해 자연스럽게 질문하세요.
다음 단계 노드의 지침입니다. **지금 만드는 전환 문장부터** 이 지침을 지키세요. …
**이 턴에서 물을 것은 다음 하나뿐입니다:** "몸은 좀 어떠세요?" …
이 턴은 주제를 넘기는 턴입니다. **질문은 다음 주제에 대해 한 개만** 하세요. …

"넘어가지 마세요" 뒤에 넘어가라는 지시가 네 개 붙어 있었다. 뒤쪽이 훨씬 구체적이라 그쪽이 이긴다.

원인은 _add_slot_progress 였다. continue_conversation 을 보지 않고 all_slots_filled 만으로 전환 턴을 선언한다.

if turn.all_slots_filled:
    rules.add_progress(_slots_filled_rule(turn))    # "다음 주제인 X를 물으세요"
    if not rules.suppress_progress_notes:
        rules.is_handoff_turn = True                # → 6단계 전환 규칙 전부 켜짐

12단계 에서 추가한 HANDOFF_OPENING_QUESTION 이 여기에 함께 실리면서 증상이 더 선명해졌다. 다음 노드의 열 질문을 문자 그대로 지목해 주고 있었으니 그대로 나온 것이다.

고친 것

1. stay_in_node 플래그

suppress_progress_notes 와 같은 자리의 스위치를 하나 더 뒀다.

_Rules 필드 타입 기본값 의미
is_handoff_turn bool false 다음 노드로 넘기는 턴인지
stay_in_node bool false min_turns 미충족으로 슬롯 완료 후에도 현재 노드에 머무는지
단계 stay_in_node 일 때
4. 턴 상황 그대로 (CONTINUE_CONVERSATION 이 여기서 켠다)
5. 슬롯 진행 수집 완료 안내를 붙이지 않는다
6. 노드 전환 통째로 건너뛴다
7. 턴 예산 그대로
if turn.all_slots_filled and not rules.stay_in_node:
    ...

def _add_handoff(rules, turn):
    if not rules.is_handoff_turn or rules.suppress_progress_notes or rules.stay_in_node:
        return

2. 머물 주제를 이름으로 못박는다

"다음 주제로 넘어가지 마세요"만으로는 무엇을 물어야 하는지를 알려주지 않는다. 이 턴에 할 일을 명시했다.

CONTINUE_CONVERSATION = (
    "\n[대화 이어가기] 이 노드에서 필요한 답은 이미 들었지만 **아직 이 주제에 더 머물 차례입니다.** "
    "**다음 주제로 넘어가지 마세요.** 다음 노드가 물을 것을 미리 꺼내면 안 됩니다.\n"
    "이미 받은 답을 그대로 다시 묻지 말고, **앞선 대화를 돌아보고** 사용자가 한 말 중 한 조각을 "
    "골라 구체적으로 반응하세요. 그리고 **같은 주제 안에서 아직 묻지 않은 것**을 한 가지만 "
    "더 물어보세요. 받은 답에서 한 걸음 더 들어가는 질문이어야 합니다. "
    "더 들어갈 것이 마땅치 않으면 질문 없이 공감이나 짧은 맞장구로 끝내도 좋습니다."
)

# 이름을 주지 않으면 "이 주제"가 무엇인지 모델이 다음 노드 쪽으로 넓게 해석한다.
CONTINUE_CONVERSATION_TOPIC = (
    "\n지금 머물러야 할 주제는 '{topic}' 입니다. 이 주제와 관련된 것만 물으세요."
)

이전 문구는 "질문은 하지 않아도 됩니다" 로 끝나 이 턴을 사실상 비워 두고 있었다. 비어 있으니 뒤에 붙은 전환 지시가 그 자리를 채웠다.

결과

이전   [meal_check] U: 머리가 좀 아퍼
                   B: 머리가 아프시군요. … 혹시 몸 상태는 어떠신가요?        ← 다음 노드 질문

지금   [meal_check] U: 머리가 좀 아퍼
                   B: 머리가 아프시군요. 식사는 잘 챙겨 드셨다고 하셨는데,
                      혹시 드신 음식이 어떤 거였는지 궁금해요.               ← 같은 주제 심화
       [health_check] U: 괜찮아요 좀 쉬면 나아요
                     B: 머리가 아프시다고 하셨는데, 몸 상태는 좀 어떠세요?   ← 이제서야 전환

daily_life_check 도 같다 — "집에 계시는군요. 집에서 주로 어떤 일로 시간을 보내세요?". 한 곳을 고쳐 세 노드가 모두 달라졌다. 노드별 문구가 아니라 공통 규칙이 원인이었기 때문이다.

12단계의 이중 질문 개선도 회귀하지 않았다.

30턴 중 이중 질문 1건   (baseline 5건, 12단계 직후 1건)

테스트

test_continue_conversation_turn_carries_no_handoff_rules
    # 수집 완료 안내 + 전환 규칙 4종이 모두 없는지. "몸은 좀 어떠세요?" 문자열까지 확인
test_continue_conversation_turn_names_the_topic_to_stay_on
test_slots_filled_without_continue_still_hands_off
    # 억제가 과하지 않은지 — min_turns 가 찼으면 평소대로 전환
python -m pytest    # 178 passed

test_every_rule_starts_on_its_own_line 가 새 문구의 개행 누락을 바로 잡아냈다. 규약 테스트가 값을 했다.

원칙

프롬프트에 서로 반대되는 지시가 함께 실릴 수 있는지를 먼저 확인할 것. 모델은 모순을 알려주지 않고, 대개 더 구체적인 쪽을 따른다.

그리고 "하지 마세요"만 있는 턴은 비어 있는 턴이다. 대신 무엇을 할지를 함께 줘야 한다.

11·12단계와 같은 계열이다 — 문구를 덧붙이기 전에 어떤 정보가 실리는지 먼저 볼 것.