Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
34a7b8b
build: Lombok 의존성 추가
Soundbar91 May 9, 2026
ef83a9c
feat: POST /api/chat 초기 스켈레톤 코드 추가
Soundbar91 May 9, 2026
31a476e
fix: 요청/응답 DTO 클래스 네이밍 수정
Soundbar91 May 9, 2026
4518ab0
feat: OpenAI API 연동
Soundbar91 May 9, 2026
71f066d
feat: FAQ 컨텍스트 제공 추가
Soundbar91 May 11, 2026
336c798
feat: 정책 컨텍스트 제공 추가
Soundbar91 May 11, 2026
5019f9d
chore: LLM 요청/응답 로깅 추가
Soundbar91 May 12, 2026
f53777e
fix: 폐지 정책 컨텍스트 제공 삭제
Soundbar91 May 12, 2026
afd182a
fix: 시스템 프롬프트 수정
Soundbar91 May 12, 2026
7073245
chore: 토큰 사용량 로깅 추가
Soundbar91 May 12, 2026
28a05aa
fix: 시스템 프롬프트 수정
Soundbar91 May 12, 2026
289c43e
fix: 잘못된 파일 이름 추가 로직 수정
Soundbar91 May 13, 2026
69d8b5a
fix: AI 응답 메소드 호출부 수정
Soundbar91 May 13, 2026
f06ef96
build: VectorStore 의존성 추가
Soundbar91 May 14, 2026
27e5167
build: MarkdownReader 의존성 추가
Soundbar91 May 14, 2026
0b39720
fix: 평가 스크립트에서 영어로 질문하도록 수정
Soundbar91 May 16, 2026
f0d94de
feat: VectorStore 설정 클래스 추가
Soundbar91 May 16, 2026
478262e
feat: ChatClient 설정 클래스 추가
Soundbar91 May 16, 2026
3602fb0
fix: 파일 읽기 로직 삭제
Soundbar91 May 16, 2026
88b0967
feat: FAQ 리더 클래스 추가
Soundbar91 May 16, 2026
6f5011f
fix: VectorStore에서 컨텍스트를 조회하도록 수정
Soundbar91 May 16, 2026
36fc12c
feat: Policy 리더 클래스 추가
Soundbar91 May 16, 2026
fcaa431
feat: VectorStore에 Document 적재 로직 추가
Soundbar91 May 16, 2026
4373bf0
fix: 시스템 프롬프트 수정
Soundbar91 May 16, 2026
0db51d6
feat: 각 Layer별로 Document를 조회할 수 있는 로직 추가
Soundbar91 May 16, 2026
00d7534
feat: Chatlog 리더 클래스 추가
Soundbar91 May 16, 2026
4b8ab69
fix: 평가 기준 수정
Soundbar91 May 18, 2026
a121fba
fix: 시스템 프롬프트 수정
Soundbar91 May 18, 2026
df3badc
fix: 임베딩 데이터 수정에 따른 Reader 클래스 수정
Soundbar91 May 18, 2026
cb26abd
fix: 임베딩 데이터 수정에 따른 Reader 클래스 수정
Soundbar91 May 18, 2026
8c9cac2
chore: 패키징
Soundbar91 May 18, 2026
417cb4f
fix: 검증 프롬프트 수정
Soundbar91 May 18, 2026
640c7bd
fix: ChatService 내부 레코드 클래스 삭제
Soundbar91 May 20, 2026
28e468e
fix: InternalPolicy 메타 데이터 수정
Soundbar91 May 20, 2026
ab270bc
fix: 시스템 프롬프트 수정
Soundbar91 May 20, 2026
70c53bd
chore: 코드 포멧팅
Soundbar91 May 20, 2026
ccfeb1c
fix: InternalPolicy filterExpression 수정
Soundbar91 May 20, 2026
14f0f85
fix: 로깅 삭제
Soundbar91 May 20, 2026
29f2289
docs: wall-report 작성
Soundbar91 May 21, 2026
d21404a
build: 미사용 의존성 삭제
Soundbar91 May 21, 2026
486ebf4
chore: eof 수정
Soundbar91 May 21, 2026
52598a3
Update src/main/java/com/cholog/bootcamp/reader/ChatLogReader.java
Soundbar91 May 21, 2026
7801477
Update src/main/java/com/cholog/bootcamp/reader/CurrentPolicyReader.java
Soundbar91 May 21, 2026
6f39a50
Update src/main/java/com/cholog/bootcamp/reader/FaqReader.java
Soundbar91 May 21, 2026
c88bb20
Update src/main/java/com/cholog/bootcamp/reader/InternalPolicyReader.…
Soundbar91 May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencyManagement {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'

implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Expand Down
22 changes: 22 additions & 0 deletions data/eval_result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"total": 150,
"correct": 112,
"incorrect": 38,
"error": 0,
"accuracy": 0.7466666666666667,
"tier_results": {
"easy": {
"correct": 23,
"total": 30
},
"medium": {
"correct": 69,
"total": 94
},
"hard": {
"correct": 20,
"total": 26
}
},
"elapsed_seconds": 556.8951289653778
}
8 changes: 5 additions & 3 deletions data/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ def judge_answer(question: str, expected: str, actual: str) -> dict:
실제 답변 (챗봇): {actual}

실제 답변이 기대 답변과 사실적으로 일치하는지 평가하세요.
- 표현이 달라도 핵심 사실이 같으면 정답입니다
- 핵심 사실이 빠져있거나 틀렸으면 오답입니다
- 부분적으로만 맞으면 오답으로 처리하세요
- 질문에 필요한 핵심 사실을 정확히 답했다면, 기대 답변보다 짧거나 부가 정보가 없어도 정답입니다.
- 핵심 사실이 빠졌거나 틀렸거나, 기대 답변과 충돌하는 정보가 있으면 오답입니다.
- 문장 일치가 아니라 질문에 대한 충분성을 기준으로 판단하세요.

JSON으로만 응답하세요:
{{"score": 1, "reason": "..."}} (정답)
Expand Down Expand Up @@ -138,6 +138,7 @@ def main():
for i, q in enumerate(questions):
qid = q.get("id", f"Q{i+1}")
question_ko = q["question_ko"]
question_en = q["question_en"]
expected = q["expected_answer"]
tier = q.get("tier", "unknown")

Expand All @@ -147,6 +148,7 @@ def main():

# 서버에 질문
response = ask_server(question_ko)
# response = ask_server(question_en)
if response is None:
results["error"] += 1
if args.verbose:
Expand Down
75 changes: 63 additions & 12 deletions mission/wall-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,87 @@

> 구현하면서 잘 안 됐던 것, 예상과 달랐던 것을 적어주세요.

-

### 토큰 사용량 개선
- 원인
- 매 요청마다 입력 토큰이 약 9,600개가 소모되는 상황이였습니다.
- FAQ, Policy, Chatlog 세 개의 데이터를 모두 컨텍스트로 제공함에 따라 토큰 사용량이 늘어나는 것은 당연함을 알게 됐습니다.
- 고민
- 세 개의 데이터를 모두 컨텍스트로 제공해도, 답변을 하는 과정에서 불필요한 데이터가 분명 있을 것이라 생각했습니다.
- 그렇다면 답변에 필요한 컨텍스트만 제공해야겠다는 생각이 들었고, 관련해서 Spring AI 공식문서를 찾던 중 ETL 파이프라인에 대해서 알게 됐습니다.
- 해결
- Spring AI에서 제공해주는 DocumentReader/Transformer 구현체를 그대로 사용하지 않고 각 데이터에 맞는 Reader 클래스를 구현했습니다.
- 각 데이터 레이어가 이미 정형화되어 있어 토큰 분할이 불필요하고, DocumentTransformer로 메타데이터를 뽑을 때마다 임베딩 모델을 호출하기 때문에 불필요한 비용이 발생한다고 판단했기 때문입니다.
- 이후, VectorStore에서 사용자 질문과 연관된 내용을 찾아 AI에게 컨텍스트를 제공함으로써 토큰 사용량이 9,600개에서 592개로 줄어들었습니다.
- 학습
- 임베딩, 코사인 유사도 개념에 대해서 학습했습니다.
- Spring AI의 ETL 파이프라인에 대해서 학습했습니다.

### 평가 기준 개선
- 원인
- `적립 포인트 1점은 얼마인가요?`라는 질문에 `1포인트는 1원입니다.`라는 답을 했음에도 오답처리가 됨을 확인했습니다.
- 핵심 정보는 전달했으나, `1,000 포인트 이상부터 사용 가능`이라는 부가적인 정보를 전달하지 않아 오답처리가 됐습니다.
- 고민
- 질문에 직접 대응하는 답변만 정확히 했다면 정답이라고 봐야한다고 생각했습니다.
- 개인적으로 AI에게 질문을 던졌을 때, 답변 이외의 부가정보를 너무 많이 전달하여 어디서부터 읽어야할지 혼란을 겪은 경험이 있었기 때문입니다.
- 다만, 기업의 관점에서는 한 번에 많은 정보를 담은 정보를 전달하여 리소스 절감을 하는데 목적이 있을 수 있다고 생각했습니다.
- 해결
- 결론적으로는 직접 대응하는 답변을 정확히 했다면 정답 처리가 되도록 검증 스크립트를 수정했습니다.
- 사용자 입장에서도 필요한 정보만 전달하는 것이 좋다고 판단했고, 많은 정보를 전달하는 것이 사용자 경험상으로 불편함을 느낄 수 있을 가능성이 높다고 생각했기 때문입니다.
- 정확도가 `81개 -> 113개`로 증가했지만, 평균 응답 시간이 `2.6초 -> 3.3초`로 증가함을 확인했습니다.
- 프롬프트 개선을 통해 정확도가 `113개 -> 108개`로 감소했지만, 평균 응답 시간이 `3.3초 -> 2.7초`로 감소함을 확인했습니다.
- 학습
- 평가 기준에 대해서 고민하고 개선하는 경험을 할 수 있었습니다.

### 할루시네이션 개선
- 원인
- `VIP 등급 조건`을 물어보는 질문에 대해서 `800만원`이라고 계속 답하여 오답처리가 됨을 확인했습니다.
- 해당 질문의 컨텍스트로 제공되는 데이터를 확인한 결과 Chatlog에서 상담원이 800만원이라 답변한 로그가 존재했고, FAQ에는 80만원이라고 명시되어 있었습니다.
- 고민
- Chatlog에서 agent_accuracy가 correct 처리가 되어 있어도, 잘못된 데이터가 있을 수 있으니 필터링을 해야하는 상황이였습니다.
- Chatlog의 데이터는 너무 많아 직접 확인해서 라벨링을 수정하는 과정은 리소스가 많이 든다고 생각했습니다.
- 또한, Chatlog를 임베딩할 때 OpenAI API를 호출하여 Chatlog 데이터가 FAQ와 Policy의 내용과 적합한지를 판단하고 적절하다면 임베딩하는 방법을 생각했습니다.
- 애플리케이션이 실행될 때마다 임베딩을 하기 때문에 비용이 발생하기 때문에 적용하기 어렵다고 생각했습니다.
- 해결
- `FAQ -> Policy -> Chatlog` 순서로 정렬해서 컨텍스트를 정렬해서 제공하면 우선적으로 FAQ를 읽기 때문에 할루시네이션이 해소될 것이라 생각했습니다.
- 여전히 800만원으로 답변을 했습니다.
- Chatlog에 부정확한 데이터가 존재하기 때문에 이를 제외하고 FAQ와 Policy만 제공하면 할루시네이션을 개선할 수 있을 거 같았습니다.
- 80만원으로 답변을 했지만, 정확도가 `113개 -> 88개`로 감소했습니다.
- Chatlog를 포함하되, FAQ와 Policy의 내용 검증을 통해 일치한 경우에만 참고하도록 프롬프트를 수정했습니다.
- 80만원으로 답변을 했으며, 정확도가 `88개 -> 112개`로 증가했습니다.
- 학습
- 할루시네이션 개선을 위해 다양한 가설을 세우고 이를 검증하는 경험을 했습니다.
- 라벨링의 중요성을 알게 됐습니다.

## 2. 해결하지 못한 것

> 시도했지만 결국 해결 못한 문제가 있다면 적어주세요.

-

- 평가 기준과 할루시네이션 개선을 통해 프롬프트를 수정했지만, 간혈적으로 개선된 점이 적용되지 않아 오답처리가 되는 질문들이 있습니다.
- 이를 해결하기 위해 각 데이터 레이어에서 가져오는 데이터의 개수와 유사도를 조절하면서 검증을 진행했으나 개선 결과가 미미했습니다.

## 3. 정확도 측정 결과

> 테스트 질문 100개로 측정한 정확도를 기록해주세요.

| 난이도 | 정확도 | 비고 |
|--------|--------|------|
| easy | | |
| medium | | |
| hard | | |
| 난이도 | 정확도 | 비고 |
|--------|-------|-----|
| easy | 23/30 | 77% |
| medium | 20/26 | 77% |
| hard | 69/94 | 73% |


## 4. 왜 그런 결과가 나왔는지

> 정확도가 낮은 난이도의 질문을 몇 개 살펴보고, 왜 틀렸는지 분석해주세요.

-

- 핵심 사실을 전달했으나, 부가적인 정보를 전달하지 않아 오답처리가 된 질문들이 몇 개 있었습니다.
- 이는 검증 스크립트에서 호출하는 AI에게 제공하는 프롬프트의 문제라 생각됩니다.
- `저번에 물어본 배송 건 어떻게 됐어요?`와 같은 질문에 대해서 고객의 주문 번호를 요청하지 않고, 직접 확인하라는 답변을 하여 오답처리가 된 질문이 있습니다.
- 현재 구현한 챗봇은 과거의 기록 혹은 이전 질문에 대한 컨텍스트가 없기 때문에 답변이 어렵기 때문에, 이런 유형의 질문이 들어오면 과거의 이력을 조회하기 위한 정보를 요청할 수 있도록 프롬프트를 수정해야할 거 같습니다.

## 5. 개선하고 싶은 것

> 시간이 더 있었다면 시도해보고 싶은 개선점을 적어주세요.

-
- 현재는 프롬프트를 통해 Chatlog에서 노이즈를 우회적으로 제거하고 있지만, 프롬프트 이외의 방법으로 노이즈를 제거할 수 있는 방법이 있다면 적용하여 개선하고 싶습니다.
- 세 개의 난이도에 대해서 평균 80%의 정확도로 답변할 수 있도록 개선하고 싶습니다.
15 changes: 15 additions & 0 deletions src/main/java/com/cholog/bootcamp/config/ChatClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.cholog.bootcamp.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/cholog/bootcamp/config/VectorStoreConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.cholog.bootcamp.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.cholog.bootcamp.reader.ChatLogReader;
import com.cholog.bootcamp.reader.CurrentPolicyReader;
import com.cholog.bootcamp.reader.FaqReader;
import com.cholog.bootcamp.reader.InternalPolicyReader;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class VectorStoreConfig {

@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}

@Bean
public ApplicationRunner vectorStoreInitializer(
FaqReader faqReader,
CurrentPolicyReader currentPolicyReader,
InternalPolicyReader internalPolicyReader,
ChatLogReader chatLogReader,
VectorStore vectorStore
) {
return args -> {
List<Document> documents = new ArrayList<>();
documents.addAll(faqReader.read());
documents.addAll(currentPolicyReader.read());
documents.addAll(internalPolicyReader.read());
documents.addAll(chatLogReader.read());

vectorStore.add(documents);
log.info("Loaded {} documents into VectorStore", documents.size());
};
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/cholog/bootcamp/controller/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.cholog.bootcamp.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.cholog.bootcamp.dto.QuestionAskRequest;
import com.cholog.bootcamp.dto.QuestionAskResponse;
import com.cholog.bootcamp.service.ChatService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/chat")
public class ChatController {

private final ChatService chatService;

@PostMapping
public ResponseEntity<QuestionAskResponse> askQuestion(@RequestBody QuestionAskRequest request) {
QuestionAskResponse response = chatService.askQuestion(request);
return ResponseEntity.ok(response);
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/cholog/bootcamp/dto/QuestionAskRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.cholog.bootcamp.dto;

public record QuestionAskRequest(
String question
) {

}
27 changes: 27 additions & 0 deletions src/main/java/com/cholog/bootcamp/dto/QuestionAskResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.cholog.bootcamp.dto;

public record QuestionAskResponse(
String answer,
InnerTokenUsageResponse tokenUsage
) {
public record InnerTokenUsageResponse(
Integer promptTokens,
Integer completionTokens,
Integer totalTokens
) {
public static InnerTokenUsageResponse from(
Integer promptTokens, Integer completionTokens, Integer totalTokens
) {
return new InnerTokenUsageResponse(promptTokens, completionTokens, totalTokens);
}
}

public static QuestionAskResponse from(
String answer, Integer promptTokens, Integer completionTokens, Integer totalTokens
) {
return new QuestionAskResponse(
answer,
InnerTokenUsageResponse.from(promptTokens, completionTokens, totalTokens)
);
}
}
87 changes: 87 additions & 0 deletions src/main/java/com/cholog/bootcamp/reader/ChatLogReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.cholog.bootcamp.reader;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.springframework.ai.document.Document;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class ChatLogReader {

private static final Path DIRECTORY = Path.of("data/layer3_chatlogs");

private final ObjectMapper objectMapper = new ObjectMapper();

public List<Document> read() {
try (Stream<Path> paths = Files.list(DIRECTORY)) {
List<Document> documents = new ArrayList<>();
paths.forEach(path -> documents.addAll(readFile(path)));
return documents;
} catch (IOException e) {
throw new RuntimeException("채팅 로그 디렉토리를 읽는 중 오류가 발생했습니다.", e);
}
}

private List<Document> readFile(Path path) {
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
List<Document> documents = new ArrayList<>();
lines.forEach(line -> {
Document document = parse(path, line);
if (document != null) {
documents.add(document);
}
});
return documents;
} catch (IOException e) {
throw new IllegalArgumentException("");
}
}

private Document parse(Path path, String line) {
try {
JsonNode root = objectMapper.readTree(line);
if (!"correct".equals(root.path("agent_accuracy").asText())) {
return null;
}

return toDocument(path, root);
} catch (IOException e) {
throw new IllegalArgumentException("");
}
}

private Document toDocument(Path path, JsonNode root) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("layer", "layer3_chatlogs");
metadata.put("filepath", path.toString());
metadata.put("conversation_id", root.path("conversation_id").asText());
metadata.put("primary_intent", root.path("primary_intent").asText());
metadata.put("agent_accuracy", root.path("agent_accuracy").asText());

return new Document(turns(root.path("turns")), metadata);
}

private String turns(JsonNode turns) {
StringBuilder sb = new StringBuilder();

for (JsonNode turn : turns) {
sb.append(turn.path("role").asText())
.append(": ")
.append(turn.path("text").asText())
.append('\n');
}

return sb.toString().trim();
}
}
Loading