본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 03. 14:28

AI 에이전트의 숨겨진 비용: TypeScript에서의 토큰, 도구 호출(Tool Calls) 및 재시도(Retries) 추적

요약

AI 에이전트 운영 시 발생하는 숨겨진 비용 문제를 TypeScript 프로젝트를 통해 분석합니다. 단순 로그를 넘어 도구 호출, 재시도, 라우팅 등 에이전트 내부의 복잡한 상호작용이 어떻게 LLM 지출을 증가시키는지 다룹니다.

핵심 포인트

  • 에이전트의 작은 결정들이 모여 막대한 비용을 발생시킴
  • 단순 로그만으로는 도구 호출과 모델 호출의 비용 차이를 파악하기 어려움
  • 재시도 및 후처리 단계가 비용 가시성을 저해하는 주요 원인임
  • 비용 인식형(Cost-aware) 에이전트 설계의 중요성 강조

AI 에이전트는 한꺼번에 비용이 많이 발생하는 것이 아닙니다.

한 번에 하나씩, 작은 결정들이 모여 비용을 발생시킵니다.

추가적인 라우팅 호출 하나. 하나의 신뢰도 확인(Confidence check). 하나의 재시도(Retry). 또 다른 LLM 요청을 유발하는 하나의 도구 실패(Tool failure). 첫 설계 당시 더 깔끔하다고 느껴져서 추가된 하나의 포맷팅 에이전트(Formatting agent).

개별적으로 보면 이러한 호출들은 무해해 보입니다. 하지만 이들이 모이면 단순한 고객 지원 요청을 모델 호출, 도구 실행, 재시도, 그리고 로그만으로는 쉽게 설명할 수 없는 후처리 단계(Post-processing steps)의 사슬로 변질시킬 수 있습니다.

이것이 제가 작은 TypeScript 프로젝트를 통해 탐구하고자 했던 문제입니다. 바로 들어오는 요청을 라우팅하고, 작업을 검증하며, 내부 도구를 호출하고, 최종 응답을 생성하는 비용 인식형(Cost-aware) 고객 지원 에이전트입니다.

목표는 단순히 또 다른 에이전트 데모를 만드는 것이 아니었습니다. 목표는 더 실질적인 엔지니어링 질문에 답하는 것이었습니다.

LLM 지출은 실제로 어디로 가고 있는가?

Article content

에이전트 비용 가시성의 문제점

대부분의 팀은 단순한 로그로 시작합니다.

[10:14:02] RouterAgent: 요청을 ORDER_CHANGE로 분류함
[10:14:03] OrderAgent: 주문 상세 정보를 가져옴
[10:14:05] OrderAgent: 응답을 생성함
...

이것은 유용하지만, 표면적인 수준에 불과합니다.

이 로그는 무언가 일어났다는 사실은 알려줍니다. 하지만 그 상호작용이 얼마나 비용이 많이 들었는지는 알려주지 않습니다. 각 에이전트 내부에서 얼마나 많은 LLM 호출이 발생했는지 보여주지 않습니다. 도구 호출(Tool calls)과 모델 호출(Model calls)을 명확하게 구분하지 않습니다. 재시도(Retries)가 발생했는지 보여주지 않습니다. 에이전트가 더 저렴한 모델, 캐시(Cache), 또는 단순한 규칙으로 처리할 수 있었던 작업을 위해 여러 번의 호출을 수행했는지 여부도 밝혀내지 못합니다.

시스템이 확장될 때, 이것은 실질적인 문제가 됩니다.

전형적인 고객 메시지를 예로 들어보겠습니다:

"주문 번호 #12345의 배송 주소를 변경하고 싶습니다."

표면적으로 이것은 배송 주소 변경이라는 단일하고 좁은 의도 (Intent) 로 보입니다. 제품 백로그 (Product Backlog) 를 살펴보는 개발자라면 이 작업을 빠르고 저렴한 작업으로 추정할 것입니다. 하지만 런타임 (Runtime) 시의 실제 상황은 종종 매우 다릅니다.

에이전트 흐름 (Agent Flow) 의 단순한 버전은 다음과 같을 수 있습니다:

  1. RouterAgent가 요청을 분류 (Classify) 합니다.
  2. OrderAgent가 주문 상세 정보를 가져옵니다.
  3. OrderAgent가 주소 변경 가능 여부를 검증 (Validate) 합니다.
  4. OrderAgent가 확인 메시지를 생성합니다.
  5. ResponseAgent가 최종 응답을 포맷팅합니다.

런타임 동작을 조사하기 전까지는 이 구조가 합리적으로 보입니다.

라우터 (Router) 는 쿼리를 분류하기 위해 한 번의 LLM 호출을 수행하고, 신뢰도 (Confidence) 를 확인하기 위해 또 다른 호출을 하며, 신뢰도가 낮을 경우 세 번째 호출을 할 수도 있습니다. 주문 에이전트 (Order Agent) 는 주문 상태를 데이터베이스에서 명확하게 읽을 수 있는 상황임에도 검증을 위해 LLM을 사용할 수 있습니다. 응답 에이전트 (Response Agent) 는 템플릿 (Template) 으로 생성할 수 있었던 메시지를 단순히 다시 작성하기 위해 또 다른 LLM을 사용할 수도 있습니다.

갑자기, 하나의 고객 지원 요청은 더 이상 하나의 AI 상호작용이 아니게 됩니다. 그것은 하나의 작은 실행 그래프 (Execution Graph) 이며, 그 그래프의 모든 엣지 (Edge) 에는 비용이 부과됩니다.

해당 그래프를 볼 수 없다면, 최적화 (Optimize) 할 수 없습니다. 그리고 개발 단계에서 최적화할 수 없다면, 그 비용은 운영 (Production) 환경에서 조용히 누적됩니다.

Article content

비용 인지적 에이전트 워크플로우 구축하기

이 프로젝트를 위해 저는 세 가지 기본 아이디어를 중심으로 워크플로우를 구축했습니다.

첫째, 모든 의미 있는 에이전트 단계 (Phase) 에는 이름이 있어야 합니다. 익명 실행 (Anonymous execution) 은 비용 가시성 (Cost visibility) 의 적입니다. 단계에 정체성이 없다면, 해당 비용을 설계 결정 (Design decision) 과 연관 지을 방법이 없습니다.

둘째, 모든 LLM 호출은 단순히 응답 문자열(Response string)뿐만 아니라 토큰 사용량(Token usage), 모델 이름(Model name), 목적(Purpose), 그리고 예상 비용(Estimated cost)을 캡처해야 합니다. 응답 내용(Response content)은 애플리케이션이 관심을 갖는 부분입니다. 메타데이터(Metadata)는 엔지니어링 측면에서 관심을 가져야 할 부분입니다.

셋째, 전체 상호작용은 평면적인 로그(Flat logs)에 흩어져 있는 것이 아니라, 하나의 실행 트리(Execution tree)로서 검사 가능해야 합니다. 트리는 에이전트(Agents), 도구 호출(Tool calls), 그리고 모델 호출(Model calls) 사이의 부모-자식 관계(Parent-child relationships)를 보존합니다. 이를 통해 재시도(Retries)를 미스터리한 현상이 아닌, 형제 관계(Siblings)로 명확히 볼 수 있게 해줍니다.

다음은 이러한 아이디어들을 실제로 적용한 LLM 클라이언트의 간소화된 래퍼(Wrapper)입니다:

import { step } from "agent-inspect";

import { OpenAI } from "openai";
...

가격 책정에 관한 참고 사항: MODEL_RATES에 포함된 정확한 요율은 항상 제공업체의 최신 가격 페이지를 기준으로 해야 합니다. 모델 가격은 빈번하게 변경되며, 때로는 눈에 띄는 공지 없이 변경되기도 합니다. 중요한 점은 패턴입니다. 즉, 모든 LLM 호출은 콘텐츠와 비용을 이해하는 데 필요한 메타데이터를 모두 반환해야 한다는 것입니다. 가격표는 단지 설정(Configuration)의 문제입니다.

step.llm() 경계는 단순히 호출을 감싸는 것 이상의 역할을 합니다. 이는 모델 호출(Model invocation)을 더 큰 에이전트 실행 트리(Agent execution tree) 내부의 이름이 지정된 구조화된 노드(Structured node)로 만듭니다. 이는 나중에 "어떤 에이전트의 어떤 단계에서 이 지출이 발생했는가?"라고 물었을 때 실제 답변을 얻을 수 있음을 의미합니다.

에이전트 실행 래핑하기 (Wrapping the Agent Run)

다음으로, 저는 전체 지원 워크플로(Support workflow)를 inspectRun()으로 감쌌습니다. 이것이 가장 바깥쪽 경계이며, 전체 상호작용에 단일하고 검사 가능한 정체성을 부여하는 컨테이너입니다.

import { inspectRun, step } from "agent-inspect";

type SupportRequest = {
...

이 구조는 대부분의 에이전트 프레임워크(Agent frameworks)가 생략하는 것, 즉 **이름이 지정된 단계(Named phases)를 가진 단일 진입점(Single entry point)**을 강제합니다. step()에 대한 각 호출은 이름이 지정된 경계입니다. 에이전트 내부의 각 step.llm()은 이름이 지정된 모델 호출입니다. 그 결과, 단순한 사건의 나열이 아니라 여러분의 아키텍처(Architecture)를 반영하는 실행 추적(Execution trace)이 만들어집니다.

이 지점에서 이 글의 핵심 아이디어가 구체화됩니다. 코드는 단순히 실패를 디버깅하기 위해 계측(Instrumented)된 것이 아닙니다. 실패가 발생하기 _전_에 비용 동작(Cost behavior)을 이해하기 위해 계측되었습니다. 실행 추적(Trace)은 다음과 같은 질문에 답합니다: 시간은 어디에 쓰였는가? 토큰은 어디로 사라졌는가? 비용이 많이 드는 작업이 올바른 단계에서 수행되었는가?

RouterAgent 문제

프로젝트의 첫 번째 버전에서는 라우터(Router)가 너무 많은 일을 하고 있었습니다.

class RouterAgent {

  constructor(private llm: CostAwareLLMClient) {}
...

처음에는 이것이 합리적으로 보였습니다. 라우터가 신중하게 동작하고 있었기 때문입니다. 라우터는 분류(Classify)하고, 검증(Verify)한 다음, 불확실할 때는 재분류(Reclassify)했습니다. 이러한 패턴은 에이전트 설계에서 흔히 볼 수 있습니다. 신뢰도 임계값(Confidence thresholds)을 설정하는 것은 시스템이 스스로의 작업을 점검하는 것처럼 책임감 있게 느껴집니다.

하지만 신중함이 항상 저렴한 것은 아닙니다. 그리고 이 경우, '신중함'은 더 심각한 문제를 일으키고 있었습니다. 바로 쉬운 요청을 포함한 모든 요청에 비용을 앞당겨 부과(Front-loading cost)하고 있었다는 점입니다.

모든 요청에 대해 라우터는 고성능 모델(High-capability model)을 사용하여 분류를 수행했습니다. 많은 요청에 대해 라우터는 동일한 모델로 신뢰도 확인(Confidence check)을 한 번 더 수행했습니다. 일부 요청의 경우, 세 번째 호출로서 재분류(Reclassification)를 수행했습니다. 이는 실제 비즈니스 로직이 실행되기도 전에 시스템의 가장 첫 단계에서 요청당 예산의 대부분을 소비할 수 있음을 의미했습니다.

이것은 평면적인 로그(Flat logs)에서는 놓치기 쉬운 종류의 문제입니다. RouterAgent: classified request라고 적힌 로그 한 줄은 라우터가 내부적으로 두 번 또는 세 번의 모델 호출을 수행했다는 사실을 보여주지 않습니다. 그것은 재시도(Retry)를 숨깁니다. 모델 버전(Model version)을 숨깁니다. 비용(Cost)을 숨깁니다. 하지만 실행 트리(Execution tree)는 그렇지 않습니다.

실행 추적(Trace) 조사

몇 가지 샘플 고객 지원 요청을 실행한 후, CLI를 사용하여 로컬 실행 추적을 조사했습니다:

npx agent-inspect list --dir ./.agent-inspect

npx agent-inspect view <run-id> --dir ./.agent-inspect

출력 결과는 구조적인 문제를 즉시 가시화해 주었습니다:

출력 결과는 구조적인 문제를 즉시 가시화해 주었습니다:

support-agent-request                                      [7.8s] ✓
├─ route-request                                           [3.9s] ✓
│  └─ router-agent                                         [3.8s] ✓
...

이 수치들은 명확한 이야기를 들려줍니다. 총 7.8초 중, 단 한 줄의 비즈니스 로직이 실행되기 전에 정확히 절반에 해당하는 3.9초가 소요되었습니다. 의도를 레이블링하는 역할을 하는 라우터(router)가 실제 주문 처리만큼이나 많은 벽시계 시간과 토큰 예산을 소비하고 있었습니다.

또한 이 추적(trace)은 두 가지 부차적인 문제점도 즉시 드러냈습니다:

  • response-agent는 순전히 포맷팅을 위해 모델 호출을 수행하고 있었는데, 이는 모델이 전혀 필요하지 않은 작업입니다.
  • validate-address-change 단계에서는 LLM을 호출했지만, 주문 정보 가져오기(order fetch)가 그보다 먼저 발생했습니다. 이것은 실제로 올바른 순서이지만, 트리 구조에서만 눈에 보일 뿐입니다. 평면적인 로그(flat logs)에서는 결정론적 데이터(deterministic data)를 사용하여 모델 프롬프트를 제약했는지 여부를 알 수 없습니다.

이 추적 덕분에 대화의 내용이 바뀌었습니다. 단순히

여기서는 단순히 정규 표현식(regex) 체크를 추가한 것 이상의 두 가지 변화가 있었습니다.

첫째, LLM 폴백(fallback)이 이제 gpt-4.1 대신 gpt-4.1-mini를 사용합니다. 라우팅(Routing)은 짧고 복잡도가 낮은 분류 작업입니다. 이 작업에 고성능 모델을 실행할 이유는 없습니다. 만약 메시지가 분류하기 정말 어렵더라도, mini 모델은 대부분의 경우 올바르게 분류할 것입니다. 만약 분류에 실패하더라도, 에이전트의 다운스트림(downstream) 에러 핸들링(error handling)이 모든 요청에 비싼 모델을 실행하는 것보다 훨씬 저렴하게 드문 오라우팅(misroute) 사례를 처리할 수 있습니다.

둘째, 신뢰도 확인(confidence-check) 루프가 완전히 사라졌습니다. 모델에게 스스로 확신이 있는지 묻는 대신, 이제 아키텍처(architecture)가 소스 단계에서 신뢰도를 정의합니다. 규칙 기반 매칭(rule-based matches)은 명시적인 신뢰도 점수를 가지며, LLM 폴백은 고정된 낮은 점수를 가집니다. 두 번째 LLM 호출로서 신뢰도를 확인하는 것은 설계 결함(design smell)입니다. 이는 시스템 수준의 결정을 모델의 비용을 들여 모델에게 외주를 주는 방식이기 때문입니다.

이것은 모든 곳에서 AI를 규칙으로 대체하려는 것이 아닙니다. 워크플로우(workflow)의 적절한 시점에 적절한 도구를 사용하는 것에 관한 것입니다. 결정론적 규칙(deterministic rule)이 명백한 사례를 안전하게 라우팅할 수 있다면, 그 결정에 대해 모델 비용을 지불해서는 안 됩니다.

Article content

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0