Skip to content

[고객지원 챗봇 만들기] 이영수 제출합니다#6

Open
youngsu5582 wants to merge 17 commits into
cho-log:mainfrom
youngsu5582:youngsu5582
Open

[고객지원 챗봇 만들기] 이영수 제출합니다#6
youngsu5582 wants to merge 17 commits into
cho-log:mainfrom
youngsu5582:youngsu5582

Conversation

@youngsu5582
Copy link
Copy Markdown
Contributor

챗봇 구현후 제출합니다!

정확도를 어렵지 않게 개선할 수 있을거라 생각했는데 생각보다 어렵네요..
코드 퀄리티나 객체지향적은 크게 고려하지 않고, 외적인 부분들에 관심이 가서 접근해봤습니다.

  • 토큰 출력량을 실제 $ 로 변환해서 계산
  • 테스트 방법 추가 및 병렬 실행 스크립트

질문

  • 평가의 기준을 LLM 이 LLM 응답을 다시 평가 하게 한 내용이 궁금합니다!

평소에, 아직까지 LLM 은 80% 정도의 퀄리티를 응답해준다고 생각합니다.
80%의 응답을 80%로 평가해주면 60% 의 퀄리티에 기대를 할 거 같은데 제가 RAG 나 더 정교한 방법을 몰라서 그런건지 궁금합니다.

  • 해당 챗봇의 기대

제가 생각한 해당 챗봇에게 기대한 것은 상담원의 업무 완화였습니다.
그러기 위해선, 숫자에 대해선 틀리지 않는 정확한 응답 + 거짓 응답 을 하지 않는걸 생각했습니다.

코치님들이 생각하는 챗봇의 목적이나 기대치가 궁금합니다!


오랜만에 미션 하는거 같아서 재밌었습니다🙇‍♂️

- DTO 및 Controller 선언
- 서비스 로직은 TODO
- 개발 편의성 위한 lombok 추가
- local profile 용 gitignore 추가
- spring ai 통한 기능 구현
- litellm github 기반 비용 JSON 로드해서 사용
- spring ai 표준 기반 처리(캐시 비용 처리X)
- 시스템 프롬프트 + layer1, layer2 데이터 주입
- 진행 결과 progress md 로 정리
- 정확도 다소 상승(Easy 는 일정치 도달)
- SimpleVectorStore 통한 로컬 세팅
- chunk 400 + temperature 0.2 로 설정
- 도메인 숫자 및 정책에 대해 검증하는 스크립트 추가
- 이를 통한 관련 내용 progress.md 에 작성
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 introduces a Retrieval-Augmented Generation (RAG) system for a FAQ chatbot, including vector store configuration, pricing calculation logic, and evaluation scripts. Key changes include the addition of VectorStoreConfig for document indexing, FrequentlyQuestionChatApiService for handling RAG-based queries, and a new strict_evaluate.py script for deterministic evaluation using regex patterns. Feedback focuses on several implementation issues: loaded knowledge base resources are currently missing from the system prompt in ChatClientConfig, an unnecessary argument is passed to the prompt() method in the chat service, and excessive logging of document chunks during startup should be reduced. Additionally, the evaluation script's default server URL port should be updated to match the standard application port.

Comment on lines +44 to +46
var faq = concatResources(faqResources, "faq");
var policies = concatResources(policyResources, "policies");
var examples = concatResources(exampleResources, "examples");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

faq, policies, examples 변수들을 통해 지식 베이스 리소스를 로드하고 있지만, 실제 ChatClient 빈을 생성할 때는 사용되지 않고 있습니다. Javadoc(17-22행)에서는 이 내용들을 시스템 프롬프트에 임베드한다고 설명하고 있으나, 현재 코드에서는 systemTemplate만 설정되어 있습니다. 이로 인해 RAG를 사용하지 않는 chat 메서드 등에서 정확한 답변을 제공하기 어려울 수 있습니다. 시스템 프롬프트 템플릿에 해당 변수들을 포함하도록 수정이 필요합니다.

.map(d -> "## " + d.getMetadata().get("source") + "\n" + d.getText())
.collect(Collectors.joining("\n\n---\n\n"));

var response = chatClient.prompt(context)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

chatClient.prompt(context) 호출 시 전달되는 context 인자는 불필요합니다. 바로 다음 라인에서 .user(...)를 통해 사용자 메시지 템플릿을 새로 정의하고 있으며, Spring AI의 ChatClient는 마지막에 설정된 사용자 메시지를 사용하기 때문입니다. prompt()를 인자 없이 호출하여 의도를 명확히 하는 것이 좋습니다.

Suggested change
var response = chatClient.prompt(context)
var response = chatClient.prompt()

Comment on lines +45 to +50
chunks.forEach(chunk -> {
if (chunk.getText() != null) {
log.info("chunk ID: {}, TEXT: {}", chunk.getId(),
chunk.getText().substring(0, Math.min(80, chunk.getText().length())).replace("\n", " "));
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

애플리케이션 시작 시 모든 청크의 내용을 info 레벨 로그로 출력하고 있습니다. 문서량이 많아질 경우 로그 양이 방대해져 시작 속도와 가독성에 영향을 줄 수 있습니다. 확인용 로그라면 debug 레벨로 변경하거나 제거하는 것을 권장합니다.

Comment thread data/strict_evaluate.py
import requests

DATA_DIR = Path(__file__).parent
SERVER_URL = "http://localhost:11240/api/chat"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

서버 URL의 포트 번호가 11240으로 설정되어 있습니다. 프로젝트의 기본 설정인 8080과 일치하지 않아, 다른 환경에서 스크립트를 실행할 때 연결 오류가 발생할 수 있습니다. 범용성을 위해 기본 포트인 8080으로 수정하는 것이 좋습니다.

Suggested change
SERVER_URL = "http://localhost:11240/api/chat"
SERVER_URL = "http://localhost:8080/api/chat"

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.

고생 많으셨습니다 : )

var systemTemplate = loadSystemTemplate(systemTemplateResource);
var faq = concatResources(faqResources, "faq");
var policies = concatResources(policyResources, "policies");
var examples = concatResources(exampleResources, "examples");
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.

faq, policies, examples 변수를 만들지만 defaultSystem(spec -> spec.text(systemTemplate)) 에는 사용되지 않고 있네요.
layer1/2/3 파일을 읽는 IO 비용이 부팅마다 발생하는데, 어떤 작업을 하다 놓치셨을까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

밑에 내용과 동일합니다!
테스트 해보면서 흔적으로 남겼는데 다 정리하는게 맞을거 같아요!

public FrequentlyQuestionChatResponseDto chat(FrequentlyQuestionChatRequestDto requestDto) {
var prompt = Prompt.builder()
.content(requestDto.question())
.build();
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.

chat()은 Controller에서 호출되지 않고 chatWithRag() 만 사용되고 있어요.

학습 과정에서 "RAG 없는 버전과 있는 버전"을 비교해본 흔적이라면 wall-report.md에 기록하고 코드는 한 흐름만 남기는 게 오해가 적어질 것 같아요!

.param("context", context)
.param("question", question))
.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.

response == null"응답이 없습니다"를 반환하고 있는데, 이게 stateless 단발 API에서는 동작하지만 아래와 같은 문제가 있습니다.

  • LLM 호출이 실제로 실패한 이유(rate limit / timeout / quota)가 어디에도 기록 안 됨
  • 사용자에게 가는 메시지는 한 줄 — 사용자가 이걸 받으면 어떻게 행동해야 할지 알 수 없음
  • multi-turn으로 발전했을 때 LLM은 자신의 직전 호출이 실패했다는 사실을 모름 → 같은 실수 반복

핵심은 실패를 LLM이 다음 결정에 활용할 수 있는 형태로 만들기입니다. 예를 들어 "검색 서버 일시 장애. 키워드 검색으로 폴백 가능" 같은 힌트를 다음 호출 컨텍스트에 넣으면 LLM이 자동으로 대안을 시도할 수 있어요.

로깅·alert·재시도 정책을 어떻게 둘지 한 번 고민해보면 좋겠습니다!

.query(question)
.topK(8)
.build());
log.info("hits 결과: {}", hits.stream().map(Document::getId).toList());
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.

PR 본문의 "숫자 정확성" 목표와 직접 닿는 부분이라 남겨둘게요.

topK(8) 단일값으로 layer 무관하게 가져오고 있는데, 결과 8개 안에 layer1_faq만 8개가 들어오고 policy가 0개일 가능성이 있습니다 (vector similarity 운).

"숫자에 대해 틀리지 않는다" 라는 목표는 결국 정책 문서가 답변 근거로 반드시 들어가야 한다 는 뜻인데, 현재 코드는 그것을 보장하지 못해요.

대안 예시)

docs.addAll(search(question, 4, "layer == 'faq'"));
docs.addAll(search(question, 3, "layer == 'policy'"));
docs.addAll(search(question, 1, "layer == 'example'"));

그러면 정책이 항상 최소 3개는 컨텍스트에 들어옵니다. 답변 품질 디버깅은 검색 알고리즘을 의심하기 전에 어떤 청크가 어떤 비율로 들어가는지 부터 보는 게 출발점이에요.

eval 점수가 medium에서 떨어졌다면 여기를 한 번 의심해봐도 좋겠습니다~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

topK 랑 filterExpression 을 통해 요소의 원하는 개수를 지정 가능하군영 😮

@@ -0,0 +1,24 @@
당신은 Cholog Corporation의 FAQ 챗봇입니다.
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.

외부 파일로 분리하신 부분 너무 좋네요 👍

다음 단계로 한 칸 더 가본다면:

  • 버전 관리faq-system-v1.st, faq-system-v2-strict.st 처럼 두고 application.yml로 어떤 버전을 쓸지 외부 주입하면 A/B 비교가 자연스러워집니다.
  • LLM 직전 로깅 — 이 .st 파일 외에 Spring AI Advisor가 자동으로 더 prepend하는지 확인해보세요. 예를 들어 QuestionAnswerAdvisor를 붙이면 Context information is below... Given the context and provided history information and not prior knowledge, reply to the user comment 같은 영어 지시문이 자동으로 추가됩니다. 본인이 작성한 한국어 prompt와 충돌하면 답변 톤이 흔들릴 수 있어요.
  • 회귀 테스트strict_evaluate.py 가 이 역할을 일부 하는데, prompt 변경 PR마다 자동 실행되는 형태(예: CI)는 아니에요. 이 부분도 한 번 고민해보면 좋겠습니다!



TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(400)
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.

TokenTextSplitter(chunkSize=400, minChunkSizeChars=200) — 토큰 단위 균일 청킹이네요.

markdown heading (## / ###) 기준으로 청킹하는 것도 고민해 보셨을까요?,

  • 토큰 균일 청킹: 토큰 비용 예측 가능, 문서 구조 무시 (한 정책 항목이 두 청크로 갈리거나, 두 정책 항목이 한 청크에 섞임)
  • 헤더 기준 청킹: 문서 의미 단위 보존, 청크 크기 불균일

FAQ/Policy처럼 섹션 단위 의미가 분명한 문서에서는 헤더 기준이 보통 더 잘 동작합니다 — 한 청크 = 한 정책 항목 = 한 답변 근거.

eval 결과로 두 방식 비교해본 흔적이 있다면 wall-report에 한 줄 기록해두시면 회고 자료로 좋을 것 같아요~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

헤더 기준으로도 청킹이 가능한지 몰랐네요!
이게 더 유의미한 방법일거 같습니다!

@@ -0,0 +1,77 @@
package com.cholog.bootcamp.service;
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.

토큰 비용을 코드 안에서 추적하는 부분 너무 좋네요 👍 운영 관점에서 production 챗봇이 반드시 해야 하는 일이에요.

한 예: LLM이 결과 0건인 검색을 반복하다 보면 시간당 호출이 1,800회 = 약 $3.6까지 갈 수 있습니다 (gpt-4.1-nano 기준). LLM 시점에선 매번 "새로운 검색 시도"라 진전이 있어 보이지만 실제론 같은 빈 결과의 반복이에요. 이런 self-destruction을 잡을 수 있는 자리가 바로 여기입니다.

다음 단계로 생각해볼 만한 것:

  • conversation 당 누적 비용
  • 일/주 단위 alert (예: 일 $5 초과 시 슬랙 알림)
  • 모델별 비용 비교를 위한 A/B (gpt-4.1-nano vs gpt-4o-mini)

참고로 40,000줄짜리 model_prices.json을 직접 가져온 부분은 다소 무거워서, 외부 API/dependency로 빼는 게 더 깔끔할 수 있을 것 같아요~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

해당 부분은 현재 사내 코드에서도, 고민 중인 부분이라 가볍게 적용해봤습니다!

생각한건, DB 에
Provider + 모델 + 호출 함수 + token 정보 + prompt (필요하다면 저장, 개인정보는 고려)

등을 통해, 요금 및 요청 기록을 관리할 거 같아여?
langfuse 라는 관측용 프레임워크도 확인해볼거 같습니다 🫡

Comment thread progress.md
Comment on lines +172 to +175
RAG 사용하지 않고, 그냥 다 집어넣은 버전
두개 큰 차이가 없는거 같다. 쩝...

=> 2주내 POC 를 내야하므로, 이정도로 마무리한다. No newline at end of file
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.

차이가 없는 원인이 뭐라고 생각하시나요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

RAG 청킹 단위나, 검색할 때 세세하게 컨트롤을 못해서 결과가 안나왔다고 생각했는데

막상, 넣어도 88.4% 에서 안 달라지네요...
평가지(strict_evaluate) 자체가 깊게 생각한게 아니고, claude 를 기반으로 만든거라

  • 평가가 적절한지
  • 질문이 올바른지

를 좀 더 확인해볼거 같아요.

근데, 이와 별개로 비결정적인 특성상 100% 가 나올까? 에 대해서는 아직도 확신을 못 내리겠습니다.

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