LLM 기반 Application, LBox AI 개발기

Dongjun Lee
LBOX Team
Published in
13 min readMay 27, 2024

안녕하세요, LBox ML Engineer 이동준 입니다.

그동안 ML 도메인에서 일을 해오면서, 지금과 같이 기술이 빠르게 발전하고 또 패러다임이 바뀌고 있던 때가 있었나? 라는 생각을 해보게 됩니다. 약 2020년에 진행했던 예약 전화를 받는 Ai 프로젝트 경험에 빗대어 생각해 보면, 팀으로 풀어야 했던 문제를 소수의 인원으로, LLM 을 활용해서 풀 수 있는 시대가 되었습니다.

새로운 기술이 나오면 늘 그렇듯, 다양한 시행착오를 거치게 됩니다. 이 글에서도 정답이 아닌, 프로젝트를 진행하며 겪었던 다양한 경험에 대해 공유하고자 합니다.

💡 TL;DR
LLM 기반 서비스 만의 특징을 살펴보고, 서비스를 개발하면서 나왔던 다양한 니즈들과 이를 해결하기 위한 시도들을 다룹니다(비용 트래킹, 시스템 측면의 Hyperparameter, 속도/비용 최적화, 구조화된 출력 데이터 및 해당 데이터의 정합성). LLM 프레임워크는 주로 LangChain 을 기준으로 다뤄집니다.

LLM 기반 서비스 특성

먼저 LLM 기반 서비스의 특성에 대해서 다뤄보겠습니다.

LLM 의 동작 원리 자체는 아주 단순합니다. ‘다음 단어를 예측하는 언어 모델’ 로서 주어진 입력에 대한 답변을 글로 표현가능한 다양한 형태로 생성할 수 있다는 것입니다. 주어진 뉴스 제목으로부터 본문을 생성하는 것을 시작으로, 표를 만들기도 하고, 코드 예시를 작성해주거나 DB의 데이터를 읽어오는 SQL 문까지도 생성이 가능합니다.
이러한 특징과 더불어, 상용 언어모델 (e.g. ChatGPT, Claude)의 성능이 빠르게 올라가면서, 다양한 서버스에 LLM 이 활용되고 있습니다.

서비스에서 고려해야할 사항들

이렇게 강력한 LLM을 서비스에 적용할 때, 몇가지 중요한 고려사항이 있습니다.

  1. 신뢰성 및 최신성 : LLM의 대표적인 한계점은, 환각현상(hallucination)으로 그럴듯한 거짓말을 한다는 것입니다. 정보의 신뢰성과 최신 내용이 빠르게 반영되어야 하는 서비스에서는 LLM 을 그대로 사용하기에는 어려움이 존재합니다.
  2. 비용 : 상용 언어모델의 경우, 입력 Token, 출력 Token을 구분해서 비용을 발생하는 구조입니다. 이는 단순한 서버 유지비 외에도 서비스를 사용할 때마다 비용이 발생함을 의미합니다. 만약 상용 언어모델을 사용하지 않고, 직접 LLM 을 구축하여 서빙하더라도 GPU 머신과 더불어 운영에 많은 비용이 요구됩니다.
  3. 속도 : 속도 즉, 응답속도(latency)는 서비스에서 중요한 지표 중에 하나입니다. LLM 기반의 서비스가 스트리밍으로 동작하는 이유는 바로 이 응답속도 때문입니다. LLM 자체가 속도 측면에서 많은 개선이 있지만, 약 500자를 생성하기 위해서는 30초 내외의 시간이 소요됩니다. 30초를 기다리는 대신 스트리밍으로 답변이 생성되는 과정을 볼 수 있게 함으로서, 응답속도라는 문제를 우회하고 있는 것입니다.

Compound AI System

Retrieval-Augmented Generation for Large Language Models: A Survey(2312.10997v5), Figure 3

환각현상을 해결하기 위한 대표적인 기술 중 한 가지는 바로 ‘RAG(Retrieval Augmented Generation)’ 입니다. RAG는 검색된 문서를 기반으로 답변을 생성하는 방식으로서, 위과 같이 다양한 기능들을 연결하여 답변을 생성하는 Modular RAG의 구조로 발전하고 있습니다. 시스템의 관점에서 본다면, LLM 과 검색 시스템이 연결이 되는 것입니다.

이 RAG에서 한 단계 추상화하여 시스템을 바라본다면, 다수의 LLM과 검색, 백엔드 서버, DB 등 모든 시스템의 연결을 통해서 LLM 기반의 시스템이 구성될 수 있음을 의미합니다.

https://bair.berkeley.edu/blog/2024/02/18/compound-ai-systems/

위 그림은 BAIR에서 작성된 블로그 글에서 LLM 기반 서비스가 하나의 모델이 아닌, 다양한 요소로 구성되는 Compound AI System으로 발전하게 될 것 임을 시사하고 있습니다. 대표적으로 다음의 네 가지 이유를 들고 있습니다.

  1. Task에 따라서, 시스템 디자인을 통한 쉬운 개선이 가능
  2. 시스템이 동적일 수 있음
  3. 시스템을 통해 조작 가능하고, 신뢰성을 올릴 수 있음
  4. 목표 성과의 다양성

서비스를 개발하며 고민했던 아이템들

다음은 실제 LLM Application 을 개발하면서, 왜 위와 같이 복합적인 AI 시스템이 구성되어야 하는지, 직접 피부로 느꼈던 이슈들에 대해서 다뤄보고자 합니다.

LBox AI 에서 가장 중요하게 봐야 하는 지표는 크게 3가지였습니다.

  • 성능, 비용, 응답속도

서비스의 특성에 따라, ‘성능’을 가장 중시하며 ‘비용’에 관대할 수도 있고, 혹은 ‘응답속도’를 가장 중요시 할 수도 있습니다.

1) 비용 트래킹

가장 먼저 비용 트래킹에 대해 살펴보겠습니다. LLM 기반 서비스는 특성에서 설명했던 것처럼, 특정 기능이 동작할 때마다 비용이 발생하는 구조입니다. 이는 서비스의 가격 결정에 있어서 중요한 고려 요소로서, 적절한 서비스 비용을 책정하기 위해서는 LLM 에서 사용되는 모든 비용을 각 기능 별로 구분하여 추적할 수 있어야 함을 의미합니다.

(* 비용 ~ 입력 Token, 출력 Token 의 각각의 수와 사용된 모델의 비용으로 계산)

2) 시스템 측면의 Hyperparamter

ML 모델의 특징으로, 다양한 실험을 통해서 최적의 Hyperparameter를 찾아가는 과정이 필수적입니다. Compound AI System 은 LLM 과 System 의 연결로 구성이 되어있기 때문에, LLM 뿐만 아니라 시스템 측면에서의 Hyperparameter를 정의할 수 있으며, 이를 기반으로 최적의 Hyperparameter 값을 찾아가는 과정을 동일하게 진행할 수 있습니다.

다음은 Elasticsearch로 검색이 구성되는 간단한 RAG Module 입니다.

질문이 들어왔을 때, Elasticsearch를 통해 관련 문서를 검색하여 답변을 생성하는 기본적인 RAG

Elasticsearch 에서 제공하는 기능 중, 검색 결과 수(size)와 하이라이트를 통해 추출한 스니펫의 수(number_of_fragments), 길이(fragement_size)는 답변의 품질에 많은 영향을 주는 변수입니다. 즉, Elasticsearch 시스템의 Hyperparameter로 볼 수 있는 것입니다.

딥러닝 모델에 AutoML 을 통해 최적의 hyperparameter를 찾아가는 것처럼, 추후 LLM 시스템에서 최적의 hyperparameter 를 찾는 기술 또한 곧 나올 것 같습니다.

현재 LBox AI 에서는, 미리 선언되어있는 LLM 과 시스템의 Hyperpameter 를 조절해가면서 성능을 평가해보고 있고, 성능/비용/응답속도 지표의 밸런스를 고려하여 최적의 Hyperpameter를 선택하고 있습니다.

3) 속도 최적화

LLM 에서의 응답시간은 두 가지로 구분해서 볼 수 있습니다. 1) 스트리밍으로 생성이 시작되기까지의 시간, 2) 모든 답변이 완료되는 시간입니다. 서비스 측면에서 사용자에게 더욱 직접적으로 연결되어 있는 지표는 1번 첫 Token 전달 시간이나, 2번 생성 완료 시간은 전체 시스템 관점에서 모든 부분에 영향을 미치게 됩니다.

즉, 첫 Token 전달 시간뿐 아니라, 각 Module 의 응답속도 또한 관리가 필요함을 의미합니다. LLM 에서 속도 를 최적화할 수 있는 방안에는 병렬처리와 Multi-Task 방식이 있습니다.

MULTI-TASK INFERENCE: Can Large Language Models Follow Multiple Instructions at Once? (2402.11597), Figure 1

병렬처리는 Single-Task 를 기준으로 동시에 여러 개의 Task 를 실행시키는 방법으로, 동시에 실행이 되어도 괜찮은 Task가 다수 있는 경우에는 속도 최적화에 많은 도움이 됩니다. LangChain 에서는 RunnableParallel 이라는 API가 제공되며, 이는 Python의 ThreadPoolExecutor 를 통해서 병렬처리가 구현되어 있습니다.

또한 위 논문에서는 Multi-Task 의 경우, LLM 에게 A, B, C Task를 모두 한꺼번에 추론하도록 프롬프트를 작성함으로써, 빠른 속도와 더 나은 성능을 얻을 수 있음이 기술되어 있습니다.

4) 구조화된 생성 데이터

서비스를 구성함에 있어서 데이터에 대한 Type 정의나, 구조화는 기본사항입니다. LLM 에서 생성한 출력값과 구조화된 데이터는 어떤 식으로 연결이 될까요?

LangChain 에서는 보통 다음과 같이, 프롬프트에 응답 형식을 적고 Pydantic Parser를 통해서 LLM의 출력값을 파싱 하게 됩니다.

_PROMPT = """
답변은 Json 형식으로 아래와 같습니다.
{ \"score\": float }

답변 예시)
{ "score": 90 }
"""

class ScoreModel(BaseModel):
score: int

parser = PydanticOutputParser(pydantic_object=ScoreModel)

chain = prompt_template | llm | parser

>>> llm 출력값 : { "score": 95 } # Token: 15개 생성
>>> parser를 통해서 ScoreModel 반환

위와 같이 생성값을 구조화할 경우, 시스템의 측면에서 데이터의 정합성을 미리 검증할 수 있고, 코드의 가독성이 좋아진다는 장점이 있습니다. 다만 LLM 생성에서 특정 포맷을 요구하기 때문에 부가적인 Token 이 더 사용됩니다. 이는 응답시간과 비용이 추가로 소요된다는 말과 같습니다. 그렇다면, LLM 에게는 딱 필요한 내용만 출력하면서, 구조화된 데이터로 로직을 구현하기 위해서는 어떤 방식이 있을까요?

4-1)응답 데이터가 1개의 속성으로 구성되어 있을 때

LLM 에서는 딱 필요한 값만 생성하도록, Prompt 를 구성하고 코드 상에서는 아래와 같이 mapper 를 추가하여 구조화된 데이터로 연결할 수 있습니다. 이렇게 되면 생성에 사용되는 Token이 15개에서 2개로 크게 줄어들게 됩니다. (약 86% 감소)

_PROMPT = """
답변은 단일 형식으로 아래와 같습니다.
float

답변 예시)
90
"""

class ScoreModel(BaseModel):
score: int

mapper = RunnableLambda(lambda x: f"{{ \\"score\\": {x.content} }}")
parser = PydanticOutputParser(pydantic_object=ScoreModel)

chain = prompt_template | llm | parser

>>> llm 출력값 : 95 # Token: 2개 생성
>>> mapper : { "score": 95 }
>>> parser를 통해서 ScoreModel 반환

4-2)응답 데이터가 2개의 속성으로 구성되어 있을 때

데이터가 2개 이상의 속성을 가질 때는, 기본 JSON 구조를 활용해야 합니다. 여기서도 생성되는 Token의 수를 줄일 수 있는데, 바로 변수의 이름을 축약하는 방식입니다. 이때 변수명뿐만 아니라, 생성되는 값을 축약어로 사용할 수 있습니다.

아래는 예시로서, score를 ‘s’ 로 축약하여 사용하고 있고 @property 를 통해서 코드 상에서는 그대로 ScoreModel.score 로 사용할 수 있게 됩니다.

class ScoreModel(BaseModel):
s: int # score 축약어

@property
def score(self):
return self.s

>>> llm 출력값 : { "s": 95 }
>>> parser를 통해서 ScoreModel 반환

그 외에도 JSON 대신 YAML 형식으로 구조화된 데이터를 생성 했을 때, Token 이 덜 사용된다고 공유 되기도 하였습니다.

5) 생산성

생산성은 투입된 양에 대한 산출물에 대한 지표로서, LLM 기반의 서비스에서는 기본적인 코드, 기능 구현 외에도 모델의 성능/비용/속도에 대한 산출물이 있습니다. 즉, 간단하게 기능을 구현할 수 있고, 다양한 실험을 할 수 있어야 함을 의미합니다.

LBOX AI Architecture

LBOX AI에 대한 아키텍처는 위 고민들이 담겨 있습니다. 기본적으로 다수의 LLM 과 여러 가지 System을 함께 연결하여 구성하는, Compound AI System으로서 시스템을 설계하였고, 구성된 시스템은 아래와 같은 특징을 갖고 있습니다.

  • 특정 기능을 잘 수행할 수 있는 Assistant 들이 있다.
  • 각각의 Assistant는 Module 과 Tool의 조합으로 구성된다.
  • Module 은 [Prompt + LLM + 구조화된 출력]의 구조를 가지며, Module 별로 독립적으로 토큰 사용량, 비용, 시간을 추적할 수 있고 성능 또한 평가할 수 있다.
  • Tool 은 외부 System을 의미하며, Assistant/Module 에서 연결해서 사용할 수 있다.
  • Assistant 및 Module 에서 사용하는 LLM 은 상용 LLM을 포함하여, 직접 서빙하는 경우에도 적합한 모델을 선택해서 사용할 수 있다.

예를 들어, 위 아키텍쳐에 맞춰서 구성이 되는 RAG 용 Assistant는 다음과 같은 구조를 가질 수 있게 될 것입니다. (위 RAG Module과 동일)

  • RAG Assistant ~ [Query Rewriting module] + [Elasticsearch Tool] + [Answer module]

끝으로

LLM 기반의 서비스가 가지는 특성과 서비스를 개발하면서 맞닥뜨린 이슈 및 개선방향을 다뤄보았습니다. 기술의 발전이 너무나도 빠르기 때문에, 지금의 시행착오가 빠르게 deprecated 될 수도 있습니다. 그러나 전체 시스템의 관점에서는 Compound AI System 의 방향은 어느 정도 유지가 될 것이라 생각이 됩니다.

개인적으로 백엔드 엔지니어링, LLM(기존 NLP 기술을 포함하여) 양쪽 모두 경험이 있기 때문에, 두 분야가 결합되어 탄생한 Compund AI System을 다루면서 각 분야의 경험을 바탕으로 다양한 생각을 많이 해 볼 수 있었습니다. 이 글이 LLM Application 을 만드시는 분들에게 도움이 되기를 기대해봅니다.

--

--