-
Notifications
You must be signed in to change notification settings - Fork 9
[고객지원 챗봇 만들기] 김수민 제출합니다. #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ee29856
a5c216b
c6f3000
69e58b0
572c726
ad01fbf
8ae5af6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "total": 150, | ||
| "correct": 66, | ||
| "incorrect": 84, | ||
| "error": 0, | ||
| "accuracy": 0.44, | ||
| "tier_results": { | ||
| "easy": { | ||
| "correct": 13, | ||
| "total": 30 | ||
| }, | ||
| "medium": { | ||
| "correct": 47, | ||
| "total": 94 | ||
| }, | ||
| "hard": { | ||
| "correct": 6, | ||
| "total": 26 | ||
| } | ||
| }, | ||
| "elapsed_seconds": 468.1384799480438 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,36 +7,69 @@ | |
|
|
||
| > 구현하면서 잘 안 됐던 것, 예상과 달랐던 것을 적어주세요. | ||
|
|
||
| - | ||
| [처음 시작] | ||
|
|
||
| - 어떻게 구현하는지조차 잘 몰라서 바로 hint1을 봐야했습니다. | ||
| - 챗봇을 구현할 때에 어떤 것을 고려해서 구현해야하는지도 전혀 몰랐습니다. 그래서 RAG가 무엇인지, 임베딩은 언제 수행되는지, 벡터 계산은 어떻게 시키는건지 차근차근 | ||
| 알아가보려고 했습니다. | ||
| - 임베딩 과정을 통해 문서와 질문을 벡터로 변환하고, 이 벡터를 `VectorStore`에 저장 + 검색에 사용한다는 점을 알아봤습니다. 질문이 들어오면 먼저 관련 문서를 | ||
| `VectorStore`에서 찾고, 그 검색 결과를 문맥으로 받은 Chat API가 생성한다는 구조를 대략적으로 받아들이고 진행했습니다. | ||
| - 챗봇 기능 구현 자체는 ai를 통해 진행했습니다. | ||
|
|
||
| [구현 과정 중] | ||
|
|
||
| - 처음에는 chat log를 전부 포함하려고 했는데, 문서 수가 많아 앱 시작 시 임베딩 호출이 너무 오래 걸렸습니다. 그래서 chat log는 | ||
| `agent_accuracy=correct`인 데이터만 사용하도록 줄이고, FAQ와 policy는 각각 제목 단위로 chunking해서 `VectorStore`에 | ||
| 넣어보았습니다. | ||
| - 그리고 평가가 어떤 기준으로 되는지 모르겠고, 어떤 지점을 변경해야 평가지표가 좋아지는지도 감이 전혀 오질 않았습니다. | ||
| - 프롬포트를 다듬어야 하는건지, RAG 검색 topK값을 변경 해야 하는건지, chunking 기준을 변경 해야 하는건지, chat log 데이터에서 다른 것을 포함해야하는지 | ||
| 감이 오질 않았습니다. 그래서 하나씩 해봤는데 품질에 별다른 변화가 없었다고 느꼈습니다. | ||
|
|
||
| ## 2. 해결하지 못한 것 | ||
|
|
||
| > 시도했지만 결국 해결 못한 문제가 있다면 적어주세요. | ||
|
|
||
| - | ||
|
|
||
| - 프롬프트, topK, chunking 기준을 바꿔보며 정확도를 높이려고 했지만, 어떤 변경이 점수 향상에 가장 큰 영향을 주는지 명확히 파악하지 못했습니다. | ||
| - 검색 결과 로깅을 추가해 어떤 문서 chunk가 검색되는지는 확인할 수 있게 했지만, 오답의 원인이 검색 단계에 있는지 답변 생성 단계에 있는지 체계적으로 구분하지는 | ||
| 못했습니다. | ||
| - 검색된 문서가 맞았는데도 답변이 부족한 경우와, 애초에 잘못된 문서가 검색된 경우를 나누어 분석하는 방법을 아직 잘 모르는 것 같습니다. | ||
| - chatlog를 활용해보려고 했지만, 처음에는 데이터가 너무 많아 임베딩 시간이 오래 걸렸습니다. agent_accuracy=correct인 데이터만 사용하도록 줄였지만, 실제 | ||
| 정확도 향상에 도움이 되는지 노이즈가 되는지 파악하지 못했습니다. | ||
| - 부분적으로 맞는 답변도 score=0으로 처리되는 경우가 있는 것 같습니다. 점수를 더 세분화하면 개선 방향을 분석하는 데 도움이 될 수 있을 지 궁금했지만, | ||
| 현재 평가 기준 자체를 바꾸는 것이 적절한지는 판단하지 못했습니다. | ||
|
|
||
| ## 3. 정확도 측정 결과 | ||
|
|
||
| > 테스트 질문 100개로 측정한 정확도를 기록해주세요. | ||
|
|
||
| | 난이도 | 정확도 | 비고 | | ||
| |--------|--------|------| | ||
| | easy | | | | ||
| | medium | | | | ||
| | hard | | | | ||
| 테스트 질문 150개 기준으로 측정했습니다. (`eval_result.json`결과 작성) | ||
|
|
||
| | 난이도 | 정확도 | 비고 | | ||
| |--------|---------------|-----------------------------| | ||
| | easy | 43.3% (13/30) | 기본 질문에서도 조건/예외 누락으로 오답 발생 | | ||
| | medium | 50.0% (47/94) | 가장 높은 정확도이나 세부 정책 누락이 많음 | | ||
| | hard | 23.1% (6/26) | 복합 조건, 예외 정책, 최신 정책 구분에서 취약 | | ||
|
|
||
| ## 4. 왜 그런 결과가 나왔는지 | ||
|
|
||
| > 정확도가 낮은 난이도의 질문을 몇 개 살펴보고, 왜 틀렸는지 분석해주세요. | ||
|
|
||
| - | ||
| 완전히 다른 답변을 한 경우보다는 핵심 사실의 '일부'만 포함한 경우(함께 설명되어야 하는 조건, 예외, 제한사항을 빠뜨리는 경우)가 많았습니다. | ||
|
|
||
| 하나의 질문에 여러 정책 조항이 함께 필요한 경우 일부 정보만 답변에 반영되어 정확도가 낮았습니다. | ||
| => 관련 문서를 어느 정도 찾았더라도, 답변 생성 과정에서 필요한 조건을 모두 종합하지 못하면 오답이 되었습니다. | ||
|
|
||
| 현재 구현은 topK로 검색된 일부 chunk만 문맥으로 전달하기 때문에, 필요한 근거가 검색 결과에 포함되지 않거나, 포함되더라도 답변에서 충분히 사용되지 않는 문제가 | ||
| 있었습니다. | ||
|
|
||
| ## 5. 개선하고 싶은 것 | ||
|
|
||
| > 시간이 더 있었다면 시도해보고 싶은 개선점을 적어주세요. | ||
|
|
||
| - | ||
| - 임베딩과 벡터 검색의 원리가 궁금합니다. 제공되는 힌트를 보니까 cosine similarity, 벡터 차원 등의 키워드가 있던데 아직 잘 모릅니다... | ||
| - incorrect하다고 판단한 원인을 좀 더 알아보고 싶습니다. 로깅을 통해 어떤 chunk가 검색되었는지는 확인할 수 있지만, 오답이 검색 실패 때문인지, 검색된 문서를 | ||
| 충분히 활용하지 못한 답변 생성 문제인지 모르겠습니다. 그래서 더 어떤 시도를 해야하는지 답답했던 것 같습니다.(제가 늦게 참여해서 그런 걸까요..?) | ||
|
Comment on lines
+70
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "어떤 시도를 해야 할지 답답했다" 라는 것은 적용할 때 어떤 것을, 어떻게, 왜 하는지에 대한 명확한 기준이 없었기 때문일 확률이 높을 것 같은데요. 먼저 "검색 실패인지 생성 실패인지 구분하는 방법" 자체가 해당 미션의 중요한 포인트기 때문에, 인지했다는 것 만으로도 가치가 있다 생각이 들고, |
||
|
|
||
| > 추가로 궁금한 것 | ||
|
|
||
| - 점수 계산은 어떤 것을 기준으로 하는지 궁금합니다. 점수가 높을 수록 실제 사용감이 좋아지는건지 궁금합니다. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이것이 보예가 설정하고 개선할 핵심입니다 ㅋㅋ 측정과 개선 또한 보예의 가설을 세우고 측정하고 개선하는 방향으로 접근해보길 권장드려요! |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.cholog.bootcamp; | ||
|
|
||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.ai.embedding.EmbeddingModel; | ||
| import org.springframework.ai.vectorstore.SimpleVectorStore; | ||
| import org.springframework.ai.vectorstore.VectorStore; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| public class AiConfig { | ||
|
|
||
| @Bean | ||
| VectorStore vectorStore(EmbeddingModel embeddingModel) { | ||
| return SimpleVectorStore.builder(embeddingModel).build(); | ||
| } | ||
|
|
||
| @Bean | ||
| ChatClient chatClient(ChatClient.Builder chatClientBuilder) { | ||
| return chatClientBuilder.build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.cholog.bootcamp.chat; | ||
|
|
||
| import com.cholog.bootcamp.chat.dto.ChatRequest; | ||
| import com.cholog.bootcamp.chat.dto.ChatAnswerResponse; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| public class ChatController { | ||
|
|
||
| private final ChatService chatService; | ||
|
|
||
| public ChatController(ChatService chatService) { | ||
| this.chatService = chatService; | ||
| } | ||
|
|
||
| @PostMapping("/api/chat") | ||
| public ChatAnswerResponse chat(@RequestBody ChatRequest request) { | ||
| return chatService.ask(request.question()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package com.cholog.bootcamp.chat; | ||
|
|
||
| import com.cholog.bootcamp.chat.dto.ChatAnswerResponse; | ||
| import jakarta.annotation.PostConstruct; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.ai.chat.metadata.Usage; | ||
| import org.springframework.ai.chat.model.ChatResponse; | ||
| import org.springframework.ai.document.Document; | ||
| import org.springframework.ai.vectorstore.SearchRequest; | ||
| import org.springframework.ai.vectorstore.VectorStore; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class ChatService { | ||
|
|
||
| private final ChatClient chatClient; | ||
| private final VectorStore vectorStore; | ||
| private final RagProperties ragProperties; | ||
| private final DocumentLoader documentLoader; | ||
|
|
||
| @PostConstruct | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @PostConstruct
void loadFaqContext() {
vectorStore.add(documentLoader.load());
}이 구조의 트레이드오프:
"이 챗봇은 한 가지를 잘 하는가" 라는 질문을 ChatService에 던졌을 때 "답변 + 초기화 둘 다" 가 되는 게 분리가 필요한 신호일 수 있어요~ |
||
| void loadFaqContext() { | ||
| vectorStore.add(documentLoader.load()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| public ChatAnswerResponse ask(String question) { | ||
| List<Document> retrievedDocuments = vectorStore.similaritySearch( | ||
| SearchRequest.builder() | ||
| .query(question) | ||
| .topK(ragProperties.getTopK()) | ||
| .build() | ||
| ); | ||
|
|
||
| logSearchResults(question, retrievedDocuments); | ||
|
|
||
| String supportContext = retrievedDocuments | ||
| .stream() | ||
| .map(Document::getText) | ||
| .collect(Collectors.joining("\n\n===\n\n")); | ||
|
|
||
| ChatResponse response = chatClient.prompt() | ||
| .system(""" | ||
| - 당신은 Cholog Corporation의 고객 전용 챗봇 서비스이다. | ||
| - 제공된 컨텍스트만을 활용하라. | ||
| - 제공된 컨텍스트로 답할 수 없다면, '고객센터에 문의해주세요'라고 답하라. | ||
| - 한국어로 답하라. | ||
| """) | ||
| .user(""" | ||
| Customer question: | ||
| %s | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. system prompt가 ChatService에 인라인이네요. "고객센터에 문의해주세요" 라는 거절 규칙이 들어 있는 만큼 외부 파일로 분리하면 좋겠습니다. 외부화 시 장점:
별도로 — |
||
|
|
||
| Support context: | ||
| %s | ||
| """.formatted(question, supportContext)) | ||
| .call() | ||
| .chatResponse(); | ||
|
|
||
| Usage usage = response.getMetadata().getUsage(); | ||
|
|
||
| return new ChatAnswerResponse( | ||
| response.getResult().getOutput().getText(), | ||
| new ChatAnswerResponse.TokenUsage( | ||
| usage == null || usage.getPromptTokens() == null ? 0 : usage.getPromptTokens(), | ||
| usage == null || usage.getCompletionTokens() == null ? 0 : usage.getCompletionTokens(), | ||
| usage == null || usage.getTotalTokens() == null ? 0 : usage.getTotalTokens() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usage null 방어 좋네요 👍 usage == null || usage.getPromptTokens() == null ? 0 : usage.getPromptTokens()한 칸 더 가본다면, 아래 케이스도 같이 다뤄볼 수 있어요.
사용자에게 "일시 장애로 답변이 어렵습니다" 메시지를 명시적으로 돌려주는 분기 하나 추가하면, 운영자가 alert 잡는 데도 도움이 될 수 있어요. 더 나아가 에러를 LLM이 다음 결정에 활용 가능한 형태로 만드는 사고 도 한 번 고민해보면 좋겠습니다! |
||
| ) | ||
| ); | ||
|
Comment on lines
+67
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LLM 응답 결과가 비어있을 경우 String answer = (response.getResult() != null && response.getResult().getOutput() != null)
? response.getResult().getOutput().getText()
: "고객센터에 문의해주세요.";
return new ChatAnswerResponse(
answer,
new ChatAnswerResponse.TokenUsage(
usage == null || usage.getPromptTokens() == null ? 0 : usage.getPromptTokens(),
usage == null || usage.getCompletionTokens() == null ? 0 : usage.getCompletionTokens(),
usage == null || usage.getTotalTokens() == null ? 0 : usage.getTotalTokens()
)
); |
||
| } | ||
|
|
||
| private void logSearchResults(String question, List<Document> documents) { | ||
| String resultSummary = documents.isEmpty() | ||
| ? "no documents retrieved" | ||
| : documents.stream() | ||
| .map(this::formatDocumentSummary) | ||
| .collect(Collectors.joining(" | ")); | ||
|
|
||
| log.info("RAG search question='{}' topK={} results={}", | ||
| question, | ||
| ragProperties.getTopK(), | ||
| resultSummary | ||
| ); | ||
| } | ||
|
|
||
| private String formatDocumentSummary(Document document) { | ||
| Map<String, Object> metadata = document.getMetadata(); | ||
| String sourceType = String.valueOf(metadata.getOrDefault("sourceType", "UNKNOWN")); | ||
| String source = String.valueOf(metadata.getOrDefault("source", "UNKNOWN")); | ||
| Object sectionTitle = metadata.get("sectionTitle"); | ||
|
|
||
| if (sectionTitle == null) { | ||
| return "%s/%s".formatted(sourceType, source); | ||
| } | ||
|
|
||
| return "%s/%s#%s".formatted(sourceType, source, sectionTitle); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 부분은 수학적인 개념이 깊게 필요한 부분이라 제가 설명드리기 어려울 것 같고... (저도 모름 ㅠㅠ)
제가 학습할 때 참고한 영상들 아래 링크로 올려둘게요~