데모가 끝난 커스텀 AI 에이전트의 호출 루프와 토큰 비용 통제 방안
28 de julio de 2026
0
컴퓨터/소프트웨어Comments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
프레임워크를 가져와 사내 데이터로 AI 에이전트 만드는 일은 프로토타입 단계까지 순조롭습니다. 진짜 문제는 이 코드를 실서비스에 올리는 순간 시작됩니다. 화면 안에서 잘 동작하던 에이전트가 예상치 못한 입력 하나에 갇혀 호출을 무한 반복하고, 주말 사이에 수백만 원의 API 비용이 청구되기도 합니다.
현장에서 일어나는 장애의 상당수는 LLM 모델 자체의 한계보다 상태 관리 실패와 외곽 제어 부재 때문에 발생합니다. 경영진 지시로 커스텀 툴을 만들었지만, 정작 하루 종일 뜬금없이 터지는 에이전트 환각을 디버깅하고 쏟아지는 토큰 영수증을 처리하느라 정작 본업을 못 하는 상황을 끝내야 합니다.
데모 환경을 벗어난 에이전트는 블랙박스 오퍼레이션, 비결정적 루프, 데이터 유출 위험이라는 세 가지 벽에 부딪힙니다.
추론(Chain-of-Thought) 과정, 외부 도구 호출(Tool Call), 벡터 데이터베이스 검색 단계별 입출력이 로그에 남지 않으면 문제가 생겨도 원인을 찾을 수 없습니다. 어디서 프롬프트가 꼬였는지 추적하는 데만 꼬박 하루가 날아갑니다.
더 심각한 건 무한 루프입니다. ReAct(Reasoning + Acting) 구조를 가진 에이전트는 도구 실행 결과가 모호하면 똑같은 요청을 계속 다시 보냅니다.
┌─────────────────────────────────────────────────────────────────────────┐
│ ReAct Architecture Loop │
│ │
│ ┌────────────┐ User Query ┌────────────┐ Tool Call Request │
│ │ User │ ──────────────> │ Main LLM │ ────────────────────┐ │
│ └────────────┘ └────────────┘ │ │
│ ▲ ▼ │
│ │ Observation ┌──────┐ │
│ │ (Ambiguous/Failed) │ Tool │ │
│ └─────────────────────── │ A │ │
│ └──────┘ │
│ * Problem: When Observation fails, LLM retries Tool A endlessly. │
└─────────────────────────────────────────────────────────────────────────┘
종료 조건을 빠져나가지 못하고 동일한 인자로 API를 계속 때리는 순간, 몇 분 만에 한 달 치 토큰 예산이 고갈됩니다. 주 에이전트와 하위 에이전트가 서로를 호출하며 재시도하는 연쇄 실패는 전체 시스템 장애 원인의 30% 이상을 차지합니다.
대화 이력이 계속 쌓여 컨텍스트 윈도우가 가득 차면 모델 성능 자체가 떨어지는 문제도 생깁니다. 여기에 파이썬 코드 안에 문자열로 하드코딩한 프롬프트는 단순 문구 하나 수정할 때도 전체 시스템을 다시 빌드하고 배포하게 만듭니다.
로컬 디버깅 솔루션과 프로덕션 관측 툴을 분리해서 수집 포인트를 잡아내야 합니다. 개발 단계에서는 OpenTelemetry 기반의 Arize Phoenix로 RAG 임베딩 품질과 도구 호출을 확인합니다. 실서비스 환경에서는 ClickHouse를 백엔드로 쓰는 Langfuse를 붙여 트레이스 로그와 토큰 소모량을 실시간으로 모니터링합니다.
┌─────────────────────────────────────────────────────────────────────────┐
│ Semantic Caching & Routing Flow │
│ │
│ Client Query │
│ │ │
│ ▼ │
│ ┌───────────┐ Similarity >= 0.92? ┌─────────────────────────────┐ │
│ │ Redis Vector │ ─────────────────────────> │ Return Cached Response │ │
│ │ Cache │ (Cache Hit) │ (Latency -88%, Cost -86%) │ │
│ └───────────┘ └─────────────────────────────┘ │
│ │ │
│ │ (Cache Miss) │
│ ▼ │
│ ┌───────────┐ Execution & Save Cache ┌─────────────────────────────┐ │
│ │ External │ ────────────────────────> │ Store Result as Vector in │ │
│ │ LLM API │ │ Backend Database │ │
│ └───────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
똑같은 질의가 반복되면서 생기는 API 비용은 시맨틱 캐싱(Semantic Caching) 레이어로 막습니다.
AWS가 발표한 60,000건의 질의 분석 데이터에 따르면, 코사인 유사도 기준을 적용한 시맨틱 캐시를 뒀을 때 LLM 추론 비용이 최대 86% 줄고 응답 지연 시간도 88% 개선됐습니다. LMSYS의 RouteLLM 프레임워크도 질문 난이도에 따라 고비용 모델과 저비용 모델로 요청을 나누어 85%의 비용을 절감했습니다.
시맨틱 캐싱은 다음 순서로 구축합니다.
무한 루프를 근본적으로 차단하려면 에이전트 상태를 검증하는 서킷 브레이커 패턴을 파이프라인 제어 흐름에 직접 집어넣어야 합니다. 프레임워크가 제공하는 기본 재시도 옵션에 의존하면 예외가 터지면서 서비스 전체가 죽습니다. 도구 이름과 인자 값을 SHA-256으로 해싱해서 리스트에 넣고, 같은 해시가 3번 연속 쌓이면 강제로 다른 노드로 튕겨내야 안전합니다.
import hashlib
from typing import TypedDict, Annotated, List
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
steps: int
tool_hashes: List[str]
def hash_tool_call(tool_name: str, tool_args: str) -> str:
raw_str = f"{tool_name}:{tool_args}"
return hashlib.sha256(raw_str.encode('utf-8')).hexdigest()
def agent_circuit_breaker_router(state: AgentState) -> str:
# 1. 전체 실행 횟수가 5회를 넘어가면 정해진 퇴로 노드로 이동
if state["steps"] > 5:
return "fallback_graceful_node"
# 2. 동일한 Tool과 Argument를 3회 연속 호출하면 즉시 차단
hashes = state.get("tool_hashes", [])
if len(hashes) >= 3 and hashes[-1] == hashes[-2] == hashes[-3]:
return "fallback_graceful_node"
# 3. 정상 종료 키워드 확인
last_message = state["messages"][-1]
if "FINAL_ANSWER" in last_message.content:
return "end"
return "continue_tools"
LLM의 비결정적 출력 상태와 상관없이 파이썬 실행 환경에서 이 라우터가 조건문을 검증하므로 환각에 따른 루프 진입을 물리적으로 막아냅니다.
코드 안에 프롬프트를 방치하면 모델 수정 후 기존에 잘 되던 기능까지 먹통이 되는 회귀 장애(Regression Failure)를 잡을 수 없습니다. 프롬프트는 파이썬 코드 밖으로 빼내 독립된 YAML 파일로 관리합니다.
name: "agent_reasoning"
version: "1.2.0"
model: "gpt-4o"
temperature: 0.1
messages:
- role: "system"
content: |
당신은 사내 데이터베이스 분석 에이전트입니다.
지정된 JSON 스키마에 맞춰 답변해야 하며, 정보가 부족할 경우 추가 질문을 생성하십시오.
- role: "user"
content: "{{ user_input }}"
이렇게 분리한 프롬프트는 오픈소스 평가 프레임워크인 DeepEval과 Pytest를 묶어 CI/CD 파이프라인에서 자동으로 테스트합니다. G-Eval 지표를 기준점으로 잡아 두고 배포 전에 모델 응답 수치를 검증합니다.
import pytest
from deepeval import assert_test
from deepeval.metrics import GEval, TaskCompletionMetric
from deepeval.test_case import LLMTestCase, SingleTurnParams
# 1. 평가 기준 정의
correctness_metric = GEval(
name="정확성 및 스키마 준수",
criteria="LLM 응답이 질문에 정확히 답변하고, 요청된 JSON 형식을 완벽히 준수하는가?",
evaluation_params=[SingleTurnParams.ACTUAL_OUTPUT, SingleTurnParams.EXPECTED_OUTPUT],
threshold=0.7
)
# 2. Pytest 기반 회귀 테스트
@pytest.mark.parametrize(
"user_input, expected_output",
[
("2024년 1분기 매출 데이터를 요약해줘.", "1분기 총 매출은 50억 원입니다."),
("퇴직금 계산 규정을 알려줘.", "퇴직금은 근속연수 1년에 대해 30일분 이상의 평균임금입니다.")
]
)
def test_agent_regression(user_input, expected_output):
actual_output = run_in_house_agent(user_input)
test_case = LLMTestCase(
input=user_input,
actual_output=actual_output,
expected_output=expected_output
)
# 설정한 기준 점수 미달 시 빌드 중단
assert_test(test_case, [correctness_metric, TaskCompletionMetric(threshold=0.8)])
평가 체계는 세 단계로 연동합니다.
deepeval test run 명령어가 자동으로 실행되도록 액션을 구성합니다.name: AI Agent Evaluation Gate
on:
pull_request:
branches: [ main ]
jobs:
eval-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Dependencies
run: |
pip install poetry
poetry install
- name: Run DeepEval Suite
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
poetry run deepeval test run tests/test_evals.py
사내 데이터 유출을 막으려면 API 요청이 외부로 나가기 전에 개인정보(PII)를 비식별화하는 가드레일이 필요합니다. 게이트웨이에 Microsoft Presidio 엔진을 걸어두면 주민등록번호, 이메일, 전화번호, 사번을 알아서 마스킹합니다.
from presidio_analyzer import AnalyzerEngine, PatternRecognizer
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
# 1. Analyzer Engine 및 사번 규칙 등록
analyzer = AnalyzerEngine()
employee_id_recognizer = PatternRecognizer(
supported_entity="EMPLOYEE_ID",
regex="EMP-[0-9]{6}",
score=0.95
)
analyzer.registry.add_recognizer(employee_id_recognizer)
# 2. Anonymizer Engine 초기화
anonymizer = AnonymizerEngine()
def sanitize_user_prompt(raw_prompt: str) -> str:
results = analyzer.analyze(
text=raw_prompt,
entities=["PERSON", "PHONE_NUMBER", "EMAIL_ADDRESS", "EMPLOYEE_ID"],
language="en"
)
anonymized_result = anonymizer.anonymize(
text=raw_prompt,
analyzer_results=results,
operators={
"DEFAULT": OperatorConfig("replace", {"new_value": "<REDACTED>"}),
"EMPLOYEE_ID": OperatorConfig("mask", {"chars_to_mask": 6, "masking_char": "*", "from_end": True})
}
)
return anonymized_result.text
RAG 검색을 붙일 때도 사용자 권한 레벨에 맞는 문서만 끌어오도록 조치해야 합니다. Qdrant나 Pinecone 같은 벡터 데이터베이스는 유사도 검색을 할 때 메타데이터 필터링(Metadata Filtering)으로 권한 구분을 처리합니다.
from qdrant_client import QdrantClient
from qdrant_client.http import models
client = QdrantClient(host="localhost", port=6333)
def search_documents_with_rbac(query_vector: list, user_department: str, user_clearance_level: int):
search_result = client.search(
collection_name="enterprise_knowledge_base",
query_vector=query_vector,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="department",
match=models.MatchValue(value=user_department)
),
models.FieldCondition(
key="security_level",
range=models.Range(lte=user_clearance_level)
)
]
),
limit=5
)
return search_result
프롬프트 인젝션 방어는 시스템 프롬프트 채널과 사용자 입력 채널을 완전히 떼어놓는 게 기본입니다. 데이터베이스를 수정하거나 외부 API를 호출하는 위험한 작업은 사람이 직접 승인하는 절차(Human-in-the-Loop)를 거치게 만들거나 격리된 샌드박스 내부로 실행 권한을 묶어야 안전합니다.
SaaS 모듈을 대체해 사내 시스템으로 계속 운영하려면 주기적인 점검 루틴이 필요합니다.
| 주기 | 운영 점검 항목 | 세부 작업 |
|---|---|---|
| 매일 | 에러율 및 토큰 사용량 | Langfuse 대시보드에서 HTTP 5xx 에러율과 부서별 토큰 소모량 확인 |
| 매일 | 서킷 브레이커 차단 이력 | 루프 감지로 끊긴 세션의 도구 이름과 인자 패턴 수집 후 수정 |
| 매주 | RAG 검색 실패 질의 추출 | 유사도 점수 0.6 미만 요청을 골라내 테스트 정답 집합에 추가 |
| 매주 | 개인정보 마스킹 오탐 검증 | Presidio 처리 로그를 표본 추출해 마스킹 누락 여부 확인 |
| 매월 | CI/CD 평가 기준 업데이트 | 비즈니스 변경 사항을 반영해 자동 테스트 케이스 수정 |
자체 모델 서빙(vLLM 기반)과 외부 API 구독을 고민 중이라면 일일 트래픽을 기준으로 계산해봅니다.
NVIDIA A10G GPU 1장이 들어간 AWS EC2 g5.2xlarge 인스턴스의 월 이용료는 약 $880입니다. 반면 GPT-4o API 단가는 입력 100만 토큰당 $2.50, 출력 100만 토큰당 $10.00 수준입니다. 일일 트래픽이 5,000만 토큰 미만이라면 데브옵스 엔지니어의 관리 공수와 GPU 고정비를 고려했을 때 API 구독 방식이 유리합니다. 하루 트래픽이 1억 토큰을 넘어서거나 완벽한 사내망 격리가 필수일 때 비로소 vLLM 기반 자체 서빙으로 넘어가는 게 맞습니다.
메인 모델 장애에 대비하려면 4단계 백업 라우팅 구조를 만듭니다.
┌─────────────────────────────────────────────────────────────────────────┐
│ 4-Tier Graceful Degradation Architecture │
│ │
│ [Tier 1] Primary High-Performance Model (e.g., GPT-4o) │
│ │ │
│ ▼ (API Failure / Timeout / Circuit Breaker) │
│ [Tier 2] Lightweight Routing Model (e.g., GPT-4o-mini / On-Prem vLLM) │
│ │ │
│ ▼ (Continuous Outage) │
│ [Tier 3] Deterministic Regex & SQL Rule Engine │
│ │ │
│ ▼ (Unrecoverable Error) │
│ [Tier 4] Static Error Message & Async Admin Ticket Generation │
└─────────────────────────────────────────────────────────────────────────┘
커스텀 에이전트 시스템을 다듬는 작업은 90일 단위로 끊어 진행합니다.
| 기간 | 적용 목표 | 구체적 작업 내용 |
|---|---|---|
| 1~30일차 | 가시성 및 데이터 보호 | Presidio 마스킹 엔진 설치 및 Langfuse 로깅 SDK 이식 |
| 31~60일차 | 루프 차단 및 비용 절감 | Redis 시맨틱 캐싱 적용 및 파이썬 기반 서킷 브레이커 연결 |
| 61~90일차 | 검증 자동화 및 권한 제어 | 프롬프트 Git 관리, DeepEval CI/CD 연동, Qdrant RBAC 메타데이터 적용 |
첫 달에는 개인정보 유출을 막고 모든 요청이 로그에 남도록 관측 솔루션을 세웁니다. 두 번째 달에는 반복 프롬프트 캐싱과 루프 차단 라우터를 붙여 예상치 못한 비용 지출을 잘라냅니다. 마지막 달에 프롬프트 버전 관리와 자동 테스트 평가 체계를 완성하면 배포 때마다 생기는 에이전트 오작동 염려 없이 안정적인 운영이 가능해집니다.