Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2735626
chore: env 파일 import
kjyyjk May 8, 2026
f75ad57
feat: 유저 챗 엔드포인트 및 ai 호출 구현
kjyyjk May 8, 2026
f6fa3c4
feat: llm에게 필요한 데이터 제공
kjyyjk May 9, 2026
d250287
refactor: builder에서 프롬프트 설정
kjyyjk May 9, 2026
4ecdb13
chore: advisors-vector-store 의존성 추가
kjyyjk May 9, 2026
4ac5a9a
refactor: 벡터 db 및 어드바이저 활용
kjyyjk May 9, 2026
46b20a7
chore: markdown-document-reader 의존성 추가
kjyyjk May 9, 2026
62b7aad
docs: resources/data에 layer_n 복사
kjyyjk May 9, 2026
28b3413
feat: md 파일 벡터 저장소에 저장
kjyyjk May 9, 2026
8759009
feat: 응답 content type json으로 변경
kjyyjk May 10, 2026
8ec9bac
refactor: RequestBody dto로 매핑
kjyyjk May 10, 2026
d2901a6
refactor: 테스트 incorrect 이유 글자수 2배 증가
kjyyjk May 10, 2026
64ee947
refactor: topk 2배 증가
kjyyjk May 10, 2026
aea6e10
feat: 디버깅용 엔드포인트 추가
kjyyjk May 11, 2026
e3fe990
docs: deprecated 문서 vector db에서 제거
kjyyjk May 11, 2026
071ec9b
refactor: advisor 커스텀 템플릿 설정
kjyyjk May 13, 2026
6eb21ee
refactor: 프롬프트 내 할루시네이션 방지 내용 추가
kjyyjk May 13, 2026
838f408
feat: 챗봇 응답에 token 정보 포함
kjyyjk May 15, 2026
162cb3c
feat: 디버깅 엔드포인트 응답에 token 정보 포함
kjyyjk May 15, 2026
ebaab45
refactor: controller 패키지 분리
kjyyjk May 15, 2026
ef3815c
refactor: service 분리
kjyyjk May 15, 2026
f8f89e7
feat: 대화 맥락 유지를 위한 chatmemory 추가
kjyyjk May 16, 2026
deb627d
feat: 챗봇 세션 시작/종료 엔드포인트 추가
kjyyjk May 16, 2026
2c6e041
feat: 챗봇 세션 시작/종료 엔드포인트 변경
kjyyjk May 16, 2026
9383c9a
refactor: config 설정 분리
kjyyjk May 16, 2026
3700df4
feat: 챗봇 ui 구현
kjyyjk May 16, 2026
53caf47
feat: 챗봇 ui 개선
kjyyjk May 16, 2026
71121c4
refactor: vip 연락수단 제거
kjyyjk May 16, 2026
5106f1d
refactor: 챗로그 청크 제거
kjyyjk May 17, 2026
ea159ca
chore: ai rag 의존성 추가
kjyyjk May 17, 2026
918edd4
refactor: qa 어드바이저 미사용 및 쿼리 확장 추가
kjyyjk May 18, 2026
d35b780
refactor: 미사용 코드 제거(디버깅 엔드포인트)
kjyyjk May 18, 2026
0d96e85
feat: 리랭크 구현
kjyyjk May 18, 2026
9f1ec40
refactor: 리랭크 수정 및 디버깅
kjyyjk May 18, 2026
f287a6d
refactor: 검색되 청크에 해당하는 문서 전체를 컨텍스트에 포함
kjyyjk May 19, 2026
24f09c9
refactor: 메모리 어드바이저 임시 제거
kjyyjk May 19, 2026
5f0b94c
refactor: 미사용 코드 제거
kjyyjk May 20, 2026
c5bb941
refactor: 주석 추가
kjyyjk May 20, 2026
165f724
docs: 벽 리포트 작성
kjyyjk May 20, 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
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ dependencyManagement {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
implementation 'org.springframework.ai:spring-ai-markdown-document-reader'
implementation 'org.springframework.ai:spring-ai-rag'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Expand Down
2 changes: 1 addition & 1 deletion data/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def main():
if args.verbose:
print(f"[{qid}] {marker} ({tier}) {question_ko[:40]}...")
if score == 0:
print(f" 이유: {judgment.get('reason', '')[:80]}")
print(f" 이유: {judgment.get('reason', '')[:160]}")

# 진행률 (10개마다)
if not args.verbose and (i + 1) % 10 == 0:
Expand Down
41 changes: 28 additions & 13 deletions mission/wall-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,51 @@

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

-

- 초반 조회 결과에 질문 관련 내용이 없길래 topK 값을 2배~4배까지 늘려 해당 질문에 대한 답변을 잘할 수 있도록 개선했습니다.
하지만 그 외 다른 질문에 대한 정확도는 확연히 떨어지는 것을 발견했습니다. 컨텍스트에 많은 내용이 들어가 오히려 혼란을 준 것 같습니다.
현재 구현 기준 기본값인 topK=4가 가장 최적으로 보여 롤백했습니다.
- 네이버페이에 대해 물어봤는데 카카오페이에 대한 답변을 하는 경우가 있었습니다.
알고보니 ChatMemory 의해 이전 질문이 컨텍스트에 포함되며 이전 질문을 현재 질문이라고 착각해서 생긴 문제였습니다.
ChatMemory를 제거하고 대화 내역이 보존되지 않게 했지만 추후 해결해야할 문제입니다.

## 2. 해결하지 못한 것

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

-

- 컨텍스트에 질문에 대한 정답이 포함되어 있는데 틀린 답변을 내놓습니다. 관련 케이스를 살펴보니 정답 내용이 첫번째에 오지 않을 경우 주로 발생했습니다.
해결하기 위해 리랭크 개념을 학습하고 llm에게 청크 재정렬을 맡겨봤는데 이 또한 제가 의도한대로 재정렬되지 않았습니다.
- 컨텍스트에 상반된 내용이 있을 때에 따라 다른 대답을 하는 문제를 해결하지 못했습니다. 예를 들어 `VIP는 냉장 배송도 무료배송인가요?`를 물었을 때
`vip는 모두 무료다`와 `냉장 배송은 등급 상관없이 4000원 고정이다`라는 내용이 섞여 `vip는 냉장 배송도 무료다` 라는 잘못된 답변을 내놓습니다.
프롬프트에 내용이 상반될 경우 더 상세한 것, 세부적인 것을 기준으로 하라고 명시했지만 상세하고 세부적인 것의 기준 조차 애매해 잘동작하지 않습니다.
- 명확하지 않은 단어를 잘 이해하지 못하는 경우가 많습니다. 예를 들어 `비구독 상품`이라고 하면 비구독 사용자가 상품을 구매하는 경우라고 잘못 이해해 이해해
틀린 답변을 내뱉습니다.

## 3. 정확도 측정 결과

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

| 난이도 | 정확도 | 비고 |
|--------|--------|------|
| easy | | |
| medium | | |
| hard | | |
> 테스트 질문 150개로 측정한 정확도를 기록해주세요.

| 난이도 | 정확도 | 비고 |
|--------|-------|----|
| easy | 14/30 | |
| medium | 27/94 | |
| hard | 6/26 | |

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

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

-


## 5. 개선하고 싶은 것

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

-
- 답변의 말투를 개선하고 싶습니다. 현재는 같은 내용을 반복하거나, 사용자가 응?할만한 말투를 구사합니다.
또한 내부 컨텍스트 문서의 존재를 답변에 포함하기도 합니다.
- 대화 맥락을 보존해 더욱 고객 응대에 능한 챗봇을 만들고 싶습니다.
- 인메모리 외 인프라를 구축해 정말 실무의 챗봇과 유사하게 만들어보고 싶습니다.
- 평가 방식을 개선해 모델의 성능을 제대로 측정하고 반복 개선해나가보고 싶습니다. 현재 평가는 이걸 왜 틀렸다고 하는거지?싶은 부분이 많습니다.
챗봇의 목적마다 평가 방식이 다르니 제가 만들고자 하는 챗봇을 명확히 정의하고 그에 맞는 평가 방식을 새로 만들어보고 싶습니다.
- 챗 로그, 이력을 관리하고 활용해보고 싶습니다. 주어진 데이터셋의 챗 로그는 컨텍스트에 넣지 않고 가장 많이 묻는 질문과 같은 운영 통계 목적으로 활용했습니다.
다른 방향으로 활용할 수 있다면 참고해 챗 로그까지 활용해 데이터 축적에 따른 성능 향상을 이루고 싶습니다.
- 구버전 문서도 마찬가지로 필요 없다고 느껴 제거했는데 필요하다면 포함해 응답 수준을 높여보고 싶습니다.
16 changes: 16 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,16 @@
package com.cholog.bootcamp.config;

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 VectorStoreConfig {

@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.cholog.bootcamp.controller;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ChatPageController {

@GetMapping({"/", "/chat"})
public String chat(Model model) {
model.addAttribute("pageTitle", "초록 고객지원 챗봇");
model.addAttribute("initialMessage", """
안녕하세요. 초록 고객지원 챗봇입니다.🤖
무엇을 도와드릴까요?

📞 운영시간 안내
자동 챗봇은 24시간 이용하실 수 있습니다.
상담사 연결 및 전화 상담은 평일 오전 9시부터 오후 6시까지 가능하며, 주말 및 공휴일에는 운영되지 않습니다.
운영 시간 외 문의는 챗봇을 이용하시거나 이메일로 남겨주시면 다음 영업일부터 순차적으로 확인해 드리겠습니다.

☎️ 문의 전화: 1588-0000
📧 이메일: support@cholog.kr
💬 카카오톡: @초록
""");
model.addAttribute("quickPrompts", List.of(
"배송은 보통 얼마나 걸리나요?",
"반품 신청 기준을 알려주세요.",
"멤버십 등급 혜택이 궁금해요."
));
return "chat";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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.ChatbotRequest;
import com.cholog.bootcamp.dto.ChatbotResponse;
import com.cholog.bootcamp.service.ChatbotService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

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

private final ChatbotService chatbotService;

@PostMapping
public ResponseEntity<ChatbotResponse> chat(
@RequestBody ChatbotRequest request
) {
ChatbotResponse response = chatbotService.chat(request);
return ResponseEntity.ok().body(response);
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/cholog/bootcamp/dto/ChatbotRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.cholog.bootcamp.dto;

public record ChatbotRequest(
String question
) {
}
27 changes: 27 additions & 0 deletions src/main/java/com/cholog/bootcamp/dto/ChatbotResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.cholog.bootcamp.dto;

import org.springframework.ai.chat.metadata.Usage;

public record ChatbotResponse(
String answer,
TokenUsageInfo tokenUsage
) {

public static ChatbotResponse from(String answer, Usage usage) {
return new ChatbotResponse(
answer,
new TokenUsageInfo(
usage.getPromptTokens(),
usage.getCompletionTokens(),
usage.getTotalTokens()
)
);
}

private record TokenUsageInfo(
int promptTokens,
int completionTokens,
int totalTokens
) {
}
}
106 changes: 106 additions & 0 deletions src/main/java/com/cholog/bootcamp/service/ChatbotService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.cholog.bootcamp.service;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

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.reader.TextReader;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Service;

import com.cholog.bootcamp.dto.ChatbotRequest;
import com.cholog.bootcamp.dto.ChatbotResponse;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class ChatbotService {

private final ChatClient chatClient;
private final VectorStore vectorStore;
private final ResourcePatternResolver resolver;

public ChatbotService(
VectorStore vectorStore,
MarkdownReader markdownReader,
ChatClient.Builder chatClientBuilder
) {
this.chatClient = chatClientBuilder.build();
this.vectorStore = vectorStore;
vectorStore.add(markdownReader.loadAll());
this.resolver = new PathMatchingResourcePatternResolver();
}

public ChatbotResponse chat(ChatbotRequest request) {
// 검색
SearchRequest searchRequest = getSearchRequest(request.question(), 4);
List<Document> documents = vectorStore.similaritySearch(searchRequest);

// 증강 & 생성
documents = getFullDocuments(documents);
String context = getContext(documents);
ChatResponse chatResponse = chatClient.prompt()
.system("""
당신은 초록 고객센터의 챗봇입니다.
주어진 [컨텍스트]를 기반으로 [사용자 질문]에 답변해주세요.

답변 규칙
- 제공된 컨텍스트를 기반으로만 답변하세요. 절대 일반 상식으로 추론하지 마세요.
- 만약 주어진 컨텍스트로 답변할 수 없다면 모르겠다고 안내하세요.
- 내용이 충돌하는 경우 다음 우선순위를 따라 답변 합니다.
- 질문 도메인과 가장 근접한 내용
- 더 구체적인 상황을 다루는 내용
- 더 최신 버전의 내용
""")
.user("""
[사용자 질문]
%s

[컨텍스트]
%s
""".formatted(request.question(), context))
.call()
.chatResponse();

String answer = chatResponse.getResult().getOutput().getText();
Usage usage = chatResponse.getMetadata().getUsage();
return ChatbotResponse.from(answer, usage);
}

private List<Document> getFullDocuments(List<Document> documents) {
return documents.stream()
.map(document -> document.getMetadata().get("filename").toString())
.distinct()
.map(filename -> {
try {
return resolver.getResources("classpath:data/**/" + filename)[0];
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.map(TextReader::new)
.flatMap(reader -> reader.get().stream())
.toList();
}

private static String getContext(List<Document> documents) {
return documents.stream()
.map(Document::getText)
.collect(Collectors.joining("\n\n"));
}

private SearchRequest getSearchRequest(String query, int k) {
return SearchRequest.builder()
.query(query)
.topK(k)
.build();
}
}
48 changes: 48 additions & 0 deletions src/main/java/com/cholog/bootcamp/service/MarkdownReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.cholog.bootcamp.service;

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

import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class MarkdownReader {

private final Resource[] resources;

public MarkdownReader(@Value("classpath:data/**/*.md") Resource[] resources) {
this.resources = resources;
}

public List<Document> loadAll() {
List<Document> allDocuments = new ArrayList<>();
for (Resource resource : resources) {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", resource.getFilename())
.build();

MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
allDocuments.addAll(reader.get());
}

allDocuments.forEach(doc -> log.info(
"filename={}, title={}, text={}",
doc.getMetadata().get("filename"),
doc.getMetadata().get("title"),
doc.getText()
));

return allDocuments;
}
}
2 changes: 2 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
spring:
config:
import: optional:file:.env[.properties] # 루트의 .env 파일이 존재하면 .properties 형식으로 읽어 임포트
application:
name: spring-ai-bootcamp-basic
ai:
Expand Down
Loading