50개의 에이전트 인프라 라이브러리를 출시하며 배운 점
요약
50개의 에이전트 인프라 라이브러리를 개발하며 얻은 '하나의 실패, 하나의 수정' 패턴을 소개합니다. 프로덕션 환경에서 에이전트 시스템의 안정성을 결정짓는 안전, 관측 가능성, 신뢰성, 컨텍스트 관리의 중요성을 강조합니다.
핵심 포인트
- 범위를 최소화하는 '하나의 실패, 하나의 수정' 패턴 활용
- 에이전트 실패는 모델이 아닌 인프라의 문제
- 단순 로깅을 넘어선 심층적 관측 가능성 확보 필요
- 재시도, 체크포인팅 등 분산 시스템의 신뢰성 요소 적용
숫자로 시작하지는 않겠습니다. 숫자는 흥미로운 부분이 아닙니다.
흥미로운 부분은 각 라이브러리를 빠르게 출시할 수 있게 만든 패턴입니다.
패턴
이번 스프린트의 모든 라이브러리는 동일한 방식으로 시작되었습니다. 저는 에이전트 루프 (agent loop)에서 발생하는 하나의 실패 모드 (failure mode), 즉 상황이 잘못되는 특정한 한 가지 방식을 선택했습니다. 그런 다음 그 실패 모드만을 해결하는 가장 작은 수정 사항을 작성했습니다. 그다음 수정 사항이 유지된다는 확신이 들 때까지 테스트를 작성했습니다. 그러고 나서 멈췄습니다.
'하나의 실패, 하나의 수정'이라는 제약 조건은 범위 (scope)를 작게 유지하도록 강제했습니다. 이는 제가 새로운 라이브러리를 시작하고, GitHub에 출시하고, 테스트를 작성하고, 몇 시간 만에 마무리할 수 있음을 의미했습니다. 또한 각 라이브러리가 이해하기 쉬운 상태를 유지함을 의미했습니다. "이것은 X도 처리합니다"와 같은 확산 (sprawl)이 없었습니다.
이 패턴은 복잡하지 않습니다. 어려운 점은 일반화 (generalize)하려는 욕구를 참는 것입니다.
지루한 문제들의 실체
에이전트를 충분히 오래 구축하다 보면, 반복되는 일련의 작은 문제들에 부딪히게 됩니다. 이 문제들은 화려하지 않습니다. 데모 (demo)에서도 나타나지 않습니다. 하지만 이것들이 바로 프로덕션 에이전트 시스템 (production agent systems)이 무너지는 이유입니다.
저는 이 문제들을 다섯 가지 범주로 분류했습니다.
안전 및 제어 (Safety and control)
에이전트는 당신이 지켜보고 있지 않을 때 예상치 못한 행동을 합니다. 동일한 도구 (tool)를 40번이나 호출합니다. 예산에서 허용하는 것보다 더 많은 토큰 (token)을 소비합니다. 당신이 승인하지 않은 도메인으로 외부 HTTP 호출을 보냅니다. 잘못된 타입의 도구 인자 (tool arguments)를 수락하고 조용히 잘못된 결과를 만들어냅니다.
이것들은 모델 (model)의 문제가 아닙니다. 인프라 (infrastructure)의 문제입니다. 모델은 당신이 시킨 대로 정확히 수행하고 있습니다. 단지 당신이 가드레일 (guardrails)을 설치하지 않았을 뿐입니다.
관측 가능성 (Observability)
대부분의 에이전트 프레임워크 (agent frameworks)는 로깅 (logging) 기능을 제공합니다. 로깅은 관측 가능성 (observability)이 아닙니다. 관측 가능성이란 다음과 같은 질문에 답할 수 있음을 의미합니다: 에이전트가 왜 이런 결정을 내렸는가, 이 출력은 어디에서 왔는가, 와이어 레벨 (wire level)에서의 정확한 JSON 페이로드 (payload)는 어떤 모습이었는가, 그리고 이 실행에 실제로 비용이 얼마나 들었는가.
이러한 답변 중 어느 것도 거저 주어지지 않습니다.
신뢰성 (Reliability)
에이전트는 실패합니다. 제공업체(Providers)는 속도 제한(Rate-limit)을 겁니다. 컨텍스트 창(Context windows)은 가득 찹니다. 오래 실행되는 작업은 중간에 충돌(Crash)합니다. 만약 당신의 에이전트가 처음부터 다시 시작하지 않고 실패로부터 복구할 수 없다면, 당신은 엄청난 양의 토큰과 인내심을 낭비하게 될 것입니다.
신뢰성(Reliability)은 지루한 인프라입니다. 재시도 로직(Retry logic), 멱등성 키(Idempotency keys), 체크포인팅(Checkpointing), 서킷 브레이커(Circuit breakers). 15년 전 분산 시스템(Distributed systems) 전문가들이 해결했던 것과 동일한 요소들이지만, 이제는 실패하는 대상이 데이터베이스 쓰기(Database write) 대신 LLM 호출(LLM call)이라는 점이 다를 뿐입니다.
컨텍스트 및 프롬프트 관리 (Context and prompt management)
컨텍스트 창(Context window)은 에이전트의 작업 기억(Working memory)입니다. 이를 제대로 관리하지 못하면 환각(Hallucinations), 컨텍스트 누락(Dropped context), 캐시 미스(Cache misses)가 발생합니다. 이를 잘 관리하는 것은 마법이 아닙니다. 그것은 세심하고 기계적인 작업입니다. 적절한 메시지를 다듬고(Trim), 도구 호출(Tool calls)을 그 결과와 쌍으로 유지하며
몇몇 라이브러리에는 그 가치가 명확한 선택적 추가 기능(optional extras)이 있습니다. prompt-token-counter는 사용자가 직접 토크나이저(tokenizer)를 가져와 사용할 수 있게 해줍니다. tool-result-cache는 선택적인 Redis 백엔드를 지원합니다. 하지만 핵심 경로(core path)는 추가적인 설치 없이도 작동합니다.
테스트 개수가 신호인 이유
이 시리즈의 모든 포스트에는 테스트 개수가 나열되어 있습니다. 누군가에게 깊은 인상을 남기기 위해 그 숫자를 적어 넣은 것이 아닙니다. 라이브러리가 실제로 말하는 대로 작동한다는 것을 보여주는 가장 명확한 신호이기 때문에 그 숫자를 적었습니다.
테스트가 20개인 라이브러리는 범위(scope)가 좁습니다. 테스트 파일을 읽고 5분 안에 계약(contract)을 이해할 수 있습니다. 테스트가 200개인 라이브러리는 아마도 범위 확장(scope creep)이 발생했을 가능성이 높습니다. 테스트 개수를 원래의 제약 조건이 유지되었는지 판단하는 대략적인 대리 지표(proxy)로 사용할 수 있습니다.
각 라이브러리의 목표는 20개에서 50개의 테스트였습니다. 일반적인 경로(normal paths), 예외 케이스(edge cases), 그리고 에러 조건(error conditions)을 커버하기에 충분한 양입니다. 그렇다고 테스트 스위트(test suite)가 구현 코드보다 읽기 어려울 정도로 많지는 않은 수준입니다.
전체 라이브러리 목록
| 카테고리 | 라이브러리 | 해결하는 문제 |
|---|---|---|
| 안전 및 제어 (Safety and control) | prompt-shield | 인젝션 탐지, 5가지 조합 가능한 규칙 |
| ... |
교훈
범위 제약(scope constraint)이 핵심입니다. 라이브러리의 목적이 모호해질 때마다 구현은 더 어려워졌고 테스트는 더 길어졌습니다. 가장 깔끔하게 출시된 라이브러리들은 코드를 작성하기 전에 문제 정의(problem statement)를 한 문장으로 쓸 수 있었던 것들이었습니다.
테스트 개수는 결과가 아니라 입력값입니다. 저는 각 라이브러리를 시작하기 전에 20개에서 50개의 테스트라는 목표를 설정했습니다. 그 목표 덕분에 구현을 생각하기 전에 계약(contract)에 대해 먼저 생각할 수 있었습니다. '내가 커버해야 할 케이스는 무엇인가?'라는 질문은 코드가 무엇을 해야 하는지 묻는 것보다 종종 더 나은 설계 도구가 됩니다.
조합성 (Composability)에는 절제가 필요합니다. 저는 두 개의 라이브러리를 하나로 결합하는 편의 메서드(convenience methods)를 추가하고 싶은 유혹을 적극적으로 뿌리쳐야 했습니다. 그런 식의 병합은 특정 애플리케이션에서는 유용할 수 있지만, 인프라 수준에서의 조합성 (Composability)을 파괴합니다. 만약 agent-resume가 내부적으로 llm-retry를 호출하기를 원한다면, 그것은 애플리케이션 단계에서 수행하십시오. 라이브러리 내부에서 수행해서는 안 됩니다.
의존성 제로 (Zero deps)는 강제적인 동기 부여 요소입니다. 라이브러리가 프로덕션 의존성 (production dependencies) 없이 배포된다고 미리 결정한 덕분에, 기존 패키지를 래핑 (wrapping)하는 대신 직접 구현해야만 했습니다. 이는 비효율적으로 들릴 수 있습니다. 하지만 실제로 SHA-256 해시 함수, LRU 교체 정책 (LRU eviction policy), 그리고 단순한 재시도 루프 (retry loop)는 모두 오후 시간 내에 올바르게 구현할 수 있을 만큼 충분히 작습니다. 그리고 이제 저는 그 동작 방식을 완전히 제어할 수 있습니다.
지루한 문제들이 하중을 견디는 핵심입니다. 아무도 자신의 서킷 브레이커 (circuit breaker)를 시연하지 않습니다. 아무도 자신의 멱등성 키 생성기 (idempotency key generator)를 자랑하지 않습니다. 하지만 이러한 요소들이 바로 에이전트를 일일이 감시하지 않고도 프로덕션 환경에서 실행할 수 있게 해주는 핵심입니다. 흥미로운 모델의 동작은 이 지루한 인프라 위에 놓여 있습니다. 만약 인프라가 누락되었거나 신뢰할 수 없다면, 모델의 동작은 아무런 의미가 없습니다.
다음 단계
이 라이브러리들 대부분은 Python으로 작성되었습니다. 몇몇은 제가 함께 출시한 Rust 기반의 형제 라이브러리들을 가지고 있습니다. 다음 단계는 PyPI 게시 제한(스프린트 기간 동안 마지막 20여 개를 배포할 때 속도 제한이 걸렸습니다)을 해제한 다음, 트래픽이 더 높은 몇 가지 라이브러리를 골라 더 나은 문서를 작성하는 것입니다.
이번 스프린트를 성공적으로 이끈 패턴은 제가 앞으로도 계속 사용할 패턴입니다. 하나의 실패 모드 (failure mode), 하나의 수정 (fix), 하나의 라이브러리. 에이전트 루프 (agent loops)에는 아직 깔끔하고 작은 수정 방안이 없는 실패 모드들이 여전히 많이 남아 있습니다. 그것들이 바로 흥미로운 문제들입니다. 이 포스트에 담긴 것들은 흥미로운 문제가 아니었습니다. 그것들은 그저 작업(work)이었을 뿐입니다. 그리고 그 작업을 수행함으로써 흥미로운 문제들을 더 쉽게 찾을 수 있게 됩니다.
모든 라이브러리는 MukundaKatta의 GitHub에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기