Skip to content

[고객지원 챗봇 만들기] 신관규 제출합니다.#5

Open
Soundbar91 wants to merge 45 commits into
cho-log:mainfrom
Soundbar91:soundbar91
Open

[고객지원 챗봇 만들기] 신관규 제출합니다.#5
Soundbar91 wants to merge 45 commits into
cho-log:mainfrom
Soundbar91:soundbar91

Conversation

@Soundbar91
Copy link
Copy Markdown
Contributor

@Soundbar91 Soundbar91 commented May 21, 2026

구현 내용

챗봇 플로우는 다음과 같습니다.

  • FaqReader, CurrentPolicyReader, InternalPolicy, ChatlogReader 클래스에서 대응되는 데이터 레이어를 읽어 Document로 변환합니다.
  • 이후 VectorStore에 Document를 적재합니다.
  • 사용자 질의가 들어오면 각 데이터 레이어에 대해서 유사도 검색을 통해 Document를 조회합니다.
  • 이후 조회된 Document를 사용자 질의와 함께 OpenAI에 전송합니다.

학습 내용

학습 내용은 아래 노션 페이지와 wall-report.md에 정리했습니다.

Soundbar91 added 30 commits May 9, 2026 13:00
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 RAG-based chatbot system using Spring AI, featuring custom document readers for FAQ, policies, and chat logs, a vector store for context retrieval, and a chat service for processing queries. It also includes an evaluation framework and documentation on performance optimizations. Review feedback focuses on improving error handling in document readers by providing descriptive exception messages and adding null safety checks when processing AI model responses to prevent potential runtime errors.

Comment thread src/main/java/com/cholog/bootcamp/reader/ChatLogReader.java Outdated
Comment thread src/main/java/com/cholog/bootcamp/reader/CurrentPolicyReader.java Outdated
Comment thread src/main/java/com/cholog/bootcamp/reader/FaqReader.java Outdated
Comment thread src/main/java/com/cholog/bootcamp/reader/InternalPolicyReader.java Outdated
Comment on lines +53 to +60
Usage usage = chatResponse.getMetadata().getUsage();

return QuestionAskResponse.from(
chatResponse.getResult().getOutput().getText(),
usage.getPromptTokens(),
usage.getCompletionTokens(),
usage.getTotalTokens()
);
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

chatResponse.getMetadata().getUsage()chatResponse.getResult()null을 반환할 가능성이 있습니다. 특히 모델 응답이 차단되거나 특정 오류 상황에서 메타데이터가 누락될 수 있으므로, 이에 대한 방어적인 널 체크(null check) 로직을 추가하는 것을 권장합니다.

        Usage usage = chatResponse.getMetadata().getUsage();
        String answer = chatResponse.getResult() != null ? chatResponse.getResult().getOutput().getText() : "";

        return QuestionAskResponse.from(
            answer,
            usage != null ? usage.getPromptTokens() : 0,
            usage != null ? usage.getCompletionTokens() : 0,
            usage != null ? usage.getTotalTokens() : 0
        );

Soundbar91 and others added 5 commits May 21, 2026 14:48
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
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.

고생 많으셨습니다 : )

} catch (IOException e) {
throw new IllegalArgumentException("");
}
}
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 IllegalArgumentException("");

이 코드가 production에서 발화하면 다음과 같은 문제가 있습니다.

  • 사용자: 500 에러
  • 운영자: stack trace만 보고 "왜?"를 모름
  • 어떤 파일의 어떤 line에서 깨졌는지 알 길 없음

같은 패턴이 CurrentPolicyReader:51, FaqReader, InternalPolicyReader에서 반복되고 있어요.

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

throw new IllegalStateException("chatlog 파싱 실패: " + path, e);

추가로, 만약 이 reader가 부팅 시점이 아니라 runtime에 도구로 호출된다면 (예: 운영 중 FAQ 추가), 이 메시지가 LLM의 다음 결정에 활용 가능한 형태여야 한다는 것도 같이 고민해보면 좋겠습니다~


Internal Policies
%s

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.

getContext()InternalPolicies 까지 LLM에 보내고 있어요. 자료 위치(data/layer2_policies/internal/)는 이름부터 운영자 내부 메모 임을 시사하는 부분이라 한 번 짚어둘게요.

실제 위험:

  • 내부 처리 기준이 답변에 인용될 수 있음 (예: "CS팀 예외 처리 기준에 따르면...")
  • 사용자가 직접 인용을 유도하는 프롬프트 인젝션 시도 가능
  • 내부 메모가 사용자 답변에 노출되는 사고 → 운영팀 vs 고객 간 신뢰 손상

"자동으로 누적되는 컨텍스트는 자동으로 누출될 수 있다" 라는 점, 의도된 선별이 필요한 자리라는 것을 같이 고민해보면 좋겠습니다!

private final VectorStore vectorStore;

public QuestionAskResponse askQuestion(QuestionAskRequest request) {
String context = getContext(request.question());
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가 코드 인라인이네요. "Chatlog는 다른 layer와 충돌 시 근거로 쓰지 마라" 같은 잘 짠 지시가 들어 있어서 외부화 가치가 더 큰 부분입니다.

외부화하면 아래와 같은 장점이 있습니다.

  • v1, v2 비교가 자연스러워짐 (예: chatlog 우선순위 지시를 뺀 버전과 비교)
  • 운영자가 직접 수정 가능
  • 회귀 테스트 만들기 쉬워짐

예시)

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

추가적으로 LLM 호출 직전 단계에 advisor 하나를 붙여서 (CallAroundAdvisor 구현체) 시스템 메시지 + 유저 메시지 전체를 log로 한 번 찍어보면, Spring AI Advisor가 본인이 작성한 prompt 외에 자동으로 무언가 prepend하지 않는지 확인할 수 있습니다!

sectionBody.append(line).append('\n');
}
}

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.

# category / ## section 2단계 구조로 청크를 분리하고, metadata에 category/section을 명시한 부분 너무 좋네요 👍

장점:

  • 청크 자체가 "어디서 왔는지" 자체 설명적
  • 디버깅 시 metadata만 보면 출처 추적 가능
  • 나중에 metadata 기반 필터링(예: category = '환불'만 검색) 확장이 자연스러움

"무엇을 넣고 뺄지 결정한다" 의 좋은 구현이에요.

다만 (84-95줄) sectionBody.toString().trim() 후 새 StringBuilder.setLength(0) 으로 재사용하는 부분은 mutable 상태 공유 가 됩니다. record나 immutable container가 더 안전할 수 있다는 점, 한 번 참고해주세요~

.user("""
참고 자료
%s

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.

Usage usage = chatResponse.getMetadata().getUsage();

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

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

또 별도 관점으로, 이 메서드가 LLM 호출과 응답 빌드를 한 번에 합니다. "검색된 4개 문서 중 1개라도 layer1_faq면 답변에 포함한다" 같은 분기를 추가하고 싶을 때 단위 테스트는 LLM mock이 필요해요. 결정만 떼어내면 "이 검색 결과 → 어떤 응답 형태" 를 순수 함수로 검증할 수 있습니다.

예시)

// service: 결정
AnswerPlan plan = decideAnswer(question, retrievedDocs);

// caller: 효과
if (plan.kind() == ASK_OPERATOR) {
  escalate(plan);
} else {
  send(plan);
}

plan만 검증하면 LLM 없이 분기 로직 테스트가 가능해지는 구조로 한 번 고민해보면 좋겠습니다!

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