SW 개발
Azure OpenAI LLM, Azure AI Search, LangChain4j로 커스텀 챗봇 제작하기
지단로보트
2024. 6. 3. 16:03
개요
- LLM과 RAG을 이용한 커스텀 챗봇 개발은 현재 전지구에서 가장 핫한 주제 중에 하나이다. 이번 글에서는 Azure 인프라에서 안전하게 격리된
GPT-4o
모델을 셋업하고, 역시 안전하게 격리된Azure AI Search
를 연동하여LangChain4j
라이브러리로 커스텀 챗봇을 제작하는 방법을 정리했다.
순서
- Azure OpenAI Instance 생성
- Azure OpenAI Deployment 생성
- Azure AI Search Service 생성
- LangChain4j 소스 코드를 Azure AI Search에 저장
- LangChain4j 코딩을 도와주는 챗봇 제작
Azure OpenAI Instance 생성
- OpenAI의 다양한 모델을 내 인프라에 설치하려면 선호하는 리전에
OpenAI Instance
를 생성해야 한다.
Microsoft Azure Portal
→ [Azure OpenAI] 클릭
→ [Create Azure OpenAI] 클릭
# [1] Basics
# Project Details
→ Subscription: {your-subscription} 선택
→ Resource groupo: [Create new] 클릭 → Name: {your-resource-group} (입력) → [OK] 클릭
# Instance Details
→ Region: {your-region} 선택
→ name: {your-instance-name} (입력)
→ Pricing tier: {your-pricing-tier} 선택
→ [Next] 클릭
# [2] Network
→ Type: [All networks, including the internet, can access this resource.] 선택
→ [Next] 클릭
→ [Next] 클릭
# [4] Review + submit
→ [Create] 클릭
Azure OpenAI Deployment 생성: GPT-4o
- 질문을 전송할 LLM으로 OpenAI의 최신 대화형 멀티 모달 모델인
GPT-4o
를 생성한다.
Microsoft Azure Portal
→ [Azure OpenAI] 클릭
→ {your-instance} 클릭
→ [Model deployments] 클릭
→ [Manage Deployments] 클릭
# Azure AI Studio
→ [Create new deployment] 클릭
# Deploy model
→ Select a model: [gpt-4o] 선택
→ Model version: [2024-05-13] 선택
→ Deployment type: [Standard] 선택
→ Deployment name: {your-deployment-name} (입력)
→ Tokens per Minute Rate Limit (thousands): 150K
→ Enable Dynamic Quota: [Enabled] 선택
→ [Create] 클릭
Azure OpenAI Deployment 생성: text-embedding-ada-002
- 원본 텍스트의 벡터 변환을 담당할 Embedding Model을 생성한다. OpenAI의 최신의 텍스트 임베딩 모델인
text-embedding-ada-002
를 선택했다.
Microsoft Azure Portal
→ [Azure OpenAI] 클릭
→ {your-instance} 클릭
→ [Model deployments] 클릭
→ [Manage Deployments] 클릭
# Azure AI Studio
→ [Create new deployment] 클릭
# Deploy model
→ Select a model: [text-embedding-ada-002] 선택
→ Model version: [2] 선택
→ Deployment type: [Standard] 선택
→ Deployment name: {your-deployment-name} (입력)
→ Tokens per Minute Rate Limit (thousands): 350K
→ Enable Dynamic Quota: [Enabled] 선택
→ [Deploy] 클릭
Azure AI Search 생성
- RAG 저장소 및 검색엔진으로 사용할
Azure AI Search
를 생성한다.
Microsoft Azure Portal
→ [Azure AI Search] 클릭
→ [Create Azure AI Search] 클릭
# [1] Basics
# Project Details
→ Subscription: {your-subscription} 선택
→ Resource Group: {your-resource-group} 선택
# Instance Details
→ Service name: {your-ai-search-name} (입력)
→ Location: {your-region} 클릭
→ Pricing tier: {your-pricing-tier} 선택
→ [Next] 클릭
→ [Create] 클릭
- Pricing tier 선택시 주의할 점은 Hybrid Search에 해당하는
Semantic ranker
기능을 사용하려면 Basic 이상의 티어를 선택해야 한다.
Azure AI Search Index 생성
- RAG 데이터를 저장할 인덱스를 생성한다.
Microsoft Azure Portal
→ [AI Search] 클릭
→ {your-ai-search} 클릭
→ [Add index] 클릭
→ [Add index (JSON)] 클릭
→ (아래 JSON 내용 붙여넣기)
→ [Save] 클릭
- 아래는
LangChain4j
가 사용하는 Azure AI Search의 Index 템플릿인데, 텍스트 원본을 저장하는 content 필드의 최대 크기를 32,766 bytes에서 도큐먼트가 허용하는 최대 크기(16 MB)까지 저장할 수 있도록 수정한 것이다.
{
"name": "{your-ai-search-index-name}",
"fields": [
{
"name": "id",
"type": "Edm.String",
"searchable": true,
"filterable": true,
"retrievable": true,
"stored": true,
"sortable": true,
"facetable": true,
"key": true,
"indexAnalyzer": null,
"searchAnalyzer": null,
"analyzer": null,
"normalizer": null,
"dimensions": null,
"vectorSearchProfile": null,
"vectorEncoding": null,
"synonymMaps": []
},
{
"name": "content",
"type": "Edm.String",
"searchable": true,
"filterable": false,
"retrievable": true,
"stored": true,
"sortable": false,
"facetable": false,
"key": false,
"indexAnalyzer": null,
"searchAnalyzer": null,
"analyzer": null,
"normalizer": null,
"dimensions": null,
"vectorSearchProfile": null,
"vectorEncoding": null,
"synonymMaps": []
},
{
"name": "content_vector",
"type": "Collection(Edm.Single)",
"searchable": true,
"filterable": false,
"retrievable": true,
"stored": true,
"sortable": false,
"facetable": false,
"key": false,
"indexAnalyzer": null,
"searchAnalyzer": null,
"analyzer": null,
"normalizer": null,
"dimensions": 1536,
"vectorSearchProfile": "vector-search-profile",
"vectorEncoding": null,
"synonymMaps": []
},
{
"name": "metadata",
"type": "Edm.ComplexType",
"fields": [
{
"name": "source",
"type": "Edm.String",
"searchable": true,
"filterable": true,
"retrievable": true,
"stored": true,
"sortable": true,
"facetable": true,
"key": false,
"indexAnalyzer": null,
"searchAnalyzer": null,
"analyzer": null,
"normalizer": null,
"dimensions": null,
"vectorSearchProfile": null,
"vectorEncoding": null,
"synonymMaps": []
},
{
"name": "attributes",
"type": "Collection(Edm.ComplexType)",
"fields": [
{
"name": "key",
"type": "Edm.String",
"searchable": true,
"filterable": true,
"retrievable": true,
"stored": true,
"sortable": false,
"facetable": true,
"key": false,
"indexAnalyzer": null,
"searchAnalyzer": null,
"analyzer": null,
"normalizer": null,
"dimensions": null,
"vectorSearchProfile": null,
"vectorEncoding": null,
"synonymMaps": []
},
{
"name": "value",
"type": "Edm.String",
"searchable": true,
"filterable": true,
"retrievable": true,
"stored": true,
"sortable": false,
"facetable": true,
"key": false,
"indexAnalyzer": null,
"searchAnalyzer": null,
"analyzer": null,
"normalizer": null,
"dimensions": null,
"vectorSearchProfile": null,
"vectorEncoding": null,
"synonymMaps": []
}
]
}
]
}
],
"scoringProfiles": [],
"corsOptions": null,
"suggesters": [],
"analyzers": [],
"normalizers": [],
"tokenizers": [],
"tokenFilters": [],
"charFilters": [],
"encryptionKey": null,
"similarity": {
"@odata.type": "#Microsoft.Azure.Search.BM25Similarity",
"k1": null,
"b": null
},
"semantic": {
"defaultConfiguration": "semantic-search-config",
"configurations": [
{
"name": "semantic-search-config",
"prioritizedFields": {
"titleField": null,
"prioritizedContentFields": [
{
"fieldName": "content"
}
],
"prioritizedKeywordsFields": [
{
"fieldName": "content"
}
]
}
}
]
},
"vectorSearch": {
"algorithms": [
{
"name": "vector-search-algorithm",
"kind": "hnsw",
"hnswParameters": {
"metric": "cosine",
"m": 4,
"efConstruction": 400,
"efSearch": 500
},
"exhaustiveKnnParameters": null
}
],
"profiles": [
{
"name": "vector-search-profile",
"algorithm": "vector-search-algorithm",
"vectorizer": null,
"compression": null
}
],
"vectorizers": [],
"compressions": []
}
}
build.gradle.kts
- 인프라 레벨에서의 준비는 이제 끝났다. 본격적인 코드 작업을 위해 프로젝트에
LangChain4j
를 임포트한다.
val langChain4jVersion = "0.31.0"
dependencies {
implementation("dev.langchain4j:langchain4j-core:$langChain4jVersion")
implementation("dev.langchain4j:langchain4j-embeddings:$langChain4jVersion")
implementation("dev.langchain4j:langchain4j-easy-rag:$langChain4jVersion")
implementation("dev.langchain4j:langchain4j-open-ai:$langChain4jVersion")
implementation("dev.langchain4j:langchain4j-azure-open-ai:$langChain4jVersion")
implementation("dev.langchain4j:langchain4j-azure-ai-search:$langChain4jVersion")
}
Embedding, LLM Model 및 RAG 오브젝트 생성
LangChain4j
는Azure OpenAI
와Azure AI Search
생태계를 완벽 지원한다. 아래와 같이 쉽게 관련 오브젝트를 생성할 수 있다.
// Azure OpenAI text-embedding-ada-002의 EmbeddingModel 오브젝트 생성
val embeddingModel: EmbeddingModel = AzureOpenAiEmbeddingModel.builder()
.endpoint("https://{your-azrue-open-ai-text-embedding-ada-002-deployment-name}.openai.azure.com")
.serviceVersion("2023-05-15")
.apiKey("{your-azure-openai-instance-api-key}")
.deploymentName("{your-azrue-open-ai-text-embedding-ada-002-deployment-name}")
.build()
// Document Splitter 오브젝트 생성
val documentSplitter = DocumentSplitters.recursive(
8191,
256,
OpenAiTokenizer("gpt-4o-2024-05-13")
)
// Hybrid Search를 적용한 Azure AI Search의 ContentRetriever 오브젝트 생성
val contentRetriever: ContentRetriever = AzureAiSearchContentRetriever.builder()
.endpoint("https://{your-azure-ai-search-service-name}.search.windows.net")
.apiKey("{your-azure-ai-search-admin-key}")
.dimensions(1536)
.indexName("{your-azure-ai-search-index-name}")
.createOrUpdateIndex(false)
.embeddingModel(embeddingModel)
.queryType(AzureAiSearchQueryType.HYBRID_WITH_RERANKING)
.maxResults(50)
.minScore(0.0)
.build()
// Azure OpenAI GPT-4o의 ChatLanguageModel 오브젝트 생성
val chatLanguageModel = AzureOpenAiChatModel.builder()
.endpoint("https://{your-azrue-open-ai-gpt-4o-deployment-name}.openai.azure.com")
.apiKey("{your-azure-openai-instance-api-key}")
.deploymentName("{your-azrue-open-ai-gpt-4o-deployment-name}")
.serviceVersion("2024-02-01")
.timeout(Duration.ofSeconds(360))
.temperature(0.3)
.topP(0.3)
.build()
데이터를 Azure AI Search에 저장
Azure AI Search
는 데이터 원본이 되는 Text와 이를 다차원 소수 배열로 변환한 Vector 데이터를 같이 하나의 Document로 저장할 수 있다. 이런 특징 덕분에 Semanctic Search와 Keyword Search가 결합된 Hybrid Search가 가능하다.- 아래는
LangChain4j
라이브러리 소스 코드를 클로닝하여 Azure AI Search에 저장하는 예제이다. 먼저 프로젝트 루트 디렉토리에 관련 저장소를 클로닝한다.
# LangChain4j 라이브러리 저장소 클로닝
$ git clone https://github.com/langchain4j/langchain4j.git
$ git clone https://github.com/langchain4j/langchain4j-embeddings.git
$ git clone https://github.com/langchain4j/langchain4j-examples.git
- 클로닝한 소스 코드 파일을
text-embedding-ada-002
임베딩 모델을 사용하여 Vector 데이터로 변환하여 원본 텍스트와 함께 Azure AI Search에 저장한다.
// 클로닝한 LangChain4j 라이브러리를 소스 코드 파일만 Azure AI Search에 저장
val embeddings: MutableList<Pair<Embedding, TextSegment>> = mutableListOf()
arrayOf("langchain4j", "langchain4j-embeddings", "langchain4j-examples").forEach { directory ->
Files.walk(Paths.get(directory)).use { paths ->
paths.filter {
Files.isRegularFile(it) && arrayOf(
"md",
"xml",
"gradle",
"kts",
"java",
"kt"
).contains(it.fileName.toString().substringAfterLast('.', ""))
}
.forEach { path ->
val document = Document.document(path.toFile().readText())
val segments = documentSplitter.split(document)
segments.forEach { segment ->
embeddings.add(Pair(embeddingModel.embed(segment).content(), segment))
}
}
}
try {
contentRetriever.addAll(embeddings.map { it.first }, embeddings.map { it.second })
} catch (ex: IndexBatchException) {
ex.indexingResults.filter { !it.isSucceeded }.forEach {
// Azure AI Search 저장 중에 발생한 에러 메시지 출력
println(it.errorMessage)
}
}
LLM 모델에 RAG 연동 질의
- Azure AI Search에 LangChain4j 관련 소스 코드가 저장되었으므로 이제 RAG로 질의한 정보 목록을 LLM의 프롬프트에 포함하여 질의할 수 있다.
// [1] LLM 질문 작성
val question = "RAG에서 Semantic Search와 Keyword Search의 장점을 결합한 Hybrid Search에 대해서 자세히 설명해줘. 그리고 LangChain4j에서 Azure AI Search에 데이터를 저장하고 질의하는 예제를 작성해줘."
// [2] Azure AI Search로부터 Re-ranking 적용된 Hybrid Search 검색 결과 획득
val contents: List<Content> = contentRetriever.retrieve(Query.from(question))
// [3] LLM 답변 획득
val aiMessage = chatLanguageModel.generate(
SystemMessage(
"""
당신은 LangChain4j 라이브러리 사용법 안내 및 예제 작성을 도와주는 코딩 어시스턴트야. 예제 작성은 Kotlin 언어로 제작해줘. 질문에 대한 데이터는 아래 Information을 참고해서 대답해줘.
Information: \\\
${contents.take(25).joinToString("\n\n") { it.textSegment().text() }}
\\\
""".trimIndent()
),
UserMessage(question)
)
// [4] LLM 답변 출력
println(aiMessage.content().text())
- 다른 방법으로
AiServices
를 이용하여 제작할 수 있다.
// [1] LLM 질문 작성
val question = "{your-question}"
// [2] 커스텀 어시스턴트 생성
interface Assistant {
fun ask(userMessage: String): String
}
val assistant = AiServices.builder(Assistant::class.java)
.chatLanguageModel(chatLanguageModel)
.contentRetriever(contentRetriever)
.systemMessageProvider { "당신은 LangChain4j 라이브러리 사용법 안내 및 예제 작성을 도와주는 코딩 어시스턴트야. 예제 작성은 Kotlin 언어로 제작해줘." }
.build()
// [3] LLM 답변 획득
val answer = assistant.ask(question)
// [4] LLM 답변 출력
println(assistant.ask(question))