AI 에이전트의 병목 현상: 과도하게 설계된 LLM 워크플로우 디버깅 및 리팩토링
요약
LLM 에이전트 워크플로우에서 발생하는 비효율적인 아키텍처 안티 패턴을 지적합니다. 결정론적이고 비용 효율적인 작업에 범용 LLM 추론 엔진을 사용하는 것은 과도한 오케스트레이션으로 이어져 시스템의 신뢰성과 비용을 고갈시킵니다. 따라서 에이전트 설계 시, 확률적(probabilistic) 작업과 단순 로직 기반의 결정론적(deterministic) 작업을 명확히 분리하고 리팩토링하는 것이 중요합니다.
핵심 포인트
- 결정론적 작업에 LLM을 사용하는 것은 비효율적인 안티 패턴입니다.
- 단순 라우팅이나 데이터 추출은 정규 표현식, DB 쿼리로 대체해야 합니다.
- LLM의 역할은 복잡한 의미론적 이해나 생성(Generation)에 집중해야 합니다.
- 에이전트 설계 시 결정론적/확률적 작업을 명확히 분리하는 것이 핵심입니다.
Originally published on tamiz.pro.
확률적 무차별 대입의 안티 패턴
대규모 언어 모델 (LLM) 애플리케이션이 빠르게 확장되는 생태계에서, 엔지니어링 예산과 시스템 신뢰성을 조용히 고갈시키는 특정 아키텍처 안티 패턴이 등장했습니다. 그것은 근본적으로 결정론적이며 계산 비용이 저렴한 작업을 수행하기 위해 범용적이고 비결정론적인 추론 엔진(LLM)을 사용하는 관행입니다. 우리는 이것을
- 입력: 사용자가 질문합니다: "제 주문 #123은 왜 지연되었나요?"
- 오케스트레이터 LLM: 주된 '두뇌'가 이 입력을 받습니다. 그리고 생각합니다: "사용자는 주문 상태를 알고 싶어 한다. 나는
get_order_status도구를 사용해야겠다." - 액션: 오케스트레이터는 도구 요청을 형식화하기 위해 LLM을 호출합니다.
- 도구 실행: 백엔드로 API 호출이 이루어집니다. 백엔드는 JSON을 반환합니다:
{ "status": "delayed", "reason": "Shipping hold", "eta": "2 days" }. - 오케스트레이터 LLM (다시): LLM은 이 JSON을 받습니다. 그리고 생각합니다: "사용자는 왜 지연되었는지 물었다. 이유는 '배송 보류(Shipping hold)'이다. 나는 정중한 답변을 작성해야겠다."
- 출력: "안녕하세요, 배송 보류로 인해 주문이 지연되었습니다. 2일 후에 도착할 예정입니다."
비효율성은 어디에 있을까요? 단계 2와 5입니다. LLM은 도구를 사용해야 한다고 '결정'할 필요가 없었습니다. 사용자의 의도('주문 지연')는 특정 질의로 직접 매핑되었기 때문입니다. 또한, LLM이 답변을 '정중하게 다듬을(polite-ify)' 필요도 없습니다. 템플릿이 그것을 더 잘 수행합니다.
하지만 다단계 에이전트에서는 비효율성이 복리처럼 쌓입니다. 만약 에이전트에 '메모리' 모듈이 있다면, 무엇을 저장할지 결정하기 위해 LLM을 호출할 수 있습니다. '검색(search)' 모듈이 있다면, 벡터 데이터베이스에 대한 질의를 작성하기 위해 LLM을 호출할 수도 있습니다. 이 그래프의 모든 가장자리는 인간 개발자가 5줄의 코드로 처리할 수 있는 결정을 나타낼 가능성이 높습니다.
2. 진단적 관점: 결정론적 로직 식별
리팩토링을 하기 전에, 에이전트의 어떤 부분이 '결정론적(deterministic)'이고 어떤 부분이 진정으로 '확률적(probabilistic)'인지 식별해야 합니다.
어떤 작업이 확률적인지란 해결 공간이 너무 커서 열거하기 어렵거나, 비정형 데이터에 대한 의미론적 이해가 필요할 때를 말합니다. 예시: "이 50페이지짜리 법률 문서를 요약해 주세요", "셰익스피어 스타일로 시를 써 주세요", 또는 "미묘한 맥락을 기반으로 이 모호한 티켓을 올바른 부서로 라우팅하세요."
작업(task)이 **결정론적(deterministic)**이라는 것은 그 로직을 불리언 연산자(boolean operators), 데이터베이스 쿼리(database queries), 또는 단순 상태 전이(simple state transitions)로 표현할 수 있다는 의미입니다. 예시: "사용자가 관리자인지 확인하기", "현재 시간을 가져오기 위해 API 호출하기", "텍스트에서 전화번호 추출하기".
위험 신호 (The Red Flags)
에이전트의 로그나 프롬프트 엔지니어링을 검토할 때, 다음 진단 지표들을 찾아보세요:
- 반복적인 라우팅(Repetitive Routing): 특정 키워드가 95%의 확률로 특정 도구를 트리거한다면, LLM은 그저 비싼 정규 표현식(regex)에 불과합니다.
- 형식화 환각(Formatting Hallucinations): 만약 LLM에게 도구 호출을 위한 엄격한 JSON 출력을 요구하고 있다면, 당신은 LLM이 강력한 파서(parser) 역할을 할 것이라고 의존하고 있는 것입니다. 결정론적 파서는 무료이며 신뢰할 수 있습니다.
- 불필요한 요약(Redundant Summarization): 에이전트가 컨텍스트에 이미 충분히 들어갈 만큼 작은 로그를 요약하는 바람에 컨텍스트 창(context window)이 부풀어 오른다면, LLM은 단순 슬라이스 작업으로 처리할 수 있는 일을 하고 있는 것입니다.
3. 리팩토링 전략: "하향식(Push Down)" 방법
과도하게 설계된 에이전트를 리팩토링하는 핵심 원칙은 **결정론적 로직을 스택의 아래쪽으로 하향 배치(push deterministic logic down)**하여 LLM으로부터 멀어지게 하는 것입니다. 우리는 LLM이 모호성, 의도 파악, 합성(synthesis)을 처리하는 "인지 코어(cognitive core)"가 되기를 원하며, 주변 인프라가 정밀성과 라우팅을 담당하게 해야 합니다.
1단계: LLM 기반 라우팅을 규칙 기반 디스패처로 대체하기
LLM에게 "어떤 도구를 사용해야 할까요?"라고 묻는 대신, 사전 비행 인터셉터(pre-flight interceptor)를 설계하세요.
이전 (LLM 의존적):
- 입력: "파리의 날씨는 어때요?"
- 에이전트: LLM 호출 ->
get_weather도구 사용 결정.
이후 (규칙 기반):
- 입력: "파리의 날씨는 어때요?"
- 인터셉터: 정규 표현식 또는 키워드 매처가 "날씨" + 도시 이름을 감지함.
- 액션:
get_weather도구를 직접 호출합니다. 라우팅을 위해 LLM 호출은 없습니다. - LLM 호출: 최종 자연어 응답을 _형식화(format)_하는 용도로만 사용됩니다.
이렇게 하면 지연 시간(latency)이 50~70% 감소하고, 잠재적인 실패 지점(비슷하게 들리는 단어 때문에 LLM이 혼란스러워할 수 있는 경우)이 제거됩니다.
2단계: 추출은 LLM을 사용하고, 검증은 코드를 사용하기
개발자들은 종종 LLM을 사용하여 사용자 입력을 구조화된 데이터로 파싱합니다. LLM은 지저분하고 비정형적인 입력(예: "3월에 산 파란 셔츠에 대해 20% 할인이 필요해요")을 처리하는 데는 뛰어나지만, 그 결과를 _검증(validate)_할 것이라고 신뢰해서는 안 됩니다.
리팩토링:
- LLM 단계: 구조화된 출력 모델(예: JSON 모드)을 사용하여 다음을 추출합니다:
{"item": "blue shirt", "time": "March", "discount": 0.2}. - 코드 단계: Python 함수가 이 JSON을 스키마와 비교하여 검증합니다.
- "March"가 데이터베이스의 유효한 월에 해당합니까?
- 할인율이 이 사용자 등급에서 허용되는 범위 내에 있습니까?
- 피드백 루프: 검증에 실패하면, 그때 그리고 오직 그때만, 오류를 LLM에게 보내 수정하도록 합니다.
이러한 "코드 우선 검증(Code-First Validation)" 방식은 단순한 타입 에러를 잡기 위해 GPU 사이클을 낭비하는 것을 방지합니다.
4. 실질적인 구현: 에이전트 그래프 재작성
이 리팩토링이 코드를 어떻게 변경하는지 살펴보겠습니다. 가상의 Agent 클래스를 사용하는 Python 환경이라고 가정하겠습니다.
과도하게 설계된 기준(The Over-Engineered Baseline)
# ❌ 안티 패턴: "LLM이 모든 것을 처리하는" 접근 방식
class OverEngineeredAgent:
...
비평: LLM은 단순한 로직을 "결정"하고 단순한 템플릿을 "작성"하는 데 사용됩니다. 둘 다 가치가 낮은 LLM 작업입니다.
리팩토링된, 로직 우선 접근 방식(The Refactored, Logic-First Approach)
# ✅ 리팩토링됨: "코드가 결정론적 로직을 처리하는" 접근 방식
import re
...
분석:
- 지연 시간 감소:
_determine_intent메서드는 마이크로초(microsecond) 수준에서 실행됩니다. - 비용 절감: 최종 합성(synthesis)을 위해 LLM을 호출하는 경우만 있습니다. 추가적으로, 희귀한 엣지 케이스인
unknown의도에 대해서만 LLM을 호출합니다. - 신뢰성 증가: 사용자가 "주문 상태 확인"이라고 말하면, 정규 표현식(regex)이 이를 포착합니다. 우리는 그 구절을 도구로 올바르게 매핑하는 것에 LLM에 의존하지 않습니다.
5. 고급 패턴: 메모리 및 컨텍스트 관리
5. 고급 패턴: 메모리 및 컨텍스트 관리
오버 엔지니어링(Over-engineering)은 라우팅을 넘어 상태 관리(state management)까지 확장됩니다. 흔한 실수는 LLM을 데이터베이스에 있어야 할 사실들의 기억 저장소로 취급하는 것입니다.
'무한 컨텍스트' 함정 (The "Infinite Context" Trap)
일부 개발자들은
'무한 컨텍스트' 함정 (The "Infinite Context" Trap)
일부 개발자들은
- 추적 로그(Trace Logs): 코드를 계측하여 모든 결정론적(deterministic) 분기를 기록하도록 합니다.
Log.info("Regex를 통해 'Order'로 라우팅")와 같이 작성합니다. - 프롬프트 주입 테스트(Prompt Injection Testing): 이제 LLM에 구조화된 입력을 사용하므로, LLM의 합성 단계(synthesis step)를 쉽게 단위 테스트할 수 있습니다. 알려진 JSON 입력을 전달하고 출력을 단언(assert)합니다.
- 폴백 모니터(Fallback Monitor):
unknown의도 핸들러가 얼마나 자주 트리거되는지 추적합니다. 이 횟수가 급증한다면, 결정론적 규칙이 실패하고 있다는 의미이므로, 정규 표현식/규칙 엔진에 새로운 케이스를 추가해야 합니다.
"환각 확인(Hallucination Check)"
라우팅을 위해 코드를 사용하기 때문에, 이제 사용자에게 도달하기 전에 LLM의 출력을 근거 진실 데이터(ground truth data)와 비교하는 엄격한 검증 계층을 구현할 수 있습니다. 모델에게
반복 횟수를 세는 대신, 에이전트 계획의 현재 상태와 이전 상태 간의 코사인 유사도(cosine similarity)를 계산합니다. 만약 이 유사도가 두 단계 연속으로 임계값(예: 0.95)을 초과하면, 에이전트가 막혔다고 가정합니다.
import numpy as np
from sentence_transformers import SentenceTransformer
...
회로가 끊어졌을 때, 단순히 에이전트를 종료해서는 안 됩니다. 대신, 모델에게 왜 막혔는지 성찰하도록 요청하는 '메타 프롬프트(Meta-Prompt)'를 주입해야 합니다.
"당신은 동일한 계획 단계를 세 번 반복했습니다. 멈추세요. 이전 시도들을 분석하세요. 진행을 방해하는 특정 제약 조건이나 오류를 파악하고, 근본적으로 다른 접근 방식을 제안하세요."
이는 모델이 '실행 모드(execution mode)'에서 '진단 모드(diagnostic mode)'로 전환하도록 강제하며, 이 과정에서 루프를 유발했던 숨겨진 오해를 밝혀내는 경우가 많습니다.
깊게 중첩된 프롬프트 체인 리팩토링하기
LLM 시스템에서 기술 부채(technical debt)의 가장 큰 원인은 '프롬프트 사다리(Prompt Ladder)'입니다. 이는 각 단계가 이전 단계의 출력에 의존하는, 깊게 중첩된 프롬프트 순서이며, 컨텍스트 윈도우(context window)는 중간 추론 과정으로 가득 차게 됩니다.
다음과 같은 안티 패턴(anti-pattern)을 고려해 보세요:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기