docs: rb8001 응답 품질 리서치 작성 — 톤 과장·문맥 유실·과잉 제안 원인 확정
프롬프트 과잉 서비스 유도, OpenAI 대화 이력 미전달, 감정 constraints 무조건 주입 확인. 해결 방안 3단계 제안 (프롬프트 v2 + 이력 전달 + JSON 강제 선택적) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0a46c56ddd
commit
12d59a5819
223
journey/research/260319_rb8001_응답품질_톤과장_문맥유실_전수조사_리서치.md
Normal file
223
journey/research/260319_rb8001_응답품질_톤과장_문맥유실_전수조사_리서치.md
Normal 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곳)
|
||||||
@ -9,6 +9,7 @@ tags: [robeing, rb8001, prompt, intent, context, rag, troubleshooting]
|
|||||||
**범위**: Slack 실사용 경로의 일반 대화, Company X RAG 질문, 후속질문 응답 품질
|
**범위**: Slack 실사용 경로의 일반 대화, Company X RAG 질문, 후속질문 응답 품질
|
||||||
|
|
||||||
## 관련 문서
|
## 관련 문서
|
||||||
|
- [rb8001 응답 품질 톤 과장·문맥 유실 전수 조사 리서치](../research/260319_rb8001_응답품질_톤과장_문맥유실_전수조사_리서치.md)
|
||||||
- [Company X RAG 답변 합성 회귀](./260312_companyx_rag_answer_composition_regression.md)
|
- [Company X RAG 답변 합성 회귀](./260312_companyx_rag_answer_composition_regression.md)
|
||||||
- [rb8001 Slack Signing Secret 오값 복구 및 실유입 검증](../worklog/260317_rb8001_slack_signing_secret_오값복구_및_실유입검증.md)
|
- [rb8001 Slack Signing Secret 오값 복구 및 실유입 검증](../worklog/260317_rb8001_slack_signing_secret_오값복구_및_실유입검증.md)
|
||||||
- [의도 파싱 greeting/context followup fix](./251122_intent_parsing_greeting_context_followup_fix.md)
|
- [의도 파싱 greeting/context followup fix](./251122_intent_parsing_greeting_context_followup_fix.md)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user