콘텐츠로 이동

12. 전환 턴의 이중 질문 — 프롬프트가 아니라 구조로 고쳤다

노드가 바뀌는 턴에서 봇이 질문을 두세 개씩 던지던 문제. 프롬프트 강화는 실패했고, 전달하는 정보를 바꾸니 해결됐다.


먼저 실패한 시도

10단계 에서 규칙 문구를 두 번 강화했다.

LISTENING            "**물음표가 두 개면 이미 많습니다.** 같은 질문을 표현만 바꿔 덧붙이지 말고…"
HANDOFF_ONE_QUESTION "**질문은 다음 주제에 대해 한 개만**… 새 주제 질문을 던진 뒤에
                      그 질문을 부연하는 두 번째 질문을 덧붙이지 마세요."

4회 실측했고 전혀 움직이지 않았다.

[1,2,1,1,2,2,0,1,1] → [1,2,1,2,2,2,2,1,1] → [1,2,1,2,2,2,0,1,1] → [1,2,1,1,2,2,1,1,1]
      ↑ 2·5·6번 턴이 매번 같은 자리에서 두 개

같은 자리에서 반복된다는 건 모델의 변덕이 아니라 입력의 문제라는 뜻이다.

진단 — 전환 턴이 무엇을 받고 있었나

한 턴은 "직전 질문의 답을 받고 다음 노드의 질문을 던지는" 구조다. 그래서 전환 턴에는 다음 노드의 description 이 실린다. 실제로 실린 내용을 찍어봤다.

meal_check → 다음 노드: 건강챙기기

전환 턴에 실리는 지침 (앞 400자):
  건강 상태를 확인하는 노드. 채우는 슬롯: health_status (adequate | complaint),
  불편하다고 하실 때만 symptom · health_issue · pain_level. …
  **수집 순서**: 먼저 몸이 어떤지 한 문장으로만 묻습니다. … 불편하다고 하시면
  어디가 어떻게 불편한지 한 가지만 듣고, 그 증상 이름을 그대로 넣어 정도를 물으세요. …

그 노드의 responses.default: 몸은 좀 어떠세요?

전환 턴이 그 노드의 수집 계획 전체를 받고 있었다. 슬롯 4개와 3단계 수집 순서. 계획을 읽은 모델이 한 턴에 여러 단계를 커버하려는 것은 자연스러운 반응이다. "질문은 하나만"이라는 문장 하나로 이길 수 없다.

그리고 그 노드가 실제로 던져야 할 한 문장(responses.default)은 전환 턴에 실리지 않고 있었다.

고친 것 — 열 질문 하나를 지목한다

전환 턴은 주제를 열기만 하면 된다. 수집 계획은 다음 턴부터 그 노드가 직접 받는다.

1. 다음 노드 요약에 열 질문을 담는다

dm/core/dialog/dst/dst_execution.py

responses = next_info.get("responses")
opening = (responses or {}).get("default") if isinstance(responses, dict) else None
return {
    ...
    # 그 노드가 처음 던지는 질문. 전환 턴은 주제를 **열기만** 하면 되므로
    # 수집 계획 전체가 아니라 이 한 문장만 목표로 삼게 한다.
    "opening": opening if isinstance(opening, str) else "",
}

2. 규칙으로 범위를 못박는다

dm/core/llm/prompts/domain_rules_text.py

HANDOFF_OPENING_QUESTION = (
    "\n**이 턴에서 물을 것은 다음 하나뿐입니다:** \"{opening}\" "
    "위 지침의 나머지(수집 순서·추가 슬롯)는 **다음 턴부터** 해당됩니다. 지금 미리 묻지 마세요. "
    "이 문장을 그대로 쓰지 말고 앞 대화에 이어지도록 자연스럽게 바꿔서 한 번만 물으세요."
)

_add_handoff 에서 지침 바로 뒤에 붙인다. 계획을 읽은 직후에 범위를 좁혀야 효과가 있다.

if turn.next_node_guide:
    rules.add(text.HANDOFF_NEXT_NODE_GUIDE.format(guide=turn.next_node_guide))
if turn.next_node_opening:
    rules.add(text.HANDOFF_OPENING_QUESTION.format(opening=turn.next_node_opening))

실측 — 같은 스크립트로 A/B

고정된 사용자 발화 9턴, 봇 응답의 물음표 개수를 센다. 규칙만 껐다 켜서 3회씩 돌렸다.

턴별 물음표 수 물음표 2개 이상인 턴
끄고 (baseline) [1,1,1,1,1,1,1,1,1,0] 0
[1,2,1,1,2,3,1,1,1,0] 3
[1,1,1,1,2,2,1,1,1,0] 2
켜고 [1,1,2,1,1,1,1,1,1,0] 1
[1,1,1,1,1,1,1,1,1,0] 0
[1,1,1,1,1,1,1,1,1,0] 0
[1,1,1,1,1,1,1,1,1,0] 0
[1,1,1,1,1,1,1,1,1,0] 0
baseline   30턴 중 5턴이 이중 질문
적용 후    50턴 중 1턴

meal_check → health_check 전환(6번 턴)이 baseline 에서 매번 2~3개였고, 적용 후 5회 모두 1개다. 남은 1건은 전환 턴이 아니라 daily_life_check 내부 턴이다.

실제 응답:

baseline  U: 김치찌개 먹었어요
          B: 김치찌개 드셨군요. 그럼 몸은 좀 어떠세요? 혹시 어디 불편하신 데는 없으세요?
             무릎이나 허리는 괜찮으신가요?                                    ← 3개

적용 후   U: 김치찌개 먹었어요
          B: 김치찌개 드셨군요. 맛있게 드셨길 바라요. 산책도 하시고 기분이
             좋으셨겠어요. 몸은 좀 어떠세요?                                  ← 1개

원칙

프롬프트 규칙이 세 번 실패하면 모델이 아니라 입력을 의심할 것.

"하지 마세요"를 강하게 쓰는 것보다 하지 말아야 할 것을 애초에 보여주지 않는 쪽이 낫다.

11단계의 fallback 정리와 같은 형태의 교훈이다. 거기서는 규칙의 잘못된 전제가, 여기서는 전달하는 정보의 범위가 원인이었다. 둘 다 문구를 덧붙여 덮으려던 시도가 먼저 실패했다.

테스트

test_next_node_brief_carries_the_next_nodes_opening_question
    # responses.default 만 opening 이 되고 follow_up 은 안 실린다
test_next_node_brief_opening_is_empty_without_responses
test_handoff_turn_names_the_single_question_to_ask
    # 수집 계획이 들어간 지침 + 열 질문이 함께 올 때 범위 제한이 붙는지
test_handoff_without_an_opening_question_is_unchanged
python -m pytest    # 175 passed

부수 효과

responses.default두 곳에서 쓰이게 됐다.

위치 역할
그 노드의 턴 기준 문장 (원래)
직전 노드의 전환 턴 열 질문 지목 (추가)

default 를 고치면 그 노드의 첫 질문이 한 턴 앞에서부터 바뀐다. 11단계 B절 의 표에 이 경로를 함께 봐야 한다.

이 지목이 전환 턴이 아닌 곳에도 실리고 있던 것이 뒤이어 드러났다. min_turns 가 안 찬 턴이 다음 노드의 열 질문을 그대로 던졌다 → 13