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 and Conversation은 개발자가 생성형 AI 기반 검색엔진과 챗봇을 가장 빠르게 빌드할 수 있는 방법을 제공합니다.
Gemini를 기반으로 하는 Google Cloud를 위한 Gemini는 Google Cloud와 IDE 전반에서 사용할 수 있는 AI 기반 공동작업 도구로 더 많은 작업을 더 빠르게 수행할 수 있도록 도와줍니다. Gemini Code Assist는 코드 완성, 코드 생성, 코드 설명을 제공하며 Gemini와 채팅하여 기술적인 질문을 할 수 있습니다.
Gemini란 무엇인가요?
Gemini는 멀티모달 사용 사례를 위해 설계되고 Google DeepMind에서 개발된 생성형 AI 모델 제품군 중 하나입니다. 멀티모달은 텍스트, 코드, 이미지, 오디오 등 다양한 종류의 콘텐츠를 처리하고 생성할 수 있음을 의미합니다.
Gemini는 다양한 변형과 크기로 제공됩니다.
- Gemini Ultra: 복잡한 작업을 위한 가장 크고 강력한 버전입니다.
- Gemini Flash: 가장 빠르고 비용 효율적이며 대용량 작업에 최적화되어 있습니다.
- Gemini Pro: 중간 크기로, 다양한 작업으로 확장하는 데 최적화되어 있습니다.
- Gemini Nano: 기기 내 작업을 위해 설계된 가장 효율적입니다.
주요 특징:
- 멀티모달리티: 여러 정보 형식을 이해하고 처리할 수 있는 Gemini의 기능은 기존의 텍스트 전용 언어 모델을 뛰어넘는 중요한 단계입니다.
- 성능: Gemini Ultra는 여러 벤치마크에서 현재 첨단 기술을 능가하며, 까다로운 MMLU (Massive Multitask Language Understanding) 벤치마크에서 인간 전문가를 능가하는 최초의 모델입니다.
- 유연성: 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를 사용하는 방법
- 구조화되지 않은 콘텐츠에서 구조화된 유용한 정보를 추출하는 방법
- 프롬프트 템플릿 조작 방법
- 감정 분석과 같은 텍스트 분류 방법
- 내 문서와 채팅하는 방법 (검색 증강 생성)
- 함수 호출로 챗봇을 확장하는 방법
- Gemma를 Ollama 및 TestContainers와 함께 로컬에서 사용하는 방법
필요한 항목
- 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을 프로비저닝하고 연결하는 데 몇 분 정도만 걸립니다.
가상 머신에는 필요한 개발 도구가 모두 들어 있습니다. 영구적인 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 편집기로 코드를 엽니다.
Cloud Code 편집기에서 File
->를 선택하여 Codelab 소스 폴더를 엽니다. Open Folder
로 설정하고 Codelab 소스 폴더 (예: /home/username/gemini-workshop-for-java-developers/
)에 복사합니다.
Java용 Gradle 설치
클라우드 코드 편집기가 Gradle과 함께 제대로 작동하도록 하려면 Gradle용 Gradle 확장 프로그램을 설치합니다.
먼저 Java 프로젝트 섹션으로 이동하여 더하기 기호를 누릅니다.
Gradle for Java
선택:
Install Pre-Release
버전을 선택합니다.
설치가 완료되면 Disable
버튼과 Uninstall
버튼이 표시됩니다.
마지막으로 작업공간을 정리하여 새 설정을 적용합니다.
그러면 워크숍을 새로고침하고 삭제하라는 메시지가 표시됩니다. Reload and delete
를 선택합니다.
파일 중 하나(예: App.java)를 열면 구문 강조 표시와 함께 편집기가 올바르게 작동하는 것을 볼 수 있습니다.
이제 Gemini를 대상으로 샘플을 실행할 준비가 되었습니다.
환경 변수 설정
Terminal
->를 선택하여 Cloud Code 편집기에서 새 터미널을 엽니다. New Terminal
입니다. 코드 예시를 실행하는 데 필요한 두 가지 환경 변수를 설정합니다.
- 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-001")
.build();
System.out.println(model.generate("Why is the sky blue?"));
}
}
이 첫 번째 예에서는 ChatModel
인터페이스를 구현하는 VertexAiGeminiChatModel
클래스를 가져와야 합니다.
main
메서드에서 VertexAiGeminiChatModel
용 빌더를 사용하여 채팅 언어 모델을 구성하고 다음을 지정합니다.
- 프로젝트
- 위치
- 모델 이름 (
gemini-1.5-flash-001
)입니다.
이제 언어 모델이 준비되었으므로 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 dev.langchain4j.model.StreamingResponseHandler;
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-001")
.build();
model.generate("Why is the sky blue?", new StreamingResponseHandler<>() {
@Override
public void onNext(String text) {
System.out.println(text);
}
@Override
public void onError(Throwable error) {
error.printStackTrace();
}
});
}
}
이번에는 StreamingChatLanguageModel
인터페이스를 구현하는 스트리밍 클래스 변형 VertexAiGeminiStreamingChatModel
를 가져옵니다. StreamingResponseHandler
도 필요합니다.
이번에는 generate()
메서드의 서명이 약간 다릅니다. 문자열을 반환하는 대신 반환 유형은 void입니다. 프롬프트 외에도 스트리밍 응답 핸들러를 전달해야 합니다. 여기서는 두 메서드 onNext(String text)
와 onError(Throwable error)
가 있는 익명 내부 클래스를 만들어 인터페이스를 구현합니다. 전자는 응답의 새 부분이 사용 가능할 때마다 호출되지만 후자는 오류가 발생하는 경우에만 호출됩니다.
실행:
./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA
이전 클래스와 비슷한 답변을 얻을 수 있지만, 이번에는 전체 답변이 표시될 때까지 기다리는 것이 아니라 셸에 점진적으로 답변이 표시되는 것을 확인할 수 있습니다.
추가 구성
구성의 경우 프로젝트, 위치, 모델 이름만 정의했지만 모델에 지정할 수 있는 다른 매개변수도 있습니다.
temperature(Float temp)
: 원하는 창의적 응답 정의 정의 (0은 낮은 광고 소재이고 대체로 사실에 기반함, 1은 더 많은 광고 소재 결과물에 해당)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-001")
.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 서비스가 Google에서 정의하고, LangChain4j가 구현하고, String
쿼리를 취하고 String
응답을 반환하는 커스텀 ConversationService
인터페이스를 어떻게 활용하는지 확인하세요.
이제 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-001")
.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. 구조화되지 않은 텍스트에서 구조화된 정보 추출
보고서 문서, 이메일 또는 기타 긴 형식의 텍스트에 구조화되지 않은 방식으로 중요한 정보가 제공되는 경우가 많습니다. 구조화되지 않은 텍스트에 포함된 주요 세부정보를 구조화된 객체 형식으로 추출할 수 있는 것이 이상적입니다. 그 방법을 알아보겠습니다.
그 사람의 약력이나 설명을 통해 사람의 이름과 나이를 추출하고자 한다고 가정해 보겠습니다. 영리하게 조정된 프롬프트 (일반적으로 '프롬프트 엔지니어링'이라고 함)를 사용하여 구조화되지 않은 텍스트에서 JSON을 추출하도록 LLM에 지시할 수 있습니다.
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.UserMessage;
public class ExtractData {
static record Person(String name, int age) {}
interface PersonExtractor {
@UserMessage("""
Extract the name and age of the person described below.
Return a JSON document with a "name" and an "age" property, \
following this structure: {"name": "John Doe", "age": 34}
Return only JSON, without any markdown markup surrounding it.
Here is the document describing the person:
---
{{it}}
---
JSON:
""")
Person extractPerson(String text);
}
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.temperature(0f)
.topK(1)
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
Person person = extractor.extractPerson("""
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.
"""
);
System.out.println(person.name()); // Anna
System.out.println(person.age()); // 23
}
}
이 파일의 다양한 단계를 살펴보겠습니다.
Person
레코드는 사람을 설명하는 세부정보 ( 이름 및 나이)를 나타내도록 정의됩니다.PersonExtractor
인터페이스는 구조화되지 않은 텍스트 문자열이 주어진 경우Person
인스턴스를 반환하는 메서드로 정의됩니다.extractPerson()
는 프롬프트를 연결하는@UserMessage
주석으로 주석 처리됩니다. 이 프롬프트는 모델이 정보를 추출하고 JSON 문서 형식으로 세부정보를 반환하는 데 사용하는 프롬프트입니다. 그러면 파싱되어Person
인스턴스로 마셜링이 취소됩니다.
이제 main()
메서드의 콘텐츠를 살펴보겠습니다.
- 채팅 모델이 인스턴스화됩니다. 매우 낮은
temperature
은 0이고topK
은 1로만 사용하여 매우 확정적인 답변을 보장합니다. 이렇게 하면 모델이 안내를 더 잘 따를 수 있습니다. 특히 Gemini가 추가 마크다운 마크업으로 JSON 응답을 래핑하는 것을 원하지 않습니다. - LangChain4j의
AiServices
클래스 덕분에PersonExtractor
객체가 생성됩니다. - 그런 다음 간단히
Person person = extractor.extractPerson(...)
를 호출하여 구조화되지 않은 텍스트에서 사람의 세부정보를 추출하고 이름과 나이가 포함된Person
인스턴스를 반환할 수 있습니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData
다음과 같은 출력이 표시됩니다.
Anna 23
네, 저는 애나이고 23살입니다.
이 AiServices
접근 방식을 사용하면 강타입(strongly typed) 객체로 작업할 수 있습니다. LLM과 직접 상호작용하고 있지 않습니다. 대신 추출된 개인 정보를 나타내는 Person
레코드와 같은 구체적인 클래스를 사용하고 Person
인스턴스를 반환하는 extractPerson()
메서드가 있는 PersonExtractor
객체가 있습니다. LLM의 개념은 추상화되며 Java 개발자는 일반 클래스와 객체를 조작할 뿐입니다.
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-001")
.maxOutputTokens(500)
.temperature(0.8f)
.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());
}
}
평소와 같이 높은 온도와 높은 TopP 및 topK 값을 사용해 높은 수준의 창의성을 사용해 VertexAiGeminiChatModel
모델을 구성합니다. 그런 다음 프롬프트 문자열을 전달하여 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 dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
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.Map;
public class TextClassification {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-1.5-flash-001")
.maxOutputTokens(10)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
Analyze the sentiment of the text below. Respond only with one word to describe the sentiment.
INPUT: This is fantastic news!
OUTPUT: POSITIVE
INPUT: Pi is roughly equal to 3.14
OUTPUT: NEUTRAL
INPUT: I really disliked the pizza. Who would use pineapples as a pizza topping?
OUTPUT: NEGATIVE
INPUT: {{text}}
OUTPUT:
""");
Prompt prompt = promptTemplate.apply(
Map.of("text", "I love strawberries!"));
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
main()
메서드에서 평소와 같이 Gemini 채팅 모델을 만듭니다. 단, 짧은 응답만 원하므로 최대 출력 토큰 번호를 작게 만듭니다. 텍스트는 POSITIVE
, NEGATIVE
, NEUTRAL
입니다.
그런 다음 모델에 몇 가지 입력 및 출력 예에 관해 지시하여 퓨샷 프롬프팅 기법으로 재사용 가능한 프롬프트 템플릿을 만듭니다. 이렇게 하면 모델이 실제 출력을 따르는 데도 도움이 됩니다. Gemini는 완전한 문장으로 대답하지 않고 대신 한 단어로만 대답하도록 지시합니다.
apply()
메서드로 변수를 적용하여 {{text}}
자리표시자를 실제 매개변수 ("I love strawberries"
)로 바꾸고 이 템플릿을 toUserMessage()
가 있는 사용자 메시지로 변환합니다.
샘플을 실행합니다.
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
다음과 같이 한 단어가 표시됩니다.
POSITIVE
딸기를 사랑하는 사람은 긍정적인 감정인 것 같아!
10. 검색 증강 생성(RAG)
LLM은 대량의 텍스트를 학습합니다. 그러나 그들의 지식은 학습 중에 목격한 정보만 다룹니다. 모델 학습 마감일 이후에 새 정보가 공개되면 해당 세부정보는 모델에서 사용할 수 없습니다. 따라서 모델은 확인하지 못한 정보에 대한 질문에 답변할 수 없습니다.
그렇기 때문에 검색 증강 생성 (RAG)과 같은 접근 방식은 LLM이 사용자의 요청을 충족하고 학습 시 액세스할 수 없는 개인 정보나 최신 정보에 대응하기 위해 알아야 하는 추가 정보를 제공하는 데 도움이 됩니다.
대화로 돌아가서 이번에는 문서에 관해 질문할 수 있습니다. 더 작은 조각('청크')으로 분할된 문서가 포함된 데이터베이스에서 관련 정보를 검색할 수 있는 챗봇을 빌드하면, 이 정보는 학습에 포함된 지식에만 의존하는 대신 모델에서 답변의 근거를 마련하는 데 사용됩니다.
RAG에는 두 단계가 있습니다.
- 수집 단계 - 문서가 메모리에 로드되고, 작은 청크로 분할되고, 벡터 임베딩 (청크의 고차원 다차원 벡터 표현)이 계산되어 시맨틱 검색을 수행할 수 있는 벡터 데이터베이스에 저장됩니다. 이 수집 단계는 보통 새 문서를 문서 자료에 추가해야 할 때 한 번 수행됩니다.
- 쿼리 단계 - 이제 사용자가 문서에 대해 질문할 수 있습니다. 이 질문은 벡터로도 변환되고 데이터베이스의 다른 모든 벡터와 비교됩니다. 가장 유사한 벡터는 일반적으로 의미론적으로 관련되며 벡터 데이터베이스에서 반환합니다. 그런 다음 LLM에는 대화의 맥락, 데이터베이스가 반환한 벡터에 해당하는 텍스트 청크가 제공되고, 이러한 청크를 검토하여 답변의 근거를 제시하라는 요청을 받습니다.
서류 준비하기
이 새로운 데모에서는 'Attention Is All You Need', 연구 논문. Google이 개척한 Transformer 신경망 아키텍처에 대해 설명합니다. Transformer는 오늘날 모든 현대적인 대규모 언어 모델이 구현되는 방식을 보여줍니다.
이 논문은 이미 저장소의 attention-is-all-you-need.pdf에 다운로드되어 있습니다.
챗봇 구현
2단계 접근 방식을 빌드하는 방법을 살펴보겠습니다. 먼저 문서 수집으로 이뤄진 후 사용자가 문서에 대해 질문하는 쿼리 시간을 살펴보겠습니다.
이 예에서는 두 단계가 모두 동일한 클래스에서 구현됩니다. 일반적으로 수집을 처리하는 애플리케이션과 사용자에게 챗봇 인터페이스를 제공하는 애플리케이션이 있습니다.
또한 이 예에서는 메모리 내 벡터 데이터베이스를 사용합니다. 실제 프로덕션 시나리오에서는 수집과 쿼리 단계가 서로 다른 두 개의 애플리케이션에서 분리되며 벡터는 독립형 데이터베이스에 유지됩니다.
문서 수집
문서 처리 단계의 첫 번째 단계는 이미 다운로드한 PDF 파일을 찾아 읽을 PdfParser
를 준비하는 것입니다.
URL url = new URI("https://github.com/glaforge/gemini-workshop-for-java-developers/raw/main/attention-is-all-you-need.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("textembedding-gecko@003")
.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-001")
.maxOutputTokens(1000)
.build();
embeddingStore
변수의 벡터 데이터베이스를 임베딩 모델과 연결하기 위한 검색기 클래스도 필요합니다. 사용자의 쿼리에 대한 벡터 임베딩을 계산하여 벡터 데이터베이스를 쿼리하여 데이터베이스에서 유사한 벡터를 찾습니다.
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
기본 메서드 외부에서 LLM 전문가 어시스턴트를 나타내는 인터페이스를 만듭니다. 이 인터페이스는 모델과 상호작용하기 위해 AiServices
클래스가 구현하는 인터페이스입니다.
interface LlmExpert {
String ask(String question);
}
이제 새 AI 서비스를 구성할 수 있습니다.
LlmExpert expert = AiServices.builder(LlmExpert.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 large language models,\s
you excel at explaining simply and clearly questions about LLMs.
Here is the question: {{userMessage}}
Answer using the following information:
{{contents}}
"""))
.build())
.contentRetriever(retriever)
.build())
드디어 질문할 준비가 되었습니다.
List.of(
"What neural network architecture can be used for language models?",
"What are the different components of a transformer neural network?",
"What is attention in large language models?",
"What is the name of the process that transforms text into vectors?"
).forEach(query ->
System.out.printf("%n=== %s === %n%n %s %n%n", query, expert.ask(query)));
);
전체 소스 코드는 app/src/main/java/gemini/workshop
디렉터리의 RAG.java
에 있습니다.
샘플을 실행합니다.
./gradlew -q run -DjavaMainClass=gemini.workshop.RAG
출력에 질문에 대한 답변이 표시됩니다.
=== What neural network architecture can be used for language models? === Transformer architecture === What are the different components of a transformer neural network? === The different components of a transformer neural network are: 1. Encoder: The encoder takes the input sequence and converts it into a sequence of hidden states. Each hidden state represents the context of the corresponding input token. 2. Decoder: The decoder takes the hidden states from the encoder and uses them to generate the output sequence. Each output token is generated by attending to the hidden states and then using a feed-forward network to predict the token's probability distribution. 3. Attention mechanism: The attention mechanism allows the decoder to attend to the hidden states from the encoder when generating each output token. This allows the decoder to take into account the context of the input sequence when generating the output sequence. 4. Positional encoding: Positional encoding is a technique used to inject positional information into the input sequence. This is important because the transformer neural network does not have any inherent sense of the order of the tokens in the input sequence. 5. Feed-forward network: The feed-forward network is a type of neural network that is used to predict the probability distribution of each output token. The feed-forward network takes the hidden state from the decoder as input and outputs a vector of probabilities. === What is attention in large language models? === Attention in large language models is a mechanism that allows the model to focus on specific parts of the input sequence when generating the output sequence. This is important because it allows the model to take into account the context of the input sequence when generating each output token. Attention is implemented using a function that takes two sequences as input: a query sequence and a key-value sequence. The query sequence is typically the hidden state from the previous decoder layer, and the key-value sequence is typically the sequence of hidden states from the encoder. The attention function computes a weighted sum of the values in the key-value sequence, where the weights are determined by the similarity between the query and the keys. The output of the attention function is a vector of context vectors, which are then used as input to the feed-forward network in the decoder. The feed-forward network then predicts the probability distribution of the next output token. Attention is a powerful mechanism that allows large language models to generate text that is both coherent and informative. It is one of the key factors that has contributed to the recent success of large language models in a wide range of natural language processing tasks. === What is the name of the process that transforms text into vectors? === The process of transforming text into vectors is called **word embedding**. Word embedding is a technique used in natural language processing (NLP) to represent words as vectors of real numbers. Each word is assigned a unique vector, which captures its meaning and semantic relationships with other words. Word embeddings are used in a variety of NLP tasks, such as machine translation, text classification, and question answering. There are a number of different word embedding techniques, but one of the most common is the **skip-gram** model. The skip-gram model is a neural network that is trained to predict the surrounding words of a given word. By learning to predict the surrounding words, the skip-gram model learns to capture the meaning and semantic relationships of words. Once a word embedding model has been trained, it can be used to transform text into vectors. To do this, each word in the text is converted to its corresponding vector. The vectors for all of the words in the text are then concatenated to form a single vector, which represents the entire text. Text vectors can be used in a variety of NLP tasks. For example, text vectors can be used to train machine translation models, text classification models, and question answering models. Text vectors can also be used to perform tasks such as text summarization and text clustering.
11. 함수 호출
정보를 검색하거나 작업을 수행하는 원격 웹 API 또는 일종의 계산을 수행하는 서비스와 같이 LLM이 외부 시스템에 액세스해야 하는 상황도 있습니다. 예를 들면 다음과 같습니다.
원격 웹 API:
- 고객 주문 추적 및 업데이트
- Issue Tracker에서 티켓을 찾거나 만듭니다.
- 주식 시세 또는 IoT 센서 측정값과 같은 실시간 데이터를 가져옵니다.
- 이메일을 보냅니다.
계산 도구:
- 고급 수학 문제를 위한 계산기.
- LLM에 추론 로직이 필요할 때 코드를 실행하기 위한 코드 해석
- LLM이 데이터베이스를 쿼리할 수 있도록 자연어 요청을 SQL 쿼리로 변환합니다.
함수 호출은 모델이 자신을 대신하여 하나 이상의 함수 호출을 수행하도록 요청하여 더 많은 최신 데이터로 사용자의 프롬프트에 올바르게 응답할 수 있는 기능입니다.
사용자의 특정 프롬프트와 해당 컨텍스트와 관련이 있을 수 있는 기존 함수에 대한 지식이 주어지면 LLM은 함수 호출 요청으로 응답할 수 있습니다. 그러면 LLM을 통합하는 애플리케이션이 함수를 호출한 다음 응답을 통해 LLM에 다시 응답할 수 있습니다. 그러면 LLM은 텍스트 답변으로 답장하여 해석합니다.
함수 호출의 4단계
함수 호출의 예를 살펴보겠습니다. 일기 예보에 관한 정보 가져오기입니다.
Gemini나 다른 LLM에 파리 날씨에 관해 물으면 Gemini나 다른 LLM에 일기예보에 관한 정보가 없다고 답할 것입니다. LLM이 날씨 데이터에 실시간으로 액세스할 수 있도록 하려면 LLM이 사용할 수 있는 몇 가지 함수를 정의해야 합니다.
다음 다이어그램을 살펴보세요.
1️⃣ 먼저 사용자가 파리 날씨에 관해 질문합니다. 챗봇 앱은 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-001")
.maxOutputTokens(100)
.build();
호출할 수 있는 함수를 설명하는 도구 사양을 지정합니다.
ToolSpecification weatherToolSpec = ToolSpecification.builder()
.name("getWeatherForecast")
.description("Get the weather forecast for a location")
.addParameter("location", JsonSchemaProperty.STRING,
JsonSchemaProperty.description("the location to get the weather forecast for"))
.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-flash-001")
.maxOutputTokens(100)
.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 채팅 모델을 구성합니다. 그런 다음 'function' 모델이 호출을 요청할 것입니다
이제 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-001")
.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 및 외부 환경에 부작용을 일으킬 수 있는 서비스 (예: 이메일 전송, 티켓 생성 등)를 통해 외부 환경과 상호작용할 수 있습니다.
이렇게 강력한 에이전트를 만들 때는 책임감을 가지고 만들어야 합니다. 자동 작업을 실행하기 전에 인간 참여형(Human-In-The-Loop)을 고려해야 합니다. 외부 환경과 상호작용하는 LLM 기반 에이전트를 설계할 때는 안전을 염두에 두어야 합니다.
13. Ollama 및 TestContainers로 Gemma 실행
지금까지는 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 컨테이너를 만들어야 합니다. 이 이미지는 이전 실행에서 이미 존재하거나 곧 생성됩니다. 이미지가 이미 있는 경우 기본 Ollama 이미지를 Gemma 기반 변형으로 대체하겠다고 TestContainer에 알립니다.
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 통합으로 애플리케이션을 개선할 차례입니다.