Agent 2.0 exdev 서버 배포 스택
- server-dev start/stop/deploy 및 Gitea push 자동 배포 - local-dev 로컬 개발 환경 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
프롬프트 빌더 모듈
|
||||
────────────────
|
||||
LLM 프롬프트 생성 로직
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
class PromptBuilder:
|
||||
"""LLM 프롬프트 생성기"""
|
||||
|
||||
# 시스템 프롬프트 (일반 답변)
|
||||
SYSTEM_PROMPT = (
|
||||
"당신은 한국도로공사 채팅상담 챗봇입니다.\n"
|
||||
"한국도로공사의 고객 문의에 답변하는 전문 상담원으로서 행동하세요.\n"
|
||||
"\n"
|
||||
"【대화 이력 활용】\n"
|
||||
"1. 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.\n"
|
||||
"2. '그럼', '그거', '그건', '아까', '방금', '그때' 등의 지시어가 있다면:\n"
|
||||
" - 이전 대화에서 언급된 주제를 파악하세요.\n"
|
||||
" - 해당 주제와 현재 질문을 연결하여 답변하세요.\n"
|
||||
"3. 이전 대화와 현재 질문이 같은 주제라면 자연스럽게 이어서 답변하세요.\n"
|
||||
"4. 현재 질문이 인사, 감사, 종료, 새 주제라면 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.\n"
|
||||
"5. 이전 대화가 없거나 무관하다면 현재 질문만 기준으로 답변하세요.\n"
|
||||
"\n"
|
||||
"【답변 작성 원칙】\n"
|
||||
"1. 제공된 참고자료 중 고객의 상황에 일반적으로 적용 가능한 내용만 사용하세요.\n"
|
||||
"2. 특정 개인, 특정 지역, 특수한 상황에만 해당하는 내용은 제외하세요.\n"
|
||||
"3. 일반적인 정책, 절차, 규정에 관한 내용을 우선적으로 활용하세요.\n"
|
||||
"4. 참고자료가 고객의 질문과 직접적으로 관련이 없거나 특수 사례만 있다면,\n"
|
||||
" '관련 정보를 정확히 안내드리기 어렵습니다. 한국도로공사 콜센터(1588-2504)로 문의해 주시면 자세히 안내드리겠습니다.'라고 답변하세요.\n"
|
||||
"5. 참고자료에 없는 내용은 추측하지 마세요.\n"
|
||||
"\n"
|
||||
"【특수 케이스 식별 기준 - 다음 내용이 포함된 참고자료는 제외】\n"
|
||||
"❌ 특정 인명, 차량번호, 계좌번호, 주민등록번호 등 개인정보\n"
|
||||
"❌ '00아파트', '00지역 주민만', '특정 구간 한정' 등 특정 지역 한정\n"
|
||||
"❌ '2023년 특별 이벤트', '한시적 조치', '임시 운영' 등 기간 한정\n"
|
||||
"❌ '귀하의 경우', '고객님만', '해당 건에 한해' 등 개별 맞춤 답변\n"
|
||||
"❌ '예외적으로', '특별히', '이번 건에 한해서만' 등 특수 조건\n"
|
||||
"❌ 과거 민원 처리 결과나 개별 사례의 구체적 내용\n"
|
||||
"\n"
|
||||
"【용어 및 형식】\n"
|
||||
"- 답변 시 'KEC' 대신 '한국도로공사'라는 명칭을 사용하세요.\n"
|
||||
"- 답변은 한국어로 작성하고, 존댓말을 사용하세요.\n"
|
||||
"- 간결하고 명확하게 답변하세요.\n"
|
||||
"- 카카오톡 챗봇 응답이므로 3~4문장 이내로 답변하세요.\n"
|
||||
"- 인사말과 마무리 인사는 생략하세요.\n"
|
||||
"- 최종 답변에서 '참고자료', '제공된 자료', '주어진 자료', '자료를 확인한 결과' 같은 내부 근거 표현을 사용하지 마세요.\n"
|
||||
"- 개인정보나 특수 사례가 포함된 참고자료는 절대 언급하지 마세요."
|
||||
)
|
||||
|
||||
DOMAIN_DATA_PRIORITY_PROMPT = (
|
||||
"\n\n"
|
||||
"【DB 조회 결과 우선 원칙】\n"
|
||||
"1. 【DB 조회 결과】가 제공된 경우, 이 데이터는 시스템이 실시간으로 조회한 정확한 결과입니다.\n"
|
||||
"2. DB 조회 결과가 고객 질문에 답할 수 있으면, 참고자료 유무와 관계없이 DB 조회 결과를 최우선으로 사용하세요.\n"
|
||||
"3. 참고자료는 DB 조회 결과를 보완하는 용도로만 사용하고, DB 조회 결과와 충돌하면 DB 조회 결과를 따르세요.\n"
|
||||
"4. DB 조회 결과에 없는 세부 내용은 추측하지 말고, 제공된 값만 자연스럽게 설명하세요."
|
||||
)
|
||||
|
||||
def _format_reference(self, index: int, ref: Dict[str, Any], score: Any = None) -> str:
|
||||
"""참고자료를 LLM 프롬프트용 텍스트로 변환"""
|
||||
source = ref.get("source") or "unknown"
|
||||
category = ref.get("category")
|
||||
url = ref.get("url")
|
||||
|
||||
lines = [
|
||||
f"[참고자료 {index}]",
|
||||
f"출처: {source}",
|
||||
]
|
||||
if category:
|
||||
lines.append(f"분류: {category}")
|
||||
if score is not None:
|
||||
lines.append(f"점수: {score}")
|
||||
lines.extend([
|
||||
f"질문: {ref['q']}",
|
||||
f"답변: {ref['a']}",
|
||||
])
|
||||
if url:
|
||||
lines.append(f"공식 FAQ URL: {url}")
|
||||
lines.append("주의: 이 URL은 고객에게 상세 원문 확인 링크로 제공할 수 있습니다.")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
# 시스템 프롬프트 (질문 유도)
|
||||
GUIDANCE_SYSTEM_PROMPT = (
|
||||
"당신은 한국도로공사 채팅상담 챗봇입니다.\n"
|
||||
"한국도로공사 관련 문의를 돕는 챗봇이라는 역할 안에서만 답변하세요.\n"
|
||||
"\n"
|
||||
"【상황】\n"
|
||||
"고객 질문에 대해 검색 가능한 DB/벡터 자료에서 정확한 근거를 찾지 못한 상황입니다.\n"
|
||||
"\n"
|
||||
"【중요: 절대 금지 사항】\n"
|
||||
"❌ 한국도로공사 업무 정보, 제도, 요금, 정책, 운영 현황을 임의로 만들어내지 마세요.\n"
|
||||
"❌ 추측하거나 가정하여 구체적인 정보를 답변하지 마세요.\n"
|
||||
"❌ 새로운 질문을 추천하거나 다른 질문을 유도하지 마세요.\n"
|
||||
"❌ '참고자료', '제공된 자료', '주어진 자료' 같은 내부 표현을 사용하지 마세요.\n"
|
||||
"\n"
|
||||
"【답변 원칙】\n"
|
||||
"1. '넌 누구야?', '자기소개해줘', '뭐 하는 챗봇이야?' 같은 챗봇 정체성/가벼운 질문에는 짧게 답변하세요.\n"
|
||||
"2. 업무 정보에 대한 질문인데 정확한 근거가 없으면, 현재 확인 가능한 정보가 없다고 안내하고 한국도로공사 콜센터(1588-2504)로 문의하도록 안내하세요.\n"
|
||||
"3. 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.\n"
|
||||
"4. 현재 질문이 인사, 감사, 종료, 새 주제이거나 이전 대화와 무관한 경우:\n"
|
||||
" - 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.\n"
|
||||
"\n"
|
||||
"【용어 및 형식】\n"
|
||||
"- 정중하고 친절한 톤을 유지하세요.\n"
|
||||
"- 답변은 한국어로 작성하고, 존댓말을 사용하세요.\n"
|
||||
"- 간결하게 2~4문장으로 답변하세요.\n"
|
||||
"- 인사말과 마무리 인사는 생략하세요.\n"
|
||||
"- 콜센터 안내 시: 한국도로공사 콜센터(1588-2504)"
|
||||
)
|
||||
|
||||
def build_answer_prompt_messages(
|
||||
self,
|
||||
original_query: str,
|
||||
rewritten_query: Optional[str],
|
||||
references: List[Dict[str, Any]],
|
||||
scores: List[float],
|
||||
conversation_history: List[Dict[str, str]] = None,
|
||||
emotion_instruction: Optional[str] = None,
|
||||
emotion_name: Optional[str] = None,
|
||||
domain_data: Optional[Dict[str, Any]] = None
|
||||
) -> List[Dict[str, str]]:
|
||||
"""일반 답변 프롬프트 생성 (Messages Format)
|
||||
|
||||
Args:
|
||||
original_query: 원본 질문
|
||||
rewritten_query: 재작성된 질문 (없으면 None)
|
||||
references: 참고자료 리스트
|
||||
scores: 참고자료 점수
|
||||
conversation_history: 대화 이력 (messages format)
|
||||
emotion_instruction: 감정별 추가 지시사항
|
||||
emotion_name: 감정 이름 (angry, frustrated, worried 등)
|
||||
domain_data: chatbotApi 도메인 서비스 DB 조회 결과 (있으면 프롬프트에 포함)
|
||||
|
||||
Returns:
|
||||
messages 리스트
|
||||
"""
|
||||
has_domain_data = self._has_usable_domain_data(domain_data)
|
||||
|
||||
# 시스템 프롬프트에 domainData/감정 지시사항 추가
|
||||
system_prompt = self.SYSTEM_PROMPT
|
||||
if has_domain_data:
|
||||
system_prompt = system_prompt + self.DOMAIN_DATA_PRIORITY_PROMPT
|
||||
if emotion_instruction:
|
||||
system_prompt = system_prompt + emotion_instruction
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt}
|
||||
]
|
||||
|
||||
# 대화 이력 추가 (messages format)
|
||||
if conversation_history:
|
||||
messages.extend(conversation_history)
|
||||
|
||||
# 현재 질문 구성
|
||||
current_query_parts = []
|
||||
|
||||
# 감정 정보 추가 (부정적 감정인 경우)
|
||||
if emotion_name and emotion_name in ["angry", "frustrated", "worried"]:
|
||||
emotion_labels = {
|
||||
"angry": "화난/불만",
|
||||
"frustrated": "답답한/짜증난",
|
||||
"worried": "걱정되는/불안한"
|
||||
}
|
||||
current_query_parts.extend([
|
||||
f"【⚠️ 고객 감정 상태: {emotion_labels.get(emotion_name, emotion_name)}】",
|
||||
"고객이 부정적인 감정을 느끼고 있습니다.",
|
||||
"이전 대화나 현재 상황에서 무엇이 불편했는지 파악하고,",
|
||||
"그 점에 대해 구체적으로 공감한 후 실질적인 해결책을 제시하세요.",
|
||||
""
|
||||
])
|
||||
|
||||
if rewritten_query:
|
||||
# Query Rewriting이 적용된 경우
|
||||
current_query_parts.extend([
|
||||
"【원본 질문】",
|
||||
original_query,
|
||||
"",
|
||||
"【맥락 기반 재작성 질문】",
|
||||
rewritten_query,
|
||||
"(이전 대화를 참고하여 재작성된 질문입니다)",
|
||||
""
|
||||
])
|
||||
else:
|
||||
# Query Rewriting이 없는 경우 원본 질문 명시
|
||||
current_query_parts.extend([
|
||||
"【고객 질문】",
|
||||
original_query,
|
||||
""
|
||||
])
|
||||
|
||||
# DB 조회 결과 추가 (domain_data가 있을 때만, 참고자료보다 앞에 위치)
|
||||
if has_domain_data:
|
||||
summary = domain_data.get("llmSummary")
|
||||
if summary is not None and str(summary).strip():
|
||||
current_query_parts.extend([
|
||||
"【DB 조회 결과】",
|
||||
"(아래는 시스템 요약(llmSummary)입니다. 참고자료보다 우선하여 활용하세요.)",
|
||||
str(summary).strip(),
|
||||
""
|
||||
])
|
||||
else:
|
||||
db_parts = []
|
||||
for key, value in domain_data.items():
|
||||
if key not in ("status", "statusMsg", "errorMsg") and value is not None:
|
||||
db_parts.append(f"- {key}: {value}")
|
||||
if db_parts:
|
||||
current_query_parts.extend([
|
||||
"【DB 조회 결과】",
|
||||
"(아래는 시스템에서 조회한 정확한 데이터입니다. 참고자료보다 우선하여 활용하세요.)",
|
||||
"\n".join(db_parts),
|
||||
""
|
||||
])
|
||||
|
||||
# 참고자료 추가
|
||||
context_parts = []
|
||||
for i, (ref, score) in enumerate(zip(references, scores), 1):
|
||||
context_parts.append(self._format_reference(i, ref, score))
|
||||
|
||||
if context_parts:
|
||||
current_query_parts.extend([
|
||||
"【참고자료】",
|
||||
"\n".join(context_parts),
|
||||
"",
|
||||
"【답변 지시사항】"
|
||||
])
|
||||
else:
|
||||
current_query_parts.extend([
|
||||
"【참고자료】",
|
||||
"제공된 참고자료가 없습니다.",
|
||||
"",
|
||||
"【답변 지시사항】"
|
||||
])
|
||||
if emotion_name in ["angry", "frustrated", "worried"]:
|
||||
current_query_parts.append(
|
||||
f"⚠️ 고객이 {emotion_labels.get(emotion_name, emotion_name)} 상태입니다. "
|
||||
"답변 시작 부분에 무엇이 불편했는지 구체적으로 언급하며 공감하고, "
|
||||
"그 후 명확한 해결 방법을 제시하세요."
|
||||
)
|
||||
if rewritten_query:
|
||||
current_query_parts.append("- 원본 질문은 간단하지만, 재작성된 질문의 의도를 파악하여 답변하세요.")
|
||||
current_query_parts.append("- 답변 시에는 사용자가 실제로 물어본 질문에 대해 자연스럽게 답변하세요.")
|
||||
if conversation_history:
|
||||
current_query_parts.append("- 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.")
|
||||
current_query_parts.append("- 현재 질문이 인사, 감사, 종료, 새 주제라면 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.")
|
||||
|
||||
if has_domain_data:
|
||||
current_query_parts.append("- 【DB 조회 결과】를 최우선 근거로 고객의 질문에 답변해주세요.")
|
||||
current_query_parts.append("- 참고자료가 없거나 질문과 무관해도, DB 조회 결과가 질문에 답할 수 있으면 콜센터 안내로 대체하지 마세요.")
|
||||
current_query_parts.append("- 참고자료는 DB 조회 결과를 보완할 때만 사용하고, DB 조회 결과와 충돌하면 DB 조회 결과를 따르세요.")
|
||||
if domain_data and domain_data.get("msgMap"):
|
||||
current_query_parts.append("- 휴게소 메뉴 목록은 전체를 모두 나열하지 말고, 대표 메뉴 3~5개만 짧게 언급한 뒤 상세 목록은 시스템 응답의 메뉴 목록을 확인하도록 안내하세요.")
|
||||
current_query_parts.append("- 메뉴가 많거나 방향/매장이 여러 개인 경우에도 답변 본문은 5문장 이내로 간결하게 작성하세요.")
|
||||
current_query_parts.append("- 공식 FAQ URL이 있는 참고자료가 관련 있다면, 답변 본문에는 간단히 안내하고 시스템 응답의 링크 버튼으로 상세 확인을 유도할 수 있습니다.")
|
||||
else:
|
||||
current_query_parts.append("- 위 참고자료를 바탕으로 고객의 질문에 답변해주세요.")
|
||||
current_query_parts.append("- 참고자료가 질문과 무관하거나 특수 사례만 있다면 콜센터로 안내하세요.")
|
||||
current_query_parts.append("- 공식 FAQ URL이 있는 참고자료가 관련 있다면, 답변 본문에는 간단히 안내하고 시스템 응답의 링크 버튼으로 상세 확인을 유도할 수 있습니다.")
|
||||
current_query_parts.append("- 카카오톡 챗봇 응답이므로 3~4문장 이내로 답변하세요.")
|
||||
current_query_parts.append("- 인사말과 마무리 인사는 생략하세요.")
|
||||
current_query_parts.append("- 최종 답변에서 '참고자료', '제공된 자료', '주어진 자료', '자료를 확인한 결과' 같은 내부 근거 표현을 사용하지 마세요.")
|
||||
|
||||
# 현재 질문 추가
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": "\n".join(current_query_parts)
|
||||
})
|
||||
|
||||
return messages
|
||||
|
||||
def _has_usable_domain_data(self, domain_data: Optional[Dict[str, Any]]) -> bool:
|
||||
if not domain_data:
|
||||
return False
|
||||
status = domain_data.get("status")
|
||||
if status is False:
|
||||
return False
|
||||
if isinstance(status, str) and status.lower() == "false":
|
||||
return False
|
||||
return True
|
||||
|
||||
def build_answer_prompt(
|
||||
self,
|
||||
original_query: str,
|
||||
rewritten_query: Optional[str],
|
||||
references: List[Dict[str, Any]],
|
||||
scores: List[float],
|
||||
conversation_context: Optional[str] = None,
|
||||
emotion_instruction: Optional[str] = None
|
||||
) -> tuple[str, str]:
|
||||
"""일반 답변 프롬프트 생성
|
||||
|
||||
Args:
|
||||
original_query: 원본 질문
|
||||
rewritten_query: 재작성된 질문 (없으면 None)
|
||||
references: 참고자료 리스트
|
||||
scores: 참고자료 점수
|
||||
conversation_context: 대화 이력 텍스트
|
||||
emotion_instruction: 감정별 추가 지시사항
|
||||
|
||||
Returns:
|
||||
(system_prompt, user_prompt)
|
||||
"""
|
||||
# 시스템 프롬프트에 감정 지시사항 추가
|
||||
system_prompt = self.SYSTEM_PROMPT
|
||||
if emotion_instruction:
|
||||
system_prompt = system_prompt + emotion_instruction
|
||||
|
||||
user_prompt_parts = []
|
||||
|
||||
# 이전 대화 이력
|
||||
if conversation_context:
|
||||
user_prompt_parts.append("【이전 대화 이력】")
|
||||
user_prompt_parts.append("(현재 질문과 명확히 이어지는 경우에만 참고하세요)")
|
||||
user_prompt_parts.append(conversation_context)
|
||||
user_prompt_parts.append("")
|
||||
|
||||
# 질문 (Query Rewriting 적용 여부에 따라)
|
||||
if rewritten_query:
|
||||
user_prompt_parts.extend([
|
||||
"【사용자의 현재 질문】",
|
||||
original_query,
|
||||
"",
|
||||
"【맥락을 고려한 완전한 질문】",
|
||||
rewritten_query,
|
||||
"(이전 대화를 참고하여 재작성된 질문입니다. 이 질문에 대해 답변해주세요.)",
|
||||
"",
|
||||
])
|
||||
else:
|
||||
user_prompt_parts.extend([
|
||||
"【현재 질문】",
|
||||
f"고객 질문: {original_query}",
|
||||
"",
|
||||
])
|
||||
|
||||
# 참고자료
|
||||
context_parts = []
|
||||
for i, (ref, score) in enumerate(zip(references, scores), 1):
|
||||
context_parts.append(self._format_reference(i, ref, score))
|
||||
|
||||
user_prompt_parts.extend([
|
||||
"【참고자료】",
|
||||
"\n".join(context_parts),
|
||||
"",
|
||||
"【지시사항】"
|
||||
])
|
||||
|
||||
# 추가 지시사항
|
||||
if rewritten_query:
|
||||
user_prompt_parts.append("- 사용자의 현재 질문은 간단하지만, 재작성된 완전한 질문의 의도를 파악하여 답변하세요.")
|
||||
user_prompt_parts.append("- 답변 시에는 사용자가 실제로 물어본 현재 질문에 대해 자연스럽게 답변하세요.")
|
||||
if conversation_context:
|
||||
user_prompt_parts.append("- 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.")
|
||||
user_prompt_parts.append("- 현재 질문이 인사, 감사, 종료, 새 주제라면 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.")
|
||||
|
||||
user_prompt_parts.append("- 위 참고자료를 바탕으로 고객의 질문에 답변해주세요.")
|
||||
user_prompt_parts.append("- 참고자료가 질문과 무관하거나 특수 사례만 있다면 콜센터로 안내하세요.")
|
||||
user_prompt_parts.append("- 공식 FAQ URL이 있는 참고자료가 관련 있다면, 답변 본문에는 간단히 안내하고 시스템 응답의 링크 버튼으로 상세 확인을 유도할 수 있습니다.")
|
||||
user_prompt_parts.append("- 최종 답변에서 '참고자료', '제공된 자료', '주어진 자료', '자료를 확인한 결과' 같은 내부 근거 표현을 사용하지 마세요.")
|
||||
|
||||
return system_prompt, "\n".join(user_prompt_parts)
|
||||
|
||||
def build_guidance_prompt(
|
||||
self,
|
||||
original_query: str,
|
||||
conversation_context: Optional[str] = None
|
||||
) -> tuple[str, str]:
|
||||
"""질문 유도 프롬프트 생성
|
||||
|
||||
Args:
|
||||
original_query: 원본 질문
|
||||
conversation_context: 대화 이력 텍스트
|
||||
|
||||
Returns:
|
||||
(system_prompt, user_prompt)
|
||||
"""
|
||||
user_prompt_parts = []
|
||||
|
||||
# 이전 대화 이력
|
||||
if conversation_context:
|
||||
user_prompt_parts.append(conversation_context)
|
||||
user_prompt_parts.append("")
|
||||
|
||||
# 현재 상황
|
||||
user_prompt_parts.extend([
|
||||
"【현재 상황】",
|
||||
f"고객 질문: {original_query}",
|
||||
"",
|
||||
"위 질문에 대해 검색 가능한 DB/벡터 자료에서 정확한 근거를 찾지 못했습니다.",
|
||||
"",
|
||||
"【지시사항】",
|
||||
"- 챗봇 정체성이나 가벼운 질문이면 한국도로공사 채팅상담 챗봇이라는 역할 안에서 짧게 답변하세요.",
|
||||
"- 업무 정보에 대한 질문이면 현재 정확한 정보를 확인하기 어렵다고 안내하고 콜센터로 연결하세요.",
|
||||
"- 임의로 새로운 정보를 만들어내지 마세요.",
|
||||
"- 새로운 질문을 추천하거나 다른 질문을 유도하지 마세요."
|
||||
])
|
||||
|
||||
return self.GUIDANCE_SYSTEM_PROMPT, "\n".join(user_prompt_parts)
|
||||
Reference in New Issue
Block a user