먼저 LLM 기반 서비스의 특성에 대해서 다뤄보겠습니다.
LLM 의 동작 원리 자체는 아주 단순합니다. ‘다음 단어를 예측하는 언어 모델’ 로서 주어진 입력에 대한 답변을 어떠한 형태로든 생성할 수 있다는 것입니다. 주어진 뉴스 제목에 맞춰서 글을 생성하는 것을 시작으로, 표륾 만들기도 하고, 코드 예시를 작성해주거나 DB의 데이터를 읽어오는 SQL 문까지도 생성할 수 있게 되었습니다. 이와 더불어, 상용 언어모델 (e.g. ChatGPT, Claude)의 성능이 빠르게 올라가면서, 다양한 서버스에 LLM 이 활용되고 있습니다.
서비스에서 고려해야할 사항들
이렇게 강력한 LLM을 서비스에 적용할 때, 몇가지 고려사항이 있습니다.
- 신뢰성 및 최신성 : LLM의 대표적인 한계점은, 환각현상(hallucination)으로 그럴듯한 거짓말을 한다는 것입니다. 정보의 신뢰성과 최신 내용이 빠르게 반영되어야 하는 서비스에서는 LLM 을 그대로 사용하기에는 어려움이 존재합니다.
- 비용 : 상용 언어모델의 경우, 입력 Token, 출력 Token을 구분해서 비용을 발생하는 구조입니다. 이는 단순한 서버 유지비 외에도 서비스를 사용할 때마다 비용이 발생함을 의미합니다. 만약 상용 언어모델을 사용하지 않고, 직접 LLM 을 구축하고 서빙한다고 하더라도 GPU 머신과 더불어 운영에 많은 비용이 요구됩니다.
- 속도 : 속도 즉, 응답속도(latency)는 서비스에서 중요한 지표 중에 하나입니다. LLM 기반의 서비스가 스트리밍으로 동작하는 이유는 바로 이 응답속도 때문입니다. LLM 자체가 속도 측면에서 많은 개선이 있지만, 약 500자를 생성하기 위해서는 30초 내외의 시간이 소요됩니다. 30초를 기다리는 대신 스트리밍으로 답변이 생성되는 과정을 볼 수 있게 함으로서, 응답속도라는 문제를 우회하고 있는 것입니다.
Compound AI System
환각현상을 해결하기 위한 대표적인 기술 중 한 가지는 바로 ‘RAG(Retrieval Augmented Technology)’ 입니다. RAG는 검색된 문서를 기반으로 답변을 생성하는 방식으로서, 위과 같이 다양한 기능들을 연결하여 답변을 생성하는 Modular RAG의 구조로 발전하고 있습니다. 시스템의 관점에서 본다면, LLM 과 검색 시스템이 연결이 되는 것입니다.
이 RAG에서 한 단계 추상화하여 시스템을 바라본다면, 다수의 LLM과 검색, 백엔드 서버, DB 등 모든 시스템의 연결을 통해서 LLM 기반의 시스템이 구성될 수 있음을 의미합니다.
위 그림은 BAIR에서 작성된 블로그 글에서 Compound AI System으로 발전하게 될 것임을 시사하고 있습니다. 대표적으로 다음의 네 가지 이유를 들고 있습니다.
- Task에 따라서, 시스템 디자인을 통한 쉬운 개선이 가능
- 시스템이 동적일 수 있음
- 시스템을 통해 조작 가능하고, 신뢰성을 올릴 수 있음
- 목표 성과의 다양성
LBox AI 또한 다수의 LLM 과 여러 가지 System을 함께 연결하여 구성하는, Compound AI System으로서 시스템을 설계하고 구성하였습니다.
- 특정 기능을 잘 수행할 수 있는 다양한 Assistant 들이 있다.
- Assistant는 Module 과 Tool의 조합으로 구성되며, Module 은 Immediate + LLM + 구조화된 출력의 구조를 가지며, Device 은 외부 System을 의미한다.
- Module 은 Device 을 연결해서 구성될 수 있다.
- Assistant 및 Module 에서 사용하는 LLM 은 상용 LLM을 포함하여, 직접 서빙하는 경우에도 선택해서 사용할 수 있다.
다음은 실제 Utility 을 개발하면서, 왜 위와 같이 복합적인 AI 시스템이 구성되어야 하는지, 직접 피부로 느겼던 이슈들에 대해서 다뤄보고자 합니다.
LBox AI 에서 가장 중요하게 봐야 하는 지표는 크게 3가지였습니다.
- 성능, 비용, 응답속도
서비스의 특성에 따라, ‘성능’이 가장 중요하고 ‘비용’ 에 관대할 수도 있고, 또 다른 서비스는 ‘응답속도’가 가장 중요할 수 있습니다.
1) 비용 트래킹
가장 먼저 비용 트래킹입니다. LLM 기반 서비스는 특성에서 설명했던 것처럼, 특정 기능이 동작할 때마다 비용이 발생하는 구조입니다. 이는 서비스의 가격 결정에 있어서 중요한 고려 요소로서, 적절한 서비스 비용을 책정하기 위해서는 LLM 에서 사용되는 모든 비용을 모듈 별로 구분하여 추적할 수 있어야 함을 의미합니다.
(* 비용 ~ 입력 Token, 출력 Token 의 각각의 수와 사용된 모델의 비용으로 계산)
위 아키텍처에 맞춰서, 모든 Module과 Assitant의 비용을 추적할 수 있도록 구성됩니다.
2) 시스템 측면의 Hyperparamter
ML 모델 자체의 특징으로, 다양한 실험을 통해서 최적의 Hyperparameter를 찾아가는 과정은 필수적인 과정입니다. Compound AI System 은 LLM 과 System 의 연결로 구성이 되어있기 때문에, LLM 뿐만 아니라 시스템 측면에서의 Hyperparameter를 정의할 수 있으며, 이를 기반으로 최적의 Hyperparameter 값을 찾아가는 과정을 동일하게 진행할 수 있습니다.
다음은 Elasticsearch로 검색이 구성되는 간단한 RAG Module 입니다.
Elasticsearch 에서 제공하는 기능 중, 검색 결과 수(dimension)와 하이라이트를 통해 추출한 스니펫의 수(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-Job 방식이 있습니다.
병렬처리는 Single-Job 를 기준으로 동시에 여러 개의 Job 를 실행시키는 방법으로, 동시에 실행이 되어도 괜찮은 Task가 다수 있는 경우에는 속도 최적화에 많은 도움이 됩니다. LangChain 에서는 RunnableParallel 이라는 API가 제공되며, 이는 Python의 ThreadPoolExecutor 를 통해서 병렬처리가 구현되어 있습니다.
Multi-Job 의 경우, LLM 에게 A, B, C Task를 모두 한꺼번에 추론하도록 프롬프트를 작성함으로써, 빠른 속도와 더 나은 성능을 얻을 수 있음을 위 논문에서 말하고 있습니다.
4) 구조화된 생성 데이터
서비스를 구성함에 있어서 데이터에 대한 Sort 정의나, 구조화는 기본사항입니다. LLM 에서 생성한 출력값과 구조화된 데이터는 어떤 식으로 연결이 될까요?
LangChain 에서는 보통 다음과 같이, 프롬프트에 응답 형식을 적고 Pydantic Parser를 통해서 LLM의 출력값을 파싱 하게 됩니다.
class ScoreModel(BaseModel):
rating: intparser = PydanticOutputParser(pydantic_object=ScoreModel)
chain = prompt_template | llm | parser
>>> llm 출력값 : { "rating": 95 } # Token: 15개 생성
>>> parser를 통해서 ScoreModel 반환
위와 같이 생성값을 구조화된 데이터로 만드는 경우, 시스템의 측면에서는 데이터의 정합성을 미리 검증할 수 있고, 코드의 가독성이 좋아집니다. 다만 LLM 생성에서 특정 포맷을 요구하기 때문에 부가적인 Token 이 더 사용됩니다. 이는 응답시간과 비용이 추가로 소요된다는 말과 같습니다. 그렇다면, LLM 에게는 딱 필요한 내용만 출력하면서, 구조화된 데이터로 로직을 구현하기 위해서는 어떤 방식이 있을까요?
1)응답 데이터가 1개의 속성으로 구성되어 있을 때
LLM 에서는 딱 필요한 값만 생성하도록, Immediate 를 구성하고 코드 상에서는 아래와 같이 mapper 를 추가하여 구조화된 데이터로 연결할 수 있습니다. 이렇게 되면 생성에 사용되는 Token이 15개에서 2개로 확 줄어들게 됩니다. (약 86% 감소)
class ScoreModel(BaseModel):
rating: intmapper = RunnableLambda(lambda x: f"{{ "rating": {x.content material} }}")
parser = PydanticOutputParser(pydantic_object=ScoreModel)
chain = prompt_template | llm | parser
>>> llm 출력값 : { "rating": 95 } # Token: 2개 생성
>>> parser를 통해서 ScoreModel 반환
2)응답 데이터가 2개의 속성으로 구성되어 있을 때
데이터가 2개 이상의 속성을 가질 때는, 기본 JSON 구조를 활용해야 합니다. 여기서도 생성되는 Token의 수를 줄일 수 있는데, 바로 변수의 이름을 축약하는 방식입니다. 이때 변수명뿐만 아니라, 생성되는 값을 축약어로 사용할 수 있습니다.
아래는 예시로서, score를 ‘s’ 로 축약하여 사용하고 있고 @property
를 통해서 코드 상에서는 그대로 ScoreModel.rating 로 사용할 수 있게 됩니다.
class ScoreModel(BaseModel):
s: int # rating 축약어@property
def rating(self):
return self.s
>>> llm 출력값 : { "s": 95 }
>>> parser를 통해서 ScoreModel 반환
그 외에도 JSON 대신 YAML 형식으로 구조화된 데이터를 생성 했을 때, Token 이 덜 사용된다고 공유 되기도 하였습니다.
LLM 기반의 서비스가 가지는 특성과 서비스를 개발하면서 맞닥뜨린 이슈 및 개선방향을 다뤄보았습니다. 기술의 발전 방향이 너무나도 빠르기 때문에, 지금의 시행착오가 빠르게 deprecated 될 수도 있습니다. 전체 시스템의 관점에서는 Compound AI System 의 방향은 어느 정도 유지가 될 것이라 생각이 됩니다.
개인적으로 LLM 과 엔지니어링 양쪽 모두 경험이 있기 때문에, Compound AI System 관점에서 생각을 많이 해볼 수 있는 기회였습니다. 이 글이 LLM Utility 을 만드시는 분들에게 도움이 되기를 기대해봅니다.