Logo Ernie.SG
Berlayar: 안정적이고 유연한 A.I. 스택

Berlayar: 안정적이고 유연한 A.I. 스택

2023년 9월 7일
10분 읽기
사용 가능한 태그가 없습니다
Table of Contents

1부에서 나는 지금까지 만든 간단한 프로토타입들과, 그것들이 어떻게 나를 LLM 기반 제품을 위한 안정적이고, 확장하기 쉽고, 유연하며, 규모를 키울 수 있는 스택이 필요한 지점으로 이끌었는지 이야기했다. 만들 만한 흥미로운 것이 너무 많기 때문이다. 레지스트리와 기반 클래스로 A.I. 기반 소프트웨어 묶음의 구성 요소를 유연하게 교체해 개발자가 더 쉽게 협업할 수 있게 하는 방법을 논의했는데, 최종 사용자 경험은 어떨까? 내가 목표로 하는 유연성과 모듈성에는 레지스트리 패턴과 인터페이스 중심 설계를 결합하는 것이 이상적인 접근법일 수 있다. 이 글에서는 내가 제안하는 A.I./LLM 엔지니어링 스택을 다룬다. 레거시 코드베이스 리팩터링 자동화, 이메일과 문서 위의 지식베이스, 소셜 미디어 콘텐츠 생성과 발행, 청중이 데이터를 소비하는 새로운 방식 등 관심 있는 거의 모든 배포 사례에 일반화할 수 있는 스택이다.

계속하기 전에, 작품 주석 문제를 다루는 전통적 소프트웨어 개발과 머신러닝 기반 접근법의 차이를 다시 정리해보자.

기준

작품 주석과 태깅을 위한 전통적 소프트웨어 개발

작품 주석과 태깅을 위한 머신러닝 기반 개발(예: OpenAI의 CLIP)

하는 일

미리 정의한 규칙과 메타데이터를 사용해 작품에 주석과 태그를 붙인다.

데이터에서 학습한 패턴을 사용해 작품에 동적으로 주석과 태그를 붙인다.

주석 깊이

명시적 규칙과 사용 가능한 메타데이터에 제한된다.

더 넓은 특징, 주제, 요소를 식별할 수 있다.

적응성

새로운 스타일과 추세에 수동 업데이트가 필요하다.

재훈련을 통해 새로운 스타일과 추세에 적응할 수 있다.

사용자 참여

사용자 피드백을 수동으로 반영한다.

모델 미세 조정을 위해 사용자 피드백을 자동으로 반영할 수 있다.

계산 복잡도

일반적으로 계산 집약적이지 않다.

더 깊은 분석에는 더 많은 계산 자원이 필요하다.

해석 가능성

명시적 규칙 때문에 해석하기 쉽다.

종종 “블랙박스”로 여겨져 해석하기 어렵다.

범위

메타데이터의 가용성과 정확성에 의존한다.

작품의 시각 요소와 직접 작업할 수 있다.

수작업

규칙과 템플릿을 정의하는 데 큰 노력이 필요하다.

모델이 훈련 데이터에서 일반화할 수 있어 수작업이 줄어든다.

접근법의 이런 차이 때문에, A.I.의 장점을 얻고 싶은 조직이라면 작업 흐름과 개발 측면의 중요한 차이에 주목해야 한다. 두 방식의 교집합과 차이는 다음과 같다:

교집합:

  1. 통합: 머신러닝 모델은 더 큰 소프트웨어 시스템 안의 구성 요소로 쓰이는 경우가 많고, 그 시스템은 전통적 소프트웨어 공학 기법으로 만들어진다.

  2. 테스트와 검증: 두 접근법 모두 코드나 모델이 예상대로 동작하는지 검증하기 위해 테스트를 사용한다. 다만 구체적인 테스트 방식은 다를 수 있다.

  3. 배포와 유지보수: 머신러닝 모델이든 전통적 소프트웨어든 배포, 모니터링, 유지보수가 필요하다.

  4. 버전 관리: 두 개발 모두 Git 같은 버전 관리 시스템으로 변경 사항과 이력을 관리한다. 하지만 머신러닝은 데이터, 모델, 하이퍼파라미터, 결과에 대한 버전 관리도 필요하다.

차이점:

  1. 결정론과 확률론: 전통적 소프트웨어 공학은 일반적으로 결정론적이며 명시적 규칙과 논리를 따른다. 머신러닝은 확률론적이고 데이터에서 패턴을 학습한다.

  2. 해석 가능성: 전통적 소프트웨어는 일반적으로 디버그하고 해석하기 쉽지만, 머신러닝 모델, 특히 복잡한 모델은 이해하기 어렵다.

  3. 데이터 의존성: 머신러닝은 훈련, 검증, 테스트를 위해 데이터에 크게 의존하지만, 전통적 소프트웨어 공학은 이런 단계에 본질적으로 데이터를 요구하지 않는다.

  4. 피드백 루프: 머신러닝은 모델이 새 데이터에서 배우는 지속적 피드백 루프를 포함하는 경우가 많지만, 전통적 소프트웨어는 본질적으로 이런 특징을 갖지 않는다.

  5. 기술 역량: 전통적 소프트웨어 공학과 머신러닝은 서로 다른 기술 조합을 요구한다. 다만 소프트웨어 엔지니어가 더 데이터에 능숙해지고 데이터 과학자가 소프트웨어 모범 사례를 배우면서 둘의 겹치는 부분은 커지고 있다.

    • 머신러닝 전문 기술: 통계와 수학 이해(확률, 통계, 선형대수, 미적분), 데이터 기술(데이터 정리, 전처리, 특징 공학, 시각화), 모델 훈련과 평가(알고리즘 선택, 하이퍼파라미터 조정, 평가 지표), 전문 도구와 라이브러리(머신러닝 프레임워크, 데이터 처리 라이브러리), 실험 역량(A/B 테스트, 모델 해석 가능성)을 포함한다.

위 예시들이 A.I. 엔지니어링의 아키텍처와 디자인 패턴이 왜 그 자체로 새롭게 떠오르는 기술인지 더 잘 보여주길 바란다.

레지스트리 기반, 인터페이스 중심 디자인 패턴

객체지향 프로그래밍에서, 특히 Java, C#, TypeScript 같은 언어에서 “인터페이스”는 계약을 명시하는 타입 정의이다. 클래스가 구현해야 할 메서드(그리고 때로 속성)의 집합이다. 인터페이스 자체는 구현을 담지 않는다. 그 인터페이스를 구현한다고 주장하는 클래스에 어떤 메서드가 있어야 하는지만 정의한다.

안정성과 실험 유연성이 필요한 A.I. 기반 또는 LLM 기반 애플리케이션에서는 인터페이스와 추상 클래스가 특히 유익하다. A.I. 개발은 모델 조정, 전처리 단계 변경, 다른 데이터 소스 사용 등 많은 실험을 포함한다. 시스템이 모듈성을 염두에 두고 설계되어 있다면(인터페이스와 추상 설계 패턴으로 가능해진다), 실험을 위해 구성 요소를 교체하는 일이 훨씬 쉬워지고 시스템의 다른 부분을 깨뜨릴 위험도 줄어든다. 또한 A.I. 시스템을 확장하거나 배포할 때 모듈식 설계는 안정성과 유지보수성을 보장한다.

짧게 말해, 인터페이스는 무엇을 기대할 수 있는지에 대한 명확한 계약을 제공한다.

레지스트리, 추상 클래스, 인터페이스의 결합:

  • 각 기능(파일 처리, 분할, 임베딩, 저장, 검색 등)에 인터페이스를 둔다. 이것은 각 구성 요소가 무엇을 해야 하는지 명확하게 만든다.

  • 레지스트리는 런타임에 이러한 인터페이스의 적절한 구현을 관리하고 제공하는 데 사용된다. 시스템이 다른 시나리오나 구성에 동적으로 적응할 수 있게 한다.

예를 들어 다른 파일 유형을 처리한다고 하자:

  1. 인터페이스 중심 부분:

    • 먼저 인터페이스를 정의한다. 예를 들어 데이터를 수집하고 메타데이터를 가져오는 메서드를 가진 DataSource 인터페이스를 생각해보자. 이 인터페이스는 모든 데이터 소스 프로세서가 무엇을 해야 하는지 명확한 계약을 설정한다.

      from abc import ABC, abstractmethod
       
      class DataSource(ABC):
          @abstractmethod
          def ingest(self):
              pass
       
          @abstractmethod
          def get_metadata(self):
              pass
  2. 레지스트리 부분:

    • 데이터가 수집되면(예: Git 저장소의 파일), 시스템은 FILE_TYPE_PROCESSORS 레지스트리를 조회해 각 파일 확장자에 맞는 적절한 프로세서를 찾을 수 있다.

      FILE_TYPE_PROCESSORS = {
          ".py": PythonFileProcessor,
          ".md": MarkdownFileProcessor,
          # ...
      }

시스템이 데이터를 수집해야 할 때, 각 파일 확장자에 맞는 적절한 프로세서를 찾기 위해 이 레지스트리를 조회할 수 있다.

  1. DataSource 하위 클래스 안의 통합 워크플로:

    • DataSource 하위 클래스 안에서는 이 두 단계를 합칠 수 있다. 먼저 데이터를 수집하고, 수집된 파일마다 적절한 파일 프로세서를 사용한다.
class GitRepoSource(DataSource):
    def ingest(self):
        files = self._clone_and_filter_files()
        processed_data = []
        
        for file_path in files:
            ext = self._get_file_extension(file_path)
            processor_class = FILE_TYPE_PROCESSORS.get(ext)
            if processor_class:
                processor = processor_class()
                processed_data.append(processor.process(file_path))
                
        return processed_data

결합된 이점

이 결합 방식은 여러 장점을 제공한다:

  1. 쉬운 확장: 앞으로 새로운 데이터 소스 유형을 지원해야 할 때, DataSource 인터페이스를 따르는 새 클래스를 만들고 **DataSourceRegistry**에 등록하기만 하면 된다. 기존 코드를 건드릴 필요가 없다.

  2. 명확한 기대치: DataSource 인터페이스는 모든 데이터 소스가 공통 API를 따르도록 보장해 수집 파이프라인의 동작을 예측 가능하게 만든다.

  3. 동적 적응성: 중앙 레지스트리가 있으면 다른 데이터 소스 프로세서를 주 작업 흐름에 영향을 주지 않고 쉽게 교체할 수 있어 유연성이 커진다.

문서와 PDF용 에이전트와 코드 저장소용 에이전트처럼 서로 다른 에이전트를 가지는 다양한 사용 사례에서, 이 조합은 인터페이스를 통해 명확한 계약을 제공하면서도 레지스트리를 통해 확장하거나 적응할 능력을 유지한다. 안정성은 인터페이스에서 오고 유연성은 레지스트리에서 온다.

A.I./LLM 기반 앱에 새로운 능력을 주고 싶으면 어떻게 될까?

상호작용과 소비 방식 확장:

예를 들어 내가 “The Sound of Stories”에서 하고 싶은 일은, 추가 소비 방식으로 다국어 내레이션을 만들기 위해 LLM이 생성한 텍스트를 파싱하는 것이다. 생성된 이야기를 기반으로 이미지 생성을 가능하게 하고 싶을 수도 있다. 소비는 다른 도구나 사람이 할 수 있다. 소셜 미디어 콘텐츠 생성이라는 내 사용 사례에서는 소셜 미디어 API가 소비할 구조화된 .json 출력도 필요하다. 레지스트리 기반, 인터페이스 중심 접근법은 아래처럼 소비 방식을 확장하게 해준다.

  1. 레지스트리 만들기: 이전 예시와 마찬가지로 소비 방식(텍스트 표시, 음성 출력, 애니메이션 비디오 등)의 레지스트리를 둘 수 있다.

  2. 소비 인터페이스:

class ConsumptionInterface(ABC):
    
    @abstractmethod
    def display(self, content):
        pass
  1. 구체 구현:
class TextDisplay(ConsumptionInterface):
    def display(self, content):
        # Simply print the content
 
class VoiceOutput(ConsumptionInterface):
    def display(self, content):
        # Use Text-to-Speech to vocalise the content
 
class AnimatedVideo(ConsumptionInterface):
    def display(self, content):
        # Convert content into animated video

이제 한 걸음 물러나 A.I. 애플리케이션의 데이터 기반 개발 주기 전체를 엔드투엔드로 보자.

A.I. 앱을 위한 일반적이고 단순화한 엔드투엔드 아키텍처

A.I./LLM 앱 스택은 이제 막 떠오르는 단계이며, a16z가 매우 유용하게 이를 프롬프트 구성(검색), 프롬프트 실행(추론), 생태계 내 여러 도구 단계 중심으로 구조화했다. 데이터 흐름과 파이프라인은 이 생태계의 백엔드 배관이며, 전통적 소프트웨어 공학과 구분되는 또 다른 중요한 차이다. 아래는 내가 단순화한 일반 아키텍처와 개발 중점 구성 요소다:

예시: 작품 신경 검색 흐름

이전에 나는 작품 신경 검색 데모를 만들었다. 이 데모를 프로덕션으로 확장한다면 아래 단계를 포함할 수 있다.

단계

관련 구성요소

동작

비고

1

오케스트레이터

작품 메타데이터와 이미지 임베딩의 초기 수집

배치 모드 작업

2

사용자 인터페이스(UI)

사용자가 검색어 또는 필터 기준을 입력

3

Main Application

사용자 입력을 검증하고 설정을 가져옴

설정 관리자를 확인

4

오케스트레이터

벡터 데이터베이스에서 관련 임베딩을 검색

검색 엔진을 사용할 수도 있음

5

응답 생성기

사용자 친화적인 출력을 구성

표시를 위해 작품 메타데이터와 이미지를 형식화

6

로깅 모듈

작업을 기록

사용자 질의, 응답 시간 등을 저장

7

검증 및 평가

결과를 자동으로 평가

기본 보안 점검

8

사용자 인터페이스(UI)

사용자에게 검색 결과를 표시

9

피드백 수집기

사용자 피드백이 있으면 수집

4개월 개발 로드맵의 우선순위

이 일반 프레임워크와 아키텍처 안에서 내가 관심 있는 일은 정말 많다:

  • 작품 신경 검색

  • 생성된 이야기의 실시간 다국어 내레이션

  • 코드베이스 리팩터링과 테스트 작성

  • 문서와 데이터를 기반으로 한 지식베이스와의 대화

  • 코드 생성

  • 소셜 미디어 콘텐츠 생성과 발행

우선순위를 정하자면, 독해와 창의적 글쓰기는 서로 다른 두 기술이고 전자는 후자에 매우 가치 있을 것이므로, 내 계획은 LLM에게 배우는 법을 가르치는 일을 우선순위로 두고 작품 신경 검색과 지식베이스 같은 덜 복잡한 사용 사례에서 시작하는 것이다. 대략적인 일정은 아래처럼 진행될 수 있다:

  • 1-2개월차: 작품 신경 검색 - 기초 데이터 파이프라인을 구축하기 위해.

  • 2-3개월차: 문서와 데이터 기반 지식베이스와 대화 - 수집과 검색 방법, UI 구성 요소를 개발하기 위해.

  • 3-4개월차: 코드베이스 리팩터링과 테스트 작성 - 지식베이스 대화 단계에서 개발한 통찰과 방법을 활용해 더 똑똑한 자동화 테스트와 리팩터링을 수행한다.

위와 같이 신경 검색과 지식베이스 구축에 집중하더라도, LLM을 활용해 우리 코드베이스를 질의하고, 코드 품질 서비스에 전달한 뒤, 프롬프트 템플릿을 연결하거나 사용해 리팩터링과 테스트 작성 같은 관련 자동화를 수행하는 단순화된 작업 흐름을 시작하는 것이 좋겠다고 생각한다. 처음에는 수동 검토를 위해 브랜치로 나누고, 이후 모든 테스트가 통과하면 자동화할 수 있다.

한편, 내 Git 저장소에서 프로젝트를 이렇게 정리하려 한다. 더 발전할 것이라 예상하므로 아래는 아직 진행 중인 초안이지만, 각 폴더는 다음을 담당해야 한다고 생각한다:

  • db - CRUD 같은 모든 데이터베이스 관련 작업

  • demos - 일부 “안정된” 데모와 작업 흐름을 정의하는 곳

  • gui - 사용자가 보는 모든 것

  • src - 인터페이스와 구현의 대부분 소스 코드

  • tests - 단위 테스트와 통합 테스트, 그리고 테스트용 fixture

아직 CI/CD, 모니터링, 평가 구성 요소가 빠져 있고 프로젝트 구조도 정리 중이지만, 다른 개발자들과 분업해보고 싶다. National Arts Council Arts x Tech Lab을 위해 “The Sound of Stories”(실시간, 다국어로 생성된 이야기를 들려주는 작업)도 전달해야 하기 때문이다. 일이 잘 풀리면 이러한 개발 노력은 서로 높은 시너지를 내며 Berlayar A.I.라는 회사를 위한 기반 블록을 줄 것이다. ;)

├── agents
│   └── __init__.py
├── dataloader.ipynb
├── db
│   ├── chunks.py
│   ├── connectors.py
│   ├── __init__.py
│   ├── main.py
│   ├── ops
│   │   ├── deeplake.py
│   │   └── __init__.py
│   └── utils.py
├── demos
│   ├── chat_adf
│   │   ├── chat.py
│   │   ├── tests.py
│   │   └── utils.py
│   ├── neural_search
│   │   └── upload.py
│   └── thesoundofstories
│       ├── audio
│       └── chat.py
├── diagram.py
├── gui
│   └── __init__.py
├── ingest_pdf.log
├── __init__.py
├── log_init.py
├── main.py
├── notebooks
│   ├── architecture.ipynb
│   ├── chat_adf.ipynb
│   └── dataloader.ipynb
├── poetry.lock
├── pyproject - Copy.toml
├── pyproject.toml

한편, 나는 곧 있을 “Good Economics for Bad Times” 시험에 집중해야 해서 이 작업은 잠시 보류해 둔다.


원래 PubPub의 erniesg.pubpub.org/pub/wpnv7rhs에 게시됨.