1. 소개
이 Codelab에서는 Google Cloud의 Vertex AI에서 호스팅되는 Gemini 대규모 언어 모델 (LLM)에 중점을 둡니다. Vertex AI는 Google Cloud의 모든 머신러닝 제품, 서비스, 모델을 아우르는 플랫폼입니다.
Java를 사용하여 LangChain4j 프레임워크를 통해 Gemini API와 상호작용합니다. 질문 응답, 아이디어 생성, 항목 및 구조화된 콘텐츠 추출, 검색 증강 생성, 함수 호출에 LLM을 활용하는 구체적인 예를 살펴봅니다.
생성형 AI란 무엇인가요?
생성형 AI는 인공지능을 사용하여 텍스트, 이미지, 음악, 오디오, 동영상과 같은 새로운 콘텐츠를 만드는 것을 의미합니다.
생성형 AI는 요약, Q&A, 분류 등의 바로 사용 가능한 태스크를 멀티태스킹하고 수행할 수 있는 대규모 언어 모델 (LLM)을 기반으로 합니다. 최소한의 학습으로 기반 모델을 매우 적은 예시 데이터로 대상 사용 사례에 맞게 조정할 수 있습니다.
생성형 AI는 어떻게 작동하나요?
생성형 AI는 머신러닝 (ML) 모델을 사용하여 사람이 만든 콘텐츠의 데이터 세트에서 패턴과 관계를 학습하는 방식으로 작동합니다. 그런 다음 학습된 패턴을 사용하여 새 콘텐츠를 생성합니다.
생성형 AI 모델을 학습시키는 가장 일반적인 방법은 지도 학습을 사용하는 것입니다. 모델에는 사람이 만든 콘텐츠와 해당 라벨의 집합이 부여됩니다. 그런 다음 사람이 만든 콘텐츠와 유사한 콘텐츠를 생성하는 방법을 학습합니다.
일반적인 생성형 AI 애플리케이션에는 어떤 것이 있나요?
생성형 AI는 다음과 같은 용도로 사용할 수 있습니다.
- 향상된 채팅 및 검색 환경을 통해 고객 상호작용을 개선하세요.
- 대화형 인터페이스 및 요약을 통해 방대한 양의 구조화되지 않은 데이터를 탐색하세요.
- 제안 요청에 답장하고, 다양한 언어로 마케팅 콘텐츠를 현지화하고, 고객 계약이 규정을 준수하는지 확인하는 등의 반복적인 작업을 지원합니다.
Google Cloud에는 어떤 생성형 AI 제품이 있나요?
Vertex AI를 사용하면 ML 전문 지식이 거의 또는 전혀 없어도 기반 모델과 상호작용하고, 이를 맞춤설정하며, 애플리케이션에 임베딩할 수 있습니다. Model Garden에서 기반 모델에 액세스하거나, Vertex AI Studio에서 간단한 UI를 통해 모델을 조정하거나, 데이터 과학 노트북에서 모델을 사용할 수 있습니다.
Vertex AI Search 및 Conversation은 개발자가 생성형 AI 기반 검색엔진과 챗봇을 빌드할 수 있는 가장 빠른 방법을 제공합니다.
Gemini를 기반으로 하는 Google Cloud용 Gemini는 Google Cloud와 IDE 전반에서 사용할 수 있는 AI 기반 공동작업 도구로, 더 많은 작업을 더 빠르게 수행할 수 있도록 지원합니다. Gemini Code Assist는 코드 완성, 코드 생성, 코드 설명을 제공하며, 채팅을 통해 기술적 질문을 할 수 있습니다.
Gemini란 무엇인가요?
Gemini는 멀티모달 사용 사례를 위해 설계되고 Google DeepMind에서 개발된 생성형 AI 모델 제품군 중 하나입니다. 멀티모달은 텍스트, 코드, 이미지, 오디오와 같은 다양한 종류의 콘텐츠를 처리하고 생성할 수 있다는 뜻입니다.
Gemini는 다양한 변형과 크기로 제공됩니다.
- Gemini Ultra: 복잡한 작업을 위한 가장 크고 강력한 버전입니다.
- Gemini Flash: 가장 빠르고 비용 효율적이며 대용량 작업에 최적화되어 있습니다.
- Gemini Pro: 중간 규모로, 다양한 작업에 걸쳐 확장할 수 있도록 최적화되어 있습니다.
- Gemini Nano: 온디바이스 작업을 위해 설계된 가장 효율적인 모델입니다.
주요 특징:
- 멀티모달리티: Gemini가 여러 정보 형식을 이해하고 처리하는 기능은 기존의 텍스트 전용 언어 모델을 뛰어넘는 중요한 발전입니다.
- 성능: Gemini Ultra는 여러 벤치마크에서 현재 최신 기술보다 우수한 성능을 보이며, 까다로운 MMLU (대규모 멀티태스킹 언어 이해) 벤치마크에서 인간 전문가를 능가한 최초의 모델입니다.
- 유연성: 다양한 Gemini 크기를 사용하면 대규모 연구에서 휴대기기 배포에 이르기까지 다양한 사용 사례에 맞게 조정할 수 있습니다.
Java에서 Vertex AI의 Gemini와 상호작용하는 방법은 무엇인가요?
다음 두 가지 옵션이 있습니다.
- 공식 Gemini용 Vertex AI Java API 라이브러리입니다.
- LangChain4j 프레임워크
이 Codelab에서는 LangChain4j 프레임워크를 사용합니다.
LangChain4j 프레임워크란 무엇인가요?
LangChain4j 프레임워크는 LLM 자체와 같은 다양한 구성요소뿐만 아니라 벡터 데이터베이스 (시맨틱 검색용), 문서 로더 및 스플리터 (문서를 분석하고 문서에서 학습), 출력 파서 등 다른 도구를 조정하여 Java 애플리케이션에 LLM을 통합하기 위한 오픈소스 라이브러리입니다.
이 프로젝트는 LangChain Python 프로젝트에서 영감을 얻었지만 Java 개발자를 지원하는 것을 목표로 합니다.
학습할 내용
- Gemini 및 LangChain4j를 사용하도록 Java 프로젝트를 설정하는 방법
- 프로그래매틱 방식으로 첫 번째 프롬프트를 Gemini에 전송하는 방법
- Gemini에서 대답을 스트리밍하는 방법
- 사용자와 Gemini 간의 대화를 만드는 방법
- 텍스트와 이미지를 모두 전송하여 멀티모달 컨텍스트에서 Gemini를 사용하는 방법
- 비정형 콘텐츠에서 유용한 구조화된 정보를 추출하는 방법
- 프롬프트 템플릿을 조작하는 방법
- 감정 분석과 같은 텍스트 분류 방법
- 자체 문서와 채팅하는 방법 (검색 증강 생성)
- 함수 호출을 사용하여 챗봇을 확장하는 방법
- Ollama 및 TestContainers로 로컬에서 Gemma를 사용하는 방법
필요한 항목
- Java 프로그래밍 언어에 대한 지식
- Google Cloud 프로젝트
- Chrome 또는 Firefox와 같은 브라우저
2. 설정 및 요건
자습형 환경 설정
- Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.
- 프로젝트 이름은 이 프로젝트 참가자의 표시 이름입니다. 이는 Google API에서 사용하지 않는 문자열이며 언제든지 업데이트할 수 있습니다.
- 프로젝트 ID는 모든 Google Cloud 프로젝트에서 고유하며, 변경할 수 없습니다(설정된 후에는 변경할 수 없음). Cloud 콘솔은 고유한 문자열을 자동으로 생성합니다. 일반적으로는 신경 쓰지 않아도 됩니다. 대부분의 Codelab에서는 프로젝트 ID (일반적으로
PROJECT_ID
로 식별됨)를 참조해야 합니다. 생성된 ID가 마음에 들지 않으면 다른 임의 ID를 생성할 수 있습니다. 또는 직접 시도해 보고 사용 가능한지 확인할 수도 있습니다. 이 단계 이후에는 변경할 수 없으며 프로젝트 기간 동안 유지됩니다. - 참고로 세 번째 값은 일부 API에서 사용하는 프로젝트 번호입니다. 이 세 가지 값에 대한 자세한 내용은 문서를 참고하세요.
- 다음으로 Cloud 리소스/API를 사용하려면 Cloud 콘솔에서 결제를 사용 설정해야 합니다. 이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 튜토리얼이 끝난 후에 요금이 청구되지 않도록 리소스를 종료하려면 만든 리소스 또는 프로젝트를 삭제하면 됩니다. Google Cloud 신규 사용자는 300달러(USD) 상당의 무료 체험판 프로그램에 참여할 수 있습니다.
Cloud Shell 시작
Google Cloud를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Cloud Shell을 사용합니다.
Cloud Shell 활성화
- Cloud Console에서 Cloud Shell 활성화
를 클릭합니다.
Cloud Shell을 처음 시작하는 경우 Cloud Shell에 대한 설명이 포함된 중간 화면이 표시됩니다. 중간 화면이 표시되면 계속을 클릭합니다.
Cloud Shell을 프로비저닝하고 연결하는 데 몇 분 정도만 걸립니다.
이 가상 머신에는 필요한 모든 개발 도구가 로드되어 있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 Codelab에서 대부분의 작업은 브라우저만으로도 수행할 수 있습니다.
Cloud Shell에 연결되면 인증이 완료되었고 프로젝트가 해당 프로젝트 ID로 설정된 것을 확인할 수 있습니다.
- Cloud Shell에서 다음 명령어를 실행하여 인증되었는지 확인합니다.
gcloud auth list
명령어 결과
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com> To set the active account, run: $ gcloud config set account `ACCOUNT`
- Cloud Shell에서 다음 명령어를 실행하여 gcloud 명령어가 프로젝트를 알고 있는지 확인합니다.
gcloud config list project
명령어 결과
[core] project = <PROJECT_ID>
또는 다음 명령어로 설정할 수 있습니다.
gcloud config set project <PROJECT_ID>
명령어 결과
Updated property [core/project].
3. 개발 환경 준비
이 Codelab에서는 Cloud Shell 터미널과 Cloud Shell 편집기를 사용하여 Java 프로그램을 개발합니다.
Vertex AI API 사용 설정
Google Cloud 콘솔에서 프로젝트 이름이 Google Cloud 콘솔 상단에 표시되는지 확인합니다. 프로젝트가 선택되어 있지 않으면 프로젝트 선택을 클릭하여 프로젝트 선택기를 열고 원하는 프로젝트를 선택합니다.
Google Cloud 콘솔의 Vertex AI 섹션 또는 Cloud Shell 터미널에서 Vertex AI API를 사용 설정할 수 있습니다.
Google Cloud 콘솔에서 사용 설정하려면 먼저 Google Cloud 콘솔 메뉴의 Vertex AI 섹션으로 이동합니다.
Vertex AI 대시보드에서 모든 권장 API 사용 설정을 클릭합니다.
이렇게 하면 여러 API가 사용 설정되지만 이 Codelab에서 가장 중요한 API는 aiplatform.googleapis.com
입니다.
또는 다음 명령어를 사용하여 Cloud Shell 터미널에서 이 API를 사용 설정할 수도 있습니다.
gcloud services enable aiplatform.googleapis.com
GitHub 저장소를 클론합니다.
Cloud Shell 터미널에서 이 Codelab의 저장소를 클론합니다.
git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git
프로젝트를 실행할 준비가 되었는지 확인하려면 'Hello World' 프로그램을 실행해 보세요.
최상위 폴더에 있는지 확인합니다.
cd gemini-workshop-for-java-developers/
Gradle 래퍼를 만듭니다.
gradle wrapper
gradlew
로 실행합니다.
./gradlew run
다음과 같은 출력이 표시됩니다.
.. > Task :app:run Hello World!
Cloud 편집기 열기 및 설정하기
Cloud Shell에서 Cloud Code Editor를 사용하여 코드를 엽니다.
Cloud Code 편집기에서 File
-> Open Folder
를 선택하여 codelab 소스 폴더를 열고 codelab 소스 폴더 (예: /home/username/gemini-workshop-for-java-developers/
)에 복사합니다.
환경 변수 설정
Terminal
-> New Terminal
를 선택하여 Cloud Code 편집기에서 새 터미널을 엽니다. 코드 예시를 실행하는 데 필요한 두 가지 환경 변수를 설정합니다.
- PROJECT_ID: Google Cloud 프로젝트 ID입니다.
- LOCATION: Gemini 모델이 배포되는 리전입니다.
다음과 같이 변수를 내보냅니다.
export PROJECT_ID=$(gcloud config get-value project) export LOCATION=us-central1
4. Gemini 모델을 처음 호출
이제 프로젝트가 올바르게 설정되었으므로 Gemini API를 호출할 차례입니다.
app/src/main/java/gemini/workshop
디렉터리의 QA.java
를 살펴봅니다.
package gemini.workshop;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;
public class QA {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.build();
System.out.println(model.generate("Why is the sky blue?"));
}
}
이 첫 번째 예에서는 ChatModel
인터페이스를 구현하는 VertexAiGeminiChatModel
클래스를 가져와야 합니다.
main
메서드에서 VertexAiGeminiChatModel
의 빌더를 사용하여 채팅 언어 모델을 구성하고 다음을 지정합니다.
- 프로젝트
- 위치
- 모델 이름 (
gemini-1.5-flash-002
)
이제 언어 모델이 준비되었으므로 generate()
메서드를 호출하고 프롬프트, 질문 또는 안내를 전달하여 LLM에 보낼 수 있습니다. 여기서는 하늘이 파란 이유에 관한 간단한 질문을 던집니다.
다른 질문이나 작업을 시도해 보려면 언제든지 이 프롬프트를 변경하세요.
소스 코드 루트 폴더에서 샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.QA
다음과 비슷한 출력이 표시됩니다.
The sky appears blue because of a phenomenon called Rayleigh scattering. When sunlight enters the atmosphere, it is made up of a mixture of different wavelengths of light, each with a different color. The different wavelengths of light interact with the molecules and particles in the atmosphere in different ways. The shorter wavelengths of light, such as those corresponding to blue and violet light, are more likely to be scattered in all directions by these particles than the longer wavelengths of light, such as those corresponding to red and orange light. This is because the shorter wavelengths of light have a smaller wavelength and are able to bend around the particles more easily. As a result of Rayleigh scattering, the blue light from the sun is scattered in all directions, and it is this scattered blue light that we see when we look up at the sky. The blue light from the sun is not actually scattered in a single direction, so the color of the sky can vary depending on the position of the sun in the sky and the amount of dust and water droplets in the atmosphere.
Gemini에 처음으로 문의하신 것을 축하드립니다.
응답 스트리밍
몇 초 후 한 번에 응답이 제공되었는지 확인해 보셨나요? 스트리밍 응답 변형 덕분에 점진적으로 응답을 가져올 수도 있습니다. 스트리밍 응답: 모델은 사용 가능해질 때마다 응답을 조금씩 반환합니다.
이 Codelab에서는 스트리밍이 아닌 응답을 사용하지만 스트리밍 응답을 살펴보고 이를 실행하는 방법을 알아보겠습니다.
app/src/main/java/gemini/workshop
디렉터리의 StreamQA.java
에서 스트리밍 응답이 실행되는 모습을 확인할 수 있습니다.
package gemini.workshop;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;
import static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;
public class StreamQA {
public static void main(String[] args) {
StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(4000)
.build();
model.generate("Why is the sky blue?", onNext(System.out::println));
}
}
이번에는 StreamingChatLanguageModel
인터페이스를 구현하는 스트리밍 클래스 변형 VertexAiGeminiStreamingChatModel
를 가져옵니다. Java 람다 표현식으로 스트리밍 핸들러를 만들기 위한 StreamingResponseHandler
를 제공하는 편의 메서드인 LambdaStreamingResponseHandler.onNext
도 정적 가져와야 합니다.
이번에는 generate()
메서드의 서명이 약간 다릅니다. 문자열을 반환하는 대신 반환 유형은 void입니다. 프롬프트 외에도 스트리밍 응답 핸들러를 전달해야 합니다. 여기서는 위에 언급한 정적 가져오기 덕분에 onNext()
메서드에 전달하는 람다 표현식을 정의할 수 있습니다. 람다 표현식은 새 응답 부분을 사용할 수 있을 때마다 호출되는 반면, 후자는 오류가 발생하는 경우에만 호출됩니다.
실행:
./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA
이전 클래스와 비슷한 대답이 표시되지만 이번에는 전체 대답이 표시될 때까지 기다리지 않고 답변이 셸에 점진적으로 표시됩니다.
추가 구성
구성의 경우 프로젝트, 위치, 모델 이름만 정의했지만 모델에 지정할 수 있는 다른 매개변수도 있습니다.
temperature(Float temp)
: 응답의 창의성을 정의합니다 (0은 창의성이 낮고 사실에 기반한 응답을 의미하며 2는 보다 창의적인 결과를 의미함).topP(Float topP)
: 총 확률이 부동 소수점 수 (0과 1 사이)와 합산되는 가능한 단어를 선택합니다.topK(Integer topK)
: 텍스트 자동 완성에 사용될 수 있는 최대 개수의 단어 (1~40개) 중에서 무작위로 단어를 선택합니다.maxOutputTokens(Integer max)
: 모델에서 제공하는 답변의 최대 길이를 지정합니다 (일반적으로 토큰 4개는 대략 3단어를 나타냅니다).maxRetries(Integer retries)
: 시간당 요청 할당량을 초과하거나 플랫폼에 기술적 문제가 있는 경우 모델이 호출을 3번 다시 시도하도록 할 수 있습니다.
지금까지 Gemini에게 한 번의 질문을 했지만 여러 번의 대화를 나눌 수도 있습니다. 다음 섹션에서 이를 살펴봅니다.
5. Gemini와 채팅하기
이전 단계에서는 하나의 질문을 했습니다. 이제 사용자와 LLM 간에 실제 대화를 나눠 보겠습니다. 각 질문과 답변은 이전 질문과 답변을 토대로 실제 토론을 형성할 수 있습니다.
app/src/main/java/gemini/workshop
폴더의 Conversation.java
를 살펴봅니다.
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
import java.util.List;
public class Conversation {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.build();
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
interface ConversationService {
String chat(String message);
}
ConversationService conversation =
AiServices.builder(ConversationService.class)
.chatLanguageModel(model)
.chatMemory(chatMemory)
.build();
List.of(
"Hello!",
"What is the country where the Eiffel tower is situated?",
"How many inhabitants are there in that country?"
).forEach( message -> {
System.out.println("\nUser: " + message);
System.out.println("Gemini: " + conversation.chat(message));
});
}
}
이 클래스의 몇 가지 새로운 흥미로운 가져오기:
MessageWindowChatMemory
: 대화의 멀티턴 측면을 처리하고 이전 질문과 답변을 로컬 메모리에 유지하는 데 도움이 되는 클래스입니다.AiServices
: 채팅 모델과 채팅 메모리를 연결하는 상위 수준의 추상화 클래스
메인 메서드에서 모델, 채팅 메모리, AI 서비스를 설정합니다. 모델은 평소와 같이 프로젝트, 위치, 모델 이름 정보로 구성됩니다.
채팅 메모리의 경우 MessageWindowChatMemory
의 빌더를 사용하여 교환된 마지막 20개의 메시지를 유지하는 메모리를 만듭니다. 컨텍스트가 Java 클래스 클라이언트에 로컬로 유지되는 대화에 대한 슬라이딩 창입니다.
그런 다음 채팅 모델을 채팅 메모리에 바인딩하는 AI service
를 만듭니다.
AI 서비스가 정의된 커스텀 ConversationService
인터페이스를 사용하는 방법을 살펴보세요. 이 인터페이스는 LangChain4j에서 구현하며 String
쿼리를 받아 String
응답을 반환합니다.
이제 Gemini와 대화를 나눠 보세요. 먼저 간단한 인사말이 전송된 후 에펠탑이 있는 국가를 알아보기 위한 첫 번째 질문이 표시됩니다. 마지막 문장은 첫 번째 질문의 답변과 관련이 있습니다. 에펠탑이 있는 국가의 인구가 얼마나 되는지 궁금해하면서 이전 답변에서 언급된 국가를 명시적으로 언급하지 않았기 때문입니다. 이전 질문과 답변이 모든 프롬프트와 함께 전송되는 것을 보여줍니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation
다음과 유사한 세 가지 답변이 표시됩니다.
User: Hello! Gemini: Hi there! How can I assist you today? User: What is the country where the Eiffel tower is situated? Gemini: France User: How many inhabitants are there in that country? Gemini: As of 2023, the population of France is estimated to be around 67.8 million.
Gemini에게 단일 질문을 하거나 여러 번에 걸쳐 대화할 수 있지만 지금까지는 텍스트만 입력할 수 있었습니다. 이미지는 어떻게 되나요? 다음 단계에서 이미지를 살펴보겠습니다.
6. Gemini의 멀티모달리티
Gemini는 멀티모달 모델입니다. 텍스트뿐만 아니라 이미지나 동영상도 입력으로 허용합니다. 이 섹션에서는 텍스트와 이미지를 혼합하는 사용 사례를 확인할 수 있습니다.
Gemini가 이 고양이를 인식할까요?
눈 위에 있는 고양이 사진(위키피디아에서 가져옴)https://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg
app/src/main/java/gemini/workshop
디렉터리의 Multimodal.java
를 살펴봅니다.
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
public class Multimodal {
static final String CAT_IMAGE_URL =
"https://upload.wikimedia.org/wikipedia/" +
"commons/b/b6/Felis_catus-cat_on_snow.jpg";
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.build();
UserMessage userMessage = UserMessage.from(
ImageContent.from(CAT_IMAGE_URL),
TextContent.from("Describe the picture")
);
Response<AiMessage> response = model.generate(userMessage);
System.out.println(response.content().text());
}
}
가져오기에서 다양한 종류의 메시지와 콘텐츠를 구분합니다. UserMessage
에는 TextContent
객체와 ImageContent
객체가 모두 포함될 수 있습니다. 텍스트와 이미지를 혼합하는 멀티모달이 작동하는 것입니다. 간단한 문자열 프롬프트만 전송하는 것이 아니라 이미지 콘텐츠와 텍스트 콘텐츠로 구성된 사용자 메시지를 나타내는 더 구조화된 객체를 전송합니다. 모델은 AiMessage
가 포함된 Response
를 다시 전송합니다.
그런 다음 content()
를 통해 응답에서 AiMessage
를 가져오고 text()
를 통해 메시지 텍스트를 가져옵니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal
사진의 이름을 통해 사진에 포함된 내용을 알 수 있지만 Gemini 출력은 다음과 유사합니다.
A cat with brown fur is walking in the snow. The cat has a white patch of fur on its chest and white paws. The cat is looking at the camera.
이미지와 텍스트 프롬프트를 혼합하면 흥미로운 사용 사례가 열립니다. 다음과 같은 작업을 할 수 있는 애플리케이션을 만들 수 있습니다.
- 사진에서 텍스트를 인식합니다.
- 이미지를 표시해도 안전한지 확인합니다.
- 이미지 캡션을 만듭니다.
- 일반 텍스트 설명이 포함된 이미지 데이터베이스를 검색합니다.
이미지에서 정보를 추출하는 것 외에도 비정형 텍스트에서 정보를 추출할 수도 있습니다. 다음 섹션에서 알아보겠습니다.
7. 구조화되지 않은 텍스트에서 구조화된 정보 추출
보고서 문서, 이메일 또는 기타 긴 형식의 텍스트에 중요한 정보가 비정형 방식으로 제공되는 경우가 많습니다. 비정형 텍스트에 포함된 주요 세부정보를 구조화된 객체의 형태로 추출하는 것이 이상적입니다. 방법을 알아보겠습니다.
사람의 약력, 이력서 또는 설명을 토대로 사람의 이름과 나이를 추출하려고 한다고 가정해 보겠습니다. 영리하게 조정된 프롬프트를 사용하여 LLM에 비정형 텍스트에서 JSON을 추출하도록 지시할 수 있습니다 (일반적으로 '프롬프트 엔지니어링'이라고 함).
하지만 아래 예에서는 JSON 출력을 설명하는 프롬프트를 만드는 대신 구조화된 출력이라는 Gemini의 강력한 기능을 사용합니다. 이 기능은 지정된 JSON 스키마에 따라 모델이 유효한 JSON 콘텐츠만 출력하도록 강제하는 제약된 디코딩이라고도 합니다.
app/src/main/java/gemini/workshop
의 ExtractData.java
를 살펴보세요.
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;
public class ExtractData {
record Person(String name, int age) { }
interface PersonExtractor {
@SystemMessage("""
Your role is to extract the name and age
of the person described in the biography.
""")
Person extractPerson(String biography);
}
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.responseMimeType("application/json")
.responseSchema(fromClass(Person.class))
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
String bio = """
Anna is a 23 year old artist based in Brooklyn, New York. She was born and
raised in the suburbs of Chicago, where she developed a love for art at a
young age. She attended the School of the Art Institute of Chicago, where
she studied painting and drawing. After graduating, she moved to New York
City to pursue her art career. Anna's work is inspired by her personal
experiences and observations of the world around her. She often uses bright
colors and bold lines to create vibrant and energetic paintings. Her work
has been exhibited in galleries and museums in New York City and Chicago.
""";
Person person = extractor.extractPerson(bio);
System.out.println(person.name()); // Anna
System.out.println(person.age()); // 23
}
}
이 파일의 다양한 단계를 살펴보겠습니다.
Person
레코드는 사람을 설명하는 세부정보 (이름 및 연령)를 나타내도록 정의됩니다.PersonExtractor
인터페이스는 비구조화된 텍스트 문자열이 주어지면Person
인스턴스를 반환하는 메서드로 정의됩니다.extractPerson()
에는 안내 프롬프트를 연결하는@SystemMessage
주석이 지정됩니다. 이는 모델이 정보를 추출하는 데 사용하는 프롬프트이며, JSON 문서 형식으로 세부정보를 반환합니다. 이 세부정보는 자동으로 파싱되고Person
인스턴스로 변환됩니다.
이제 main()
메서드의 콘텐츠를 살펴보겠습니다.
- 채팅 모델이 구성되고 인스턴스화됩니다. 모델 빌더 클래스의 두 가지 새로운 메서드인
responseMimeType()
및responseSchema()
를 사용합니다. 첫 번째는 Gemini에 출력에서 유효한 JSON을 생성하도록 지시합니다. 두 번째 메서드는 반환되어야 하는 JSON 객체의 스키마를 정의합니다. 또한 후자는 Java 클래스 또는 레코드를 적절한 JSON 스키마로 변환할 수 있는 편의 메서드에 위임합니다. - LangChain4j의
AiServices
클래스로 인해PersonExtractor
객체가 생성됩니다. - 그런 다음
Person person = extractor.extractPerson(...)
를 호출하여 구조화되지 않은 텍스트에서 사용자의 세부정보를 추출하고 이름과 연령이 포함된Person
인스턴스를 가져올 수 있습니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData
다음과 같은 출력이 표시됩니다.
Anna 23
예, 저는 23세인 애나입니다.
이 AiServices
접근 방식을 사용하면 강력한 유형의 객체로 작업할 수 있습니다. LLM과 직접 상호작용하지 않습니다. 대신 추출된 개인 정보를 나타내는 Person
레코드와 같은 구체적인 클래스로 작업하고 있으며 Person
인스턴스를 반환하는 extractPerson()
메서드가 있는 PersonExtractor
객체가 있습니다. LLM 개념은 추상화되며 Java 개발자는 이 PersonExtractor
인터페이스를 사용할 때 일반 클래스와 객체만 조작합니다.
8. 프롬프트 템플릿으로 프롬프트 구성
일반적인 지시나 질문을 사용하여 LLM과 상호작용할 때 프롬프트의 일부는 변경되지 않으며 다른 부분에는 데이터가 포함됩니다. 예를 들어 레시피를 만들려면 '재능 있는 셰프님, 다음 재료로 레시피를 만들어 주세요.'와 같은 프롬프트를 사용하고 재료를 텍스트 끝에 추가할 수 있습니다. 프로그래밍 언어의 보간 문자열과 마찬가지로 프롬프트 템플릿은 이 작업을 위해 존재합니다. 프롬프트 템플릿에는 LLM의 특정 호출에 적합한 데이터로 대체할 수 있는 자리표시자가 포함되어 있습니다.
구체적으로 app/src/main/java/gemini/workshop
디렉터리의 TemplatePrompt.java
를 살펴보겠습니다.
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;
import java.util.HashMap;
import java.util.Map;
public class TemplatePrompt {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(500)
.temperature(1.0f)
.topK(40)
.topP(0.95f)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
You're a friendly chef with a lot of cooking experience.
Create a recipe for a {{dish}} with the following ingredients: \
{{ingredients}}, and give it a name.
"""
);
Map<String, Object> variables = new HashMap<>();
variables.put("dish", "dessert");
variables.put("ingredients", "strawberries, chocolate, and whipped cream");
Prompt prompt = promptTemplate.apply(variables);
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
평소와 같이 VertexAiGeminiChatModel
모델을 구성합니다. 온도와 topP, topK 값이 높아 창의성이 높습니다. 그런 다음 프롬프트의 문자열을 전달하여 from()
정적 메서드로 PromptTemplate
를 만들고 중괄호 자리표시자 변수 {{dish}}
및 {{ingredients}}
를 사용합니다.
자리표시자 이름과 이를 대체할 문자열 값을 나타내는 키-값 쌍 맵을 사용하는 apply()
를 호출하여 최종 프롬프트를 만듭니다.
마지막으로 prompt.toUserMessage()
명령어를 사용하여 프롬프트에서 사용자 메시지를 만들어 Gemini 모델의 generate()
메서드를 호출합니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt
다음과 같이 생성된 출력이 표시됩니다.
**Strawberry Shortcake** Ingredients: * 1 pint strawberries, hulled and sliced * 1/2 cup sugar * 1/4 cup cornstarch * 1/4 cup water * 1 tablespoon lemon juice * 1/2 cup heavy cream, whipped * 1/4 cup confectioners' sugar * 1/4 teaspoon vanilla extract * 6 graham cracker squares, crushed Instructions: 1. In a medium saucepan, combine the strawberries, sugar, cornstarch, water, and lemon juice. Bring to a boil over medium heat, stirring constantly. Reduce heat and simmer for 5 minutes, or until the sauce has thickened. 2. Remove from heat and let cool slightly. 3. In a large bowl, combine the whipped cream, confectioners' sugar, and vanilla extract. Beat until soft peaks form. 4. To assemble the shortcakes, place a graham cracker square on each of 6 dessert plates. Top with a scoop of whipped cream, then a spoonful of strawberry sauce. Repeat layers, ending with a graham cracker square. 5. Serve immediately. **Tips:** * For a more elegant presentation, you can use fresh strawberries instead of sliced strawberries. * If you don't have time to make your own whipped cream, you can use store-bought whipped cream.
지도에서 dish
및 ingredients
값을 변경하고 온도, topK
, tokP
를 조정하고 코드를 다시 실행해 보세요. 이렇게 하면 이러한 매개변수 변경이 LLM에 미치는 영향을 관찰할 수 있습니다.
프롬프트 템플릿은 LLM 호출에 재사용 가능하고 매개변수화 가능한 안내를 제공하는 좋은 방법입니다. 사용자 제공의 다양한 값에 맞게 데이터를 전달하고 프롬프트를 맞춤설정할 수 있습니다.
9. 퓨샷 프롬프트를 사용한 텍스트 분류
LLM은 텍스트를 다양한 카테고리로 분류하는 데 능숙합니다. 텍스트 예시와 관련 카테고리를 제공하여 LLM이 이 작업을 수행하도록 도울 수 있습니다. 이러한 접근 방식을 퓨샷 프롬프트라고 합니다.
app/src/main/java/gemini/workshop
디렉터리에서 TextClassification.java
을 열어 특정 유형의 텍스트 분류인 감정 분석을 수행해 보겠습니다.
package gemini.workshop;
import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import java.util.List;
public class TextClassification {
enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(10)
.maxRetries(3)
.responseSchema(Schema.newBuilder()
.setType(Type.STRING)
.addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
.build())
.build();
interface SentimentAnalysis {
@SystemMessage("""
Analyze the sentiment of the text below.
Respond only with one word to describe the sentiment.
""")
Sentiment analyze(String text);
}
MessageWindowChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
memory.add(UserMessage.from("This is fantastic news!"));
memory.add(AiMessage.from(Sentiment.POSITIVE.name()));
memory.add(UserMessage.from("Pi is roughly equal to 3.14"));
memory.add(AiMessage.from(Sentiment.NEUTRAL.name()));
memory.add(UserMessage.from("I really disliked the pizza. Who would use pineapples as a pizza topping?"));
memory.add(AiMessage.from(Sentiment.NEGATIVE.name()));
SentimentAnalysis sentimentAnalysis =
AiServices.builder(SentimentAnalysis.class)
.chatLanguageModel(model)
.chatMemory(memory)
.build();
System.out.println(sentimentAnalysis.analyze("I love strawberries!"));
}
}
Sentiment
enum은 부정적, 중립적, 긍정적 등 감정의 다양한 값을 나열합니다.
main()
메서드에서는 평소와 같이 Gemini 채팅 모델을 만들지만 짧은 대답만 원하는 경우 최대 출력 토큰 수를 작게 지정합니다. 텍스트는 POSITIVE
, NEGATIVE
또는 NEUTRAL
입니다. 이러한 값만 반환하도록 모델을 제한하려면 데이터 추출 섹션에서 살펴본 구조화된 출력 지원을 활용하면 됩니다. 따라서 responseSchema()
메서드가 사용됩니다. 이번에는 SchemaHelper
의 편의 메서드를 사용하여 스키마 정의를 추론하지 않고 대신 Schema
빌더를 사용하여 스키마 정의가 어떤 모습인지 알아봅니다.
모델이 구성되면 LangChain4j의 AiServices
가 LLM을 사용하여 구현할 SentimentAnalysis
인터페이스를 만듭니다. 이 인터페이스에는 analyze()
라는 메서드 하나가 포함되어 있습니다. 입력에서 분석할 텍스트를 가져와 Sentiment
enum 값을 반환합니다. 따라서 인식된 감정 클래스를 나타내는 강력한 유형의 객체만 조작합니다.
그런 다음 모델이 분류 작업을 수행하도록 유도하는 '몇 번의 예시'를 제공하기 위해 채팅 메모리를 만들어 텍스트와 텍스트와 관련된 감정을 나타내는 사용자 메시지와 AI 응답 쌍을 전달합니다.
SentimentAnalysis
인터페이스, 사용할 모델, 퓨샷 예시가 포함된 채팅 메모리를 전달하여 AiServices.builder()
메서드로 모든 항목을 함께 결합해 보겠습니다. 마지막으로 분석할 텍스트를 사용하여 analyze()
메서드를 호출합니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
단어가 하나 표시됩니다.
POSITIVE
딸기를 좋아하는 것은 긍정적인 감정인 것 같습니다.
10. 검색 증강 생성(RAG)
LLM은 대량의 텍스트를 바탕으로 학습됩니다. 하지만 학습 중에 확인한 정보만 지식에 포함됩니다. 모델 학습 마감일 이후에 새로운 정보가 발표되면 모델에서 이러한 세부정보를 사용할 수 없습니다. 따라서 모델은 보지 못한 정보에 관한 질문에 답변할 수 없습니다.
따라서 이 섹션에서 다룰 검색 증강 생성 (RAG)과 같은 접근 방식은 LLM이 사용자의 요청을 처리하기 위해 알아야 할 추가 정보를 제공하여 더 최신 정보로 응답하거나 학습 시 액세스할 수 없는 비공개 정보에 응답하는 데 도움이 됩니다.
대화로 돌아가 보겠습니다. 이번에는 문서에 관해 질문할 수 있습니다. 더 작은 단위 ('청크')로 분할된 문서가 포함된 데이터베이스에서 관련 정보를 검색할 수 있는 챗봇을 빌드합니다. 이 정보는 모델이 학습에 포함된 지식에만 의존하지 않고 답변을 근거로 하는 데 사용됩니다.
RAG에는 두 가지 단계가 있습니다.
- 처리 단계: 문서가 메모리에 로드되고 더 작은 청크로 분할되며 벡터 임베딩 (청크의 고차원 벡터 표현)이 계산되어 시맨틱 검색을 실행할 수 있는 벡터 데이터베이스에 저장됩니다. 이 처리 단계는 일반적으로 문서 자료에 새 문서를 추가해야 할 때 한 번 실행됩니다.
- 쿼리 단계: 이제 사용자가 문서에 관해 질문할 수 있습니다. 질문도 벡터로 변환되어 데이터베이스의 다른 모든 벡터와 비교됩니다. 가장 유사한 벡터는 일반적으로 의미상 관련이 있으며 벡터 데이터베이스에서 반환됩니다. 그런 다음 LLM에 대화의 맥락과 데이터베이스에서 반환된 벡터에 해당하는 텍스트 청크가 제공되며, 이러한 청크를 보고 답변을 근거로 하라는 요청을 받습니다.
문서 준비하기
이 새로운 예에서는 가상의 자동차 제조업체인 Cymbal Starlight의 가상의 자동차 모델에 관해 질문합니다. 가상의 자동차에 관한 문서는 모델의 지식에 포함되어서는 안 된다는 개념입니다. 따라서 Gemini가 이 자동차에 관한 질문에 올바르게 답변할 수 있다면 RAG 접근 방식이 작동한다는 의미이며, 문서를 검색할 수 있다는 의미입니다.
챗봇 구현
두 단계 접근 방식을 빌드하는 방법을 알아보겠습니다. 먼저 문서 처리 단계를 거친 후 사용자가 문서에 관해 질문하는 쿼리 시간('검색 단계'라고도 함)을 거칩니다.
이 예에서는 두 단계가 모두 동일한 클래스에 구현됩니다. 일반적으로 처리를 담당하는 애플리케이션과 사용자에게 챗봇 인터페이스를 제공하는 애플리케이션이 하나씩 있습니다.
또한 이 예에서는 인메모리 벡터 데이터베이스를 사용합니다. 실제 프로덕션 시나리오에서는 처리 단계와 쿼리 단계가 두 가지 애플리케이션으로 분리되고 벡터는 독립형 데이터베이스에 유지됩니다.
문서 처리
문서 처리 단계의 첫 번째 단계는 가상의 자동차에 관한 PDF 파일을 찾아 읽을 PdfParser
를 준비하는 것입니다.
URL url = new URI("https://raw.githubusercontent.com/meteatamel/genai-beyond-basics/main/samples/grounding/vertexai-search/cymbal-starlight-2024.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());
먼저 일반적인 채팅 언어 모델을 만드는 대신 임베딩 모델의 인스턴스를 만듭니다. 텍스트 조각 (단어, 문장 또는 단락)의 벡터 표현을 만드는 역할을 하는 특정 모델입니다. 텍스트 응답을 반환하는 대신 부동 소수점 숫자의 벡터를 반환합니다.
VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
.endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.publisher("google")
.modelName("text-embedding-005")
.maxRetries(3)
.build();
다음으로 다음 작업을 위해 공동작업할 몇 가지 클래스가 필요합니다.
- PDF 문서를 청크로 로드하고 분할합니다.
- 이러한 모든 청크의 벡터 임베딩을 만듭니다.
InMemoryEmbeddingStore<TextSegment> embeddingStore =
new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 100))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
storeIngestor.ingest(document);
벡터 임베딩을 저장하기 위해 메모리 내 벡터 데이터베이스인 InMemoryEmbeddingStore
의 인스턴스가 생성됩니다.
DocumentSplitters
클래스 덕분에 문서가 청크로 분할됩니다. PDF 파일의 텍스트를 500자씩의 스니펫으로 분할하고 100자씩 겹치게 처리합니다 (단어나 문장이 잘리지 않도록 다음 청크를 사용함).
저장소 처리기는 문서 분할기, 벡터를 계산하는 임베딩 모델, 메모리 내 벡터 데이터베이스를 연결합니다. 그러면 ingest()
메서드가 처리를 처리합니다.
이제 첫 번째 단계가 완료되었으며 문서가 연결된 벡터 임베딩이 포함된 텍스트 청크로 변환되어 벡터 데이터베이스에 저장되었습니다.
질문하기
이제 질문을 준비할 시간입니다. 대화를 시작할 채팅 모델을 만듭니다.
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(1000)
.build();
또한 embeddingStore
변수의 벡터 데이터베이스를 임베딩 모델과 연결하는 검색기 클래스도 필요합니다. 사용자의 쿼리에 대한 벡터 임베딩을 계산하여 벡터 데이터베이스를 쿼리하고 데이터베이스에서 유사한 벡터를 찾는 것이 이 컴포저블의 역할입니다.
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
자동차 전문가 어시스턴트를 나타내는 인터페이스를 만듭니다. 이 인터페이스는 AiServices
클래스가 모델과 상호작용할 수 있도록 구현합니다.
interface CarExpert {
Result<String> ask(String question);
}
CarExpert
인터페이스는 LangChain4j의 Result
클래스로 래핑된 문자열 응답을 반환합니다. 이 래퍼를 사용해야 하는 이유는 무엇인가요? 이렇게 하면 답변을 얻을 수 있을 뿐만 아니라 콘텐츠 검색기에서 반환한 데이터베이스의 청크를 검사할 수도 있습니다. 이렇게 하면 최종 답변을 사용자에게 제공하는 데 사용된 문서의 출처를 표시할 수 있습니다.
이제 새 AI 서비스를 구성할 수 있습니다.
CarExpert expert = AiServices.builder(CarExpert.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(retriever)
.build();
이 서비스는 다음을 함께 결합합니다.
- 이전에 구성한 채팅 언어 모델입니다.
- 대화를 추적하는 채팅 메모
- retriever는 벡터 임베딩 쿼리를 데이터베이스의 벡터와 비교합니다.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("""
You are an expert in car automotive, and you answer concisely.
Here is the question: {{userMessage}}
Answer using the following information:
{{contents}}
the following information:
{{contents}}
"""))
.build())
.contentRetriever(retriever)
.build())
이제 질문을 할 준비가 되었습니다.
List.of(
"What is the cargo capacity of Cymbal Starlight?",
"What's the emergency roadside assistance phone number?",
"Are there some special kits available on that car?"
).forEach(query -> {
Result<String> response = expert.ask(query);
System.out.printf("%n=== %s === %n%n %s %n%n", query, response.content());
System.out.println("SOURCE: " + response.sources().getFirst().textSegment().text());
});
전체 소스 코드는 app/src/main/java/gemini/workshop
디렉터리의 RAG.java
에 있습니다.
샘플을 실행합니다.
./gradlew -q run -DjavaMainClass=gemini.workshop.RAG
출력에 질문에 대한 답변이 표시됩니다.
=== What is the cargo capacity of Cymbal Starlight? === The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. SOURCE: Cargo The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. The cargo area is located in the trunk of the vehicle. To access the cargo area, open the trunk lid using the trunk release lever located in the driver's footwell. When loading cargo into the trunk, be sure to distribute the weight evenly. Do not overload the trunk, as this could affect the vehicle's handling and stability. Luggage === What's the emergency roadside assistance phone number? === The emergency roadside assistance phone number is 1-800-555-1212. SOURCE: Chapter 18: Emergencies Roadside Assistance If you experience a roadside emergency, such as a flat tire or a dead battery, you can call roadside assistance for help. Roadside assistance is available 24 hours a day, 7 days a week. To call roadside assistance, dial the following number: 1-800-555-1212 When you call roadside assistance, be prepared to provide the following information: Your name and contact information Your vehicle's make, model, and year Your vehicle's location === Are there some special kits available on that car? === Yes, the Cymbal Starlight comes with a tire repair kit. SOURCE: Lane keeping assist: This feature helps to keep you in your lane by gently steering the vehicle back into the lane if you start to drift. Adaptive cruise control: This feature automatically adjusts your speed to maintain a safe following distance from the vehicle in front of you. Forward collision warning: This feature warns you if you are approaching another vehicle too quickly. Automatic emergency braking: This feature can automatically apply the brakes to avoid a collision.
11. 함수 호출
정보를 검색하거나 작업을 실행하는 원격 웹 API, 일종의 계산을 실행하는 서비스와 같이 LLM이 외부 시스템에 액세스하도록 하려는 경우가 있습니다. 예를 들면 다음과 같습니다.
원격 웹 API:
- 고객 주문을 추적하고 업데이트합니다.
- Issue Tracker에서 티켓을 찾거나 만듭니다.
- 주식 시세 또는 IoT 센서 측정값과 같은 실시간 데이터를 가져옵니다.
- 이메일을 보냅니다.
계산 도구:
- 고급 수학 문제를 위한 계산기
- LLM에 추론 로직이 필요한 경우 코드를 실행하기 위한 코드 해석입니다.
- LLM이 데이터베이스를 쿼리할 수 있도록 자연어 요청을 SQL 쿼리로 변환합니다.
함수 호출 (도구 또는 도구 사용이라고도 함)은 모델이 최신 데이터로 사용자의 프롬프트에 올바르게 응답할 수 있도록 모델을 대신하여 하나 이상의 함수 호출을 요청하는 기능입니다.
사용자의 특정 프롬프트와 해당 맥락과 관련이 있을 수 있는 기존 함수에 대한 지식을 고려하여 LLM은 함수 호출 요청으로 응답할 수 있습니다. 그러면 LLM을 통합하는 애플리케이션이 LLM을 대신하여 함수를 호출한 후 응답을 포함하여 LLM에 다시 답장할 수 있으며, LLM은 텍스트 답변으로 응답하여 다시 해석합니다.
함수 호출의 4단계
함수 호출의 예시인 일기예보에 관한 정보를 가져오는 방법을 살펴보겠습니다.
Gemini 또는 다른 LLM에 파리의 날씨에 관해 물어보면 현재 일기예보에 관한 정보가 없다고 답변합니다. LLM이 날씨 데이터에 실시간으로 액세스하도록 하려면 사용을 요청할 수 있는 몇 가지 함수를 정의해야 합니다.
다음 다이어그램을 살펴보세요.
1️⃣ 먼저 사용자가 파리의 날씨를 묻습니다. LangChain4j를 사용하는 챗봇 앱은 LLM이 쿼리를 처리하는 데 도움이 되는 함수가 하나 이상 있음을 알고 있습니다. 챗봇은 초기 프롬프트와 호출할 수 있는 함수 목록을 모두 전송합니다. 여기서는 위치의 문자열 매개변수를 사용하는 getWeather()
라는 함수가 있습니다.
LLM은 일기예보를 알지 못하므로 텍스트를 통해 답장하는 대신 함수 실행 요청을 다시 전송합니다. 챗봇은 "Paris"
를 위치 매개변수로 사용하여 getWeather()
함수를 호출해야 합니다.
2️⃣ 챗봇이 LLM을 대신하여 해당 함수를 호출하고 함수 응답을 가져옵니다. 여기서는 응답이 {"forecast": "sunny"}
이라고 가정합니다.
3️⃣ 챗봇 앱이 JSON 응답을 LLM으로 다시 전송합니다.
4️⃣ LLM은 JSON 응답을 확인하고 정보를 해석한 후 파리의 날씨가 맑다는 텍스트로 응답합니다.
각 단계를 코드로 표시
먼저 평소와 같이 Gemini 모델을 구성합니다.
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(100)
.build();
호출할 수 있는 함수를 설명하는 도구 사양을 정의합니다.
ToolSpecification weatherToolSpec = ToolSpecification.builder()
.name("getWeather")
.description("Get the weather forecast for a given location or city")
.parameters(JsonObjectSchema.builder()
.addStringProperty(
"location",
"the location or city to get the weather forecast for")
.build())
.build();
함수의 이름과 매개변수의 이름 및 유형이 정의되지만 함수와 매개변수 모두에 설명이 제공됩니다. 설명은 매우 중요하며 LLM이 함수가 무엇을 할 수 있는지 실제로 이해하고 대화의 맥락에서 이 함수를 호출해야 하는지 판단하는 데 도움이 됩니다.
파리의 날씨에 관한 첫 번째 질문을 보내면서 1단계를 시작해 보겠습니다.
List<ChatMessage> allMessages = new ArrayList<>();
// 1) Ask the question about the weather
UserMessage weatherQuestion = UserMessage.from("What is the weather in Paris?");
allMessages.add(weatherQuestion);
2단계에서 모델이 사용하려는 도구를 전달하면 모델은 도구 실행 요청으로 답장합니다.
// 2) The model replies with a function call request
Response<AiMessage> messageResponse = model.generate(allMessages, weatherToolSpec);
ToolExecutionRequest toolExecutionRequest = messageResponse.content().toolExecutionRequests().getFirst();
System.out.println("Tool execution request: " + toolExecutionRequest);
allMessages.add(messageResponse.content());
3단계 이제 LLM에서 호출하려는 함수를 알 수 있습니다. 코드에서는 외부 API를 실제로 호출하지 않고 가상의 일기 예보를 직접 반환합니다.
// 3) We send back the result of the function call
ToolExecutionResultMessage toolExecResMsg = ToolExecutionResultMessage.from(toolExecutionRequest,
"{\"location\":\"Paris\",\"forecast\":\"sunny\", \"temperature\": 20}");
allMessages.add(toolExecResMsg);
4단계에서 LLM은 함수 실행 결과를 학습한 후 텍스트 응답을 합성할 수 있습니다.
// 4) The model answers with a sentence describing the weather
Response<AiMessage> weatherResponse = model.generate(allMessages);
System.out.println("Answer: " + weatherResponse.content().text());
출력은 다음과 같습니다.
Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer: The weather in Paris is sunny with a temperature of 20 degrees Celsius.
위의 출력에서 도구 실행 요청과 답변을 확인할 수 있습니다.
전체 소스 코드는 app/src/main/java/gemini/workshop
디렉터리의 FunctionCalling.java
에 있습니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling
다음과 비슷한 출력이 표시됩니다.
Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer: The weather in Paris is sunny with a temperature of 20 degrees Celsius.
12. LangChain4j가 함수 호출을 처리합니다.
이전 단계에서는 일반 텍스트 질문/답변과 함수 요청/응답 상호작용이 어떻게 교차되는지 알아봤으며, 그 사이에서 실제 함수를 호출하지 않고 요청된 함수 응답을 직접 제공했습니다.
하지만 LangChain4j는 대화를 평소와 같이 처리하는 동시에 함수 호출을 투명하게 처리할 수 있는 고급 추상화도 제공합니다.
단일 함수 호출
FunctionCallingAssistant.java
를 하나씩 살펴보겠습니다.
먼저 함수의 응답 데이터 구조를 나타내는 레코드를 만듭니다.
record WeatherForecast(String location, String forecast, int temperature) {}
응답에는 위치, 예측, 온도에 관한 정보가 포함됩니다.
그런 다음 모델에 제공하려는 실제 함수가 포함된 클래스를 만듭니다.
static class WeatherForecastService {
@Tool("Get the weather forecast for a location")
WeatherForecast getForecast(@P("Location to get the forecast for") String location) {
if (location.equals("Paris")) {
return new WeatherForecast("Paris", "Sunny", 20);
} else if (location.equals("London")) {
return new WeatherForecast("London", "Rainy", 15);
} else {
return new WeatherForecast("Unknown", "Unknown", 0);
}
}
}
이 클래스에는 단일 함수가 포함되어 있지만 모델이 호출을 요청할 수 있는 함수의 설명에 해당하는 @Tool
주석으로 주석 처리됩니다.
함수의 매개변수 (여기서는 하나)에도 주석이 지정되지만, 이 짧은 @P
주석은 매개변수에 대한 설명도 제공합니다. 더 복잡한 시나리오에서 모델에 사용할 수 있도록 원하는 만큼 함수를 추가할 수 있습니다.
이 클래스에서는 미리 준비된 응답을 반환하지만 실제 외부 일기 예보 서비스를 호출하려면 해당 메서드의 본문에서 해당 서비스를 호출하면 됩니다.
이전 접근 방식에서 ToolSpecification
를 만들 때 보았듯이 함수의 기능을 문서화하고 매개변수가 무엇에 해당하는지 설명하는 것이 중요합니다. 이렇게 하면 모델이 이 함수를 사용할 수 있는 방법과 시기를 이해하는 데 도움이 됩니다.
다음으로 LangChain4j를 사용하면 모델과 상호작용하는 데 사용할 계약에 해당하는 인터페이스를 제공할 수 있습니다. 여기서는 사용자 메시지를 나타내는 문자열을 사용하고 모델의 응답에 해당하는 문자열을 반환하는 간단한 인터페이스입니다.
interface WeatherAssistant {
String chat(String userMessage);
}
더 복잡한 상황을 처리하려면 LangChain4j의 UserMessage
(사용자 메시지용) 또는 AiMessage
(모델 응답용) 또는 TokenStream
(더 복잡한 객체에는 사용된 토큰 수와 같은 추가 정보도 포함되어 있으므로)와 관련된 더 복잡한 서명을 사용할 수도 있습니다. 단순성을 위해 입력에는 문자열을, 출력에는 문자열을 사용합니다.
모든 부분을 연결하는 main()
메서드로 마무리하겠습니다.
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-pro-002")
.build();
WeatherForecastService weatherForecastService = new WeatherForecastService();
WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(weatherForecastService)
.build();
System.out.println(assistant.chat("What is the weather in Paris?"));
}
평소와 같이 Gemini 채팅 모델을 구성합니다. 그런 다음 모델이 호출하도록 요청하는 '함수'가 포함된 일기 예보 서비스를 인스턴스화합니다.
이제 AiServices
클래스를 다시 사용하여 채팅 모델, 채팅 메모리, 도구 (예: 함수가 있는 일기 예보 서비스)를 바인딩합니다. AiServices
는 정의한 WeatherAssistant
인터페이스를 구현하는 객체를 반환합니다. 이제 남은 작업은 해당 어시스턴트의 chat()
메서드를 호출하는 것입니다. 호출 시 텍스트 응답만 표시되지만 함수 호출 요청과 함수 호출 응답은 개발자에게 표시되지 않으며 이러한 요청은 자동으로 투명하게 처리됩니다. Gemini에서 함수를 호출해야 한다고 판단하면 함수 호출 요청으로 응답하고 LangChain4j가 개발자를 대신하여 로컬 함수를 호출합니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant
다음과 비슷한 출력이 표시됩니다.
OK. The weather in Paris is sunny with a temperature of 20 degrees.
단일 함수의 예입니다.
다중 함수 호출
여러 함수를 보유하고 LangChain4j가 대신 여러 함수 호출을 처리하도록 할 수도 있습니다. 여러 함수 예시는 MultiFunctionCallingAssistant.java
를 참고하세요.
통화를 변환하는 함수가 있습니다.
@Tool("Convert amounts between two currencies")
double convertCurrency(
@P("Currency to convert from") String fromCurrency,
@P("Currency to convert to") String toCurrency,
@P("Amount to convert") double amount) {
double result = amount;
if (fromCurrency.equals("USD") && toCurrency.equals("EUR")) {
result = amount * 0.93;
} else if (fromCurrency.equals("USD") && toCurrency.equals("GBP")) {
result = amount * 0.79;
}
System.out.println(
"convertCurrency(fromCurrency = " + fromCurrency +
", toCurrency = " + toCurrency +
", amount = " + amount + ") == " + result);
return result;
}
주식의 값을 가져오는 또 다른 함수:
@Tool("Get the current value of a stock in US dollars")
double getStockPrice(@P("Stock symbol") String symbol) {
double result = 170.0 + 10 * new Random().nextDouble();
System.out.println("getStockPrice(symbol = " + symbol + ") == " + result);
return result;
}
주어진 금액에 비율을 적용하는 또 다른 함수:
@Tool("Apply a percentage to a given amount")
double applyPercentage(@P("Initial amount") double amount, @P("Percentage between 0-100 to apply") double percentage) {
double result = amount * (percentage / 100);
System.out.println("applyPercentage(amount = " + amount + ", percentage = " + percentage + ") == " + result);
return result;
}
그런 다음 이러한 모든 함수와 MultiTools 클래스를 결합하여 'AAPL 주식 가격의 10% 를 USD에서 EUR로 환산하면 얼마인가요?'와 같은 질문을 할 수 있습니다.
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-002")
.maxOutputTokens(100)
.build();
MultiTools multiTools = new MultiTools();
MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
.chatLanguageModel(model)
.chatMemory(withMaxMessages(10))
.tools(multiTools)
.build();
System.out.println(assistant.chat(
"What is 10% of the AAPL stock price converted from USD to EUR?"));
}
다음과 같이 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant
다음과 같이 여러 함수가 호출된 것을 볼 수 있습니다.
getStockPrice(symbol = AAPL) == 172.8022224055534 convertCurrency(fromCurrency = USD, toCurrency = EUR, amount = 172.8022224055534) == 160.70606683716468 applyPercentage(amount = 160.70606683716468, percentage = 10.0) == 16.07060668371647 10% of the AAPL stock price converted from USD to EUR is 16.07060668371647 EUR.
상담사 대상
함수 호출은 Gemini와 같은 대규모 언어 모델을 위한 훌륭한 확장 메커니즘입니다. 이를 통해 '에이전트' 또는 'AI 어시스턴트'라고도 하는 더 복잡한 시스템을 빌드할 수 있습니다. 이러한 에이전트는 외부 API를 통해 외부 세계와 상호작용할 수 있으며 외부 환경에 부작용을 미칠 수 있는 서비스 (예: 이메일 전송, 티켓 생성 등)와 상호작용할 수 있습니다.
이러한 강력한 에이전트를 만들 때는 책임감 있게 만들어야 합니다. 자동 작업을 실행하기 전에 사람의 개입을 고려해야 합니다. 외부 세계와 상호작용하는 LLM 기반 에이전트를 설계할 때는 안전을 염두에 두어야 합니다.
13. Ollama 및 TestContainers로 Gemma 실행
지금까지는 Gemini를 사용해 왔지만 Gemini의 자매 모델인 Gemma도 있습니다.
Gemma는 Gemini 모델을 만드는 데 사용된 것과 동일한 연구 및 기술로 빌드된 최첨단 경량 개방형 모델군입니다. Gemma는 Gemma1과 Gemma2의 두 가지 변형으로 제공되며 각각 다양한 크기가 있습니다. Gemma1은 2B 및 7B의 두 가지 크기로 제공됩니다. Gemma2는 9B 및 27B의 두 가지 크기로 제공됩니다. 가중치는 무료로 제공되며 크기가 작기 때문에 노트북이나 Cloud Shell에서도 직접 실행할 수 있습니다.
Gemma를 실행하려면 어떻게 해야 하나요?
Gemma를 실행하는 방법에는 여러 가지가 있습니다. 클라우드에서, 버튼 클릭으로 Vertex AI를 통해, 또는 일부 GPU가 있는 GKE를 통해 실행할 수 있지만 로컬에서 실행할 수도 있습니다.
Gemma를 로컬에서 실행하는 좋은 방법 중 하나는 로컬 머신에서 Llama 2, Mistral 등 다양한 소형 모델을 실행할 수 있는 도구인 Ollama를 사용하는 것입니다. Docker와 비슷하지만 LLM용입니다.
운영체제의 안내에 따라 Ollama를 설치합니다.
Linux 환경을 사용하는 경우 Ollama를 설치한 후 먼저 사용 설정해야 합니다.
ollama serve > /dev/null 2>&1 &
로컬에 설치한 후에는 명령어를 실행하여 모델을 가져올 수 있습니다.
ollama pull gemma:2b
모델이 가져올 때까지 기다립니다. 다소 시간이 걸릴 수 있습니다.
모델을 실행합니다.
ollama run gemma:2b
이제 모델과 상호작용할 수 있습니다.
>>> Hello! Hello! It's nice to hear from you. What can I do for you today?
프롬프트를 종료하려면 Ctrl+D를 누릅니다.
TestContainers의 Ollama에서 Gemma 실행
Ollama를 로컬에 설치하고 실행하는 대신 TestContainers에서 처리하는 컨테이너 내에서 Ollama를 사용할 수 있습니다.
TestContainers는 테스트에 유용할 뿐만 아니라 컨테이너 실행에도 사용할 수 있습니다. 활용할 수 있는 특정 OllamaContainer
도 있습니다.
전체적인 상황은 다음과 같습니다.
구현
GemmaWithOllamaContainer.java
를 하나씩 살펴보겠습니다.
먼저 Gemma 모델을 가져오는 파생된 Ollama 컨테이너를 만들어야 합니다. 이 이미지는 이전 실행에서 이미 존재하거나 생성됩니다. 이미지가 이미 있는 경우 TestContainers에 기본 Ollama 이미지를 Gemma 기반 변형으로 대체하겠다고 알리면 됩니다.
private static final String TC_OLLAMA_GEMMA_2_B = "tc-ollama-gemma-2b";
// Creating an Ollama container with Gemma 2B if it doesn't exist.
private static OllamaContainer createGemmaOllamaContainer() throws IOException, InterruptedException {
// Check if the custom Gemma Ollama image exists already
List<Image> listImagesCmd = DockerClientFactory.lazyClient()
.listImagesCmd()
.withImageNameFilter(TC_OLLAMA_GEMMA_2_B)
.exec();
if (listImagesCmd.isEmpty()) {
System.out.println("Creating a new Ollama container with Gemma 2B image...");
OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.1.26");
ollama.start();
ollama.execInContainer("ollama", "pull", "gemma:2b");
ollama.commitToImage(TC_OLLAMA_GEMMA_2_B);
return ollama;
} else {
System.out.println("Using existing Ollama container with Gemma 2B image...");
// Substitute the default Ollama image with our Gemma variant
return new OllamaContainer(
DockerImageName.parse(TC_OLLAMA_GEMMA_2_B)
.asCompatibleSubstituteFor("ollama/ollama"));
}
}
그런 다음 Ollama 테스트 컨테이너를 만들고 시작한 다음 사용하려는 모델로 컨테이너의 주소와 포트를 가리켜 Ollama 채팅 모델을 만듭니다. 마지막으로 평소와 같이 model.generate(yourPrompt)
를 호출합니다.
public static void main(String[] args) throws IOException, InterruptedException {
OllamaContainer ollama = createGemmaOllamaContainer();
ollama.start();
ChatLanguageModel model = OllamaChatModel.builder()
.baseUrl(String.format("http://%s:%d", ollama.getHost(), ollama.getFirstMappedPort()))
.modelName("gemma:2b")
.build();
String response = model.generate("Why is the sky blue?");
System.out.println(response);
}
다음과 같이 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer
첫 실행 시 컨테이너를 만들고 실행하는 데 다소 시간이 걸리지만 완료되면 Gemma가 응답하는 것을 볼 수 있습니다.
INFO: Container ollama/ollama:0.1.26 started in PT2.827064047S
The sky appears blue due to Rayleigh scattering. Rayleigh scattering is a phenomenon that occurs when sunlight interacts with molecules in the Earth's atmosphere.
* **Scattering particles:** The main scattering particles in the atmosphere are molecules of nitrogen (N2) and oxygen (O2).
* **Wavelength of light:** Blue light has a shorter wavelength than other colors of light, such as red and yellow.
* **Scattering process:** When blue light interacts with these molecules, it is scattered in all directions.
* **Human eyes:** Our eyes are more sensitive to blue light than other colors, so we perceive the sky as blue.
This scattering process results in a blue appearance for the sky, even though the sun is actually emitting light of all colors.
In addition to Rayleigh scattering, other atmospheric factors can also influence the color of the sky, such as dust particles, aerosols, and clouds.
Cloud Shell에서 Gemma가 실행되고 있습니다.
14. 축하합니다
축하합니다. LangChain4j 및 Gemini API를 사용하여 Java로 첫 번째 생성형 AI 채팅 애플리케이션을 빌드했습니다. 이 과정에서 멀티모달 대규모 언어 모델이 매우 강력하며 자체 문서, 데이터 추출, 외부 API와의 상호작용 등에서 질문/답변과 같은 다양한 작업을 처리할 수 있다는 것을 알게 되었습니다.
다음 단계
이제 강력한 LLM 통합으로 애플리케이션을 개선할 차례입니다.