나는 A.I.에 특히 매료되어 있다. 적어도 인지와 창의성 차원에서는, 인간이라는 것이 무엇인지에 대한 우리의 생각을 가장 가까운 곳에서 건드리고 찔러보는 기술이기 때문이다. 그렇게 말할 수 있는 기술은 많지 않다. 그리고 이 A.I.의 순간이 가진 무게가 아직 우리 어깨 위에 완전히 내려앉았다고도 생각하지 않는다. 콘텐츠 생산 비용이 극적으로 낮아지는 가운데, 우리는 살아 움직이는 역사 속에 살고 있다. 사실 이런 순간을 지나간 인류 대부분은 인쇄기 이후의 종교개혁과 그 뒤에 따라온 모든 일, 또는 인터넷과 대통령 선거를 예견하지 못했을 것이다. 그래서 몇 개의 개념증명(PoC)을 거친 뒤, 나는 전통적인 소프트웨어 작성 방식과 데이터 기반 개발 방식 사이에 근본적인 차이가 있다는 지점에 도착했다. 전통적 프로그래밍과 머신러닝의 차이는 이런 격언과 비슷하다:
“사람에게 물고기를 주면, 하루를 먹일 수 있다. 낚시하는 법을 가르치면, 평생을 먹일 수 있다.”
ML/LLM 기반 개발은 소프트웨어 개발의 패러다임 전환이다
대부분의 사람이 소프트웨어 개발에서 실제로 무슨 일이 벌어지는지 잘 모른다는 것만으로도 충분히 문제인데, 데이터 기반 개발은 여기에 많은 “유사 과학적” 시행착오와 확률적 사고를 더한다. 그래서 관련 프로세스, 작업 흐름, 도구는 숙련된 엔지니어에게도 완전히 새로운 세계가 된다. 이 글은 두 방식의 역사적 흐름과 차이를 꽤 잘 설명했다. 다만 아쉽게도 내가 관찰한 한 가지는 기술의 속도와 전파가 지역에 따라 너무 달라서, 우리는 이미 모두 타임머신 안에 살고 있다는 점이다. SFTP가 떠오른다…
문제의 핵심은 이것이다. 기계가 배우도록 어떻게 가르칠 것인가? 데이터 수집부터 차원 축소, 모델 선택, 하이퍼파라미터 조정까지 전달 과정의 모든 단계에서 계속 실험하면서, 최종 사용자에게 안정적인 서비스를 어떻게 제공할 것인가? LLM은 자연어, 지식, 추론 도구라는 강력한 무기를 제공하지만, 그 나름의 특이점도 있다. 그래서 이 도전 과제들의 합이, 물론 ChatGPT의 도움으로, 이런 시스템을 확장하고 서비스하려면 레지스트리 기반, 인터페이스 중심 접근법이 필요하다는 깨달음으로 이어졌다.
이것은 반복 작업과 작업 흐름이 있고 결과를 계속 모니터링해야 하는 과학적 과정과 조금 닮았다. 이상적으로는 서비스를 위한 트리거도 자동화한다. 그래서 전에 만든 실험과 반복되는 패턴을 정리하고, 이 시스템을 어떻게 만들지 큰 틀에서 공유하려 한다.
주말 프로토타입
주말에 무엇을 만들 수 있을까? 알고 보니 꽤 많은 것을 만들 수 있다. 하나가 다른 하나로 이어지고, 만들 수 있는 새로운 앱과 경험의 우주가 펼쳐진다. 하지만 최종 제약은 개발자 시간이다. 그래서 지금 내 초점은 어떤 개발자든 효율을 100배, 아니면 1000배까지 올릴 수 있게 해줄 기반 블록을 만드는 데 있다(바란다).
Bertrand - 프롬프트에서 게시까지
동기/사용 사례: 최소한의 노력과 유지보수로 매달 100달러씩 1년에 1000명이 나에게 지불하게 만들려면 무엇을 만들 수 있을까 생각하다가 이 작업을 하고 싶어졌다. 프롬프트에서 게시까지 이어지는 도구, 또는 완전 자동화된 소셜 미디어 대행사가 바로 그런 것이고 기술적으로도 가능해 보였다. 그래서 내가 듣는 강의의 플래시카드를 생성해보며 위 기능들을 가지고 놀아봤고, 응답이 더 관련성 있는지, 매우 제한된 시나리오에서 LLM의 생성 능력을 얼마나 늘릴 수 있는지 확인했다.
가설: 새 지식을 LLM에 넣고 기억을 붙이면, 더 나은 검색 증강 생성(RAG)을 하고 경험을 개인화할 수 있다.
Bertrand - ChatGPT 프롬프트 게시 플러그인
결과와 배운 점: 이것은 GraphQL과 벡터 데이터베이스, 구체적으로 Weaviate를 처음 접한 경험이었다. 임베딩에 대해서도 훨씬 잘 이해하게 되었고, 그것은 이전 발표에서 내가 계속 떠들던 주제이기도 했다. 나는 분리된 프로세스를 이어 붙여 새 트렌드를 학습하고, 콘텐츠를 생성한 뒤 일정 예약과 게시까지 엔드투엔드로 자동화하는 방법이 필요하다고 생각했다. 그리고 물론 그런 것이 있었다! Langchain과 Deep Lake를 발견했고, 다음 개념 증명에서 더 많이 실험하기 시작했다.
Arthur - 검색과 조회
동기/사용 사례: ChatGPT로 만들던 과정에서, 그리고 “최소 자원으로 최대 수익”을 내는 방식에 대한 일반적인 애정 때문에, 내 LLM 기반 페어 프로그래머가 문서, 자료, 웹 검색 등에서 스스로 계속 새로운 것을 배운다면 좋겠다고 생각했다. 더 좋게는 명령에 따라 자율적으로 코딩하고, 컴파일하고, 만난 오류를 바탕으로 스스로 수정할 수 있다면 좋다. 검색하고 읽는 것은 원하는 답을 바로 돌려받는 것보다 느리다. 또한 억양과 응답 대기 시간 때문에 사람 간의 소통이 어렵고 시간이 많이 드는 것도 매우 답답했다. 그냥 A.I.가 기존 코드베이스를 학습하게 하고 질문하면 안 될까? A.I.가 기존 프로젝트 구조와 코드베이스에 대한 지식을 갖추면, 프롬프트에서 기능까지 가는 일은 그리 큰 도약이 아니어 보인다.
가설: LLM이 서로 다른 지식 출처를 의미 있게 다루는 전략을 만들면, 우리는 무엇이든 그 위에서 질의하고 생성할 수 있다.
Arthur - 무엇이든 검색하고 가져오기
결과와 배운 점: 생성 콘텐츠의 품질이 정말 중요하다면 그것은 약간의 문제다. 더 큰 요점은 LLM 기반 앱이 민첩성과 반복을 완전히 새로운 수준으로 끌어올린다는 것이다. 과정의 모든 단계를 다시 돌리고 최적화할 수 있기 때문이다. 더 많은 콘텐츠를 먹이면 더 나은 콘텐츠를 생성할지도 모른다. 콘텐츠의 의미를 더 잘 이해하게 도우면 검색과 생성도 더 잘할지도 모른다. .json을 객체 단위로 나누는 방법을 만들었고 합리적으로 보였지만 토큰 제한 문제가 있어 가져오는 조각 수를 제한해야 했다. 문서, 코드, 오디오, 영상, 이미지 등 들여올 수 있는 모든 데이터 소스마다 더 작거나 더 의미 있는 단위로 나누는 방식, 사용할 수 있는 임베딩 모델, 벡터 저장소(이번에는 Deep Lake를 사용했다), 검색 전략이 다 다르다. 그래서 프로덕션에는 정말 이런 시스템이 필요하다:
-
안정적 - 사용자가 일관된 경험을 누릴 수 있도록
-
확장하기 쉬움 - 새로운 데이터 수집 출처, 임베딩 모델 등을 쉽게 추가할 수 있도록
그리고 ZOMG 가상 환경과 컨테이너화는 너무 중요하다. 코드를 한 줄 쓰기도 전에 필요한 설정 과정의 3/4 같은 것이다…
The Sound of Stories
동기/사용 사례: 나는 이 기술들을 실시간으로 진행되는 실제 공연에 배치하고, A.I.로 인간의 능력을 증강하는 데 관심이 있다. 이전에는 불가능했던 새로운 일을 할 수 있게 만들기 위해서다. 관객이 이야기꾼이나 사랑하는 사람의 목소리로 여러 언어의 이야기를 듣는 상호작용형 스토리텔링 경험으로 이어질 수 있는 여러 구성 요소를 실험하다 보니, 이 모든 것을 합쳐 사람들 앞에 놓는 것이 자연스러운 다음 단계처럼 보였다.
가설: A.I.를 통해 새로운 경험과 능력을 열고 쌓아 올릴 수 있다. 예를 들어 텍스트 생성과 다른 서비스를 이어 붙여 생성된 이야기를 낭독으로 들려주는 것이다.
The Sound of Stories
결과와 배운 점: Streamlit은 LLM 기반 앱의 UI 프로토타입을 만들기에는 별로이고, Chainlit이 더 나아 보인다. SeamlessM4T는 다소 실망스럽지만, 내가 잘 아는 언어에서는 텍스트 간 변환, 텍스트 음성 변환, 음성 복제가 이미 해결된 문제에 가깝고 지연 시간과 다른 언어가 진짜 장애물일 수 있다. 세 개념증명 모두 반복되는 패턴이 있다. 데이터 소스를 들여오고, 어떤 방식으로든 나눈 뒤(일정 문자 수 기준, 객체 기준 등), 임베딩을 생성해 벡터 데이터베이스에 저장하고 검색에 사용했다. 이 개념증명에서는 많은 부분이 “프롬프트 엔지니어링”이었다.
덧붙이면, 이 개념 증명 이후 나는 프롬프트 엔지니어링이 진짜 직업이 될 것 같지 않다고 생각한다. 설령 그렇게 되더라도 그것은 타자기 업무의 현대판일 것이다. 프롬프트를 반복 개선하는 이 전체 과정은 훨씬 더 잘 자동화될 수 있는 일처럼 느껴진다. 실제로 규모가 커질 때 가장 큰 도전 과제 중 하나는 조정의 성능을 어떻게 효과적으로 모니터링하고 평가할 것인가이다. 사람을 중간에 넣을까? MLOps로 과정을 최대한 엔드투엔드 자동화할까?
이 특정 개념증명에서 나는 서로 다른 서비스 사이에 데이터를 전달하고 다중 모달성을 더 많이 실험하고 싶었다. 그래서 LLM이 생성한 출력은 다른 서비스가 소비할 수 있다. 예를 들어 생성된 텍스트를 SeamlessM4T에 전달해 오디오를 생성할 수 있다. 또한 최종 사용자에게 어떤 형태로 표시될 수도 있다. 다시 말해 이런 RAG의 소비 또는 표현 계층도 맞춤화할 수 있다.
따라서 개발과 프로덕션 모두에서 안정적이고 확장 가능한 시스템이라면, 여기에 더해 다음도 갖춰야 한다.
-
유연함 - 작업 흐름을 쉽게 재구성하거나 구성 요소를 교체할 수 있도록
-
확장성 - 실시간에서 일정 수준의 지연 시간과 품질을 보장할 수 있도록. 본질적으로 실제 규모에서의 성능이 걱정이다
1부: 확장 가능한 수집, 분할, 임베딩 시스템 설계
그렇다면 이 시스템은 실제로 어떻게 생겼을까? 나는 아직 리팩터링하고 과정을 밟아가며, 만들면서 새로운 용어와 절차를 익히고 있다. 우선 큰 틀을 정리하면:
우리는 레지스트리를 갖게 된다.
이 레지스트리는 파일 확장자를 해당 프로세서에 매핑한다. 각 프로세서는 해당 파일 유형을 어떻게 분할하고 파싱해야 하는지 알아야 한다.
FILE_TYPE_PROCESSORS = {
".py": PythonFileProcessor,
".md": MarkdownFileProcessor,
# Add other file types and their processors here...
}기반 클래스도 있다.
from abc import ABC, abstractmethod
class DataSource(ABC):
"""
Base class for any data source. This provides a generic interface for data ingestion.
"""
@abstractmethod
def ingest(self):
"""
Ingest data and return a consistent format for further processing.
"""
pass
@abstractmethod
def get_metadata(self):
"""
Extract metadata from the data source. This metadata can vary depending on the source type.
"""
pass
class GitRepoSource(DataSource):
def ingest(self):
# Logic to clone/pull git repos and filter relevant files.
pass
def get_metadata(self):
# Extract metadata specific to git repositories.
pass
class PDFSource(DataSource):
def ingest(self):
# Logic to ingest PDFs.
pass
def get_metadata(self):필요에 따라 파일 프로세서, 분할, 임베딩 및 파이프라인의 다른 단계에도 이 방식을 반복한다.
CHUNKING_STRATEGIES = {
".py": PythonCodeChunker,
".md": MarkdownChunker,
# ... other chunkers
}이는 아래와 같은 설정 가능하고 유연한 처리 로직이 필요하다는 뜻이다:
def process_files(files, embedding_strategy_name):
embedding_strategy = EMBEDDING_STRATEGIES[embedding_strategy_name]()
for file_path in files:
file_extension = os.path.splitext(file_path)[1]
file_processor = FILE_TYPE_PROCESSORS.get(file_extension)
if not file_processor:
print(f"No processor found for {file_extension}. Skipping...")
continue
chunks = file_processor().process(file_path)
for chunk in chunks:
embedded_text = embedding_strategy.embed(chunk["content"])
# Store embedded_text and metadata...이 레지스트리와 추상 클래스 접근법은 다음 이점을 제공한다:
-
확장성: 딕셔너리(**
FILE_TYPE_PROCESSORS**와EMBEDDING_STRATEGIES)를 사용해 파일 유형과 임베딩 전략을 해당 프로세서에 쉽게 매핑한다. 새 파일 유형이나 임베딩 방법을 지원하는 일은 딕셔너리를 확장하는 것만큼 간단하므로 본질적으로 규모를 키우기 쉽다. -
견고성: 관심사를 더 명확히 분리하고 실패를 더 효과적으로 격리할 수 있다. 한 파일 프로세서의 변화가 다른 것에 영향을 주어서는 안 된다.
-
확장 가능성: 기능 확장을 염두에 둔 설계다. 앞서 말했듯 새 파일 유형이나 임베딩 전략을 지원하려면 새 클래스를 만들고 해당 레지스트리에 추가하면 되므로 기존 코드 변경을 최소화한다.
-
유연성: 매우 유연하다. 구성 요소가 딕셔너리에 등록되어 있으므로 교체하거나 새 구성 요소를 추가하는 일이 직관적이다. 예를 들어 “.py” 파일의 프로세서를 바꾸는 것은
FILE_TYPE_PROCESSORS딕셔너리의 항목을 업데이트하는 것만큼 간단하다.
위 방식은 빠른 개발과 협업에 매우 유리하지만, 프로덕션에서는 안정적이고 일관된 시스템을 원한다는 점을 기억해야 한다. 바로 여기서 인터페이스 기반 부분이 등장한다. 후속 글에서 다루겠다.
원래 PubPub의 erniesg.pubpub.org/pub/zc0zx741에 게시됨.