하위 태스크 1

Spring AI 의존성 확인

openai 프로젝트의 build.gradle 설정 확인

build.gradle:

// ...
 
ext {
	set('springAiVersion', "1.0.0")
}
 
// ...
 
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.ai:spring-ai-starter-model-openai'
	implementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector'
	implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
	// ...
}
 
// ...
  • ext: 프로젝트 전체에서 사용할 공통 변수를 선언한다. 위에서는 springAiVersion이라는 이름으로 1.0.0 값을 저장했다.
  • org.springframework.ai:spring-ai-starter-model-openai: OpenAI의 모델을 Spring 프레임워크에서 쉽게 사용할 수 있게 해주는 라이브러리다.
  • org.springframework.ai:spring-ai-starter-vector-store-pgvector: PostgreSQL의 확장 기능 pgvector를 사용해 벡터 데이터를 저장하고 검색하게 해주는 라이브러리다.
  • org.springframework.ai:spring-ai-pdf-document-reader: PDF 파일의 텍스트 콘텐츠를 추출해 AI가 학습하거나 참고할 수 있는 데이터 형식으로 변환한다.

하위 태스크 2

API Key 설정

OpenAI API 키를 안전하게 주입

[OpenAI API]에서 API 키를 발급한다.

IntelliJ IDEA의 실행/디버그 구성에서 OPENAI_API_KEY 환경 변수를 추가한다.

하위 태스크 3

기본 ChatCompletion 구현

질문→응답 REST API 구현

ChatGPT에 사용자 메시지를 보내고 응답을 받기 위한 컨트롤러를 구현한다.

controller/ApiController.java:

@RestController
@RequiredArgsConstructor
public class ApiController {
    private final OpenAiChatModel chatModel;
    private final OpenAiImageModel imageModel;
 
    @GetMapping("/api/chat")
    public String getChat(@RequestParam("message") String message) {
        return chatModel.call(message);
    }
}

Spring 애플리케이션을 실행한다.

Connection to localhost:5432 refused.

pgvector/pgvector Docker 이미지를 사용하여 PostgreSQL 데이터베이스 컨테이너를 실행한다.

docker run -d \
--name pgvector \
-e POSTGRES_DB=mydb \
-e POSTGRES_USER=myuser \
-e POSTGRES_PASSWORD=mypass \
-p 5432:5432 \
pgvector/pgvector:pg18

cURL을 사용해 ChatGPT에 질의하고 응답을 받는다.

curl -G "http://localhost:8080/api/chat" --data-urlencode "message=네 소개를 해줘."

답변은 다음과 같다.

안녕하세요! 저는 OpenAI의 언어 모델인 ChatGPT입니다. 다양한 주제에 대해 정보를 제공하고 질문에 답하며, 토론을 나누는 데 도움을 줄 수 있는 AI입니다. 언어 이해와 자연어 처리 기술을 기반으로 하여, 사용자와 소통하며 필요한 정보를 빠르고 정확하게 전달하는 데 주력하고 있습니다. 궁금한 점이 있다면 언제든지 물어보세요!

하위 태스크 4

문서 준비

테스트용 문서/FAQ 텍스트 준비

하위 태스크 5 ~ 6

임베딩 생성

문서를 벡터로 변환해 저장

벡터 검색 구현

질문 임베딩 후 유사도 검색

ApiController 컨트롤러 클래스에 문서를 벡터로 변환해 데이터베이스에 저장하는 /api/ingest 엔드포인트를 추가한다. 해당 엔드포인트로 GET 요청을 발신하면 데이터베이스에 테스트 PDF와 텍스트가 벡터로 변환되어 삽입된다. /api/chat 엔드포인트로 GET 요청을 발신하면 사용자의 질의를 임베딩으로 변환하고 벡터 DB에서 유사한 문서 조각을 찾는다. 찾은 문서 조각이 있다면 내용을 합쳐서 최종 프롬프트를 만들고, 그렇지 않다면 사용자의 원본 질문만 ChatGPT에게 보낸다.

@RestController
@RequiredArgsConstructor
public class ApiController {
    private final OpenAiChatModel chatModel;
    private final OpenAiImageModel imageModel;
    private final PgVectorStore vectorStore;
 
    @GetMapping("/api/ingest")
    public void ingest() {
        List<Document> documents = new ArrayList<>();
        documents.addAll(new PagePdfDocumentReader("classpath:/2026년도 제1차 금통위 의사록.pdf").read());
        documents.addAll(new TextReader("classpath:/The Adventures of Sherlock Holmes.txt").read());
 
        TokenTextSplitter splitter = new TokenTextSplitter();
        vectorStore.write(splitter.split(documents));
    }
 
    @GetMapping("/api/chat")
    public String getChat(@RequestParam("message") String message) {
        List<Document> similarDocuments = vectorStore.similaritySearch(
                SearchRequest.builder().query(message).topK(3).build()
        );
 
        if (!similarDocuments.isEmpty()) {
            String context = similarDocuments.stream()
                    .map(Document::getText)
                    .collect(Collectors.joining("\n\n"));
 
            message = String.format("다음 정보를 참고하여 질문에 답하세요. 문서에 없는 내용은 모른다고 답하세요.\n\n[정보]\n%s\n\n[질문]\n%s", context, message);
        }
 
        return chatModel.call(message);
    }
}

하위 태스크 7

컨텍스트 주입 ChatCompletion

검색 결과를 프롬프트에 포함해 호출

벡터 DB에 내용을 삽입하기 전, 두 가지 질의를 하고 답변을 확인한다.

질의 1:

curl -G "http://localhost:8080/api/chat" \
--data-urlencode "message=2026년도 제1차 금통위 의사록을 바탕으로 향후 금리의 방향을 예측해줘."
죄송합니다만, 2026년도 제1차 금통위 의사록에 대한 정보에 접근할 수 없습니다. 하지만 일반적으로 금리 예측은 여러 경제 지표, 인플레이션, 중앙은행의 통화 정책 및 글로벌 경제 상황 등을 고려하여 이루어집니다. 거래량, 고용 통계, 그리고 소비자 물가 지수(CPI) 등 주요 경제 지표들을 살펴보면 금리의 향방을 추정하는 데 도움이 될 수 있습니다.

최근의 경제 상황이나 금통위의 이전 지침을 기반으로 금리 방향성을 예측하려면 해당 정보에 대한 연구를 해보는 것이 좋습니다. 즉, 긍정적인 경제 지표가 계속 나온다면 금리가 상승할 가능성이 있으며, 반대로 경제가 둔화된다면 금리가 인하될 가능성이 있습니다.

더 구체적인 정보를 원하신다면, 최근 경제 상황에 대한 논의나 금통위의 선언 등을 참고하시기 바랍니다.%

질의 2:

curl -G "http://localhost:8080/api/chat" \
--data-urlencode "message=The Adventures of Sherlock Holmes의 5장 The Five Orange Pips에서 가장 첫 번째로 볼 수 있는 문장 을 말해줘."
죄송하지만, "The Adventures of Sherlock Holmes"의 텍스트를 직접 인용할 수는 없습니다. 그러나 5장 "The Five Orange Pips"의 내용이나 주제에 대해 설명해드릴 수 있습니다. 도움이 필요하시면 말씀해주세요!

/api/ingest에 요청을 보내 벡터 DB에 문서의 변환 결과를 삽입한다. 약 10초 내외의 시간이 소요된다.

curl "http://localhost:8080/api/ingest"

이전과 동일한 두 가지 질의를 하고 답변을 확인한다. 이전과 달리 문서의 내용을 참조하여 대답함을 확인할 수 있다.

질의 1:

curl -G "http://localhost:8080/api/chat" \
--data-urlencode "message=2026년도 제1차 금통위 의사록을 바탕으로 향후 금리의 방향을 예측해줘."
2026년도 제1차 금통위 의사록만을 바탕으로 향후 금리의 방향을 예측하기는 어렵습니다. 문서에는 금리에 대한 구체적인 결정이나 전망이 포함되어 있지 않으며, 경제성장률, 양질의 일자리, 반도체 수출 등의 경제 동향이 논의되었으나 이 내용이 금리에 직접적으로 어떤 영향을 미칠지를 알 수 있는 정보는 없습니다. 따라서 향후 금리의 방향에 대해 명확히 말할 수 없습니다.

질의 2:

curl -G "http://localhost:8080/api/chat" \
--data-urlencode "message=The Adventures of Sherlock Holmes의 5장 The Five Orange Pips에서 가장 첫 번째로 볼 수 있는 문장 을 말해줘."
"Holmes shook his clenched hands in the air."입니다.