spring airag 3일차 / Rag, springairag2
2026. 5. 26. 18:45ㆍ대우개발원 수업 내용/spring기반 ai
반응형
KnowledgeService
코드 수정
더보기
package com.example.springairag2.service;
import com.example.springairag2.dto.AskResponse;
import com.example.springairag2.dto.SaveRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 사내 지식베이스 RAG 서비스
*
* [저장 흐름]
* 텍스트 + 카테고리/출처
* → OpenAI Embedding API → 1536차원 벡터
* → pgvector 저장 (content, metadata, embedding)
*
* [RAG 질의 흐름]
* 질문
* → 벡터 변환 → pgvector 코사인 유사도 검색
* → 관련 문서 Top-4 추출
* → [문서 컨텍스트 + 질문] → GPT → 답변 + 출처 반환
*/
@Slf4j
@Service
public class KnowledgeService {
private final VectorStore vectorStore;
private final ChatClient chatClient;
public KnowledgeService(VectorStore vectorStore, ChatModel chatModel) {
this.vectorStore = vectorStore;
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("""
당신은 회사 내부 지식베이스를 기반으로 답변하는 AI 어시스턴트입니다.
규칙:
1. 반드시 제공된 [참고 문서]의 내용을 근거로 답변하세요.
2. 참고 문서에 없는 내용은 "해당 내용을 사내 문서에서 찾을 수 없습니다."라고 답하세요.
3. 답변은 명확하고 구체적으로 작성하세요.
4. 가능하면 요점을 번호 목록으로 정리하세요.
""")
.build();
}
// ── 1. 문서 저장 ─────────────────────────────────────────────────
public String save(SaveRequest req) {
Map<String, Object> metadata = Map.of(
"category", req.getCategory() != null ? req.getCategory() : "일반",
"source", req.getSource() != null ? req.getSource() : "미분류"
);
Document doc = new Document(req.getContent(), metadata);
vectorStore.add(List.of(doc));
log.debug("[저장] id={} category={} source={}", doc.getId(),
req.getCategory(), req.getSource());
return doc.getId();
}
// ── 2. 유사도 검색 ────────────────────────────────────────────────
public List<Document> search(String query, String category, int topK) {
// 카테고리 필터 (null이면 전체 검색)
String filter = (category != null && !category.isBlank())
? "category == '" + category + "'"
: null;
SearchRequest.Builder builder = SearchRequest.builder()
.query(query)
.topK(topK)
.similarityThreshold(0.3);
if (filter != null) {
builder.filterExpression(filter);
}
List<Document> results = vectorStore.similaritySearch(builder.build());
log.debug("[검색] query='{}' category='{}' → {}건", query, category, results.size());
return results;
}
// ── 3. RAG 질의응답 ───────────────────────────────────────────────
public AskResponse ask(String question, String category) {
// Step 1. 관련 문서 검색
List<Document> docs = search(question, category, 4);
// Step 2. 검색 결과 없을 때 처리
if (docs.isEmpty()) {
return AskResponse.builder()
.question(question)
.answer("해당 내용을 사내 문서에서 찾을 수 없습니다. 문서를 먼저 등록해 주세요.")
.sources(List.of())
.build();
}
// Step 3. 컨텍스트 조합
String context = docs.stream()
.map(d -> "[출처: %s / %s]\n%s".formatted(
d.getMetadata().getOrDefault("category", ""),
d.getMetadata().getOrDefault("source", ""),
d.getText()))
.collect(Collectors.joining("\n\n"));
// Step 4. GPT 호출
String answer = chatClient.prompt()
.user("""
[참고 문서]
%s
[질문]
%s
""".formatted(context, question))
.call()
.content();
// Step 5. 출처 문서 목록 구성
List<AskResponse.SourceDoc> sources = docs.stream()
.map(d -> AskResponse.SourceDoc.builder()
.content(truncate(d.getText(), 120))
.category(String.valueOf(d.getMetadata().getOrDefault("category", "")))
.source(String.valueOf(d.getMetadata().getOrDefault("source", "")))
.score(0.0) // pgvector는 score를 별도로 제공하지 않음
.build())
.toList();
log.debug("[RAG] question='{}' → answer length={}", question, answer.length());
return AskResponse.builder()
.question(question)
.answer(answer)
.sources(sources)
.build();
}
// ── 4. 전체 문서 수 조회 ─────────────────────────────────────────
// (간단히 검색으로 근사치 파악)
public int countDocs() {
try {
return vectorStore.similaritySearch(
SearchRequest.builder()
.query("회사 규정 정책 기술 온보딩")
.topK(1000)
.similarityThreshold(0.0)
.build()
).size();
} catch (Exception e) {
return 0;
}
}
// ── 샘플 데이터 일괄 적재 ─────────────────────────────────────────
public void loadSampleData() {
List<SaveRequest> samples = buildSampleData();
for (SaveRequest req : samples) {
save(req);
}
log.info("[샘플 데이터] {}건 적재 완료", samples.size());
}
private String truncate(String s, int max) {
return s.length() > max ? s.substring(0, max) + "…" : s;
}
private List<SaveRequest> buildSampleData() {
List<Object[]> data = List.of(
// [content, category, source]
new Object[]{"연차 유급휴가는 1년 이상 근속한 직원에게 15일이 부여됩니다. 3년 이상 근속 시 1일씩 추가되며 최대 25일까지 부여됩니다. 연차는 전년도 출근율 80% 이상인 경우에만 전일 부여됩니다.", "HR", "취업규칙 제24조"},
new Object[]{"육아휴직은 만 8세 이하 또는 초등학교 2학년 이하 자녀를 가진 직원이 신청 가능합니다. 최대 1년까지 사용할 수 있으며, 배우자와 분할 사용도 가능합니다. 신청은 휴직 개시 30일 전까지 인사팀에 제출해야 합니다.", "HR", "육아휴직 규정"},
new Object[]{"재택근무 신청은 매주 수요일 오전까지 그룹웨어에서 신청합니다. 주 2회까지 허용되며, 부서장 사전 승인이 필요합니다. 재택 중에도 코어타임(10시~16시)에는 화상 연결 가능 상태를 유지해야 합니다.", "HR", "재택근무 운영지침"},
new Object[]{"경조사 지원 기준: 본인 결혼 5일, 배우자 출산 10일, 부모/배우자 사망 5일, 조부모/자녀 사망 3일의 특별 휴가가 부여됩니다. 경조금은 결혼 20만원, 출산 10만원, 사망 10만원이 지급됩니다.", "HR", "경조사 지원 규정"},
new Object[]{"모든 직원은 업무용 PC에 백신 소프트웨어를 반드시 설치해야 하며, 자동 업데이트를 활성화해야 합니다. 외부 저장장치(USB 등) 사용 시 보안팀 사전 승인이 필요합니다. 미승인 외부 저장장치 사용은 보안 위반으로 처리됩니다.", "정책", "정보보안 정책 v3.0"},
new Object[]{"회사 기밀 정보는 외부 클라우드 서비스(개인 Google Drive, Dropbox 등)에 저장하거나 공유할 수 없습니다. 업무 자료 공유는 반드시 사내 협업 시스템(사내 드라이브)을 이용해야 합니다. 위반 시 징계 처분을 받을 수 있습니다.", "정책", "정보보안 정책 v3.0"},
new Object[]{"비밀번호는 최소 8자 이상, 영문 대/소문자, 숫자, 특수문자를 조합해야 합니다. 90일마다 변경이 권장되며, 이전 5개의 비밀번호는 재사용할 수 없습니다. 타인과 비밀번호를 공유하는 행위는 엄격히 금지됩니다.", "정책", "비밀번호 관리 지침"},
new Object[]{"출장 신청은 출장 3영업일 전까지 결재를 완료해야 합니다. 국내 출장 일비는 1일 3만원, 국외 출장은 지역별 차등 지급됩니다. 항공권은 이코노미석 원칙이며, 6시간 이상 장거리 노선은 비즈니스석이 허용됩니다.", "정책", "출장 관리 규정"},
new Object[]{"Spring AI 2.0은 OpenAI, Anthropic, Google Gemini 등 다양한 AI 모델을 통합 지원합니다. VectorStore 인터페이스를 통해 pgvector, Elasticsearch, Pinecone 등 주요 벡터 DB와 연동할 수 있습니다. 임베딩 모델은 text-embedding-3-small(1536차원)을 기본으로 사용합니다.", "기술", "Spring AI 2.0 공식 문서"},
new Object[]{"pgvector는 PostgreSQL 확장으로 벡터 연산과 유사도 검색을 지원합니다. HNSW 인덱스를 사용하면 수백만 건의 벡터도 밀리초 내 검색이 가능합니다. 코사인 유사도, L2 거리, 내적 등 다양한 거리 함수를 지원합니다.", "기술", "pgvector 기술 가이드"},
new Object[]{"RAG(Retrieval-Augmented Generation)는 LLM이 답변 생성 시 외부 지식베이스를 검색하여 참고하는 기술입니다. 환각(Hallucination)을 줄이고 최신 정보를 반영할 수 있어 기업 AI 어시스턴트 구축에 널리 사용됩니다.", "기술", "RAG 기술 가이드"},
new Object[]{"Docker Compose를 이용한 개발 환경 구성: services 하위에 postgres(pgvector 이미지), app(Spring Boot) 컨테이너를 정의합니다. 환경변수는 .env 파일로 관리하며 docker-compose up -d 명령으로 전체 환경을 실행합니다.", "기술", "개발환경 설정 가이드"},
new Object[]{"신입사원 입사 첫날: 오전 9시 인사팀 방문 → 사원증/장비 수령 → 보안 서약서 서명 → 오후 OT 교육(회사 소개, 복리후생, 보안 정책) 순서로 진행됩니다. 교육 후 소속 팀 배치 및 멘토 연결이 이루어집니다.", "온보딩", "신입사원 온보딩 가이드"},
new Object[]{"복리후생 항목: 중식비 월 15만원 지원, 건강검진 연 1회(배우자 격년), 자녀 학자금 지원(고등학교~대학교), 사내 어린이집 운영, 헬스장 이용권 50% 지원, 도서구입비 분기 20만원 지원.", "온보딩", "복리후생 안내서"},
new Object[]{"그룹웨어 초기 설정: 사내 포털(portal.company.com) 접속 → 초기 비밀번호 변경 → 이메일/메신저 계정 연동 → 전자결재 위임 설정 → 부서 공유 드라이브 접근 권한 신청 순서로 진행하세요.", "온보딩", "그룹웨어 사용 가이드"}
);
return data.stream().map(d -> {
SaveRequest req = new SaveRequest();
req.setContent((String) d[0]);
req.setCategory((String) d[1]);
req.setSource((String) d[2]);
return req;
}).toList();
}
}
controller 패키지 추가
KnowledgeApiController 생성
더보기
package com.example.springairag2.controller;
import com.example.springairag2.dto.AskRequest;
import com.example.springairag2.dto.AskResponse;
import com.example.springairag2.dto.SaveRequest;
import com.example.springairag2.service.KnowledgeService;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@Slf4j
@RestController("/api")
@RequestMapping
@RequiredArgsConstructor
public class KnowledgeApiController {
private final KnowledgeService knowledgeService;
// -- POST /api/ask -> RAG 질의 응답----------
@PostMapping("/ask")
public ResponseEntity<AskResponse> ask(@RequestBody AskRequest request) {
AskResponse response = knowledgeService.ask(
request.getQuestion(),
request.getCategory()
);
return ResponseEntity.ok(response);
}
// --- POST /api/docs -> 문서 저장 ------------
@PostMapping("/docs")
public ResponseEntity<Map<String, String>> save(@RequestBody SaveRequest request) {
String id = knowledgeService.save(request);
return ResponseEntity.ok(Map.of("id", id, "message", "문서가 저장되었습니다."));
}
// --- POST /api/docs/sample -> 샘플 데이터 적재 ------------
@PostMapping("/docs/sample")
public ResponseEntity<Map<String, String>> loadSample() {
knowledgeService.loadSampleData();
return ResponseEntity.ok(Map.of("message", "샘플 데이터 15건이 적재되었습니다."));
}
// --- GET /api/docs/count -> 문서 수 조회 ------------
@GetMapping("docs/count")
public ResponseEntity<Map<String, Integer>> count() {
return ResponseEntity.ok(Map.of("count", knowledgeService.countDocs()));
}
}
접근을 위한 controller 따로 구성
PageController 생성
더보기
package com.example.springairag2.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PageController {
// 메인 페이지 (채팅 UI)
@GetMapping("/")
public String index() { return "index"; }
// 문서 관리 페이지
@GetMapping("/docs")
public String docs() { return "docs"; }
}
docs.html
index.html 코드 templates 폴더에 추가

실행하면
샘플 데이터를 적재 후 질문 하면 ai가 답변을 해줌
'대우개발원 수업 내용 > spring기반 ai' 카테고리의 다른 글
| spring ai MCP 1,2일차 / mcp, mcpclient, mpcclientchat,mcpserver1 (0) | 2026.05.28 |
|---|---|
| spring airag 2일차 / Rag, springairag1,springairag2 (0) | 2026.05.24 |
| spring airag 1일차 / Rag, springairag1 (0) | 2026.05.21 |
| spring ai 10일차 / 다중 llm, springai5 (0) | 2026.05.20 |
| spring ai 9일차 / 다중 llm, springai5 (0) | 2026.05.19 |



