LLM 에이전트 기초 (0) - Introduction
LLM 에이전트 기초 시리즈 소개와 진행 순서, 실습 환경과 시리즈 내내 쓸 예제 도구 준비를 다룹니다.
모델이 도구를 쓰고 스스로 다음 행동을 정하게 만드는 LLM 에이전트 기초 시리즈의 0편입니다. 시리즈 소개와 진행 순서를 다루며, 개념 정리가 1편, 실습은 2편부터 시작합니다.
무엇을 다루나
프롬프트 한 번으로 끝나는 호출에서 시작해, 모델이 도구를 고르고 결과를 보고 다음 행동을 정하는 반복 구조까지 직접 만듭니다. 도구를 정의해 모델에 넘기는 일, 모델이 요청한 도구를 실행해 결과를 돌려주는 일, 그 왕복을 언제 멈출지 정하는 일, 대화가 길어질 때 컨텍스트를 관리하는 일, 되돌릴 수 없는 행동 앞에 사람의 승인을 세우는 일, 그리고 에이전트가 제대로 동작했는지 채점하는 일까지가 범위입니다.
프레임워크로 세 줄에 끝내는 대신 루프를 직접 짭니다. while 문 안에서 stop_reason을 확인하고 tool_result를 손으로 채워 넣고 나면, 프레임워크의 AgentExecutor가 무엇을 대신해 주는지가 분명해지고 에이전트가 엉뚱한 도구를 부를 때 어디를 봐야 하는지도 보입니다. 같은 구조를 LangGraph로 조립하는 방법은 LangGraph 기초에 따로 있습니다.
누구를 위한 시리즈인가
- 프롬프트로는 풀리지 않는 작업이 생겼는데 에이전트가 답인지 아닌지 판단이 서지 않는 사람
- 에이전트를 붙였는데 도구를 안 부르거나 같은 도구를 무한히 반복해서 원인을 못 찾는 사람
- LangChain으로 만들어는 봤지만 그 안에서 API가 몇 번 호출되고 토큰이 어디서 새는지 설명하지 못하는 사람
Python 기본 문법을 전제로 합니다. LLM API 호출은 프롬프트 엔지니어링 기초 시리즈 1편 수준이면 충분하고, 검색을 붙이는 부분은 RAG 기초 시리즈와 이어지지만 먼저 읽지 않아도 따라올 수 있게 씁니다.
어떻게 진행하는가
아홉 편으로 진행합니다. 각 편은 앞 편에서 만든 코드가 무너지는 지점에서 시작합니다. 프롬프트 하나로 시작해 매 편마다 한계를 하나씩 만나고, 그 한계를 넘는 장치를 붙여 가는 방식입니다.
| 편 | 주제 | 이 편이 끝나면 |
|---|---|---|
| (1) | Why Agents: 프롬프트와 파이프라인으로 안 되는 것 | 이 문제에 에이전트가 필요한지 판단할 수 있다 |
| (2) | Tool Use: 함수를 모델에 넘기고 결과를 돌려주기 | 모델이 도구를 한 번 부르고 답을 낸다 |
| (3) | Agent Loop: 관측, 판단, 행동의 순환과 종료 조건 | 도구를 여러 번 이어 부르고 스스로 멈춘다 |
| (4) | Context: 토큰 예산, 도구 결과 다이어트, 압축 | 스무 턴을 넘겨도 컨텍스트가 터지지 않는다 |
| (5) | Permissions: 되돌릴 수 없는 행동에 사람을 세우기 | 환불은 사람이 승인해야 실행된다 |
| (6) | Planning and Subagents: 작업을 나누고 위임하기 | 주문 여러 건을 병렬로 조사한다 |
| (7) | MCP: 도구를 표준 규격으로 분리하기 | 도구가 에이전트 코드 밖에서 산다 |
| (8) | Evaluation and Operations: 궤적 채점, 비용, 실패 대응 | 프롬프트를 바꿨을 때 좋아졌는지 숫자로 안다 |
세 부로 묶입니다.
1부, 뼈대 세우기 (1편에서 3편). 1편에서 도구 없이 프롬프트만으로 “이수진 고객 환불 처리해 줘”를 시켜 봅니다. 모델은 주문번호를 지어내거나 사용자에게 되묻고 끝나며, 여기서 에이전트가 필요한 조건이 드러납니다. 2편에서 도구 하나를 모델에 넘겨 한 번의 왕복을 완성합니다. stop_reason이 tool_use로 돌아오는 순간, 도구 실행은 우리 몫이고 판단은 모델 몫이라는 분업이 눈에 보입니다. 3편에서 그 왕복을 while로 감습니다. 여기서 에이전트가 완성되지만, 동시에 이 시리즈 후반부의 문제가 전부 생겨납니다. 언제 멈출 것인가, 도구가 실패하면 어떻게 알릴 것인가, 같은 도구를 계속 부르면 누가 끊을 것인가.
2부, 통제하기 (4편에서 6편). 돌아가는 루프를 실제로 쓸 수 있게 만드는 구간입니다. 4편은 대화가 길어지면서 컨텍스트가 부풀어 오르는 문제를 다룹니다. 도구 결과 하나가 수천 토큰이면 열 번만 돌아도 한도에 닿고, 요금은 매 턴 누적됩니다. 5편은 더 위험한 문제입니다. 3편의 루프는 모델이 원하면 request_refund를 그냥 실행합니다. 되돌릴 수 없는 행동 앞에 사람을 세우는 방법과, 어떤 도구에 그 게이트를 걸지 정하는 기준을 만듭니다. 6편에서는 한 에이전트가 감당하기 벅찬 작업을 쪼개 서브에이전트에 맡깁니다. 위임이 왜 대부분의 경우 손해인지, 언제 이득으로 돌아서는지가 이 편의 핵심입니다.
3부, 연결하고 운영하기 (7편과 8편). 7편에서 도구를 MCP 서버로 분리합니다. 도구가 에이전트 코드 안에 박혀 있으면 도구를 고칠 때마다 에이전트를 배포해야 하고, 다른 에이전트가 같은 도구를 쓰려면 복사해야 합니다. 8편은 운영입니다. 에이전트는 같은 입력에도 매번 다른 경로로 답하므로, 최종 답만 보는 평가로는 무엇이 나아졌는지 알 수 없습니다. 도구 호출 궤적을 채점하는 방법, 회귀를 잡는 방법, 비용과 지연을 추적하는 방법으로 마무리합니다.
2편에서 만든 주문 도구를 8편까지 그대로 씁니다. 편이 넘어갈 때 코드를 버리고 새로 시작하지 않고, 같은 파일에 계속 쌓아 올립니다.
실습 환경
Python 3.12 기준입니다.
1
2
3
python3 -m venv .venv
source .venv/bin/activate
pip install -U anthropic mcp
| 패키지 | 쓰는 곳 |
|---|---|
anthropic | 모델 호출과 도구 사용 전 구간 |
mcp | MCP 서버와 클라이언트 (7편) |
API key는 환경변수로 둡니다.
1
export ANTHROPIC_API_KEY="sk-ant-..."
모델은 claude-opus-5를 씁니다. 도구를 여러 개 놓고 어느 것을 부를지 판단하는 작업은 모델의 판단력이 결과를 크게 좌우해서, 저렴한 모델로 시작하면 에이전트 설계 문제인지 모델 한계인지 구분이 어렵습니다. 비용을 줄이는 방법은 8편에서 따로 다룹니다.
예제 도구 만들기
시리즈 내내 쓸 도구 세 개를 만듭니다. RAG 기초 시리즈에서 정책 문서를 다뤘던 그 온라인 쇼핑몰의 주문 데이터를 다루는 도구입니다.
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
# tools.py
ORDERS = {
"20260801-0012": {
"customer": "이수진",
"item": "무선 이어폰",
"price": 89000,
"ordered_at": "2026-08-01",
"status": "배송완료",
"delivered_at": "2026-08-03",
},
"20260803-0447": {
"customer": "이수진",
"item": "노트북 거치대",
"price": 24000,
"ordered_at": "2026-08-03",
"status": "배송중",
"delivered_at": None,
},
}
def search_orders(customer: str) -> list[dict]:
"""고객 이름으로 주문을 찾습니다."""
return [
{"order_id": oid, **o} for oid, o in ORDERS.items() if o["customer"] == customer
]
def get_order(order_id: str) -> dict:
"""주문번호로 주문 하나를 조회합니다."""
if order_id not in ORDERS:
return {"error": f"주문번호 {order_id}를 찾을 수 없습니다."}
return {"order_id": order_id, **ORDERS[order_id]}
def request_refund(order_id: str, reason: str) -> dict:
"""환불을 접수합니다. 되돌릴 수 없는 작업입니다."""
if order_id not in ORDERS:
return {"error": f"주문번호 {order_id}를 찾을 수 없습니다."}
ORDERS[order_id]["status"] = "환불접수"
return {"order_id": order_id, "status": "환불접수", "reason": reason}
모델에게 넘길 때는 함수 자체가 아니라 이름, 설명, 입력 스키마를 JSON으로 적습니다. 모델은 이 설명만 보고 어떤 도구를 언제 부를지 정하므로, 설명이 곧 성능입니다.
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
TOOL_SPECS = [
{
"name": "search_orders",
"description": (
"고객 이름으로 그 사람의 주문 목록을 가져옵니다. "
"주문번호를 모르는 상태에서 주문을 찾아야 할 때 먼저 호출합니다."
),
"input_schema": {
"type": "object",
"properties": {"customer": {"type": "string", "description": "고객 이름"}},
"required": ["customer"],
},
},
{
"name": "get_order",
"description": (
"주문번호로 주문 한 건의 상세 정보를 조회합니다. "
"배송 상태나 결제 금액을 확인해야 할 때 호출합니다."
),
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "주문번호. 예: 20260801-0012"}
},
"required": ["order_id"],
},
},
{
"name": "request_refund",
"description": (
"환불을 접수합니다. 되돌릴 수 없으므로 주문 상태를 먼저 확인하고, "
"고객이 환불을 명시적으로 요청했을 때만 호출합니다."
),
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "환불할 주문번호"},
"reason": {"type": "string", "description": "고객이 밝힌 환불 사유"},
},
"required": ["order_id", "reason"],
},
},
]
도구 세 개지만 에이전트의 어려운 지점이 전부 들어 있습니다. 주문번호를 모르면 search_orders를 먼저 부르고 get_order로 넘어가야 하니 순서가 있는 도구 호출이 필요하고, request_refund는 되돌릴 수 없어서 모델의 판단을 어디까지 믿을지 정해야 하며, 없는 주문번호를 넣으면 에러가 돌아오니 실패를 처리하는 경로도 필요합니다. 도구가 세 개일 때 잘 못 고르는 에이전트는 서른 개일 때도 잘 못 고릅니다.
같은 쇼핑몰의 정책 문서로 검색 질의응답을 만드는 쪽은 RAG 기초 시리즈에 있습니다. 6편에서 그 검색기를 도구 하나로 감싸 에이전트에 붙입니다.