1. 서버 구조

  1. 사설 인터넷망에 3개의 클러스터용 노드가 있습니다.
  2. 사설 인터넷망에 1개의 프로젝트 배포용 노드가 있습니다.

2. 앤서블 설정하기

2.1 배포 서버 DNS 설정하기

배포 서버를 정의하기 위해 /etc/hosts 파일에 호스트를 설정해야 합니다. 호스트 설정은 각 운영 체제마다 다를 수 있습니다. 리눅스 기반 시스템에서는 다음과 같이 /etc/hosts 파일을 수정합니다:

//... 10.12.204.13 peter-zk01.foo.bar 10.12.204.13 peter-kafka01.foo.bar 10.12.204.14 peter-zk02.foo.bar 10.12.204.14 peter-kafka02.foo.bar 10.12.204.17 peter-zk03.foo.bar 10.12.204.17 peter-kafka03.foo.bar 

2.2 SSH 허용하기

앤서블을 사용하여 서버에 접속할 때마다 비밀번호를 입력하는 것은 번거로울 수 있습니다. 따라서 배포 서버에 대한 SSH 접속을 허용하는 것이 좋습니다.

2.2.1 SSH 키 생성

ssh-keygen 명령을 사용하여 SSH 키를 생성합니다. 생성된 키 데이터는 ~/.ssh/id_rsa.pub 파일에 저장됩니다. 다음 단계에서 이 키 데이터를 사용하게 됩니다.

  1. ssh-keygen 명령을 실행하여 키를 생성합니다.
  2. 생성된 데이터를 ~/.ssh/id_rsa.pub 파일로부터 복사합니다.
  3. SSH로 접속하고자 하는 서버에 접속합니다.
  4. vi ~/.ssh/authorized_keys 명령을 사용하여 인증 키 파일을 수정합니다.
  5. 새로운 라인에 이전에 복사한 ~/.ssh/id_rsa.pub 데이터를 붙여넣습니다.
  6. chmod 600 ~/.ssh/authorized_keys 명령을 실행하여 권한을 설정합니다.
  7. 이제 로컬에서 ssh 명령을 사용하여 비밀번호 없이 서버에 접속할 수 있습니다.

2.3 앤서블 스크립트 다운로드

다음 명령을 사용하여 앤서블 스크립트를 다운로드합니다:

git clone https://github.com/onlybooks/kafka2 

그리고 kafka2/chapter2/ansible_playbook 디렉토리로 이동합니다.

hosts 파일을 다음과 같이 수정합니다. 보통 사용자는 root를 사용하므로 ansible_user를 root로 설정합니다:

[zkhosts] peter-zk01.foo.bar peter-zk02.foo.bar peter-zk03.foo.bar  [kafkahosts] peter-kafka01.foo.bar peter-kafka02.foo.bar peter-kafka03.foo.bar  [kerberoshosts] peter-zk01.foo.bar  [all:vars] ansible_user=root 

2.4 주키퍼 클러스터 설치하기

다음 명령을 사용하여 주키퍼 클러스터를 설치합니다:

ansible-playbook -i hosts zookeeper.yml 

Kafka ZooKeeper

2.5 카프카 클러스터 설치하기

다음 명령을 사용하여 카프카 클러스터를 설치합니다:

ansible-playbook -i hosts kafka.yml 

Kafka Cluster Installation

카프카는 실행되지만 내부적으로 주키퍼와의 연결을 실패하여 일정 시간 후에 타임아웃으로 종료될 수 있습니다. 서버에서 systemctl status kafka-server 명령을 실행하여 아무런 에러가 없는지 확인하면 성공입니다.

2.6 카프카 테스트하기

카프카가 올바르게 작동하는지 확인하기 위해 토픽을 생성하고 샘플 데이터를 프로듀스하고 컨슈밍해보겠습니다.

2.6.1 토픽 생성하기

서버에 접속하여 다음 명령을 실행합니다:

/usr/local/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test-topic --partitions 1 --replication-factor 3 

출력으로 다음과 같이 "Created topic test-topic."이 나오면 성공입니다.

2.6.2 메시지 전송하기

샘플 메시지를 프로듀스해보겠습니다. 다음 명령을 실행하세요:

/usr/local/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test-topic 

> 기호가 나타나면 원하는 메시지를 입력하고 엔터 키를 눌러 메시지를 전송할 수 있습니다.

2.6.3 메시지 수신하기

2번 서버에서 메시지를 수신해보겠습니다. 다음 명령을 실행하세요:

/usr/local/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test-topic 

이제 이전에 실행한 프로듀서에서 테스트 메시지를 입력하면 출력으로 확인할 수 있습니다.

Kafka Consumer Output

 

Kafka Producer Output

2.7 스프링 프로젝트와 연결하기

스프링 프로젝트와 카프카 프로젝트를 연결해보겠습니다.

  1. 먼저 리액터 카프카 패키지를 설치합니다:
implementation 'io.projectreactor.kafka:reactor-kafka:1.3.18' 
  1. application.yml 파일에 다음과 같이 카프카 서버에 대한 정보를 설정합니다:
spring:   kafka:     bootstrap-servers: peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka02.foo.bar:9092 
  1. 프로젝트를 실행하고 다음 로그가 출력되면 그룹에 성공적으로 조인했음을 의미합니다:
2023-07-12T17:59:46.090+09:00  INFO 3367 --- [y-trace-group-1] o.a.k.c.c.internals.ConsumerCoordinator  : [Consumer clientId=consumer-user-activity-trace-group-1, groupId=user-activity-trace-group] Successfully joined group with generation Generation{generationId=1, memberId='consumer-user-activity-trace-group-1-67094391-d771-4ec8-ba5e-d56cdf0e8083', protocol='range'} 

이제 앤서블을 사용하여 카프카 클러스터를 구축하고 테스트하는 방법에 대해 알게 되었습니다. 카프카를 사용하여 안정적이고 확장 가능한 메시지 시스템을 구축할 수 있습니다.

들어가며

리액티브 프로그래밍은 현대의 수요 증가와 불확실성에 대응하기 위한 새로운 소프트웨어 아키텍처와 프로그래밍 기법입니다. 이 글에서는 리액티브 프로그래밍의 필요성에 대해 다루고, 리액티브 시스템의 기본 원리, 적합한 비즈니스 사례, 적합한 프로그래밍 기술, 그리고 스프링이 리액티브로 전환되는 이유에 대해 알아보겠습니다.

책의 내용을 바탕으로 알아보는 과정에서 부족하거나 이해가 안되는 설명의 경우에는 직접 찾은 내용을 추가했습니다.

1. 왜 리액티브인가?

우리는 과거에는 톰캣과 같은 서버를 사용하여 웹 요청을 처리했습니다. 하지만 이러한 전통적인 방식은 한계가 있습니다. 예를 들어, 스레드 풀을 구성하고 초당 일정 수의 요청을 처리하는 등의 방식으로 서버를 구성합니다. 그러나 이러한 방식은 대량의 요청이 동시에 발생하는 경우에는 제대로 대응하기 어렵습니다.

예를 들어, 블랙 프라이데이와 같은 특정 이벤트 기간에는 사용자의 요청이 급증하게 됩니다. 이런 경우 스레드 부족으로 인해 요청이 누락될 수 있습니다. 또한, 서버의 부하 부담 능력이 한정되어 있어서 대량의 요청을 처리하기에는 한계가 있습니다.

톰캣은 thread per request 정책을 가지고 있습니다. 따라서 새로운 요청이 들어올 경우 새로운 스레드가 필요합니다. 자바에서는 스레드를 생성하는데 약 1MB의 메모리가 필요합니다. 동시에 수천, 수만개의 요청이 몰릴경우 메모리 부족으로인해 서버가 다운되거나 요청이 누락되어 서비스 장애가 발생할 수 있으므로 대량의 요청을 처리하기에는 한계가 있습니다.

그렇다면 이러한 문제에 대해 어떻게 대응해야 할까요? 수요와 응답에 미칠 수 있는 모든 영향을 고려해야합니다. 즉 어떠한 환경에서도 높은 응답성을 달성할 수 있어야 합니다. 높은 응답성을 달성하기 위해서는 "탄력성"과 "복원력"이 필요합니다.

1.1 탄력성

탄력성이란 사용자의 수요에 따라서 자원을 증가, 감소 시킴으로써 수요에 대응할 수 있는 능력을 뜻합니다. 이를 통해서 사용자의 수요가 많든 적든 일정한 레이턴시를 보장함으로써 사용자의 모든 수요를 충족시킬 수 있습니다. 이는 수요를 모니터링하고 적절하게 자원을 스케일 아웃 또는 스케일 업을 적용함으로써 달성할 수 있습니다.

1.2 복원력

복원력이란 만약 시스템에 장애가 발생했을 때에도 응답성을 유지할 수 있는 능력을 뜻합니다. 이는 두 가지를 통해 달성할 수 있습니다. 첫째로 시스템의 기능 요소를 격리해 장애가 전파되지 않도록해야합니다. 둘째로 복제 데이터베이스 처럼 장애가 발생한 기능 요소를 대체할 수 있는 예비 시스템이 존재해야합니다.

1.3 기존 아키텍쳐의 탄력성 한계

기존 아키텍쳐의 특징은 동기적인(순차적인) 처리 과정이 존재한다는 것입니다. 이는 아무리 인스턴스를 확장해도 처리가능한 한계가 존재한다는 것을 의미합니다. 이는 이론적 관점에서 암달의법칙과 건터의 보편적 확장성 모델로 설명할 수 있습니다.

암달의 법칙은 프로그래밍 요소에 동기적인 부분이 존재하는한 아무리 멀티코어로 병렬화를 진행해도 한계가 존재한다는 것을 의미합니다. 병렬코어를 추가하면 추가할수록 프로그램 처리 속도 증가율을 점점 더 낮아지게 되고 결국 한계에 봉착하게 됩니다.

"Universal scalability model"은 시스템이 대규모로 확장되는 능력을 지칭하는 개념입니다. 이 모델은 시스템이 증가하는 작업 부하나 사용자 수에 따라 선형적으로 확장할 수 있는지 여부를 나타냅니다. 일반적으로 시스템은 사용자나 작업 부하가 증가함에 따라 성능이 저하될 수 있습니다. 이를 해결하기 위해 시스템은 확장 가능한 아키텍처와 설계 원칙을 적용할 수 있습니다. Universal scalability model은 이러한 아키텍처와 원칙을 사용하여 시스템의 성능이 선형적으로 증가할 수 있는 모델을 말합니다.

2.메시지 기반 통신

리액티브 시스템에서는 메시지 기반 통신이 중요한 역할을 합니다. 전통적인 블로킹 방식의 단점을 극복하기 위해 비동기적인 메시지 기반 통신을 사용합니다. 이는 분산 시스템 간의 자원을 효율적으로 사용할 수 있도록 도와줍니다.

메시지 기반 통신은 컴퓨터 시스템 또는 네트워크 간에 데이터를 교환하기 위해 사용되는 통신 방식입니다. 메시지 기반 통신은 송신자가 메시지를 작성하고 수신자에게 보내는 방식으로 동작합니다. 메시지는 일련의 데이터로 구성되며, 송신자는 메시지를 생성하고 목적지 주소와 함께 수신자에게 전송합니다. 수신자는 메시지를 받아들이고 필요한 처리를 수행한 후 응답 메시지를 송신자에게 보낼 수도 있습니다. 메시지 기반 통신은 느슨한 결합(loose coupling)을 제공하므로, 송신자와 수신자 간의 시간적 또는 공간적 제약이 없습니다. 또한, 메시지 큐 등의 중간 매개체를 사용하여 메시지를 저장하고 전달하는 기능을 제공할 수 있습니다. 이는 시스템의 확장성과 유연성을 향상시키는 데 도움이 됩니다.

메시지 기반 통신은 메시지 브로커를 통해 이루어지며, 메시지 대기열을 모니터링하여 부하와 탄력성을 제어합니다. 이를 통해 응답성을 달성하기 위해서는 탄력성과 복원력이 필요하며, 메시지 기반 접근 방식을 채택하여 구성 요소를 독립적으로 격리시킴으로써 유지 보수 및 확장을 용이하게 합니다. 이러한 원칙은 리액티브 선언문의 핵심 개념으로써 사용됩니다.

리액티브 선언문은 높은 응답성을 달성하기 위해 탄력성, 복원력 그리고 메시지 기반 통신의 관계 및 필요성에 대해서 설명한 선언문입니다. 리액티브 아키텍쳐의 기초이기 때문에 직접 읽어보는 것이 좋습니다. https://www.reactivemanifesto.org/

3. 왜 리액티브 스프링인가?

리액티브 프로그래밍은 다양한 플랫폼과 언어에서 사용될 수 있습니다. 그중에서도 JVM 기반의 언어인 자바와 스칼라는 리액티브 프로그래밍을 위한 프레임워크와 라이브러리가 많이 개발되어 있습니다. 스프링의 경우, 리액티브 프로그래밍을 지원하기 위해 스프링 5 버전부터 리액티브 스프링 프레임워크를 도입하였습니다.

스프링 이외의 다른 프레임워크로는 vert.x와 Akka 등이 있습니다. vert.x는 노드를 대체하기 위한 강력한 프레임워크이지만, 스프링보다 연혁이 낮습니다. Akka는 스칼라를 주로 사용하는 프레임워크로서, 자바 개발자에게는 다소 익숙하지 않을 수 있습니다.

4. 서비스 레벨에서의 반응성

스프링을 사용하여 리액티브 시스템을 구현하는 것은 몇 가지 제한이 있습니다. 스프링 클라우드는 몇 가지 문제를 해결하고 분산 시스템의 구축을 단순화해주었습니다. 그러나 명령형 프로그래밍은 여전히 블로킹 작업과 스레드 낭비를 유발할 수 있습니다. 콜백을 사용하면 논블로킹을 구현할 수 있지만, 콜백 지옥과 멀티스레드의 복잡성이 코드에 드러납니다. Java 8의 CompletableFuture는 완전히 비동기적인 처리를 지원하지만, 스프링 4와는 호환되지 않는다는 한계가 있습니다. 스프링에서 제공하는 ListenalbeFuture는 자체적으로 멀티스레딩을 지원하지만, 스레드가 추가될수록 메모리 사용량이 증가하는 단점이 있습니다.

증상

Kubernetes 클러스터에서 Nginx Ingress Controller를 사용하여 서비스를 구성하였습니다. 그런데, 큰 파일을 업로드하려고 할 때마다 다음과 같은 에러가 발생합니다.

413 Request Entity Too Large

이 오류는 클라이언트가 보내는 요청이 서버가 처리할 수 있는 최대 크기를 초과했음을 나타냅니다. 기본적으로 Nginx는 1MB보다 큰 요청을 허용하지 않습니다.

해결 방법

이 문제를 해결하기 위해서는 Nginx Ingress Controller의 설정을 변경하여 최대 요청 크기를 늘려야 합니다. 이는 ingress 스펙을 작성한 yaml 파일에서 쉽게 설정할 수 있습니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: 8m
...

위와 같이 nginx.ingress.kubernetes.io/proxy-body-size 를 설정하면 쉽게 최대 요청 크기를 수정할 수 있습니다.

참조

https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#custom-max-body-size

1. 시도 이유

기존 챗봇은 아래와 같은 문제를 가지고 있습니다.

  1. 정적인 대답, 이는 구글이나 네이버와 같은 검색 엔진과 차별점이 존재하지 않는다.
  2. 문맥 파악 불가, “내가 기저질환이 ~~ 한 편인데, 유방암이 발병할 가능성이 높을까?”와 같은 문맥을 파악한 응답이 불가능하다.

GPT의 경우, 문맥을 파악한 동적인 대답이 가능하지만 아래와 같은 문제가 있습니다.

  1. 2021년 이전의 데이터셋을 바탕으로 개발되었기 때문에 최신 정보에 접근이 불가능하다.
  2. 인터넷에 있는 정보를 사용하였기 때문에 신뢰성이 떨어진다.
  3. 존재하지도 않는 정보를 생성하는 Hallucination 이 발생한다.

즉, 위 두 챗봇은 “검증된 정보로 사용자를 고려해 적절한 답변을 제시한다.”라는 목표에 도달하기 어렵습니다. 이를 해결하기 위해서는 검증된 데이터를 확보하고 그 데이터와 사용자 정보를 입력받아 답변을 생성하는 기술이 필요합니다.

2. 목표

따라서, 이번 개발의 목표와 결과 예시는 다음과 같습니다.

  1. 검증된 데이터만으로 답변을 생성하는 기능
  2. 데이터와 사용자 정보를 입력받아 적절한 답변을 생성하는 기능
  3. 답변에 필요한 정보가 존재하지 않는 경우 이를 알리는 기능
  4. 답변에 쓰인 데이터의 출처를 명시하는 기능

결과 예시

사용자 질문: "저는 당뇨병이 있는데, 살 찌면 항상 혈당이 높아지는데 이유가 무엇인가요?"

봇보답: 살이 찌면 체내 지방이 증가함에 따라 인슐린 저항성이 증가하고, 인슐린의 효과가 감소합니다. 이로 인해 혈당이 상승할 수 있습니다. 따라서, 당뇨병 환자는 체중을 유지하거나 감량하여 혈당을 조절하는 것이 중요합니다.

이 답변은 검증된 의료 데이터를 바탕으로 생성되었으며, 출처는 의료 전문가가 작성한 논문입니다. 해당 논문의 출처는 다음과 같습니다: [1]

감사합니다.

[1] 이명숙, 이동호, 이은정, 박창수, "당뇨병 환자에서 비만이 인슐린 저항성과 혈당 수준에 미치는 영향," 대한내분비학회지, 2005.

3. 방법

목표를 구현하기 위해선 아래 기능이 필요합니다.

  1. 사용자 질문을 받아 비슷한 정보를 검색하는 기능
  2. 검증된 정보와 출처를 데이터베이스에 저장하는 기능
  3. 사용자 질문, 정보 그리고 출처를 바탕으로 자연스러운 응답을 생성하는 기능

기능은 비교적 간단하다. 그러나 높은 효과를 보기 위해선 제약조건이 존재합니다.

예를 들어서, 검증정보가 기하급수적으로 증가해 정보 검색을 하는데 수 십분이 걸리게 되는 경우 서비스로 제공할 수 없습니다. 또한, 사용자 질문과 무관한 정보가 검색되는 경우 잘못된 응답이 생성됩니다. 이를 해결하기 위해 의미론적 텍스트 임베딩 기술과 벡터 데이터베이스가 필요합니다.

3.1 텍스트 임베딩

텍스트 임베딩을 영어, 한글과 같은 자연어를 컴퓨터가 이해하기 쉽도록 숫자열 형태로 변환하는 것을 말합니다. 이 떄, 단순히 형태를 바탕으로 임베딩을 진행할 경우 문제가 발생합니다. "사랑", "애정", "모정", "애착" 등의 단어는 유사한 단어이지만, 단순히 형태를 보면 차이가 존재해 [1,1,1…] , [20,9,11…] 등과 같이 임베딩을 진행할 경우 거리가 생길 수 있습니다. 따라서 의미를 충분히 고려한 임베딩 기술이 필요하다. 이번 구현에는 Open AI의 text-embedding-ada-002 를 사용하도록 하겠습니다.

3.2 벡터 데이터베이스

텍스트 임베딩이 잘 수행되었다면, 다음은 사용자 질문과 유사항 정보를 빠르게 찾는 것이 문제입니다. 임베딩 결과는 매우 긴 숫자열이며, 정보가 10만 줄이기만 해도, 유사성을 일일이 분석한다면 시간이 매우 오래 걸립니다. 이를 해결하기 위해선 벡터(숫자열) 정보를 저장, 조회, 삭제, 수정 그리고 동시 처리 작업을 원할하게 진행할 수 있는 벡터 데이터베이스를 도입하면됩니다. 여기에서는 데이터베이스를 쉽게 생성, 관리 할 수 있는 Pinecone 벡터 데이터베이스를 사용해보겠습니다.

4. 구현

4.1 텍스트 임베딩

4.1 텍스트 임베딩

Open AI의 text-embedding-ada-002 API를 사용하여 예시 텍스트를 임베딩해보겠습니다.

import openai
openai.api_key = "YOUR_API_KEY"

def text_embedding(text):
    response = openai.Completion.create(
        engine="text-davinci-002",
        prompt= f"embed text: {text}",
        temperature=0,
        max_tokens=256,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0
    )
    embedding = response.choices[0].text
    return embedding

위 함수를 사용하여 텍스트를 임베딩할 수 있습니다. 이 때, YOUR_API_KEY에는 Open AI API 키를 입력해주세요.

text = "당뇨병 환자에서 체중 증가와 혈당 수치의 상관관계는?"
embed = text_embedding(text)
print(embed)

위와 같이 입력하면 아래와 같이 임베딩된 결과를 확인할 수 있습니다.

[-0.461, 0.137, 0.186, 0.065, 0.238,.....]

4.1 데이터베이스 생성

https://docs.pinecone.io/docs 사이트를 보면 쉽게 데이터베이스를 생성하고 파이썬 코드로 접속할 수 있는 방법을 알 수 있습니다.

Pinecone 데이터베이스를 사용하여 벡터를 업로드하는 방법은 다음과 같습니다.

import pinecone

# Pinecone API key를 설정합니다.
pinecone.init(api_key="YOUR_API_KEY")

# 데이터베이스를 생성합니다.
pinecone.create_index(index_name="YOUR_INDEX_NAME", dimension=YOUR_DIMENSION)

# 임베딩 벡터를 리스트로 생성합니다.
vectors = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]]

# 벡터를 데이터베이스에 업로드합니다.
pinecone.insert(index_name="YOUR_INDEX_NAME", data=vectors)

# Pinecone API key를 삭제합니다.
pinecone.deinit()

위 코드에서 YOUR_API_KEY와 YOUR_INDEX_NAME, YOUR_DIMENSION은 사용자가 설정해야 하는 값입니다. YOUR_API_KEY는 Pinecone 계정의 API 키를 입력해야 하며, YOUR_INDEX_NAME은 생성하고자 하는 데이터베이스의 이름을 입력하면 됩니다. YOUR_DIMENSION은 임베딩 벡터의 차원을 나타냅니다.

또한, vectors 리스트에는 업로드할 벡터를 포함시킬 수 있습니다. 벡터는 모두 같은 차원을 가져야 하며, 리스트 내의 각 벡터는 하나의 데이터 포인트를 나타내야 합니다.

4.2 유사한 데이터를 조회

먼저, 데이터베이스에 업로드된 벡터를 사용하여 입력된 질문과 유사한 벡터를 찾는 방법을 알아보겠습니다. 이를 위해서는 pinecone.query() 함수를 사용할 수 있습니다.

import pinecone

# Pinecone API key를 설정합니다.
pinecone.init(api_key="YOUR_API_KEY")

# 데이터베이스 이름을 설정합니다.
index_name = "YOUR_INDEX_NAME"

# 입력된 질문을 임베딩합니다.
question_embedding = text_embedding("당뇨병 환자에서 체중 증가와 혈당 수치의 상관관계는?")

# Pinecone 데이터베이스에서 유사한 벡터를 검색합니다.
result = pinecone.query(index_name=index_name, query=question_embedding, top_k=3)

# 검색된 벡터를 출력합니다.
print(result)

# Pinecone API key를 삭제합니다.
pinecone.deinit()

위 코드에서 YOUR_API_KEY와 YOUR_INDEX_NAME은 사용자가 설정한 값입니다. YOUR_API_KEY는 Pinecone 계정의 API 키를 입력해야 하며, YOUR_INDEX_NAME은 생성한 데이터베이스의 이름을 입력하면 됩니다.

pinecone.query() 함수에서 index_name은 조회할 데이터베이스의 이름입니다. query는 입력된 질문을 임베딩한 벡터입니다. top_k는 조회할 벡터의 수를 나타냅니다. 위 코드에서는 3개의 벡터가 반환됩니다.

위 코드를 실행하면, 입력된 질문과 유사한 벡터 3개가 출력됩니다. 이 벡터는 질문과 유사한 정보를 포함하고 있으므로, 이를 바탕으로 자연스러운 응답을 생성할 수 있습니다.

4.3 ChatGPT에게 답변 생성 요청

이제, 가져온 정보와 입력된 질문을 바탕으로 ChatGPT에게 질문 생성을 요청합니다.

아래는 챗봇 시스템 명령어와 요청 코드입니다.

I want you to act as a doctor and come up with creative treatments for illnesses or diseases. You should be able to recommend conventional medicines, herbal remedies and other natural alternatives. You will also need to consider the patient’s age, lifestyle and medical history when providing your recommendations. 

You will given json file like below, "question" value is patient question message. And "search_result" value is searched result from reliable vector database with "question". You have to answer with "search_result". If all of search_result is not related to "question", you have to say "Sorry, there is no information about question". When search_result has not related to "question", you can ignore it. When you create response, you have to express reference with footnote format.

Below is example of request and response.

User : 
{
    "question": "유방암의 정의에 대해서 알려줘",
    "search_result": [
        {
            "query": "유방암의 정의에 대해서 알려줘",
            "results": [
                {
                    "text": "1. 유방암이란 유방 밖으로 퍼져 생명을 위협할 수 있는 악성 종양으로 유방에 있는 세포 중 어느 것이라도 암이 될 수 있습니다. 일반적으로 유관과 유엽에 존재하는 세포에서 기원합니다. 유방암의 발생률은 세계적으로 증가하고 있으며, 우리나라 여성에게 발생하는 전체암 중 2번째로 흔한 암입니다. 이에 따라 사망률도 증가하고 있지만, 2012년 기준 한국 유방암 연령표준",
                    "metadata": {
                        "source": "유방의학회 1페이지"
                    }
                }
            ]
        }
    ]
}

Response:
방암이란 유방 밖으로 퍼져 생명을 위협할 수 있는 악성 종양으로 유방에 있는 세포 중 어느 것이라도 암이 될 수 있습니다. 일반적으로 유관과 유엽에 존재하는 세포에서 기원합니다. 유방암의 발생률은 세계적으로 증가하고 있으며, 우리나라 여성에게 발생하는 전체암 중 2번째로 흔한 암입니다.[1]

[1] 유방의학회 1페이지
import openai
openai.api_key = "YOUR_API_KEY"

def generate_question(prompt, related_information):
		# prompt = "당뇨병 환자에서 체중 증가와 혈당 수치의 상관관계는?"
    # related_information = "살이 찌면 체내 지방이 증가함에 따라 인슐린 저항성이 증가하고, 인슐린의 효과가 감소합니다. 이로 인해 혈당이 상승할 수 있습니다. 따라서, 당뇨병 환자는 체중을 유지하거나 감량하여 혈당을 조절하는 것이 중요합니다.([1] 이명숙, 이동호, 이은정, 박창수, "당뇨병 환자에서 비만이 인슐린 저항성과 혈당 수준에 미치는 영향," 대한내분비학회지, 2005.)"
		system_command=
    response = oopenai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
            {"role": "system", "content": system_command},
            {"role": "user", "content": {"prompt" : prompt, "related_information" :related_information},
        ]
    )
    question = response.choices[0].text.strip()
    return question

위 코드에서 prompt 변수는 입력된 질문입니다. related_information 변수는 Pinecone 데이터베이스에서 가져온 연관 정보입니다. 이 정보를 바탕으로 ChatGPT에게 질문 생성을 요청합니다.

ChatGPT는 입력된 정보를 바탕으로 자연스러운 질문을 생성한 후 반환합니다. 이후, 이 질문을 사용자에게 전달하면 됩니다.

위 코드를 실행하면 아래와 같이 출력됩니다.

사용자 질문: "저는 당뇨병이 있는데, 살 찌면 항상 혈당이 높아지는데 이유가 무엇인가요?"

챗봇 : 살이 찌면 체내 지방이 증가함에 따라 인슐린 저항성이 증가하고, 인슐린의 효과가 감소합니다. 이로 인해 혈당이 상승할 수 있습니다. 따라서, 당뇨병 환자는 체중을 유지하거나 감량하여 혈당을 조절하는 것이 중요합니다.

이 답변은 검증된 의료 데이터를 바탕으로 생성되었으며, 출처는 의료 전문가가 작성한 논문입니다. 해당 논문의 출처는 다음과 같습니다: [1]

감사합니다.

[1] 이명숙, 이동호, 이은정, 박창수, "당뇨병 환자에서 비만이 인슐린 저항성과 혈당 수준에 미치는 영향," 대한내분비학회지, 2005.

이와 같이 출처를 명시한 자연스러운 질문을 생성할 수 있습니다.

5. 한계

데이터베이스를 이용해 검증된 정보를 바탕으로 답변을 생성하도록 할 수 있지만, 이 방법에는 몇 가지 한계가 있습니다. 첫째로, 데이터베이스에 업로드된 정보가 모든 상황에서 적절하지 않을 수 있습니다. 예를 들어, 새로운 질병이 발생했을 때, 이에 대한 정보가 데이터베이스에 없는 경우가 있을 수 있습니다. 둘째로, 데이터베이스에서 검색된 정보가 질문과 관련이 없는 경우가 있을 수 있습니다. 이 경우, 적절한 대답을 제공할 수 없습니다. 마지막으로, 데이터베이스에 업로드된 정보가 충분하지 않은 경우도 있습니다. 이 경우, 적절한 대답을 제공할 수 없습니다. 따라서, 데이터베이스를 이용해 검증된 정보를 바탕으로 답변을 생성하도록 할 때는 이러한 한계를 염두에 두어야 합니다.

ChatGPT를 이용해 검증된 정보를 기반으로 응답 생성 시도

1. 시도 이유

기존 챗봇은 아래와 같은 문제를 가지고 있다.

  1. 정적인 대답, 이는 구글이나 네이버와 같은 검색 엔진과 차별점이 존재하지 않는다.
  2. 문맥 파악 불가, “내가 기저질환이 ~~ 한 편인데, 유방암이 발병할 가능성이 높을까?”와 같은 문맥을 파악한 응답이 불가능하다.

GPT의 경우, 문맥을 파악한 동적인 대답이 가능하지만 아래와 같은 문제가 있다.

  1. 2021년 이전의 데이터셋을 바탕으로 개발되었기 때문에 최신 정보에 접근이 불가능하다.
  2. 인터넷에 있는 정보를 사용하였기 때문에 신뢰성이 떨어진다.
  3. 존재하지도 않는 정보를 생성하는 Hallucination 이 발생한다.

즉, 위 두 챗봇은 “검증된 정보로 사용자를 고려해 적절한 답변을 제시한다.”라는 목표에 도달하기 어렵다. 이를 해결하기 위해서는 검증된 데이터를 확보하고 그 데이터와 사용자 정보를 입력받아 답변을 생성하는 기술이 필요하다.

2. 목표

따라서, 이번 개발의 목표와 결과 예시는 다음과 같다.

  1. 검증된 데이터만으로 답변을 생성하는 기능
  2. 데이터와 사용자 정보를 입력받아 적절한 답변을 생성하는 기능
  3. 답변에 필요한 정보가 존재하지 않는 경우 이를 알리는 기능
  4. 답변에 쓰인 데이터의 출처를 명시하는 기능

결과 예시

사용자 질문: "저는 당뇨병이 있는데, 살 찌면 항상 혈당이 높아지는데 이유가 무엇인가요?"

봇보답: 살이 찌면 체내 지방이 증가함에 따라 인슐린 저항성이 증가하고, 인슐린의 효과가 감소합니다. 이로 인해 혈당이 상승할 수 있습니다. 따라서, 당뇨병 환자는 체중을 유지하거나 감량하여 혈당을 조절하는 것이 중요합니다.

이 답변은 검증된 의료 데이터를 바탕으로 생성되었으며, 출처는 의료 전문가가 작성한 논문입니다. 해당 논문의 출처는 다음과 같습니다: [1]

감사합니다.

[1] 이명숙, 이동호, 이은정, 박창수, "당뇨병 환자에서 비만이 인슐린 저항성과 혈당 수준에 미치는 영향," 대한내분비학회지, 2005.

3. 방법

목표를 구현하기 위해선 아래 기능이 필요하다.

  1. 사용자 질문을 받아 비슷한 정보를 검색하는 기능
  2. 검증된 정보와 출처를 데이터베이스에 저장하는 기능
  3. 사용자 질문, 정보 그리고 출처를 바탕으로 자연스러운 응답을 생성하는 기능

기능은 비교적 간단하다. 그러나 높은 효과를 보기 위해선 제약조건이 존재한다.

예를 들어서, 검증정보가 기하급수적으로 증가해 정보 검색을 하는데 수 십분이 걸리게 되는 경우 서비스로 제공할 수 없다. 또한, 사용자 질문과 무관한 정보가 검색되는 경우 잘못된 응답이 생성된다. 이를 해결하기 위해 의미론적 텍스트 임베딩 기술과 벡터 데이터베이스가 필요하다.

3.1 텍스트 임베딩

텍스트 임베딩을 영어, 한글과 같은 자연어를 컴퓨터가 이해하기 쉽도록 숫자열 형태로 변환하는 것을 말한다. 이 떄, 단순히 형태를 바탕으로 임베딩을 진행할 경우 문제가 발생한다. "사랑", "애정", "모정", "애착" 등의 단어는 유사한 단어이지만, 단순히 형태를 보면 차이가 존재해 [1,1,1…] , [20,9,11…] 등과 같이 임베딩을 진행할 경우 거리가 생길 수 있다. 따라서 의미를 충분히 고려한 임베딩 기술이 필요하다. 이번 구현에는 Open AI의 text-embedding-ada-002 를 사용하도록 하겠다.

3.2 벡터 데이터베이스

텍스트 임베딩이 잘 수행되었다면, 다음은 사용자 질문과 유사항 정보를 빠르게 찾는 것이 문제입니다. 임베딩 결과는 매우 긴 숫자열이며, 정보가 10만 줄이기만 해도, 유사성을 일일이 분석한다면 시간이 매우 오래 걸립니다. 이를 해결하기 위해선 벡터(숫자열) 정보를 저장, 조회, 삭제, 수정 그리고 동시 처리 작업을 원할하게 진행할 수 있는 벡터 데이터베이스를 도입하면됩니다. 여기에서는 데이터베이스를 쉽게 생성, 관리 할 수 있는 Pinecone 벡터 데이터베이스를 사용해보겠습니다.

4. 구현

4.1 텍스트 임베딩

4.1 텍스트 임베딩

Open AI의 text-embedding-ada-002 API를 사용하여 예시 텍스트를 임베딩해보겠습니다.

import openai
openai.api_key = "YOUR_API_KEY"

def text_embedding(text):
    response = openai.Completion.create(
        engine="text-davinci-002",
        prompt= f"embed text: {text}",
        temperature=0,
        max_tokens=256,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0
    )
    embedding = response.choices[0].text
    return embedding

위 함수를 사용하여 텍스트를 임베딩할 수 있습니다. 이 때, YOUR_API_KEY에는 Open AI API 키를 입력해주세요.

text = "당뇨병 환자에서 체중 증가와 혈당 수치의 상관관계는?"
embed = text_embedding(text)
print(embed)

위와 같이 입력하면 아래와 같이 임베딩된 결과를 확인할 수 있습니다.

[-0.461, 0.137, 0.186, 0.065, 0.238,.....]

4.1 데이터베이스 생성

https://docs.pinecone.io/docs 사이트를 보면 쉽게 데이터베이스를 생성하고 파이썬 코드로 접속할 수 있는 방법을 알 수 있습니다.

Pinecone 데이터베이스를 사용하여 벡터를 업로드하는 방법은 다음과 같습니다.

import pinecone

# Pinecone API key를 설정합니다.
pinecone.init(api_key="YOUR_API_KEY")

# 데이터베이스를 생성합니다.
pinecone.create_index(index_name="YOUR_INDEX_NAME", dimension=YOUR_DIMENSION)

# 임베딩 벡터를 리스트로 생성합니다.
vectors = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]]

# 벡터를 데이터베이스에 업로드합니다.
pinecone.insert(index_name="YOUR_INDEX_NAME", data=vectors)

# Pinecone API key를 삭제합니다.
pinecone.deinit()

위 코드에서 YOUR_API_KEY와 YOUR_INDEX_NAME, YOUR_DIMENSION은 사용자가 설정해야 하는 값입니다. YOUR_API_KEY는 Pinecone 계정의 API 키를 입력해야 하며, YOUR_INDEX_NAME은 생성하고자 하는 데이터베이스의 이름을 입력하면 됩니다. YOUR_DIMENSION은 임베딩 벡터의 차원을 나타냅니다.

또한, vectors 리스트에는 업로드할 벡터를 포함시킬 수 있습니다. 벡터는 모두 같은 차원을 가져야 하며, 리스트 내의 각 벡터는 하나의 데이터 포인트를 나타내야 합니다.

4.2 유사한 데이터를 조회

먼저, 데이터베이스에 업로드된 벡터를 사용하여 입력된 질문과 유사한 벡터를 찾는 방법을 알아보겠습니다. 이를 위해서는 pinecone.query() 함수를 사용할 수 있습니다.

import pinecone

# Pinecone API key를 설정합니다.
pinecone.init(api_key="YOUR_API_KEY")

# 데이터베이스 이름을 설정합니다.
index_name = "YOUR_INDEX_NAME"

# 입력된 질문을 임베딩합니다.
question_embedding = text_embedding("당뇨병 환자에서 체중 증가와 혈당 수치의 상관관계는?")

# Pinecone 데이터베이스에서 유사한 벡터를 검색합니다.
result = pinecone.query(index_name=index_name, query=question_embedding, top_k=3)

# 검색된 벡터를 출력합니다.
print(result)

# Pinecone API key를 삭제합니다.
pinecone.deinit()

위 코드에서 YOUR_API_KEY와 YOUR_INDEX_NAME은 사용자가 설정한 값입니다. YOUR_API_KEY는 Pinecone 계정의 API 키를 입력해야 하며, YOUR_INDEX_NAME은 생성한 데이터베이스의 이름을 입력하면 됩니다.

pinecone.query() 함수에서 index_name은 조회할 데이터베이스의 이름입니다. query는 입력된 질문을 임베딩한 벡터입니다. top_k는 조회할 벡터의 수를 나타냅니다. 위 코드에서는 3개의 벡터가 반환됩니다.

위 코드를 실행하면, 입력된 질문과 유사한 벡터 3개가 출력됩니다. 이 벡터는 질문과 유사한 정보를 포함하고 있으므로, 이를 바탕으로 자연스러운 응답을 생성할 수 있습니다.

4.3 ChatGPT에게 답변 생성 요청

이제, 가져온 정보와 입력된 질문을 바탕으로 ChatGPT에게 질문 생성을 요청합니다.

아래는 챗봇 시스템 명령어와 요청 코드입니다.

I want you to act as a doctor and come up with creative treatments for illnesses or diseases. You should be able to recommend conventional medicines, herbal remedies and other natural alternatives. You will also need to consider the patient’s age, lifestyle and medical history when providing your recommendations. 

You will given json file like below, "question" value is patient question message. And "search_result" value is searched result from reliable vector database with "question". You have to answer with "search_result". If all of search_result is not related to "question", you have to say "Sorry, there is no information about question". When search_result has not related to "question", you can ignore it. When you create response, you have to express reference with footnote format.

Below is example of request and response.

User : 
{
    "question": "유방암의 정의에 대해서 알려줘",
    "search_result": [
        {
            "query": "유방암의 정의에 대해서 알려줘",
            "results": [
                {
                    "text": "1. 유방암이란 유방 밖으로 퍼져 생명을 위협할 수 있는 악성 종양으로 유방에 있는 세포 중 어느 것이라도 암이 될 수 있습니다. 일반적으로 유관과 유엽에 존재하는 세포에서 기원합니다. 유방암의 발생률은 세계적으로 증가하고 있으며, 우리나라 여성에게 발생하는 전체암 중 2번째로 흔한 암입니다. 이에 따라 사망률도 증가하고 있지만, 2012년 기준 한국 유방암 연령표준",
                    "metadata": {
                        "source": "유방의학회 1페이지"
                    }
                }
            ]
        }
    ]
}

Response:
방암이란 유방 밖으로 퍼져 생명을 위협할 수 있는 악성 종양으로 유방에 있는 세포 중 어느 것이라도 암이 될 수 있습니다. 일반적으로 유관과 유엽에 존재하는 세포에서 기원합니다. 유방암의 발생률은 세계적으로 증가하고 있으며, 우리나라 여성에게 발생하는 전체암 중 2번째로 흔한 암입니다.[1]

[1] 유방의학회 1페이지
import openai
openai.api_key = "YOUR_API_KEY"

def generate_question(prompt, related_information):
		# prompt = "당뇨병 환자에서 체중 증가와 혈당 수치의 상관관계는?"
    # related_information = "살이 찌면 체내 지방이 증가함에 따라 인슐린 저항성이 증가하고, 인슐린의 효과가 감소합니다. 이로 인해 혈당이 상승할 수 있습니다. 따라서, 당뇨병 환자는 체중을 유지하거나 감량하여 혈당을 조절하는 것이 중요합니다.([1] 이명숙, 이동호, 이은정, 박창수, "당뇨병 환자에서 비만이 인슐린 저항성과 혈당 수준에 미치는 영향," 대한내분비학회지, 2005.)"
		system_command=
    response = oopenai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
            {"role": "system", "content": system_command},
            {"role": "user", "content": {"prompt" : prompt, "related_information" :related_information},
        ]
    )
    question = response.choices[0].text.strip()
    return question

위 코드에서 prompt 변수는 입력된 질문입니다. related_information 변수는 Pinecone 데이터베이스에서 가져온 연관 정보입니다. 이 정보를 바탕으로 ChatGPT에게 질문 생성을 요청합니다.

ChatGPT는 입력된 정보를 바탕으로 자연스러운 질문을 생성한 후 반환합니다. 이후, 이 질문을 사용자에게 전달하면 됩니다.

위 코드를 실행하면 아래와 같이 출력됩니다.

사용자 질문: "저는 당뇨병이 있는데, 살 찌면 항상 혈당이 높아지는데 이유가 무엇인가요?"

챗봇 : 살이 찌면 체내 지방이 증가함에 따라 인슐린 저항성이 증가하고, 인슐린의 효과가 감소합니다. 이로 인해 혈당이 상승할 수 있습니다. 따라서, 당뇨병 환자는 체중을 유지하거나 감량하여 혈당을 조절하는 것이 중요합니다.

이 답변은 검증된 의료 데이터를 바탕으로 생성되었으며, 출처는 의료 전문가가 작성한 논문입니다. 해당 논문의 출처는 다음과 같습니다: [1]

감사합니다.

[1] 이명숙, 이동호, 이은정, 박창수, "당뇨병 환자에서 비만이 인슐린 저항성과 혈당 수준에 미치는 영향," 대한내분비학회지, 2005.

이와 같이 출처를 명시한 자연스러운 질문을 생성할 수 있습니다.

5. 한계

데이터베이스를 이용해 검증된 정보를 바탕으로 답변을 생성하도록 할 수 있지만, 이 방법에는 몇 가지 한계가 있습니다. 첫째로, 데이터베이스에 업로드된 정보가 모든 상황에서 적절하지 않을 수 있습니다. 예를 들어, 새로운 질병이 발생했을 때, 이에 대한 정보가 데이터베이스에 없는 경우가 있을 수 있습니다. 둘째로, 데이터베이스에서 검색된 정보가 질문과 관련이 없는 경우가 있을 수 있습니다. 이 경우, 적절한 대답을 제공할 수 없습니다. 마지막으로, 데이터베이스에 업로드된 정보가 충분하지 않은 경우도 있습니다. 이 경우, 적절한 대답을 제공할 수 없습니다. 따라서, 데이터베이스를 이용해 검증된 정보를 바탕으로 답변을 생성하도록 할 때는 이러한 한계를 염두에 두어야 합니다.

+ Recent posts