docs: rb8001 응답 품질 리서치 작성 — 톤 과장·문맥 유실·과잉 제안 원인 확정

프롬프트 과잉 서비스 유도, OpenAI 대화 이력 미전달, 감정 constraints 무조건 주입 확인.
해결 방안 3단계 제안 (프롬프트 v2 + 이력 전달 + JSON 강제 선택적)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
happybell80 2026-03-19 21:45:43 +09:00
parent 0a46c56ddd
commit 12d59a5819
2 changed files with 224 additions and 0 deletions

View File

@ -0,0 +1,223 @@
---
tags: [rb8001, prompt, tone, context, quality, research]
---
# rb8001 응답 품질 — 톤 과장·문맥 유실·과잉 제안 전수 조사 리서치
## 관련 문서
### 트러블슈팅 (이 리서치가 닫아야 할 문제)
- [rb8001 프롬프트·의도분석·문맥응답 품질 문제](../troubleshooting/260317_rb8001_prompt_intent_context_response_quality_문제오픈.md)
### 상위 원칙
- [0_VALUE Coding Principles](https://github.com/happybell80/0_VALUE/blob/main/02_Governance/coding-principles.md) — #4 폴백 절제, #9 질문별 특례 하드코딩 금지
## 리서치 목적
트러블슈팅 문서의 재현 질문셋을 실제로 실행하고, 코드 경로를 추적하여 다음을 확정한다.
1. 톤 과장의 정확한 원인 (프롬프트 어느 부분이 과잉 서비스를 유발하는지)
2. 문맥 유실의 정확한 원인 (어디서 직전 대화가 끊기는지)
3. 과잉 제안의 정확한 원인 (왜 묻지 않은 것까지 나열하는지)
4. Pydantic JSON 강제 방식의 적용 가능성
5. 각 원인에 대한 최소 수정 범위
## 1. 현재 상태 (2026-03-19 E2E 실행 결과)
모델: `gpt-5-mini` (OpenAI handler 경유)
| 질문 | 현재 응답 | 문제 |
|------|----------|------|
| `로빙?` | `사용자님, 로빙입니다. 어떻게 도와드릴까요?` | 양호 |
| `오늘 날씨는 어때?` | 5줄 — 위치 없다며 장황한 안내, 준비물 언급 | 과잉 안내 |
| `난 지금 서울이야` | 7줄 — 투자자 추천, 동선 최적화, PDF 분석 등 나열 | **과잉 제안, 맥락 무시** |
| `내가 지금 어디라고?` | 6줄 — 위치 접근 불가, 안전 문제 운운 | **문맥 유실, 과장** |
## 2. 원인 분석
### 2-1. 톤 과장 — 시스템 프롬프트가 과잉 서비스를 유도
**OpenAI handler `_get_system_prompt()` (openai_handler.py:206~226)**
문제 문구:
```
- 항상 도움이 되는 방향으로 조언
- 필요시 구체적인 액션 아이템 제시
```
이 두 줄이 LLM에게 "매 응답마다 무언가를 제안해야 한다"는 압박을 줌. `서울이야`처럼 단순 사실 제공에도 투자자 추천, PDF 분석 등을 나열하는 원인.
**감정 주입 (llm_service.py:262~268)**
```
- {strategy} {length_instruction} 응답하세요.
- 사용자를 '{preferred_name}'으로 호칭하세요. 호칭은 응답 첫 문장에 1회만...
```
감정 분석 결과가 neutral이어도 `자연스럽게 중간 응답하세요`라는 지시가 붙어 짧게 끝내야 할 응답을 늘림.
**Gemini handler `_get_system_prompt()` (gemini_handler.py:606~648)**
```
<examples>
좋은 답변: "사용자님의 정보를 확인하여 정확한 답변을 드리겠습니다."
```
좋은 답변 예시 자체가 의전형. LLM이 이 톤을 학습함.
### 2-2. 문맥 유실 — OpenAI handler에 대화 이력이 전달되지 않음
**핵심 발견:**
- Gemini handler는 `context['recent_conversations']`를 읽어 `last_3_context`로 주입 → 직전 대화 최대 3개 포함
- OpenAI handler는 `context['previous_messages']`를 읽음 → **그런데 이 키는 어디에서도 채워지지 않음**
- `message_router.py:349`에서 context에 `recent_conversations`를 넣지만, `previous_messages`는 안 넣음
- 따라서 **OpenAI 경로에서는 대화 이력이 항상 비어있음**
이것이 `서울이야``어디라고?`에서 직전 맥락을 유실하는 직접 원인.
**CONTEXT_FOLLOWUP 우회 경로 (message_service.py:265~286):**
- 의도가 `CONTEXT_FOLLOWUP`으로 분류되면 직전 발화를 메시지에 직접 이어붙임
- 하지만 `내가 지금 어디라고?``CONTEXT_FOLLOWUP`으로 분류되지 않으면 이 경로를 타지 않음
- 실제로 `CONTEXT_FOLLOWUP` 판정은 `decision_engine`의 규칙 기반 패턴 매칭에 의존
### 2-3. 과잉 제안 — 프롬프트에 제한이 없음
현재 프롬프트에는 다음이 **없음**:
- 응답 길이 제한 (최대 문장 수)
- "묻지 않은 것은 제안하지 마라" 지시
- "단순 확인이면 짧게 끝내라" 지시
- 출력 구조 강제 (JSON 등)
LLM은 "도움이 되는 방향으로 조언"이라는 지시를 따라 최대한 많은 제안을 나열함.
## 3. 해결 방안
### 3-1. 시스템 프롬프트 개선 (DB에서 v2로 교체)
방금 구현한 프롬프트 DB 폐루프를 활용하여 코드 배포 없이 프롬프트를 교체.
**v2 프롬프트 설계 원칙:**
- "도움이 되는 방향으로 조언" → 삭제
- "필요시 액션 아이템 제시" → "사용자가 요청한 경우에만 제시"로 변경
- 응답 길이 제한 추가: "2~3문장 이내로 답변. 사용자가 더 원하면 그때 확장"
- "묻지 않은 것은 제안하지 마라" 명시
- 의전형 예시 제거
**OpenAI v2 프롬프트 (제안):**
```
당신은 '로빙(Robeing)'이라는 이름의 AI 어시스턴트입니다.
응답 원칙:
1. 짧고 직접적으로 답변한다. 2~3문장 이내.
2. 묻지 않은 것은 제안하지 않는다.
3. 단순 확인이나 사실 제공이면 1문장으로 끝낸다.
4. 사용자가 더 원하면 그때 확장한다.
5. 이모지 사용 금지.
6. 한국어로 응답.
```
### 3-2. 문맥 유실 수정 — OpenAI handler에 대화 이력 전달
**수정 위치:** `llm_service.py``process_request()` chat 경로
`recent_conversations`를 OpenAI의 `previous_messages` 형식으로 변환하여 context에 추가:
```python
if context.get("recent_conversations"):
prev_msgs = []
for conv in context["recent_conversations"][:5]:
if conv.get("message"):
prev_msgs.append({"role": "user", "content": conv["message"]})
if conv.get("response"):
prev_msgs.append({"role": "assistant", "content": conv["response"]})
context["previous_messages"] = prev_msgs
```
**수정 파일:** `app/services/llm/llm_service.py` 1곳
### 3-3. Pydantic JSON 강제 — 선택적 적용
**적용 모델:**
```python
class ChatResponse(BaseModel):
answer: str = Field(description="질문에 대한 직접 답변. 2~3문장 이내.")
follow_up: bool = Field(default=False, description="추가 정보가 필요한지 여부")
```
**적용 방식:**
- `response_format={"type": "json_object"}`를 chat 경로에도 추가
- LLM 응답을 `ChatResponse.model_validate()`로 검증
- 사용자에게는 `answer` 필드만 전달
**장점:**
- 응답 길이를 구조적으로 제어 (Field description으로 강제)
- 과잉 제안이 `answer` 밖으로 나갈 수 없음
- 테스트 자동화 가능 (answer 길이, follow_up 값 검증)
**단점:**
- 자연스러운 대화 흐름에 영향 가능
- 토큰 소모 약간 증가
**권장:** 3-1(프롬프트 개선)과 3-2(문맥 수정)를 먼저 적용하고, 그래도 톤이 안 잡히면 3-3(JSON 강제)을 추가.
## 4. 수정 범위 요약
| 단계 | 내용 | 수정 파일 | 코드 배포 필요 |
|------|------|----------|--------------|
| **1단계** | 시스템 프롬프트 v2 교체 | DB만 | **아니오** (폐루프 활용) |
| **2단계** | OpenAI handler 대화 이력 전달 | `llm_service.py` | 예 |
| **3단계** (선택) | Pydantic JSON 강제 | `llm_service.py`, `openai_handler.py` | 예 |
## 5. 검증 기준 (닫힘 조건)
재현 질문셋 기준:
| 질문 | 기대 응답 |
|------|----------|
| `로빙?` | 1문장 인사 |
| `오늘 날씨는 어때?` | 실시간 조회 불가 안내 1~2문장, 과잉 안내 없음 |
| `난 지금 서울이야` | `서울이시군요.` 또는 `알겠습니다.` 1문장, 묻지 않은 제안 없음 |
| `내가 지금 어디라고?` | `서울이라고 하셨어요.` 1문장, 직전 맥락 유지 |
## 6. 감정 주입 constraints 조정
현재 (llm_service.py:262~268):
```
- 사용자를 '{preferred_name}'으로 호칭하세요. 호칭은 응답 첫 문장에 1회만 자연스럽게 사용하세요.
- {strategy} {length_instruction} 응답하세요.
```
**문제:** neutral 감정에서도 "자연스럽게 중간 응답하세요"가 붙어 응답을 늘림.
**개선:**
- neutral일 때 감정 constraints 자체를 생략 (빈 문자열)
- `length_instruction` 기본값을 "중간" → "짧게"로 변경
- 호칭 지시를 조건부로 (첫 대화에만 사용, 이후 생략)
## 7. 리스크
| 리스크 | 대응 |
|--------|------|
| 프롬프트를 너무 짧게 바꾸면 필요할 때도 정보 부족 | "사용자가 더 원하면 그때 확장" 지시로 대응 |
| JSON 강제 시 자연스러움 저하 | 1~2단계 선적용 후 필요 시에만 3단계 |
| 문맥 이력 5개 추가 시 토큰 증가 | 이미 Gemini에서 3개 사용 중, OpenAI 5개는 허용 범위 |
| 감정 constraints 제거 시 감정 대응 약화 | neutral 외 감정에서는 유지 |
## 8. 구현 순서
| 순서 | 내용 | 종결 효과 |
|------|------|----------|
| **1** | DB에 시스템 프롬프트 v2 적재 + 활성화 | 톤 과장, 과잉 제안 해소 |
| **2** | `llm_service.py`에서 `recent_conversations``previous_messages` 변환 | 문맥 유실 해소 |
| **3** | neutral 감정 constraints 생략, length 기본값 "짧게" | 불필요한 응답 늘림 방지 |
| **4** | 재현 질문셋 E2E 검증 | 닫힘 조건 충족 확인 |
| **5** | (선택) Pydantic JSON 강제 | 구조적 톤 제어 |
1~4 완료 시 트러블슈팅 종결 가능.
## 9. 결론
- 톤 과장의 원인: 시스템 프롬프트의 "도움이 되는 방향으로 조언", "액션 아이템 제시" 문구 + 감정 constraints의 무조건 주입
- 문맥 유실의 원인: OpenAI handler가 `previous_messages` 키를 읽는데, 이 키를 아무도 채우지 않음
- 과잉 제안의 원인: "묻지 않은 것은 제안하지 마라" 지시 부재 + 응답 길이 제한 없음
- 해결: 프롬프트 v2 교체(DB, 배포 불필요) + 대화 이력 전달 수정(1곳) + 감정 constraints 조정(1곳)

View File

@ -9,6 +9,7 @@ tags: [robeing, rb8001, prompt, intent, context, rag, troubleshooting]
**범위**: Slack 실사용 경로의 일반 대화, Company X RAG 질문, 후속질문 응답 품질
## 관련 문서
- [rb8001 응답 품질 톤 과장·문맥 유실 전수 조사 리서치](../research/260319_rb8001_응답품질_톤과장_문맥유실_전수조사_리서치.md)
- [Company X RAG 답변 합성 회귀](./260312_companyx_rag_answer_composition_regression.md)
- [rb8001 Slack Signing Secret 오값 복구 및 실유입 검증](../worklog/260317_rb8001_slack_signing_secret_오값복구_및_실유입검증.md)
- [의도 파싱 greeting/context followup fix](./251122_intent_parsing_greeting_context_followup_fix.md)