본문으로 건너뛰기

"Guideline" 태그 — 6개 게시물

팀 내부 가이드라인, 실무 문서

모든 태그 보기

에이전트 평가를 위한 실용적 준비 체크리스트

· 약 11분
김태한
AI Research Engineer, Brain Crew

TL;DR

AI 에이전트 평가는 전통적인 소프트웨어 테스트와 다르다. 복잡한 평가 시스템을 구축하기 전에 2050개의 실제 트레이스를 직접 읽고, end-to-end 태스크 완수를 검증하는 단순한 eval부터 시작하라. 6080%의 시간을 에러 분석에 투자하고, capability eval(무엇을 할 수 있는가)과 regression eval(여전히 작동하는가)을 명확히 분리하며, 차원별 전문 채점기를 설계하라. 프로덕션 트레이스를 지속적으로 데이터셋에 편입하는 플라이휠을 구축하면, 시간이 지남에 따라 에이전트가 개선된다.

Key Takeaways

  • 단순함부터 시작: 복잡한 인프라를 만들기 전에 30분간 실제 트레이스를 수동으로 읽어라. 자동 시스템보다 더 많은 실패 패턴을 발견할 수 있다.
  • 에러 분석에 60~80% 투자: 실패 트레이스를 도메인 전문가와 함께 분석하여 프롬프트 문제, 툴 설계 문제, 모델 한계를 구분하고, 각 유형에 맞는 대응을 취하라.
  • 평가 레벨의 전략적 선택: Single-step(툴 선택), Full-turn(end-to-end 태스크 완수 + 상태 변경), Multi-turn(대화 맥락 유지) 중 Full-turn부터 시작하여 안정화 후 확장하라.
  • 차원별 전문 채점기: "정확성" 하나로 뭉뚱그리지 말고 콘텐츠 정확성, 구조, 포맷, 효율성 등 차원별로 독립된 채점기를 설계해 실패 원인을 명확히 파악하라.
  • Trace-to-Dataset 플라이휠: 프로덕션의 성공/실패 트레이스가 자동으로 데이터셋에 편입되고, 개선된 에이전트가 다시 프로덕션에 배포되는 순환 구조가 장기적 개선의 핵심이다.

상세 내용

에이전트 평가의 핵심 원칙

AI 에이전트는 전통적인 소프트웨어와 근본적으로 다르다. 소프트웨어는 결정론적이고 코드가 진리의 원천이지만, 에이전트는 LLM의 추론에 의존하며 같은 입력에도 다른 경로를 선택할 수 있다. 에이전트가 200단계를 거쳐 2분간 작업한 후 실패했을 때, 스택 트레이스는 없다. 코드가 실패한 것이 아니라 추론이 실패한 것이기 때문이다.

가장 중요한 원칙은 단순함부터 시작하는 것이다. 몇 개의 end-to-end eval로 에이전트가 핵심 태스크를 완수하는지 검증하는 것만으로도 즉시 baseline을 확보할 수 있다. 복잡한 구조는 단순한 방법이 실제 실패를 놓칠 때만 추가한다.

Eval 구축 전 준비사항

수동 트레이스 리뷰: 30분의 투자

자동화된 평가 인프라를 만들기 전에 반드시 해야 할 일이 있다. 실제 에이전트 트레이스 20~50개를 직접 읽어라. 30분의 수동 리뷰가 어떤 자동 시스템보다 많은 실패 패턴을 알려준다. LangSmith의 트레이스 뷰어와 Annotation Queue를 활용하면 트레이스를 효율적으로 수집하고 검토할 수 있다.

에이전트는 LLM과 툴을 여러 턴에 걸쳐 호출하고, 중간 결과에 따라 행동을 조정하며, 상태를 수정한다. 이러한 복잡한 행동은 코드를 읽는 것만으로는 예측할 수 없으며, 실제 실행 트레이스가 진리의 원천이 된다.

명확한 성공 기준 정의

두 명의 전문가가 pass/fail에 합의할 수 없다면, 태스크 자체를 재정의해야 한다.

나쁜 예: "이 문서를 잘 요약해줘"
좋은 예: "이 회의록에서 핵심 액션 아이템 3개를 추출하라. 각각 20단어 이내, 담당자가 언급되었으면 포함할 것"

모호한 기준은 채점기의 불일치를 만들고, 무엇이 개선인지 판단할 수 없게 만든다.

Capability Eval vs Regression Eval 분리

이 두 가지는 목적이 완전히 다르기 때문에 반드시 분리해서 관리해야 한다.

  • Capability Eval ("무엇을 할 수 있는가?"): 어려운 태스크에 대한 진전을 측정한다. 초기 pass rate가 낮고, 개선의 방향을 제시한다.
  • Regression Eval ("여전히 작동하는가?"): 이미 작동하는 기능을 보호한다. pass rate가 ~100%여야 하며, 기존 기능의 퇴보를 감지한다.

Capability eval만 있으면 기존 기능이 깨지고, regression eval만 있으면 개선이 멈춘다. 두 가지 모두 필요하다.

에러 분석에 60~80% 투자하라

전체 eval 노력의 60~80%를 에러 분석에 투자해야 한다. 이것이 가장 높은 ROI를 제공하는 활동이다. 절차는 다음과 같다:

  1. 대표적인 실패 트레이스를 수집한다.
  2. 도메인 전문가와 함께 사전 분류 없이 모든 이슈를 기록한다(open coding).
  3. 이슈를 실패 유형으로 분류한다(프롬프트 문제, 툴 설계 문제, 모델 한계, 인프라 이슈 등).
  4. 새로운 실패 유형이 나오지 않을 때까지 반복한다.

실패 유형에 따라 대응이 완전히 달라진다:

  • 프롬프트 문제: 지시가 불명확 → 프롬프트 수정
  • 툴 설계 문제: 인터페이스가 오용을 유발 → 파라미터 재설계, 예시 추가
  • 모델 한계: 지시는 명확하지만 일반화 실패 → few-shot 예시 추가, 아키텍처 변경, 모델 교체
  • 아직 모름: 충분한 실패 사례를 보지 못한 상태 → 에러 분석 계속

Anthropic 팀은 SWE-bench 에이전트를 만들 때 프롬프트 최적화보다 툴 인터페이스 최적화에 더 많은 시간을 썼다고 보고했다. 예를 들어 절대 경로를 강제하면 경로 관련 에러 전체를 제거할 수 있다.

Eval 오너십과 인프라 검증

데이터셋 유지보수, 채점기 재캘리브레이션, 새 실패 모드 분류, "충분히 좋은" 기준 결정을 한 명의 도메인 전문가가 책임져야 한다. 위원회식 의사결정은 책임 소재를 흐리고 진전을 늦춘다.

또한 인프라 문제를 먼저 배제해야 한다. Witan Labs 팀은 단일 추출 버그를 고쳤을 때 벤치마크가 50%에서 73%로 뛴 사례를 보고했다. 타임아웃, 잘못된 API 응답, 오래된 캐시 같은 인프라 이슈가 추론 실패로 위장하는 경우가 잦다.

평가 레벨 선택

에이전트 평가는 세 가지 레벨로 나뉘며, Trace-level(Full-turn)부터 시작하는 것을 권장한다.

Single-step Eval (Run 레벨)

"올바른 툴을 선택했는가?", "유효한 API 호출을 생성했는가?"를 평가한다. 자동화가 가장 쉽지만, 에이전트 아키텍처가 안정적일 때만 유효하다. 아키텍처가 자주 바뀌는 초기 단계에서는 특정 툴 호출 패턴에 의존하는 평가가 빠르게 obsolete해진다.

Full-turn Eval (Trace 레벨)

대부분의 팀이 시작해야 할 레벨이다. 세 가지 차원을 함께 평가한다:

  • 최종 응답: 출력이 정확하고 유용한가?
  • 경로(Trajectory): 합리적인 경로를 거쳤는가? (정확한 경로가 아닌, 유효한 경로)
  • 상태 변경: 올바른 아티팩트가 생성되었는가? (파일 작성, DB 업데이트, 캘린더 이벤트 등)

상태 변경 검증은 자주 간과되지만 매우 중요하다. 에이전트가 "미팅 잡았습니다!"라고 말했더라도, 실제 캘린더에 올바른 시간·참석자·설명으로 이벤트가 존재하는지 확인해야 한다. 코딩 에이전트의 경우 생성된 파일이 실행 가능한지, 유닛테스트를 통과하는지 검증해야 한다.

Multi-turn Eval (Thread 레벨)

가장 구현이 어렵다. Trace-level eval이 안정된 후에 추가한다. N-1 테스팅 기법이 유용하다: 프로덕션의 실제 대화에서 앞 N-1턴을 그대로 주고 마지막 턴만 에이전트가 생성하게 한다. 이를 통해 완전 합성 멀티턴 시뮬레이션의 compounding error를 회피할 수 있다.

데이터셋 구성 전략

품질이 양을 이긴다

검증된 20~50개가 미검증 수백 개보다 낫다. 모든 태스크는 참조 솔루션(reference solution)을 포함해야 하며, 이를 통해 태스크가 풀 수 있다는 것을 증명하고 채점의 기준선을 제공한다.

나쁜 예: "NYC행 좋은 항공편 찾아줘"
좋은 예: "SFO→JFK 왕복, 12/15~17 출발, 12/22 복귀, $400 이하, 이코노미"

포지티브 + 네거티브 케이스

"검색해야 할 때 검색하는가?"만 테스트하면, 모든 것을 검색하는 에이전트에 최적화된다. 네거티브 케이스(해당 행동을 하면 안 되는 경우)도 반드시 포함해야 한다. Anthropic은 프론티어 모델이 정적 eval의 한계를 넘어서는 창의적 해법을 발견한 사례를 보고했다—Opus 4.5가 항공권 예약 문제에서 정책의 허점을 발견해 더 나은 해법을 제시했으나, 기존 채점기로는 "실패"로 판정되었다.

에이전트 유형별 맞춤

  • 코딩 에이전트: 결정적 테스트 스위트(유닛테스트 pass/fail) + 품질 루브릭
  • 대화형 에이전트: 태스크 완수 + 상호작용 품질(공감, 명확성) 다차원 평가
  • 리서치 에이전트: 근거성 체크(주장이 소스에 뒷받침되는가?) + 커버리지 체크(핵심 사실이 포함되었는가?)

데이터 소싱: 3가지 전략 병행

  1. 독파운딩(Dogfooding): 팀이 매일 에이전트를 스트레스 테스트하고, 모든 에러를 eval로 전환한다.
  2. 외부 벤치마크 적응: Terminal Bench, BFCL 등에서 관련 태스크를 선별하여 자신의 에이전트에 맞게 변환한다.
  3. 수작업 행동 테스트: "에이전트가 툴 호출을 병렬화하는가?", "모호한 요청에 명확화 질문을 하는가?" 등 특정 행동을 검증하는 테스트를 직접 작성한다.

Trace-to-Dataset 플라이휠

프로덕션의 성공/실패 트레이스가 지속적으로 데이터셋에 편입되는 파이프라인을 구축한다. LangSmith를 사용하면 트레이스 → 어노테이션 큐 → 데이터셋 → 실험의 흐름을 자동화할 수 있다.

채점기(Grader) 설계

차원별 전문 채점기

하나의 "정확성" 채점기 대신 차원별로 분리하라. Witan Labs 팀은 콘텐츠 정확성, 구조, 시각 포맷, 수식 시나리오, 텍스트 품질 등 5개의 전문 평가기를 만들어 어디가 실패하는지 명확한 시그널을 얻었다.

채점기 유형적합한 용도주의점
코드 기반결정적 체크, 툴 호출 검증, 출력 포맷, 실행 결과유효하지만 예상치 못한 포맷에서 false-fail 가능
LLM-as-judge뉘앙스 있는 품질, 루브릭 기반 채점, 개방형 태스크사람과의 캘리브레이션 필수
사람캘리브레이션, 주관적 기준, 엣지 케이스비용 높고 느리며 확장 어려움

객관적 정답이 있는 경우 코드 기반을 기본으로 사용한다. LLM-as-judge를 객관적 태스크에 쓰면 불일치가 실제 regression을 가릴 수 있다.

Guardrail vs Evaluator 구분

GuardrailEvaluator
실행 시점실행 중, 사용자가 출력을 보기 전생성 후, 비동기
속도밀리초 (빠를 것)초~분 (비용 투자 가능)
목적위험하거나 잘못된 출력 차단품질 측정 및 regression 감지
예시PII 탐지, 포맷 검증, 안전 필터LLM-as-judge 채점, 경로 분석

바이너리 pass/fail 선호

1~5점 척도는 인접 점수 간 주관적 차이를 만들고, 통계적 유의미성을 위해 더 큰 샘플이 필요하다. 바이너리는 더 명확한 사고를 강제한다: 성공했거나 실패했거나. 복잡한 태스크는 여러 개의 바이너리 체크로 분해한다.

LLM-as-judge 캘리브레이션

  • LangSmith Align Evaluator로 20개 이상의 라벨 예시로 시작, 프로덕션급 신뢰도를 위해 ~100개까지 확대
  • 채점기의 출력에 **판단 근거(reasoning)**를 포함시켜 정확도를 높이고 감사 가능성 확보
  • 정기적 재캘리브레이션 필수 — 채점기는 시간이 지나면 드리프트함
  • few-shot 예시를 사용해 일관성 향상

결과를 채점하라, 경로가 아니라

에이전트는 창의적인 경로를 찾는다. "check_availability → create_event 순서로 호출했는가?"가 아니라 **"미팅이 올바르게 잡혔는가?"**를 평가해야 한다. 다만 효율성 측정을 위해 이상적 경로 대비 실제 스텝 수를 추적하는 것은 유용하다. 이것은 정확성 채점이 아니라 효율성 메트릭이다.

부분 점수(partial credit)도 고려한다. 문제를 정확히 식별했지만 마지막 단계에서 실패한 에이전트는, 처음부터 실패한 에이전트보다 낫다.

커스텀 평가기 사용

"helpfulness"나 "coherence" 같은 범용 메트릭은 거짓 확신을 만든다. 에러 분석에서 발견한 특정 실패 모드를 잡는 커스텀 평가기가 실제로 의미 있는 시그널을 준다. Deep Agents 팀은 각 eval에 docstring을 작성하여 무엇을 측정하는지 자체 문서화하고, tool_use 같은 카테고리 태그로 그룹 실행을 가능하게 한다.

실행 및 반복

세 가지 평가 타이밍

타이밍정의용도
Offline큐레이션된 데이터셋, 배포 전 실행변경사항을 출시 전에 검증
Online프로덕션 트래픽에 대한 지속적 평가실 트래픽에서 실패 포착
Ad-hoc수집된 트레이스에 대한 탐색적 분석예상 못한 패턴 발견

세 가지를 모두 활용해야 한다. Offline은 배포 전 품질 게이트, online은 프로덕션 모니터링, ad-hoc은 새로운 인사이트 발견에 각각 필수적이다.

비결정성 대응

모델 출력은 실행마다 달라진다. 태스크당 여러 번 반복 실행하고, 개선을 선언하기 전에 신뢰구간을 계산한다. 제품 요구사항에 따라 pass@k(k번 중 1번 이상 성공) 또는 pass^k(k번 모두 성공) 메트릭을 선택한다.

실행 환경 격리

시행 2가 시행 1의 아티팩트를 볼 수 있으면 결과가 독립적이지 않다:

  • 코딩 에이전트: 시행마다 새 컨테이너/VM
  • API 호출 에이전트: 스테이징 환경 또는 모의 서비스
  • DB 에이전트: 시행 간 스냅샷 복원

메타데이터와 효율성 메트릭

실험마다 모델, 프롬프트 버전, 검색 전략 등의 메타데이터를 기록하여, "GPT-4.1에서 Claude Sonnet으로 바꾸면 정확도가 오르는가?" 같은 질문에 답할 수 있게 한다.

품질과 함께 효율성 메트릭도 추적한다: 실제 스텝 수 / 이상적 스텝 수, 툴 호출 횟수, 토큰 사용량, 지연시간. 정확도 95%이지만 10배 느린 에이전트는 개선이 아닐 수 있다.

Pass Rate 정체 시 대응

같은 유형의 태스크를 더 추가해도 새로운 실패 모드가 발견되지 않으면, 더 어려운 태스크를 추가하거나, 새로운 기능을 테스트하거나, 다른 차원으로 이동한다. 포화된 eval 세트를 반복하는 것은 시간 낭비다.

의미 있는 Eval만 유지하라

모든 eval은 시스템에 압력을 가한다. 무분별하게 수백 개를 추가하면 프로덕션에서 중요하지 않은 것에 최적화하게 된다. 더 많은 eval이 더 나은 에이전트를 의미하지는 않는다. 주기적으로 시그널을 주지 않는 eval을 제거한다.

Task Failure vs Evaluation Failure 구분

실행 상태를 명시적으로 추적한다(완료, 에러, 타임아웃). 타임아웃을 "추론 실패"로 채점하면 메트릭이 오염된다. 에이전트의 실패와 채점기의 실패를 분리해야 한다.

프로덕션 준비

CI/CD 파이프라인 통합

일반적인 흐름:

  1. 코드/프롬프트 변경이 파이프라인을 트리거한다.
  2. Offline eval 실행: 유닛테스트, 통합테스트, 큐레이션 데이터셋 대비 평가 (저비용·고속 코드 기반 채점기).
  3. offline eval 통과 시 preview 배포.
  4. preview에서 online eval 실행 (LLM-as-judge 채점기).
  5. 모든 품질 게이트 통과 시에만 프로덕션 프로모트. 실패 시 해당 트레이스를 annotation queue로 라우팅하고 팀에 알림.

매 커밋마다 저비용 코드 기반 채점기를 CI에서 돌리고, 비용이 높은 LLM-as-judge는 preview/프로덕션 단계에 사용한다.

유저 피드백의 중요성

자동 eval은 이미 알고 있는 실패 모드만 잡는다. 사용자가 발견하는 것들이 있다: 데이터셋이 놓친 엣지 케이스, 기술적으로는 맞지만 도움이 안 되는 출력, 예상 못한 방식으로 깨지는 워크플로우. 구조화된 피드백을 데이터셋에 편입하고, 채점기를 실제 기대치에 맞게 캘리브레이션하고, 사용자에게 실제로 중요한 개선에 우선순위를 둔다.

프로덕션 플라이휠

프로덕션의 성공과 실패가 데이터셋, 에러 분석, eval 개선으로 되돌아가는 순환 구조를 구축한다. 이것이 에이전트를 시간이 지남에 따라 개선시키는 플라이휠이다:

프로덕션 트레이스 → 에러 분석 → 데이터셋 업데이트 → Eval 개선 → 에이전트 개선 → 프로덕션 배포 → (반복)

Capability → Regression 승격

꾸준히 높은 pass rate를 보이는 capability eval을 regression suite로 승격한다. "할 수 있는가?"를 검증한 태스크가 "여전히 할 수 있는가?"를 보호하는 태스크로 전환된다.

전체 체크리스트 요약

Eval 구축 전

  • ☑️ 20~50개 실제 트레이스 수동 리뷰
  • ☑️ 명확한 성공 기준 정의
  • ☑️ Capability/Regression eval 분리
  • ☑️ 실패 원인 식별 및 분류
  • ☑️ 단일 도메인 전문가에게 오너십 부여
  • ☑️ 인프라 문제 배제

평가 레벨 선택

  • ☑️ Trace-level(Full-turn)부터 시작
  • ☑️ 최종 응답 + 경로 + 상태 변경 함께 평가
  • ☑️ 아키텍처 안정화 후 Single-step 추가
  • ☑️ Multi-turn은 N-1 테스팅 기법 활용

데이터셋 구성

  • ☑️ 모든 태스크에 참조 정답 포함
  • ☑️ 포지티브 + 네거티브 케이스 균형
  • ☑️ 검증된 20~50개부터 시작
  • ☑️ 독파운딩 + 외부 벤치마크 적응 + 수작업 병행
  • ☑️ Trace-to-Dataset 파이프라인 구축
  • ☑️ 에이전트 유형별 맞춤 설계

채점기 설계

  • ☑️ 차원별 전문 채점기 분리
  • ☑️ Guardrail과 Evaluator 구분
  • ☑️ 바이너리 pass/fail 선호
  • ☑️ LLM-as-judge는 20+ 예시로 캘리브레이션
  • ☑️ 결과를 채점, 경로는 효율성 메트릭으로
  • ☑️ 에러 분석 기반 커스텀 평가기

실행 및 반복

  • ☑️ Offline/Online/Ad-hoc 세 가지 타이밍 활용
  • ☑️ 비결정성 대응: 반복 실행 + 신뢰구간
  • ☑️ 실행 환경 격리
  • ☑️ 메타데이터 + 효율성 메트릭 추적
  • ☑️ 실패 트레이스 수동 검토
  • ☑️ 포화된 eval 제거, 의미 있는 것만 유지
  • ☑️ Task/Evaluation Failure 구분

프로덕션 준비

  • ☑️ CI/CD 파이프라인 통합
  • ☑️ 온라인 평가 지속 실행
  • ☑️ 유저 피드백 구조화 수집
  • ☑️ 프롬프트/툴 정의 버전 관리
  • ☑️ Capability → Regression 승격
  • ☑️ 프로덕션 플라이휠 구축

References

[EC2] GPU 인스턴스 기초 프로비저닝 가이드

· 약 6분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

AWS EC2에서 GPU 인스턴스를 프로비저닝하는 실무 가이드입니다. P/G/Inf 시리즈 등 인스턴스 타입 선택부터 AMI 설정, 네트워크 구성, 스토리지 최적화까지 GPU 워크로드 배포 시 필수적으로 고려해야 할 사항들을 단계별로 다룹니다. 특히 리전별 가용성, Deep Learning AMI 활용, 캐퍼시티 블록 구매 시 주의사항, 인스턴스 스토어의 임시성 등 실제 운영에서 마주칠 수 있는 함정들을 강조합니다.

Key Takeaways

  • 인스턴스 타입 선택의 중요성: P 시리즈(학습용), G 시리즈(추론/렌더링), Inf 시리즈(추론 최적화)를 목적에 맞게 선택해야 하며, 리전별 가용성 사전 확인 필수 (예: 서울 리전에서는 H100 인스턴스 불가)
  • Deep Learning AMI 활용: NVIDIA 드라이버, CUDA, cuDNN이 사전 설치된 AMI를 사용하면 초기 설정 시간을 대폭 절약 가능
  • 캐퍼시티 블록 구매 주의: GPU 경쟁 과열 시 1시간 단위 즉시 예약이며 환불 불가이므로 신중한 검토 필요
  • 스토리지 전략: gp3/io2 EBS를 기본으로 사용하되, 인스턴스 스토어(NVMe)는 인스턴스 중지 시 데이터 손실되므로 임시 데이터(캐시, 버퍼)에만 활용
  • 자원 최소화 원칙: GPU 인스턴스는 고비용 리소스이므로 사용 전 리뷰 프로세스를 거치고, 불필요한 가동 시간 최소화 필요

상세 내용

GPU 리소스 사용 승인 프로세스

GPU 인스턴스는 높은 비용이 발생하는 리소스이므로, 사용 전 내부 리뷰 프로세스를 통해 적절한 타입과 용량을 검증받아야 합니다. 이는 불필요한 자원 낭비를 방지하고 비용 효율성을 확보하기 위한 필수 단계입니다.

GPU 인스턴스 타입 이해

AWS는 용도에 따라 구분된 GPU 인스턴스 패밀리를 제공합니다:

  • P 시리즈 (P3, P4, P5): 머신러닝 학습 및 고성능 컴퓨팅(HPC)에 최적화. 대규모 모델 학습이나 분산 학습 워크로드에 적합
  • G 시리즈 (G4dn, G5, G6): 그래픽 렌더링, 게임 스트리밍, ML 추론 등 그래픽 집약적 작업에 특화
  • Inf 시리즈: Amazon 자체 Inferentia 칩을 사용한 ML 추론 최적화 인스턴스. 비용 대비 추론 성능이 우수

리전별 가용성 사전 확인

모든 AWS 리전에서 모든 GPU 인스턴스 타입을 사용할 수 있는 것은 아닙니다. 특히 최신 GPU를 탑재한 인스턴스의 경우 제한적입니다.

중요 예시: ap-northeast-2(서울) 리전에서는 H100 1EA 인스턴스(p5.4xlarge) 사용이 불가능합니다. 프로젝트 시작 전 목표 리전에서 필요한 인스턴스 타입의 가용성을 반드시 확인해야 합니다.

AWS Instance 목록

AMI 선택 전략

GPU 워크로드를 위한 인스턴스는 적절한 NVIDIA 드라이버, CUDA 툴킷, cuDNN 등이 사전 설치된 AMI를 사용하는 것이 권장됩니다.

권장 AMI:

  • Deep Learning AMI (Ubuntu/Amazon Linux): NVIDIA 드라이버, CUDA, cuDNN이 사전 설치되어 있어 즉시 딥러닝 프레임워크 사용 가능
  • AWS Marketplace의 GPU 최적화 AMI: PyTorch, TensorFlow 등 특정 프레임워크가 미리 설정된 이미지

AMI 선택

일반 AMI를 선택하는 경우 NVIDIA 드라이버를 수동으로 설치해야 하므로 초기 설정 시간이 증가합니다.

인스턴스 생성 프로세스

1. 인스턴스 타입 및 용량 결정

GPU 리소스 검토 과정에서 승인받은 내역을 바탕으로 적절한 인스턴스 유형, VRAM 용량을 선택합니다.

인스턴스 생성 시작

인스턴스 설정 1

인스턴스 설정 2

인스턴스 설정 3

![인스턴스 설정 4](https://prod-files-secure.s3.us-west-2.amazonaws.com/bb84b169-cb88-81fc-90c3-00032f05f905/576f9463-ab24-45ae-a26b-081dc9018a46/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466XOL2RWTI%2F20260325%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260325T064504Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEN%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMiJHMEUCIQDqPcWww2%2FpQ%2Be1RS0GoLH2PqRGairWhkf5VAqDzJjrDgIgDFxY3LBTm2DChRO06qiqQORa%2BTwwwAGB4Irhokw2nnkqiAQIp%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDHWk54hH51hF8rmCtCrcA8SybE%2Bcehb2%2Bxjqq9d6ZrsWnzwPaWhd310kPdYge9qDhfuTquJLhuwQcydhVE3LkJeo8LjYfrE1noL6PPxX5KdYBS2OsUe822kgvtmslCvNWwl897%2Bdlw6A%2FKYpXBadSwsUbtdCmDM2CWy9UnqSPvc1MVuX26RJ7grJzCZ3FwJ%2BN0RioHjzyyQpgHNnBjzCOc5T0P639oKy%2F5%2B9vNAYW4BZqW5X5U50nf%2BhCk8%2F0It2nYhZm2fxi0jHp%2BybPeI2xY1lrcGM%2BB4M1dljm7C%2BdWI2CLLDe1%2FMwhQo5GX10j1ALiHiEBNN8aMePUlAek4CtbmJ7MxtRN1cPhkFfO7pqB1JLKI5PHvDlycWmHih%2BWftE6eJ3Es5DmY8zJ2GlrL6llNGp%2FujdYfYXfszKydwMys5s5FeurJ6IfhXC%2F24QdVJQnLjVJF6SSC29%2B1tF5Iy1gwSQr4K2bgHNkW6qBK%2FNG32gESCN9X6e2adDdyxh3Pe%2B99nKzDAZP9R72XRiOT2GYrwf9tmnGynn3cIQSn2Oc3f16V1srluXAUxx19tmZk39KFjMccQGUO37wfn5dfg6qcm%2BEHlqr3sS7aVdWgrWLKnx%2BN5tkREfAA0DRwnHAXJbwSgcbwIXb7iPkukMLv9jc4GOqUB%2BeM2AI%2BAgNC%2FLYyXnjFoaBm1dUwt%2FtgqITp8E8GcmhAxjiHbWr%2FBsOJShvWO2evdkrG8rq8Wo1o1kFTQBrkPgJGsf%2BSJSvM%2BSZvU1f9XQXFeR%2Bs%2B2oNARyVNv3RFxSbvy9pc84q8KCpgKOIL%2FidQ%2FWiTrZ7MK8ddm6oGiwh%2BheLj3BOrdd9mImjLSbeJBeG%2BcpT38ig

LangGraph Multi-Tenant PostgreSQL 설계 가이드

· 약 8분
김성연
AI Research Engineer, Brain Crew

TL;DR

LangGraph 기반 Multi-Tenant 시스템을 PostgreSQL로 구축할 때 사용할 수 있는 5가지 격리 전략을 비교 분석합니다. Thread ID + Namespace 방식은 낮은 복잡도로 빠르게 시작 가능하며, Row Level Security(RLS)는 데이터베이스 레벨에서 강력한 격리를 제공합니다. Schema 분리와 Database 분리는 더 높은 격리 수준이 필요한 금융/의료 등의 규제 환경에 적합합니다. 실무에서는 요구사항에 따라 전략을 선택하되, JWT 기반 인증과 테넌트 컨텍스트 관리를 통해 안전한 격리를 구현해야 합니다.

Key Takeaways

  • Thread ID Prefix 전략: tenant-{tenant_id}:user-{user_id}:session-{session_id} 형식으로 애플리케이션 레벨에서 간단하게 Multi-Tenant를 구현 가능. MVP나 일반 SaaS에 권장.
  • PostgreSQL RLS 활용: SET LOCAL app.tenant_id + Policy 기반으로 데이터베이스 레벨의 강력한 격리 제공. 애플리케이션 버그에도 데이터 누출 방지 가능.
  • Namespace 계층 구조: Checkpoint는 tenant-{tenant_id}, Store는 (tenant_id, user_id, "memories") 튜플로 구성해 cross-thread 상태 관리와 Long-term Memory 격리 구현.
  • Connection Pooling 고려: Schema/Database 분리 시 테넌트별 커넥션 풀 관리가 필수. 동적 스키마 라우팅과 캐싱 전략 필요.
  • 보안 체크리스트: JWT 검증, SQL Injection 방지, 감사 로깅, 정기적인 테넌트 격리 테스트를 통해 Multi-Tenant 환경의 보안 강화 필요.

상세 내용

Multi-Tenant 격리 전략 선택 가이드

LangGraph 기반의 Agent 시스템을 Multi-Tenant 환경에 배포할 때, 테넌트 간 데이터 격리는 핵심적인 아키텍처 결정입니다. 각 전략은 격리 수준, 구현 복잡도, 확장성, 그리고 사용 케이스에 따라 뚜렷한 트레이드오프를 가집니다.

전략격리 수준복잡도확장성사용 케이스
Application-level낮음낮음높음빠른 MVP
Thread ID Prefix중간낮음높음일반적인 SaaS
Schema 분리높음중간중간규제 요구사항
Row Level Security높음높음높음엔터프라이즈
Database 분리최고최고낮음금융/의료

전략 선택의 핵심은 요구되는 격리 수준운영 복잡도 간의 균형입니다. 대부분의 경우 Thread ID + Namespace 방식으로 시작하여, 보안 요구사항이 증가하면 RLS나 Schema 분리로 마이그레이션하는 것을 권장합니다.

전략 1: Thread ID + Namespace 기반 격리

가장 실용적인 시작점으로, LangGraph의 Thread와 Namespace 개념을 활용한 애플리케이션 레벨 격리 방식입니다.

핵심 설계 원칙:

  • Thread ID: tenant-{tenant_id}:user-{user_id}:session-{session_id} 형식으로 각 실행을 고유하게 식별
  • Checkpoint Namespace: tenant-{tenant_id}로 테넌트 레벨에서 그룹핑
  • Store Namespace: (tenant_id, user_id, "memories") 튜플로 Long-term Memory 계층 구조 구성

구현 예시

from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from langgraph.store.postgres.aio import AsyncPostgresStore
from pydantic import BaseModel
import jwt

# JWT 기반 테넌트 인증
security = HTTPBearer()

class TenantContext(BaseModel):
tenant_id: str
user_id: str
org_id: Optional[str] = None

def get_tenant_context(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> TenantContext:
"""JWT에서 테넌트 정보 추출"""
try:
payload = jwt.decode(
credentials.credentials,
JWT_SECRET,
algorithms=["HS256"]
)
return TenantContext(
tenant_id=payload["tenant_id"],
user_id=payload["user_id"],
org_id=payload.get("org_id")
)
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")

# Thread ID 생성 전략
def generate_thread_id(
tenant_id: str, user_id: str, session_id: str
) -> str:
return f"tenant-{tenant_id}:user-{user_id}:session-{session_id}"

def generate_checkpoint_ns(tenant_id: str) -> str:
return f"tenant-{tenant_id}"

def generate_store_namespace(tenant_id: str, user_id: str) -> tuple:
return (tenant_id, user_id, "memories")

장점:

  • 구현 복잡도가 낮고 빠르게 프로토타입 가능
  • PostgreSQL 특별 설정 불필요
  • 수평 확장성 우수 (단일 데이터베이스에서 수천 개 테넌트 지원)

제약사항:

  • 애플리케이션 코드 버그 시 데이터 누출 위험
  • 데이터베이스 레벨의 강제 격리 없음

전략 2: PostgreSQL Row Level Security (RLS)

데이터베이스 레벨에서 행 단위 접근 제어를 구현하여, 애플리케이션 로직과 무관하게 테넌트 격리를 보장합니다.

RLS 설정 예시

-- 1. tenant_id 컬럼 추가 (기존 테이블 수정)
ALTER TABLE checkpoints ADD COLUMN tenant_id TEXT;
ALTER TABLE checkpoint_writes ADD COLUMN tenant_id TEXT;

-- 2. 인덱스 생성 (성능 최적화)
CREATE INDEX idx_checkpoints_tenant
ON checkpoints(tenant_id, thread_id);

CREATE INDEX idx_checkpoint_writes_tenant
ON checkpoint_writes(tenant_id, thread_id);

-- 3. RLS 정책 활성화
ALTER TABLE checkpoints ENABLE ROW LEVEL SECURITY;
ALTER TABLE checkpoint_writes ENABLE ROW LEVEL SECURITY;

-- 4. 테넌트별 격리 정책
CREATE POLICY tenant_isolation ON checkpoints
FOR ALL
USING (tenant_id = current_setting('app.tenant_id', TRUE))
WITH CHECK (tenant_id = current_setting('app.tenant_id', TRUE));

CREATE POLICY tenant_isolation ON checkpoint_writes
FOR ALL
USING (tenant_id = current_setting('app.tenant_id', TRUE))
WITH CHECK (tenant_id = current_setting('app.tenant_id', TRUE));

LangGraph와 RLS 통합

class TenantAwarePostgresSaver(AsyncPostgresSaver):
"""RLS 지원 커스텀 Checkpointer"""

def __init__(self, conn: Connection, tenant_id: str):
super().__init__(conn)
self.tenant_id = tenant_id

async def _set_tenant_context(self):
"""세션 시작 시 tenant_id 설정"""
await self.conn.execute(
f"SET LOCAL app.tenant_id = '{self.tenant_id}'"
)

@app.post("/chat")
async def chat_endpoint(
request: ChatRequest,
tenant: TenantContext = Depends(get_tenant_context)
):
async with pool.connection() as conn:
# RLS 컨텍스트 설정
await conn.execute(
f"SET LOCAL app.tenant_id = '{tenant.tenant_id}'"
)

checkpointer = AsyncPostgresSaver(conn)
graph = create_graph().compile(checkpointer=checkpointer)

config = {
"configurable": {
"thread_id": generate_thread_id(
tenant.tenant_id,
tenant.user_id,
request.session_id
)
}
}

response = await graph.ainvoke(
{"messages": request.messages},
config
)
return response

RLS의 핵심 이점:

  • 데이터베이스가 격리를 강제하므로 애플리케이션 버그에도 안전
  • 기존 LangGraph 코드 수정 최소화
  • 감사 로깅과 결합 가능

성능 고려사항:

  • current_setting() 함수 호출 오버헤드 (일반적으로 무시 가능)
  • tenant_id 인덱스 필수 (쿼리 성능 유지)

전략 3: Schema 기반 격리

각 테넌트를 별도 PostgreSQL Schema로 격리하는 방식으로, 물리적 분리에 가까운 격리를 제공합니다.

class SchemaBasedCheckpointer:
"""테넌트별 Schema 라우팅"""

def __init__(self, pool: AsyncConnectionPool):
self.pool = pool
self.schema_cache = {}

async def get_checkpointer(
self, tenant_id: str
) -> AsyncPostgresSaver:
schema_name = f"tenant_{tenant_id}"

# Schema 자동 생성
if schema_name not in self.schema_cache:
async with self.pool.connection() as conn:
await conn.execute(
f"CREATE SCHEMA IF NOT EXISTS {schema_name}"
)
await conn.execute(
f"SET search_path TO {schema_name}"
)
# LangGraph 테이블 초기화
checkpointer = AsyncPostgresSaver(conn)
await checkpointer.setup()

self.schema_cache[schema_name] = True

# Schema 전환 후 Checkpointer 반환
async with self.pool.connection() as conn:
await conn.execute(
f"SET search_path TO {schema_name}"
)
return AsyncPostgresSaver(conn)

적용 시나리오:

  • 규제 요구사항 (GDPR, HIPAA 등)
  • 테넌트별 백업/복구 필요
  • 데이터 마이그레이션 용이성

운영 복잡도:

  • Schema 생성/삭제 자동화 필요
  • Connection Pool 관리 복잡성 증가
  • 테넌트 수가 수백 개 이상일 때 스키마 관리 부담

Long-Term Memory (Store) Multi-Tenant 격리

LangGraph Store는 Checkpoint와 별도로 Long-term Memory를 관리합니다. Namespace 튜플 구조를 활용한 계층적 격리가 핵심입니다.

# Store Namespace 전략
def generate_store_namespace(
tenant_id: str,
user_id: str
) -> tuple:
"""
계층 구조: (tenant_id, user_id, "memories")
- 레벨 1: 테넌트 격리
- 레벨 2: 사용자별 분리
- 레벨 3: 메모리 타입
"""
return (tenant_id, user_id, "memories")

# 사용 예시
async def save_user_preference(
tenant_id: str,
user_id: str,
preference: dict
):
namespace = generate_store_namespace(tenant_id, user_id)

await store.aput(
namespace=namespace,
key="preferences",
value=preference
)

# 검색 시 테넌트 자동 필터링
async def search_memories(
tenant_id: str,
user_id: str,
query: str
):
namespace = generate_store_namespace(tenant_id, user_id)

# Store는 namespace prefix로 자동 격리
results = await store.asearch(
namespace_prefix=(tenant_id,), # 테넌트 레벨 필터
query=query
)
return results

Store RLS 적용 (추가 격리층):

-- Store 테이블에도 RLS 적용
ALTER TABLE store ADD COLUMN tenant_id TEXT;

CREATE POLICY store_tenant_isolation ON store
FOR ALL
USING (
namespace[1] = current_setting('app.tenant_id', TRUE)
)
WITH CHECK (
namespace[1] = current_setting('app.tenant_id', TRUE)
);

보안 체크리스트

Multi-Tenant 시스템 배포 전 반드시 확인해야 할 보안 항목:

1. 인증/인가

  • JWT 토큰 서명 검증 구현
  • 토큰 만료 시간 적절히 설정 (권장: 1시간)
  • Refresh Token 순환 메커니즘

2. 격리 검증

  • Cross-tenant 쿼리 시도 시 접근 거부 확인
  • Thread ID에 테넌트 정보 포함 여부 검증
  • RLS 정책 우회 시도 테스트

3. SQL Injection 방지

# ❌ 위험: 문자열 포맷팅
await conn.execute(
f"SET LOCAL app.tenant_id = '{tenant_id}'"
)

# ✅ 안전: 파라미터화된 쿼리
await conn.execute(
"SELECT set_config('app.tenant_id', $1, true)",
[tenant_id]
)

4. 감사 로깅

async def audit_log(
tenant_id: str,
user_id: str,
action: str,
resource: str
):
await conn.execute("""
INSERT INTO audit_logs
(tenant_id, user_id, action, resource, timestamp)
VALUES ($1, $2, $3, $4, NOW())
""", [tenant_id, user_id, action, resource])

5. 정기 검증

  • 월간 테넌트 격리 침투 테스트
  • 분기별 권한 감사
  • 데이터 접근 로그 분석 자동화

성능 최적화 전략

Connection Pool 설정:

# Schema 분리 시 동적 풀 관리
class TenantAwarePool:
def __init__(self, base_uri: str, max_pools: int = 50):
self.pools: Dict[str, AsyncConnectionPool] = {}
self.base_uri = base_uri
self.max_pools = max_pools

async def get_pool(
self, tenant_id: str
) -> AsyncConnectionPool:
if tenant_id not in self.pools:
if len(self.pools) >= self.max_pools:
# LRU 제거
oldest = min(
self.pools.items(),
key=lambda x: x[1].last_used
)
await oldest[1].close()
del self.pools[oldest[0]]

self.pools[tenant_id] = AsyncConnectionPool(
f"{self.base_uri}?options=-c search_path=tenant_{tenant_id}",
min_size=2,
max_size=10
)

return self.pools[tenant_id]

인덱싱 전략:

-- Composite 인덱스로 테넌트 쿼리 최적화
CREATE INDEX idx_checkpoints_tenant_thread_ts
ON checkpoints(tenant_id, thread_id, checkpoint_id DESC);

-- Partial 인덱스로 활성 테넌트만 최적화
CREATE INDEX idx_active_tenants
ON checkpoints(tenant_id, thread_id)
WHERE created_at > NOW() - INTERVAL '30 days';

References

CI/CD 가이드라인

· 약 5분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

Azure VM 환경에서 GitHub Actions를 활용한 Docker 기반 CI/CD 파이프라인 구축 사례입니다. SSH를 통한 원격 배포와 Docker Compose 기반 멀티 컨테이너 관리를 구현했으며, fastclean 두 가지 배포 모드를 제공해 상황에 따라 빠른 업데이트 또는 완전한 재배포를 선택할 수 있습니다. SK Ecoplant 프로젝트에서 실제 운영된 스크립트로, RAG 파이프라인 배포에 최적화되어 있습니다.

Key Takeaways

  • 배포 모드 분리: fast 모드는 캐시 활용으로 빠른 배포를, clean 모드는 완전한 초기화를 통해 환경 일관성을 보장하여 개발/운영 단계에 따라 유연하게 대응
  • SSH 기반 원격 배포: GitHub Secrets를 활용한 안전한 인증 정보 관리와 ssh-keyscan을 통한 MITM 공격 방지로 보안성 확보
  • Docker Compose v2 명시적 설치: Runner 환경에 관계없이 일관된 Docker Compose 버전을 사용하여 예측 가능한 배포 환경 구축
  • 멀티 스테이지 이미지 구조: Base 이미지를 분리해 공통 의존성을 재사용하고, LangGraph와 FastAPI 서비스를 독립적으로 관리하여 빌드 효율성 향상
  • 수동 트리거 지원: workflow_dispatch를 통해 긴급 배포나 롤백 시나리오에서 개발자가 직접 배포 모드를 선택할 수 있는 유연성 제공

상세 내용

프로젝트 배경 및 구조

이 CI/CD 파이프라인은 SK Ecoplant 고객사 프로젝트에서 실제 운영된 배포 스크립트입니다. RAG(Retrieval-Augmented Generation) 파이프라인을 Azure VM 환경에 배포하기 위해 설계되었으며, 다음과 같은 3개의 Docker 이미지로 구성됩니다:

  • Dockerfile.base: 공통 의존성을 포함하는 베이스 이미지
  • Dockerfile.langgraph: LangGraph 기반 워크플로우 서비스
  • Dockerfile.fastapi: API 엔드포인트를 제공하는 FastAPI 서비스

이러한 멀티 이미지 구조는 각 서비스의 독립적인 스케일링과 유지보수를 가능하게 하며, 베이스 이미지 분리를 통해 빌드 시간을 최적화합니다.

GitHub Actions Workflow 구성

트리거 설정

on:
push:
branches:
- main # main push 시 자동 실행
workflow_dispatch:
inputs:
mode:
description: "배포 모드 선택 (fast / clean)"
required: true
default: "fast"
type: choice
options:
- fast
- clean

두 가지 트리거를 지원합니다:

  • 자동 트리거: main 브랜치로의 push 시 자동으로 fast 모드로 배포
  • 수동 트리거: Actions 탭에서 개발자가 직접 배포 모드를 선택하여 실행 가능

이러한 구조는 일반적인 개발 워크플로우(자동 배포)와 긴급 상황(수동 개입)을 모두 지원합니다.

SSH 연결 설정 및 보안

- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AZURE_SSH_KEY }}" > ~/.ssh/azure_vm_key.pem
chmod 600 ~/.ssh/azure_vm_key.pem
ssh-keyscan -H ${{ secrets.AZURE_VM_IP }} >> ~/.ssh/known_hosts

SSH 연결을 위해 다음 보안 조치를 적용했습니다:

  1. GitHub Secrets 활용: 민감한 정보(SSH 키, VM IP, 사용자명)를 저장소에 노출하지 않고 암호화된 secrets로 관리
  2. 적절한 권한 설정: SSH 키 파일에 600 권한을 부여하여 SSH가 요구하는 보안 기준 준수
  3. Known Hosts 등록: ssh-keyscan을 통해 대상 서버의 호스트 키를 미리 등록하여 MITM(중간자) 공격 방지

필수 GitHub Secrets:

  • AZURE_SSH_KEY: Azure VM 접속을 위한 private key
  • AZURE_VM_IP: 대상 VM의 IP 주소
  • AZURE_VM_USER: VM 로그인 사용자명

Docker Compose v2 설치

- name: Install Docker Compose v2
run: |
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
curl -SL https://github.com/docker/compose/releases/download/v2.29.2/docker-compose-linux-x86_64 \
-o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
docker compose version

GitHub Actions runner에 Docker Compose v2를 명시적으로 설치합니다. 이는 다음과 같은 이점을 제공합니다:

  • 버전 일관성: 특정 버전(v2.29.2)을 사용해 예측 가능한 동작 보장
  • 새로운 문법 지원: docker compose (v2) vs docker-compose (v1) 문법 차이 해결
  • Runner 환경 독립성: 사전 설치된 도구에 의존하지 않음

배포 모드별 전략

Fast 모드 (기본값)

echo "⚡ 빠른 배포 모드 실행..."
docker build -t sk_eco_base:latest -f environments/Dockerfile.base .
docker compose -f environments/docker-compose.yml build
docker compose -f environments/docker-compose.yml up -d --remove-orphans
docker image prune -f

특징:

  • Docker 빌드 캐시 활용으로 빠른 배포
  • 변경된 레이어만 재빌드
  • 사용하지 않는 dangling 이미지만 정리
  • 일반적인 코드 업데이트에 적합

사용 시나리오:

  • 소스 코드 수정 반영
  • 의존성 변경이 없는 일상적인 배포
  • 개발 환경의 빈번한 업데이트

Clean 모드

echo "🧹 깨끗한 재배포 모드 실행..."
docker compose -f environments/docker-compose.yml down --remove-orphans
docker system prune -af --volumes

docker build -t sk_eco_base:latest -f environments/Dockerfile.base .
docker compose -f environments/docker-compose.yml build --no-cache
docker compose -f environments/docker-compose.yml up -d

docker builder prune -af

특징:

  • 모든 컨테이너, 이미지, 볼륨, 네트워크 제거
  • --no-cache 옵션으로 완전한 재빌드
  • 빌드 캐시까지 삭제
  • 환경을 완전히 초기화

사용 시나리오:

  • 의존성 패키지 메이저 업데이트
  • 원인 불명의 빌드 오류 해결
  • 프로덕션 환경의 정기 클린업
  • 캐시 손상으로 인한 문제 해결

원격 배포 실행 흐름

ssh -i ~/.ssh/azure_vm_key.pem ${{ secrets.AZURE_VM_USER }}@${{ secrets.AZURE_VM_IP }} << 'EOF'
set -e # 에러 발생 시 즉시 중단
cd /home/${{ secrets.AZURE_VM_USER }}/sk-eco-rag-pipeline
git pull origin main
# ... 배포 스크립트 실행
EOF

SSH를 통해 원격 서버에서 다음 작업을 수행합니다:

  1. 코드 업데이트: git pull로 최신 코드 가져오기
  2. 베이스 이미지 빌드: 공통 의존성을 포함한 base 이미지 먼저 빌드
  3. 서비스 이미지 빌드: Docker Compose를 통해 나머지 서비스 빌드
  4. 컨테이너 실행: up -d로 백그라운드에서 서비스 시작
  5. 정리 작업: 미사용 이미지/빌드 캐시 제거로 디스크 공간 확보

set -e 옵션을 통해 중간에 오류가 발생하면 즉시 배포를 중단하여 부분적으로 배포된 상태를 방지합니다.

개선 가능한 영역

문서에서 명시된 것처럼 Dockerfile 최적화는 별도로 수행되지 않았습니다. 향후 개선 가능한 영역은 다음과 같습니다:

  • 멀티 스테이지 빌드: 최종 이미지 크기 감소
  • 레이어 캐싱 최적화: 의존성 설치와 소스 코드 복사 순서 조정
  • Health Check 통합: 컨테이너 시작 후 서비스 가용성 확인
  • 롤백 메커니즘: 배포 실패 시 이전 버전으로 자동 복구
  • 슬랙/이메일 알림: 배포 성공/실패 알림 시스템
  • Blue-Green 배포: 무중단 배포를 위한 전략 도입

실무 적용 시 고려사항

이 파이프라인을 다른 프로젝트에 적용할 때 고려해야 할 사항:

  1. 환경 변수 관리: .env 파일이나 secrets를 통한 환경별 설정 분리
  2. 로그 관리: 컨테이너 로그를 중앙화된 로깅 시스템으로 전송
  3. 모니터링: 배포 후 서비스 메트릭 모니터링 체계 구축
  4. 백업 전략: 볼륨 데이터에 대한 정기 백업 계획
  5. 보안 스캔: 이미지 취약점 스캔을 CI 파이프라인에 통합

References

Project Github Setting Guideline

· 약 7분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

AI/ML 프로젝트에서 긴급 이슈 발생 시 VM에 직접 접속해 코드를 수정하거나, 작업 이력 추적이 미흡한 문제를 해결하기 위한 GitHub 프로젝트 설정 가이드입니다. main/dev 브랜치 이원화, branch protection rule, squash merge, PR template 설정을 통해 코드 변경 이력을 체계적으로 관리하고, 팀 협업 시 코드 품질을 시스템적으로 보장할 수 있는 워크플로우를 구축합니다.

Key Takeaways

  • Branch 이원화 전략: main(프로덕션), dev(개발), feat(기능) 브랜치 구조로 안정성과 개발 속도를 동시에 확보
  • Protection Rule 필수 설정: main/dev 브랜치에 직접 push를 막고 PR을 강제함으로써 코드 리뷰와 이력 추적 보장
  • Squash Merge 활용: 여러 개의 작은 커밋을 하나로 합쳐 깔끔한 커밋 히스토리 유지, 롤백과 디버깅 용이
  • PR Template 표준화: 작업 내용, 변경 사항, 테스트 결과를 일관된 형식으로 문서화하여 팀 간 커뮤니케이션 효율 향상
  • VM 직접 작업 금지 원칙: 긴급 상황에서도 Git 워크플로우를 따라 변경 이력을 남기는 것이 장기적으로 유지보수성 향상

상세 내용

배경: 기존 프로젝트 작업 방식의 문제점

AI/ML 프로젝트 환경에서는 모델 학습과 서빙을 위해 VM(Virtual Machine)을 자주 사용합니다. 그러나 다음과 같은 문제가 반복적으로 발생합니다:

  • 긴급 이슈 대응 시 직접 수정: 서비스 장애나 모델 성능 문제 발생 시, VM에 SSH로 접속해 코드를 직접 수정하는 경우가 빈번합니다. 이는 Git 이력에 남지 않아 "누가, 언제, 왜" 수정했는지 추적이 불가능합니다.
  • 작업 내역 추적 미흡: 개인별 작업이 commit history로 제대로 관리되지 않고, unit test 없이 코드가 배포되어 회귀 버그(regression bug) 발생 위험이 높습니다.

이러한 문제는 특히 여러 Research Engineer가 협업하는 환경에서 코드 충돌, 재현 불가능한 실험, 롤백 어려움 등으로 이어집니다. 이 가이드는 시스템적으로 이런 문제를 방지하기 위한 GitHub 설정 방법을 제시합니다.

VM 및 SSH 설정 (준비 중)

Azure 또는 AWS, GCP 등 클라우드 환경에서 VM을 생성하고, 로컬 개발 환경에서 SSH를 통해 안전하게 접속하는 방법은 향후 업데이트 예정입니다. 핵심은 개발자가 VM에 직접 코드를 수정하지 않고, 로컬에서 작업 후 Git을 통해 배포하는 흐름을 구축하는 것입니다.

Branch 이원화 전략

Branch Structure

브랜치 구조:

  • main: 프로덕션 배포용 브랜치. 항상 안정적인 상태를 유지해야 합니다.
  • dev: 개발 통합 브랜치. 모든 feature 브랜치는 여기서 분기하고 병합됩니다.
  • feat/feature-name: 개별 기능 개발용 브랜치. dev를 base로 생성합니다.

워크플로우:

  1. dev 브랜치에서 feat/new-model-architecture 같은 feature 브랜치 생성
  2. 로컬에서 작업 후 commit & push
  3. dev로 PR(Pull Request) 생성
  4. 코드 리뷰 및 테스트 통과 후 merge
  5. dev에서 충분히 테스트된 코드만 main으로 merge

이 구조는 실험적인 모델 개발(feat)과 안정적인 서비스 운영(main)을 분리하여, Research Engineer가 자유롭게 실험하면서도 프로덕션 안정성을 유지할 수 있게 합니다.

Branch Protection Rule 설정

Protection Rule Settings 1

Protection Rule Settings 2

Branch protection rule은 main, dev 브랜치에 반드시 설정해야 합니다. 이는 VM에 직접 접속해 코드를 수정하는 습관을 시스템적으로 차단합니다.

권장 설정:

  • Require a pull request before merging: 직접 push를 막고 반드시 PR을 통해서만 병합 가능하게 합니다.
  • Require approvals: 최소 1명 이상의 리뷰어 승인을 필수로 설정합니다. 페어 프로그래밍처럼 코드 품질을 검증할 수 있습니다.
  • Require status checks to pass: CI/CD 파이프라인(unit test, linting 등)을 통과해야만 merge 가능하게 합니다.
  • Require conversation resolution before merging: PR 코멘트가 모두 해결되어야 병합됩니다.
  • Include administrators: 관리자도 이 규칙을 따르도록 강제하여 예외 없는 프로세스를 보장합니다.

이 설정으로 긴급 상황에서도 "일단 VM에 들어가서 고친다"는 접근이 불가능해지고, 반드시 Git 워크플로우를 따르게 됩니다.

Pull Request 설정

Squash Merge 전략

Squash Merge Setting

Squash Merge의 장점:

  • 개발 중 "WIP", "fix typo", "debugging" 같은 작은 커밋들이 많이 생기는데, 이를 하나의 의미 있는 커밋으로 합쳐 히스토리를 깔끔하게 유지합니다.
  • git log로 이력을 볼 때 feature 단위로 파악할 수 있어, 특정 기능을 추가한 시점을 쉽게 찾을 수 있습니다.
  • 롤백 시 해당 feature 전체를 한 번에 되돌릴 수 있습니다.

설정 방법: Repository Settings → General → Pull Requests 섹션에서 "Allow squash merging"만 체크하고 나머지는 해제합니다.

PR Template 설정

![PR Template Setting](https://prod-files-secure.s3.us-west-2.amazonaws.com/bb84b169-cb88-81fc-90c3-00032f05f905/27ed859f-3633-4899-8032-ae177e5a4bff/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RDIG27QE%2F20260325%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260325T065013Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEN%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMiJIMEYCIQDCMtPmGiArJrAqWROxY5AlrymYaVpHBLqqOqkNiO%2FiIgIhAJJ0avnMSZgMc9F1AUVlH8LdRwBlj56e7mxzmomgko%2FtKogECKf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igx6cVeEQMyQcIlUD%2FIq3AOPw2Uq92YMx3q9CRuSgOHXCGV1%2BuIrAUSm5mP20fALIZrknrvevF75AMvhMMc8K6Ih3z%2F%2FcJ5zEy4aPc6dFMg8Tb5DMqioBCa0TL%2BqWizzK065eU8UCsOifDkEXsmiF9NmR5mQH9ZlKIhA98aGHDzCUsjMTUIwlO%2FKWJuo1hmlGtbTyll6WXi9j4w5uLmvBHehfWB68znBZR3UiEAyR8N40Ro3Nh%2FRVEL8nOmL2G%2BYHzpp6GU8D518u3hm4QKYOr7%2BxglBdf4A6qcrCGu3sCjPGM%2FYS854m%2BiXsjSy5dnn03HjRB%2F1IWDBCTcI70QTILNJtYMnwPSpZbi48Q3dJuyhYgOEM%2F7Ed64fscVzcV

LangSmith를 활용한 PoC Monitoring & Evaluation

· 약 4분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

LangSmith는 LLM 애플리케이션의 PoC 단계에서 필수적인 모니터링과 평가를 지원하는 도구입니다. Chat UI 기반 서비스 배포 시, 실시간으로 프롬프트 성능을 추적하고 데이터셋 기반 평가를 수행할 수 있습니다. 이 가이드라인은 AI Research Engineer가 PoC 서비스의 품질을 체계적으로 관리하고 개선하기 위한 모니터링과 평가 프로세스를 제공합니다.

Key Takeaways

  • PoC 단계에서의 체계적 추적: LangSmith를 통해 Chat UI 기반 서비스의 모든 LLM 호출과 응답을 실시간으로 추적하여 초기 단계부터 품질을 관리할 수 있습니다.
  • Monitoring과 Evaluation의 분리: 실시간 모니터링으로 프로덕션 이슈를 즉시 파악하고, 오프라인 평가로 체계적인 성능 개선을 수행하는 이원화된 접근이 필요합니다.
  • 데이터 기반 의사결정: 실제 사용자 인터랙션 데이터를 수집하고 평가 데이터셋으로 전환하여 프롬프트 엔지니어링과 모델 선택에 활용할 수 있습니다.
  • Chat UI 특화 메트릭: 대화형 인터페이스의 특성상 응답 시간(latency), 토큰 사용량, 대화 흐름의 연속성 등 특화된 메트릭 추적이 중요합니다.

상세 내용

LangSmith 개요

LangSmith는 LangChain 생태계에서 제공하는 LLM 애플리케이션 개발 및 운영을 위한 통합 플랫폼입니다. 특히 PoC(Proof of Concept) 단계에서 빠르게 프로토타입을 검증하고 개선해야 하는 Research Engineer에게 유용한 도구로, 복잡한 인프라 구축 없이도 전문적인 모니터링과 평가 환경을 제공합니다.

Monitoring: 실시간 추적과 디버깅

Monitoring의 목적

PoC 서비스 배포 시 Monitoring은 다음과 같은 목적을 달성합니다:

  • 실시간 LLM 호출 추적 및 로깅
  • 예상치 못한 에러나 품질 저하 즉시 감지
  • 사용자 피드백과 실제 응답 연결
  • 비용 및 리소스 사용량 모니터링

Chat UI 환경에서의 Monitoring 구현

Chat UI 기반 서비스는 일반적으로 다음과 같은 특성을 가집니다:

  • 멀티턴 대화로 인한 컨텍스트 누적
  • 실시간 스트리밍 응답 요구
  • 사용자 경험에 민감한 레이턴시

LangSmith는 이러한 특성을 고려하여 각 대화 세션을 trace로 묶어 추적하고, 개별 LLM 호출을 span으로 기록합니다. 이를 통해 대화 전체의 흐름과 각 단계별 성능을 동시에 파악할 수 있습니다.

핵심 모니터링 메트릭

  • Latency: 첫 토큰까지의 시간(TTFT)과 전체 응답 시간
  • Token Usage: 입력/출력 토큰 수와 비용 환산
  • Error Rate: 실패한 요청의 비율과 에러 타입
  • Feedback Scores: 사용자 평가(thumbs up/down) 수집

Evaluation: 체계적 성능 평가

Evaluation의 필요성

Monitoring이 '무엇이 일어났는가'를 추적한다면, Evaluation은 '얼마나 잘 작동하는가'를 측정합니다. PoC 단계에서 Evaluation은:

  • 프롬프트 버전 간 성능 비교
  • 다양한 LLM 모델 벤치마킹
  • 레그레션(regression) 방지
  • 프로덕션 준비도 판단 기준 제공

데이터셋 구축

효과적인 Evaluation을 위해서는 품질 좋은 데이터셋이 필수적입니다:

  1. Monitoring 데이터 활용: 실제 사용자 쿼리 중 대표적인 케이스 선별
  2. Edge Case 추가: 예외 상황, 어려운 질문, 모호한 요청 등 포함
  3. Ground Truth 정의: 기대되는 올바른 응답 또는 평가 기준 명시
  4. 지속적 업데이트: 새로운 사용 패턴과 실패 케이스 반영

평가 메트릭 설정

Chat UI 서비스의 특성에 맞는 평가 메트릭 예시:

  • 정확성(Correctness): LLM-as-a-Judge를 활용한 응답 품질 평가
  • 관련성(Relevance): 사용자 질문과 응답의 연관성
  • 완전성(Completeness): 필요한 정보를 모두 포함하는지 여부
  • 일관성(Consistency): 동일 질문에 대한 응답의 안정성
  • 안전성(Safety): 유해 콘텐츠 생성 여부

반복적 개선 프로세스

LangSmith를 활용한 전형적인 개선 사이클:

  1. Baseline 설정: 초기 프롬프트/모델로 평가 실행
  2. 문제 영역 식별: 낮은 점수를 받은 케이스 분석
  3. 가설 수립: 프롬프트 수정 또는 모델 변경 방안 도출
  4. 실험 실행: 새 버전으로 동일 데이터셋 재평가
  5. 비교 분석: 버전 간 메트릭 비교로 개선 여부 확인
  6. 배포 결정: 충분한 개선 확인 시 PoC 환경에 적용

PoC 단계 Best Practices

1. 초기부터 추적 설정

개발 시작부터 LangSmith 통합을 구축하면 초기 실험 데이터도 유용한 학습 자료가 됩니다.

2. 사용자 피드백 수집 자동화

Chat UI에 간단한 평가 버튼을 추가하여 실시간 피드백을 LangSmith로 전송합니다.

3. 주간 평가 루틴 수립

일주일마다 누적된 데이터를 분석하고 평가 데이터셋을 업데이트하는 루틴을 만듭니다.

4. 비용 모니터링 중요성

PoC 단계에서 비용 효율성을 고려하지 않으면 프로덕션 전환 시 장애물이 됩니다.

한계와 고려사항

  • LangSmith는 LangChain 생태계와 가장 잘 통합되지만, 다른 프레임워크 사용 시 추가 작업이 필요할 수 있습니다.
  • 민감한 사용자 데이터는 로깅 전 익명화 또는 필터링 처리가 필요합니다.
  • PoC 단계의 트래픽 규모와 프로덕션 규모 차이를 고려한 확장성 검토가 필요합니다.

References