Skip to content

[고객지원 챗봇 만들기] 김준영 제출합니다.#2

Draft
kjyyjk wants to merge 39 commits into
cho-log:mainfrom
kjyyjk:main
Draft

[고객지원 챗봇 만들기] 김준영 제출합니다.#2
kjyyjk wants to merge 39 commits into
cho-log:mainfrom
kjyyjk:main

Conversation

@kjyyjk
Copy link
Copy Markdown

@kjyyjk kjyyjk commented May 13, 2026

진행도 공유 차원에서 pr 미리 만들어두었습니다!

챗봇 사용

애플리케이션 실행 후 챗봇 url로 접속합니다.


image

kjyyjk added 15 commits May 8, 2026 20:34
- 우선 vip 베송 기준에 대한 질문에 답할 수 있게끔만 구현하여 구조를 잡는다.
- QuestionAnswerAdvisor 사용을 위한 의존성 추가
- spring ai가 OpenAiEmbeddingModel을 스프링 빈으로 관리한다
- md 파일을 Document로 변환하기 위한 라이브러리
- MarkdownReader에서 classpath로 쉽게 읽어오기 위해
- MarkdownReader로 md를 변환한 Document를 벡터 저장소에 저장.

- 애플리케이션 실행하면 빈 생성 과정에서 SimpleVectorStore가 각 document에 대해
EmbeddingModel 호출하는 것 확인 가능
- 기존에는 string 반환해 plain/text
- 컨텍스트에 포함할 문서 개수를 늘려 핵심 사실을 포함할 수 있도록 수정
- 참고한 k개 문서 정보 로그로 확인 가능
- 8개(topK) 문서 중 deprecated 문서가 섞여들어가 컨텍스트가 오염되고,
이로 인해 불필요한 정보 포함 및 정작 중요한 정보는 활용 못하는 문제 해결
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a Spring AI-based chatbot system using Retrieval-Augmented Generation (RAG). Key additions include a ChatbotController that utilizes a QuestionAnswerAdvisor and SimpleVectorStore, a MarkdownReader for ingesting FAQ and policy documents from the classpath, and a comprehensive knowledge base consisting of various markdown and chat log files. Feedback was provided regarding the /debug endpoint in ChatbotController, which currently returns a hardcoded string instead of the actual AI-generated content.

Comment thread src/main/java/com/cholog/bootcamp/ChatbotController.java Outdated
Copy link
Copy Markdown
Contributor

@jaeyeonling jaeyeonling left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생 많으셨습니다 : )

.map(filename -> {
try {
return resolver.getResources("classpath:data/**/" + filename)[0];
} catch (IOException e) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분이 핵심인 것 같아서 자세히 남길게요.

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();

흐름:

  1. similaritySearch가 top-4 청크 를 반환
  2. 청크에서 filename 추출 → distinct
  3. 해당 파일을 classpath에서 다시 통째로 읽음
  4. 파일 전체를 LLM 컨텍스트에 보냄

즉 검색은 "어떤 파일이 관련 있는가" 까지만 알려주고, LLM 입력은 4개 파일 전체가 됩니다.

문제:

  • 청킹 효과 무효화 — splitter로 작은 청크를 만든 의미가 사라짐
  • 토큰 폭발return-policy-v3.md 60줄 × 4파일 = 200+줄을 매번 LLM에 보냄
  • 위치 효과 손실 — LLM은 입력의 시작과 끝을 더 강하게 참조하는데 (중간이 묻힘), 정책 본문 절반이 중간에 묻힐 가능성
  • 비용 — input token 청구 폭증

에 대해 고민해보면 좋겠습니다!

VectorStore vectorStore,
MarkdownReader markdownReader,
ChatClient.Builder chatClientBuilder
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public ChatbotService(...) {
    ...
    vectorStore.add(markdownReader.loadAll());
    ...
}

생성자에서 임베딩 호출 부수효과가 있어요. 트레이드오프:

  • 부팅 지연 — 첫 ChatbotService Bean 생성 시점에 모든 임베딩 호출
  • 테스트 어려움new ChatbotService(...) 가 매번 임베딩 호출 → OpenAI quota 소모
  • 부팅 실패 가능성 — 임베딩 API 일시 장애 시 서버가 못 뜸
  • 책임 분리 약함답변 생성임베딩 초기화 두 책임

분리 방향)

  • (a) VectorStoreInitializer implements ApplicationRunner 로 별도 컴포넌트
  • (b) @Bean ApplicationRunner 로 Config에서 분리

"생성과 초기화를 같은 자리에 두지 마라" 라는 원칙, 한 번 고민해보면 좋겠습니다~

} catch (IOException e) {
throw new RuntimeException(e);
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정보 없는 throw 안티패턴이 보이네요.

throw new RuntimeException(e);

운영 시점에 이 코드가 발화하면 아래와 같은 문제가 있습니다.

  • 어떤 filename이 실패했는지 메시지에 없음 → 디버깅 위해 stack trace + 코드 추적 필요
  • 사용자에게는 500 에러만 가고 왜 실패했는지 알 길 없음

최소한 아래와 같이 정보를 담아주면 좋겠습니다.

throw new IllegalStateException("문서 로드 실패: " + filename, e);

에러 메시지도 의미 있는 정보 여야 한다는 원칙, 한 번 참고해주세요~

%s
""".formatted(request.question(), context))
.call()
.chatResponse();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

system prompt 너무 잘 짜셨네요 👍 특히 아래 부분이 인상적이에요.

- 내용이 충돌하는 경우 다음 우선순위를 따라 답변 합니다.
    - 질문 도메인과 가장 근접한 내용
    - 더 구체적인 상황을 다루는 내용
    - 더 최신 버전의 내용

이 규칙은 코드 가 아니라 프롬프트 에 표현한 점이 좋습니다 — 정책 결정을 LLM과 공유하는 자연스러운 자리예요.

다만 위치가 인라인 String이라:

  • 운영자가 우선순위를 수정하려면 컴파일·빌드·배포
  • 우선순위 v1 vs v2 비교가 자연스럽지 않음
  • diff에서 "왜 바꿨는지" 안 보임

외부 .st 분리하는 구조로 개선해보면 어떨까요?

@Value("classpath:prompts/faq-system.st")
private Resource systemPrompt;


@GetMapping({"/", "/chat"})
public String chat(Model model) {
model.addAttribute("pageTitle", "초록 고객지원 챗봇");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

웹 UI를 직접 구현하신 부분 너무 좋네요 👍

다만 initialMessage / quickPrompts 가 컨트롤러에 인라인 String이라, 아래와 같은 확장 시 복붙이 발생할 수 있어요.

  • 다른 채널(예: 슬랙 봇)에서 같은 "환영 메시지"를 쓰려면 코드 복붙
  • 이메일에서 "운영시간 안내" 를 같이 보내려면 또 복붙
  • i18n (영어 페이지) 추가 시 또 복붙

챗봇 본체를 채널-무관 인터페이스로 두고, 채널별 어댑터가 각자의 환영 메시지를 가져가는 형태로 한 번 그려보면 어떨까요? 예시)

interface ChatChannel {
    String welcomeMessage();
    List<String> quickPrompts();
    void send(String userId, String message);
}

SearchRequest searchRequest = getSearchRequest(request.question(), 4);
List<Document> documents = vectorStore.similaritySearch(searchRequest);

// 증강 & 생성
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String answer = chatResponse.getResult().getOutput().getText();
Usage usage = chatResponse.getMetadata().getUsage();

아래 케이스에서 NPE → 사용자에게 500이 갈 가능성이 있어요.

  • chatResponse 가 null인 경우 (rate limit, network error)
  • getResult() 가 null인 경우
  • getUsage() 가 null인 경우 (provider별 가능)

방어적 코드 추가도 좋고, 더 나아가 "LLM 호출 자체가 실패했을 때 어떤 응답을 사용자에게 보낼지" 를 명시적으로 분기하는 것도 한 번 고민해보면 좋겠습니다. "일시적 장애로 답변이 어렵습니다" 같은 명시적 응답이 운영자 alert 잡는 데도 도움이 될 수 있어요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants