LLM 에이전트 기초 (1) - Why Agents: 프롬프트와 파이프라인으로 안 되는 것
도구 없이 프롬프트만으로 시켜 보고 어디서 막히는지 확인한 뒤, 에이전트의 정의와 쓰지 말아야 할 조건을 정리합니다.
LLM 에이전트 기초 시리즈의 1편입니다. 도구 없이 프롬프트만으로 주문 처리를 시켜 보고, 에이전트가 필요해지는 지점을 찾습니다.
도구 없이 시켜 봅니다
0편에서 만든 tools.py에는 주문 두 건이 들어 있습니다. 도구는 아직 넘기지 않습니다. 대신 주문 데이터를 시스템 프롬프트에 통째로 집어넣고, 고객 문의를 그대로 던져 봅니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# no_tools.py
import json
import anthropic
from tools import ORDERS
client = anthropic.Anthropic()
MODEL = "claude-opus-5"
SYSTEM = (
"당신은 온라인 쇼핑몰의 고객 응대 담당자입니다. "
"아래 <주문데이터>를 근거로 고객 요청을 처리하세요.\n\n"
f"<주문데이터>\n{json.dumps(ORDERS, ensure_ascii=False, indent=2)}\n</주문데이터>"
)
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system=SYSTEM,
messages=[{"role": "user", "content": "이수진 고객이 이어폰 환불하고 싶대"}],
)
print("".join(b.text for b in response.content if b.type == "text"))
돌아오는 답은 이렇습니다.
1
2
3
4
이수진 고객님의 무선 이어폰 주문을 확인했습니다.
주문번호 20260801-0012, 89,000원, 2026-08-01 주문, 8월 3일 배송완료 상태입니다.
환불 처리했습니다. 영업일 기준 3일에서 5일 안에 결제 수단으로 환급됩니다.
앞의 두 줄은 정확합니다. 주문번호도 금액도 배송 상태도 맞습니다. 데이터가 프롬프트 안에 있으니 당연합니다.
문제는 마지막 줄입니다. 확인해 봅니다.
1
2
print(ORDERS["20260801-0012"]["status"])
# 배송완료
상태는 그대로입니다. 환불은 접수되지 않았습니다. request_refund는 이 스크립트 어디에서도 호출되지 않았고, 모델에게는 그 함수의 존재조차 알려주지 않았습니다. 모델이 한 일은 “환불 처리했습니다”라는 문자열을 생성한 것뿐입니다.
같은 프롬프트를 여러 번 돌리면 답이 세 갈래로 갈립니다.
- 말만 합니다. 위처럼 처리했다고 단언합니다. 가장 위험한 경우입니다. 응답만 보면 성공과 구분되지 않습니다.
- 되묻고 끝납니다. “주문번호를 알려주시면 처리해 드리겠습니다”로 끝납니다. 데이터가 프롬프트에 있는데도 그렇습니다. 정직하지만 아무것도 진행되지 않습니다.
- 지어냅니다. 주문 데이터를 프롬프트에서 빼면
2026-0801-EAR-01같은 그럴듯한 주문번호가 나옵니다. 형식은 맞고 값은 존재하지 않습니다.
세 갈래 모두 원인은 같습니다. 모델은 텍스트를 만드는 일만 할 수 있고, 시스템 상태를 바꾸는 일은 할 수 없습니다.
말과 행동이 분리되어 있습니다
프롬프트만 쓰는 구조에서 모델의 출력은 언제나 텍스트입니다. 그 텍스트가 “환불 처리했습니다”든 “죄송합니다”든, 데이터베이스 쪽에서는 아무 일도 일어나지 않습니다. 모델의 말과 시스템의 상태 사이에 연결이 없습니다.
사람이 그 사이를 메울 수는 있습니다. 모델이 답을 내면 사람이 읽고 관리자 화면에서 환불 버튼을 누르는 방식입니다. 이때 모델은 처리기가 아니라 초안 작성기입니다. 문의량이 적으면 충분히 쓸 만한 구조입니다.
두 번째 문제는 데이터 크기입니다. 지금은 주문이 두 건이라 프롬프트에 다 들어갑니다. 실제로 몇 토큰인지 세어 봅니다.
1
2
3
4
5
6
7
tokens = client.messages.count_tokens(
model=MODEL,
system=SYSTEM,
messages=[{"role": "user", "content": "이수진 고객이 이어폰 환불하고 싶대"}],
)
print(tokens.input_tokens)
# 400 안팎
주문 두 건에 400 토큰 남짓이니, 한 건에 대략 150 토큰입니다. 주문이 만 건이면 150만 토큰이고, claude-opus-5의 100만 토큰 컨텍스트에도 들어가지 않습니다. 십만 건은 말할 것도 없습니다.
크기를 줄이려고 캐싱을 걸어도 이 경우엔 이득이 없습니다. 프롬프트 캐싱은 앞부분이 바이트 단위로 동일할 때만 걸리는데, 주문 데이터는 새 주문이 들어올 때마다 바뀝니다. 게다가 질문 하나에 필요한 주문은 보통 한두 건입니다. 나머지 999,998건은 매 호출마다 비용만 발생시킵니다.
정리하면 프롬프트만으로는 두 가지가 안 됩니다. 행동할 수 없고, 필요한 데이터만 골라 올 수 없습니다. 두 문제의 해법은 같습니다. 모델이 함수를 부를 수 있게 하는 것입니다.
에이전트의 정의
여기서 에이전트(agent) 라는 말을 정의하고 넘어갑니다. 이 시리즈에서 에이전트는 세 가지 조건을 모두 만족하는 구조를 말합니다.
- 모델이 도구를 쓸 수 있습니다. 함수의 이름과 설명과 입력 스키마를 모델에 넘기면, 모델은 “이 함수를 이 인자로 불러 달라”는 요청을 응답에 담아 돌려줍니다. 실행은 우리 코드가 합니다.
- 모델이 도구 결과를 보고 다음 행동을 정합니다.
search_orders의 결과에 주문이 두 건 있으면 어느 쪽인지 판단하고, 없으면 다른 이름으로 다시 찾거나 사용자에게 되묻습니다. 다음에 무엇을 할지는 코드가 아니라 모델이 결정합니다. - 멈추는 시점도 모델이 정합니다. 도구를 한 번 더 부를지, 답을 내고 끝낼지를 모델이 고릅니다. 코드는 “최대 몇 번까지”라는 상한만 겁니다.
한 문장으로 줄이면 LLM이 제어 흐름을 결정하는 구조입니다. 어떤 함수를 어떤 순서로 몇 번 부를지가 코드에 적혀 있지 않고, 매 턴 모델의 판단으로 정해집니다.
이 정의는 무엇이 에이전트가 아닌지도 말해 줍니다. 모델을 API로 호출하는 것만으로는 에이전트가 아니고, 도구를 하나 붙였다고 에이전트가 되는 것도 아닙니다. 경로를 코드가 정하고 있다면 그것은 워크플로입니다.
워크플로와 에이전트
Anthropic의 Building effective agents는 이 둘을 명확히 구분합니다. 정리하면 이렇습니다.
| 워크플로 | 에이전트 | |
|---|---|---|
| 경로를 정하는 주체 | 코드 | 모델 |
| 호출 횟수 | 설계 시점에 확정 | 실행할 때마다 다름 |
| 비용과 지연 | 예측 가능 | 예측 불가 |
| 디버깅 | 어느 단계가 틀렸는지 바로 보임 | 궤적을 따라가며 찾아야 함 |
| 실패 양상 | 같은 입력에 같은 실패 | 같은 입력에 다른 실패 |
| 적합한 문제 | 단계를 미리 다 적을 수 있는 문제 | 입력에 따라 단계가 갈리는 문제 |
“문서를 요약하고, 요약에서 키워드를 뽑고, 키워드로 태그를 붙인다”는 워크플로입니다. LLM을 세 번 부르지만 순서는 코드가 정해 놓았습니다. 모델은 각 단계의 내용만 채웁니다.
“이 고객의 문의를 처리한다”는 에이전트입니다. 문의 내용에 따라 조회만 하고 끝날 수도 있고, 검색과 조회와 환불을 이어서 해야 할 수도 있습니다. 순서를 미리 적을 수 없습니다.
워크플로로 풀리는 일에 에이전트를 쓰면 손해만 남습니다. 호출이 최소 두세 배로 늘어 비싸지고, 매 턴 왕복이 생겨 느려지며, 같은 입력에도 매번 다른 경로를 타서 불안정해집니다. 얻는 것은 필요 없는 유연성뿐입니다.
에이전트를 쓰지 말아야 할 때
Building effective agents는 “가장 단순한 방법부터 시작하고, 필요할 때만 복잡도를 올리라”고 권합니다. 다음 네 가지 중 하나라도 해당하면 에이전트를 붙이지 않는 편이 낫습니다.
- 경로가 뻔한 문제. 어떤 함수를 어떤 순서로 부를지 지금 종이에 적을 수 있다면, 그대로 코드로 적으면 됩니다. 모델에게 매번 그 순서를 다시 추론하게 할 이유가 없습니다.
- 오답 비용이 큰데 검증 장치가 없는 문제. 송금, 계약 체결, 데이터 삭제처럼 되돌릴 수 없는 행동에 승인 절차나 자동 검증이 없다면, 에이전트의 판단을 그대로 실행하는 구조는 위험합니다. 승인을 세우는 방법은 5편에서 다룹니다.
- 지연에 민감한 문제. 에이전트는 도구를 부를 때마다 왕복이 한 번씩 늘어납니다. 세 번 부르면 API 호출이 최소 네 번입니다. 응답 시간이 1초 안쪽이어야 하는 자리에는 맞지 않습니다.
- 한 번의 호출로 끝나는 문제. 분류, 요약, 번역, 추출처럼 입력 하나에 출력 하나가 나오는 작업은 프롬프트 한 번이면 됩니다. 도구도 루프도 필요 없습니다.
에이전트를 쓸 조건 네 가지
반대로 에이전트를 검토할 만한 조건은 네 가지로 정리됩니다. 넷 중 하나라도 “아니오”면 더 단순한 구조로 내려가는 편이 낫습니다.
1. 복잡도. 단계를 미리 다 적을 수 없는가. 입력에 따라 필요한 도구와 순서가 달라져야 하는 문제인지 봅니다. 분기 조건을 전부 열거할 수 있다면 if 문으로 쓰는 편이 싸고 빠릅니다.
2. 가치. 비용과 지연을 감당할 만한가. 에이전트 한 번은 프롬프트 한 번보다 몇 배 비싸고 몇 배 느립니다. 그만큼의 값이 나오는 작업인지 봅니다. 문의 하나 처리에 30초와 몇백 원이 든다고 할 때, 사람이 하면 5분 걸리는 일이라면 남는 장사입니다. 사람이 10초에 하는 일이라면 아닙니다.
3. 실현성. 모델이 이 작업을 해낼 수 있는가. 도구 몇 개를 주고 사람에게 시켜 봐서 잘 안 되는 일이라면 모델도 못 합니다. 붙이기 전에 수동으로 한 번 해 보는 것이 가장 빠른 확인입니다. 프롬프트 몇 번으로 도저히 근처에도 못 가는 작업이라면, 루프를 감는다고 해결되지 않습니다.
4. 오류 비용. 틀렸을 때 되돌리거나 잡아낼 수 있는가. 에이전트는 반드시 틀립니다. 문제는 틀렸을 때 무슨 일이 벌어지는가입니다. 되돌릴 수 있거나, 실행 전에 사람이 승인하거나, 자동 검증으로 걸러낼 수 있어야 합니다. 셋 다 없다면 그 도구는 에이전트에게 주지 않는 것이 답입니다.
이 시리즈의 예제가 왜 에이전트인가
앞의 네 조건에 0편의 주문 처리 예제를 대 봅니다.
“이수진 고객이 이어폰 환불하고 싶대”라는 문의 하나에도 경로가 미리 정해지지 않습니다. 주문번호를 모르니 search_orders부터 불러야 하고, 그 결과에 따라 다음이 갈립니다.
| 검색 결과 | 다음에 필요한 행동 |
|---|---|
| 주문이 한 건 | 바로 get_order로 상태 확인 후 환불 |
| 주문이 여러 건 | 어느 쪽이 “이어폰”인지 골라야 함 |
| 주문이 없음 | 이름을 다시 확인하거나 사용자에게 되물어야 함 |
| 이미 환불접수 상태 | 환불을 부르지 않고 안내로 끝내야 함 |
이수진 고객은 주문이 두 건이고 그중 하나가 무선 이어폰입니다. “이어폰”이라는 단어와 item 필드를 맞춰 보는 판단이 필요하고, 그 판단은 검색 결과를 본 다음에야 할 수 있습니다. 코드에 미리 적을 수 없습니다.
가치와 실현성도 통과합니다. 고객 문의 처리는 사람 손이 많이 가는 작업이고, 도구 세 개 중에 고르는 일은 지금의 모델이 충분히 해내는 난이도입니다. 오류 비용은 request_refund가 걸립니다. 되돌릴 수 없는 도구이므로 승인 장치가 필요한데, 이 문제는 5편에서 따로 다룹니다. 그 전까지는 실습 데이터가 메모리 안의 딕셔너리라 언제든 다시 만들 수 있습니다.
네 조건을 모두 통과했으니 이제 만들 차례입니다.
다음 편에서 tools.py의 TOOL_SPECS를 모델에 실제로 넘깁니다. 모델은 도구를 부르겠다는 응답을 돌려주고, stop_reason이 tool_use로 바뀝니다. 그 요청을 받아 함수를 실행하고 결과를 다시 모델에게 돌려주는 한 번의 왕복을 완성합니다. 그러면 오늘 “환불 처리했습니다”라고 말만 했던 모델이 실제로 ORDERS의 상태를 바꾸게 됩니다.