LLM 에이전트는 하나의 도구만 호출하고 끝내기 때문에 데모에서는 빠르게 작동하는 것처럼 보일 수 있습니다. 하지만 실제 운영 환경(production)에서는 같은 작업이라도 API를 조회하거나, 인증 이메일을 기다리거나, 재시도하거나, 두 번째 검증을 요청해야 할 수 있습니다. 문제는 더 이상 어떤 도구를 선택할지뿐만이 아닙니다. 각 호출이 얼마나 많은 예산을 소모할 수 있는지입니다.
저는 자동화 작업에서 이러한 예산 책정을 눈에 보이는 계약처럼 다루는 것을 선호합니다. 에이전트는 시간 제한, 시도 횟수, 근사 비용 등의 한도를 받게 되며, 도구 어댑터(tool adapter)가 이를 어떻게 적용할지 결정합니다. 이렇게 하면 모델은 무한정 자원을 소모할 수 있는 암묵적인 허가 없이 작업에 대해 추론할 수 있습니다.
문제점: 예산이 없는 도구들
테스트 계정을 만들고 메시지를 기다리는 에이전트를 상상해 보세요. 그 과정은 다음과 같습니다:
모델 → 신원 생성 → 받은 편지함 대기 → 코드 읽기 → 결과 확인
만약 받은 편지함에 시간이 걸린다면, 모델은 다시 질문할 수 있습니다. 서비스가 모호한 오류로 응답하면, 또다시 재시도할 수 있습니다. 사용자 입력에서 fake e mail com과 같은 경우나 temp gamil com처럼 오타가 난 도메인 때문에 플로우 전체가 비용이 많이 드는 호출의 연속으로 변해서는 안 됩니다.
도구별 제한이 없다면 보통 세 가지 증상이 나타납니다:
- 개별 호출은 합리적으로 보이지만, 총 지연 시간(latency)이 증가합니다;
- 재시도가 계약상의 오류를 숨기고 부작용을 두 배로 만듭니다;
- 누구도 비용이 모델에서 발생하는 것인지, 외부 공급자(provider)에서 발생하는 것인지, 아니면 폴링(polling)에서 발생하는 것인지 알지 못합니다.
에이전트는 전역적인 제한이 필요하지만, 로컬적인 제한도 필요합니다. 전체 작업에 60초의 예산이 있다고 해도, 그 몇 초를 조회와 다섯 번의 대기 사이에 어떻게 분배해야 하는지는 설명해주지 않습니다.
계약으로서의 예산
최소한의 계약은 다음과 같이 설명될 수 있습니다:
{
가장 중요한 가치는 숫자가 완벽한 것이 아니라는 점입니다. 실행 전에 선언되어야 하고, 각 어댑터는 예산이 부족해지면 호출을 거부할 수 있어야 합니다. 로컬 타임아웃은 글로벌 데드라인을 존중해야 합니다. 그렇지 않으면 한 분짜리 작업에 세 가지 도구가 각각 60초를 소모할 수 있습니다.
어댑터의 응답 또한 명확해야 합니다: `ok`, `timeout`, `budget_exhausted` 또는 `unknown`. 마지막 옵션은 부수 효과(side effects)가 있는 작업에 중요합니다. 타임아웃이 서버가 아무런 조치를 취하지 않았다는 것을 증명하지는 않습니다.
## 작은 데이터 모델
플로우를 디버깅하기 위해 전체 프롬프트 사본 대신 시도별 영수증을 보관합니다:
{
"run_id": "agent_91a",
"tool_call_id": "call_04",
...
이 데이터를 사용하면 다음과 같은 운영 질문에 답변할 수 있습니다: 조회하기 전에 시간이 부족했는지, 폴링(polling)을 몇 번 시도했는지, 어떤 도구가 예산을 소모했는지. 통합 세부 사항의 경우 에이전트의 흐름과 이메일 계약(email contracts)을 분리하는 것이 좋습니다. 예를 들어, [반복 가능한 테스트를 위한 이메일 계약](https://dev.to/silviutech/fastapi-contratos-de-email-para-pruebas-repetibles-27cn)은 받은 편지함에 유용한 경계가 될 수 있습니다.
또한 제한 사항을 모델의 프롬프트 외부에서 유지합니다. 모델은 “조금 더 기다리라”고 제안할 수 있지만, 런타임(runtime)이 자원이 남아 있는지 여부를 결정해야 합니다. 프롬프트는 가이드이며, 어댑터가 권한입니다.
## 시간 및 재시도 분배 방법
시작점으로 작동하는 세 가지 규칙이 있습니다:
1. **출력에 일부 할당.** 모든 도구가 데드라인을 소모하면, 에이전트는 결과를 설명할 시간이 없습니다.
2. **유효성 검사 오류 재시도 금지.** 유효하지 않은 도메인이나 잘못된 코드는 다른 시도로 개선되지 않습니다.
3. **폴링 점진적 감소.** 200밀리초마다 받은 편지함을 조회하는 것보다 1초, 2초, 4초 후에 조회하는 것이 더 좋습니다.
멱등성(idempotent) 작업의 경우, 재시도 시 동일한 `tool_call_id`를 사용할 수 있습니다. 리소스를 생성하는 작업의 경우, 먼저 상태를 조회하거나 멱등성 키(key)를 전송해야 합니다. 위 두 가지 옵션 중 어느 것도 존재하지 않는다면, 실패했다고 단정하기보다는 `unknown`을 반환하는 것이 더 정직합니다.
간단한 매트릭스는 아키텍처 검토에 도움이 됩니다:
| 도구 | 부작용(Side effect) | 재시도(Retry) | 최소 증거(Minimum evidence) |
| :--- | :--- | :--- |
| 받은 편지함 조회 | 없음 | 제한적 허용 | 메시지 또는 타임아웃 |
| ... |
## 체크포인트를 통한 구현
네 가지 체크포인트에 걸쳐 설계를 구현할 것입니다:
- **한계(Limit):** `deadline_remaining_ms`가 0일 때 호출을 거부합니다.
- **측정(Measurement):** 시작, 종료, 시도 횟수 및 남은 예산을 기록합니다.
- **분류(Classification):** 재시도가 가능한 오류, 최종적 오류, 알 수 없는 오류를 구분합니다.
- **테스트(Test):** 지연 시간(latency), 느린 응답, 유효하지 않은 입력, 중복을 시뮬레이션합니다.
각 체크포인트에는 관찰 가능한 신호가 존재해야 합니다. 호출 카운터는 유용하지만 충분하지 않습니다. 이를 `run_id` 및 도구 이름과 연관시켜야 합니다. 패턴 검토와 기술 게시 예시를 보려면 [DEV.to에서 이메일 테스트](https://dev.to/)를 탐색할 수도 있지만, 실제 구현은 자체 영수증을 유지해야 합니다.
## 최종 체크리스트
- 전역 마감 기한(deadline)과 도구별 타임아웃이 있습니까?
- 어댑터는 실행 전에 남은 예산을 알고 있습니까?
- 입력 오류, 타임아웃, 알 수 없는 결과를 구별합니까?
- 부작용을 가진 작업은 멱등성을 갖습니까?
- 폴링(polling)에는 최대 시도 횟수와 증가하는 간격이 있습니까?
- 각 시도는 민감한 데이터를 저장하지 않는 조회 가능한 영수증을 남깁니까?
- 에이전트는 예산이 소진되었음을 설명할 수 있습니까?
## 결론
신뢰할 수 있는 에이전트는 항상 답변을 얻어낼 때까지 고집하는 것이 아닙니다. 자신이 얼마나 지출할 수 있는지, 어떤 증거를 남겨야 하는지, 그리고 언제 멈춰야 하는지 아는 것입니다. 시간, 시도 횟수, 비용을 작은 계약으로 전환하면 도구들이 더 예측 가능해지고; 또한 시스템의 제어권을 잃지 않으면서 모델을 변경할 수 있게 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46