app:
chat:
default-system-prompt: 한국어를 사용하는 tool 지원 AI 입니다. # 기본 system prompt
etl:
pipeline:
init: true # 초기 데이터 로딩 여부
vectorstore:
in-memory.enabled: true # in-memory SimpleVectorStore 사용 여부
rag:
documents-location-pattern: classpath:spring-ai-llm2.pdf # RAG Data 위치
logging:
level:
io:
modelcontextprotocol: DEBUG
org:
springframework:
web: DEBUG
ai:
chat:
client:
advisor: DEBUG # SimpleLoggerAdvisor 등의 Advisor에서 DEBUG 로그 출력
tool: DEBUG # Tool 사용 관련 DEBUG 로그 출력
mcp: DEBUG # mcp 사용 관련 DEBUG 로그 출력
file:
path: log
name: log/${spring.application.name}.log
server:
port: 8081
spring:
application:
name: mcp-server
ai:
mcp:
server:
name: ${spring.application.name}
version: 0.1.0
type: SYNC
instructions: "Spring AI 개념을 RAG 를 사용해 제공하는 MCP 서버"
sse-message-endpoint: /mcp/messages
model:
chat: ollama # 여러 Chat 모델 사용시 auto-configurations 에서 사용할 모델 설정 필요 예: openai, ollama
embedding: ollama # 여러 Embedding 모델 사용시 auto-configurations에서 사용할 모델 지정 (예: openai, ollama)
ollama:
init:
pull-model-strategy: when_missing # when_missing, always, never 설정
chat:
options:
model: hf.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF
embedding:
options:
model: bge-m3
💁♀️
강의 영상에서 instructions이 잘못 표기되어, 강의자료에 반영해두었습니다!
해당 부분 참고 부탁드립니다.
package com.jscode.mcpserver.rag;
@Slf4j
@Configuration
public class RagConfig {
/*
Extract
*/
@Bean
public List<DocumentReader> documentReaders(@Value("${app.rag.documents-location-pattern}") String documentsLocationPattern) throws IOException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources(documentsLocationPattern);
List<DocumentReader> readers = new ArrayList<>();
for(Resource resource : resources){
readers.add(new TikaDocumentReader(resource));
}
return readers;
}
/*
Transform
*/
@Bean
public DocumentTransformer textSplitter(){
return new LengthTextSplitter(200,100);
}
/*
Load
*/
@Bean
public DocumentWriter jsonConsoleDocumentWriter(ObjectMapper objectMapper){
return documents -> {
log.info("======= 저장할 문서 조각(Chunk) 개수: {} ========", documents.size());
try {
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(documents);
log.debug("Chunk JSON: {}", jsonString);
} catch (Exception e) {
log.error("JSON 변환 중 에러가 발생했습니다: {}", e.getMessage());
}
log.info("======================================================");
};
}
@ConditionalOnProperty(prefix = "app.vectorstore.in-memory", name = "enabled", havingValue = "true")
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel){
return SimpleVectorStore.builder(embeddingModel).build();
}
@ConditionalOnProperty(prefix = "app.etl.pipeline", name = "init", havingValue = "true")
@Order(1)
@Bean
public ApplicationRunner initEtlPipeline(
List<DocumentReader> documentReaders,
DocumentTransformer textSplitter,
DocumentTransformer keywordMetadataEnricher,
List<DocumentWriter> documentWriters) {
return args -> {
log.info("[System] ETL 파이프라인 가동 시작");
for (DocumentReader reader : documentReaders) {
List<Document> rawDocuments = reader.get();
log.info("[Extract] 파일 읽기 완료");
List<Document> chunkedDocuments = textSplitter.apply(rawDocuments);
log.info("[Transform] 문서 분할 완료");
chunkedDocuments = keywordMetadataEnricher.apply(chunkedDocuments);
for (DocumentWriter writer : documentWriters) {
writer.accept(chunkedDocuments);
}
log.info("[Load] 저장소 적재 완료");
}
log.info("[System] ETL 파이프라인 적재 종료");
};
}
@Bean
public RetrievalAugmentationAdvisor retrievalAugmentationAdvisor(VectorStore vectorStore, ChatClient.Builder chatClientBuilder, Optional<DocumentPostProcessor> printDocumentsPostProcessor){
VectorStoreDocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.3)
.topK(3)
.build();
ContextualQueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build();
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.build();
TranslationQueryTransformer queryTransformer = TranslationQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.targetLanguage("korean")
.build();
RetrievalAugmentationAdvisor.Builder advisorBuilder = RetrievalAugmentationAdvisor.builder()
.documentRetriever(documentRetriever)
.queryAugmenter(queryAugmenter)
//.queryExpander(queryExpander)
//.queryTransformers(queryTransformer)
;
printDocumentsPostProcessor.ifPresent(processor ->
advisorBuilder.documentPostProcessors(processor));
return advisorBuilder.build();
}
@ConditionalOnProperty(prefix = "app.cli", name = "enabled", havingValue = "true")
@Bean
public DocumentPostProcessor printDocumentsPostProcessor() {
return (query, documents) -> {
log.info("[ Search Results ]");
log.info("===============================================");
if (documents == null || documents.isEmpty()) {
log.info(" No search results found.");
log.info("===============================================");
return documents;
}
for (int i = 0; i < documents.size(); i++) {
Document document = documents.get(i);
// String.format을 사용하여 소수점 둘째 자리까지 잘라서 로깅
log.info("▶ {} Document, Score: {}", i + 1, String.format("%.2f", document.getScore()));
log.info("-----------------------------------------------");
Optional.ofNullable(document.getText()).stream()
.map(text -> text.split("\n")).flatMap(Arrays::stream)
// 중괄호 바인딩을 통해 혹시 모를 텍스트 내의 특수문자 에러 방지
.forEach(line -> log.info("{}", line));
log.info("===============================================");
}
log.info("[ RAG 사용 응답 ]");
return documents;
};
}
}
✅ 5. Tools 구현
package com.jscode.mcpserver.tool;
@Component
public class Tools {
private static final Logger log = LoggerFactory.getLogger(Tools.class);
private final RagChatService ragChatService;
public Tools(@Lazy RagChatService ragChatService) {
this.ragChatService = ragChatService;
}
@Tool(description = "Spring AI 개념에 대해 RAG 기반 답변을 제공합니다.", returnDirect = true)
public String ragTool(@ToolParam(description = "Spring AI 강의에 대한 질문") String userPrompt) {
log.info("ragTool UserPrompt = {}", userPrompt);
return this.ragChatService.call(Prompt.builder().messages(UserMessage.builder().text(userPrompt).build()).build(),"mcp",
Optional.empty())
.getResult().getOutput().getText();
}
}
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| ragChatService
↑ ↓
| chatClientBuilder
↑ ↓
| ollamaChatModel
↑ ↓
| toolCallingManager
↑ ↓
| toolCallbackResolver
↑ ↓
| toolCallbackProvider
↑ ↓
| tools
└─────┘