5. Agent Preview — "아하!" 순간
지금까지 여러분은 여러분이 흐름을 제어하는 시스템을 구축해왔습니다. 프롬프트를 작성하고, LLM을 호출하고, 응답을 처리했죠. LLM은 강력하지만, 여전히 여러분의 지시를 따르는 역할이었습니다.
이번 챕터에서는 근본적인 사고의 전환을 소개합니다: LLM이 다음에 무엇을 할지 결정한다면 어떨까요?
이 챕터가 존재하는 이유
문제점: 이 미리보기 없이 11장까지 챗봇을 계속 만들다 보면, "에이전트는 그냥 더 나은 챗봇이구나"라고 생각하게 될 가능성이 높습니다.
진실: 에이전트는 근본적으로 다릅니다. 에이전트는 단순히 텍스트를 생성하는 것이 아니라, 여러분의 프로그램을 제어하는 결정을 내립니다.
해결책: 이 챕터는 우리가 어디로 가고 있는지 먼저 보여줍니다. 그래서 다음 챕터들에서 파이프라인, 상태 관리 등을 배울 때, 각 요소가 왜 중요한지 이해할 수 있습니다.
배울 내용 (그리고 배우지 않을 내용)
이번 챕터 (5장):
- 15분짜리 에이전트 사고방식 미리보기
- LLM 출력이 어떻게 다른 코드 경로를 트리거하는지
- 라우팅과 에이전트 루프의 개념적 차이
이후 챕터들 (12장부터):
- 도구, 루프, 자기 교정 기능을 갖춘 실제 에이전트 시스템
- 프로덕션 수준의 구현
- 멀티 에이전트 협업 같은 고급 패턴
왜 간격이 있나요? 에이전트를 구축하기 전에 기초가 필요합니다: 프롬프트 엔지니어링 (4장), 실행 파이프라인 (6장), 구조화된 출력 (7장), 상태 관리 (8장), 그리고 지식 통합 (9-11장).
학습 방식에 대한 안내: 이번 챕터는 구현 세부사항이 아닌 개념에 초점을 맞춥니다. 실제 설계와 개발 기법은 12장 이후에 다룹니다. 여기서 보는 모든 것을 어떻게 구현해야 할지 모르더라도 걱정하지 마세요—그게 의도된 것입니다. 지금은 에이전트가 무엇인지 개념적으로 이해하는 것만으로 충분합니다.
시작해봅시다.
5.1) 챗봇에서 의사결정자로
개념: 질문에 답하는 대신, LLM이 어떤 도구를 사용할지 결정합니다
3장과 4장에서 여러분은 LLM이 텍스트 응답을 생성하는 채팅 시스템을 구축했습니다. 흐름은 간단했습니다:
- 사용자가 질문을 합니다
- LLM이 답변을 생성합니다
- 답변을 표시합니다
이것은 정적 체인(static chain)입니다. 흐름이 미리 결정되어 있습니다. LLM의 유일한 역할은 텍스트를 생성하는 것입니다.
이제 요청의 유형을 바꿔보겠습니다. 사용자가 "847 × 923은 얼마인가요?"라고 묻습니다.
3장의 챗봇은 이것에 직접 답하려고 할 것입니다. 하지만 LLM은 실제로 계산을 수행하지 않습니다—그럴듯해 보이는 토큰을 예측할 뿐입니다. "2 + 2"의 경우, 훈련 데이터에서 "4"라는 답이 너무 자주 등장해서 LLM이 정답을 맞춥니다. 하지만 "847 × 923"처럼 LLM이 한 번도 본 적 없는 계산의 경우, 숫자처럼 보이지만 틀린 답을 생성할 가능성이 높습니다.
우리가 진짜 원하는 것은:
- LLM이 인식: "이것은 수학 문제구나"
- LLM이 결정: "내가 추측하지 말고 계산기 도구를 사용해야겠다"
- Python이 실행: 847 × 923 = 781,781
- 시스템이 반환: 정확한 답
LLM의 역할은 계산이 아니라, 계산할 수 있는 도구로 라우팅하는 것입니다.
핵심 통찰은 다음과 같습니다: LLM은 수학을 풀 필요가 없습니다. 계산기를 사용하기로 결정하면 됩니다.
이것이 챗봇에서 의사결정자로의 전환입니다. LLM이 요청을 검토하고 적절한 도구로 라우팅합니다. 단순히 텍스트를 생성하는 것이 아니라 프로그램 흐름에 대한 결정을 내리고 있습니다.
이 차이를 시각화해봅시다:
정적 체인에서 LLM은 모든 것에 직접 답하려고 합니다. 동적 라우팅 시스템에서 LLM은 요청을 올바른 도구로 라우팅합니다.
시각화: "선형 체인" (정적) vs. "라우터" (동적) 비교
정적 체인이 모든 사용자 요청을 처리하는 방법은 다음과 같습니다:
LLM은 모든 것에 직접 답하려고 합니다. "프랑스의 수도는 어디인가요?"든 "240의 15%는 얼마인가요?"든, LLM은 텍스트 응답을 생성합니다. 수도는 맞출 수 있지만(훈련 데이터에서 "파리"를 많이 봤으므로), 백분율 계산은 틀릴 가능성이 높습니다.
문제점: LLM은 모든 질문에 같은 접근 방식—토큰 예측—을 사용합니다. 더 나은 도구가 있는데도 말이죠.
이제 다양한 유형의 요청을 다르게 처리할 수 있는 시스템을 소개합니다:
- 사실 질문: "프랑스의 수도는 어디인가요?" → 검색 도구 사용
- 수학 문제: "240의 15%는 얼마인가요?" → 계산기 사용
- 대화: "안녕하세요?" → LLM이 직접 응답
이를 가능하게 하는 동적 라우터입니다:
LLM이 입력을 검토하고 요청 유형에 따라 경로를 선택합니다. 이것이 라우팅 로직이며, LLM이 라우터 역할을 합니다.
핵심 차이점: 항상 텍스트를 생성하는 대신, LLM은 이제 각 요청을 어떻게 처리할지 결정합니다—적절한 도구로 라우팅하는 방식으로요.
핵심 요점: 답변 생성에서 행동 선택으로
이것이 에이전틱 시스템의 근본적인 전환입니다:
전통적인 LLM 사용: 질문을 하면 LLM이 답변을 작성합니다.
에이전틱 LLM 사용: 질문을 하면 LLM이 무엇을 할지 결정하고, 여러분의 코드가 그 결정을 실행합니다.
LLM의 출력은 더 이상 사용자를 위한 텍스트가 아닙니다. 프로그램을 위한 지시사항입니다.
이렇게 생각해보세요: 전통적인 챗봇에서 LLM은 고객 질문에 답하는 직원입니다. 에이전틱 시스템에서 LLM은 각 요청을 어느 부서가 처리해야 할지 결정하는 관리자입니다.
이 의사결정 능력이 에이전틱 시스템의 본질입니다. LLM은 단순히 응답하는 것이 아니라—프로그램이 다음에 무엇을 할지 제어합니다. 입력에 따라 계산기를 실행하거나, 검색 API를 호출하거나, 직접 응답할 수 있습니다. 프로그램의 동작이 LLM의 결정에 따라 달라집니다.
실제로 이것을 어떻게 구현할까요? 이것을 작동시키는 최소한의 코드를 살펴봅시다.
5.2) 최소 실행
트리거: 구조화된 출력을 강제하는 시스템 프롬프트
LLM을 라우터로 작동시키려면 출력을 제한해야 합니다. 전체 답변을 생성하는 대신, 구조화된 결정—코드에 무엇을 해야 할지와 어떤 데이터를 사용할지 알려주는 JSON 객체—을 출력하도록 만들어야 합니다.
다음은 이를 수행하는 시스템 프롬프트입니다:
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
import json
llm = ChatOpenAI(model="gpt-4o-mini")
system_prompt = """You are a routing assistant. Analyze the user's request and respond with JSON.
Output format:
{
"action": "CALC" | "SEARCH" | "CHAT",
"input": "cleaned input for the tool"
}
Examples:
User: "Calculate 15 times 7"
Output: {"action": "CALC", "input": "15 * 7"}
User: "What's the capital of Japan?"
Output: {"action": "SEARCH", "input": "capital of Japan"}
User: "Hello there!"
Output: {"action": "CHAT", "input": "Hello there!"}
Extract the essential information and format it for the appropriate tool.
Only output valid JSON, nothing else."""
def get_routing_decision(user_input: str) -> dict:
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_input)
]
response = llm.invoke(messages)
# JSON 응답 파싱
try:
decision = json.loads(response.content.strip())
return decision
except json.JSONDecodeError:
# LLM이 유효한 JSON을 출력하지 않으면 대체
return {"action": "CHAT", "input": user_input}
# 테스트
print(get_routing_decision("What's 25 * 48?"))
# 출력: {'action': 'CALC', 'input': '25 * 48'}
print(get_routing_decision("Who wrote Hamlet?"))
# 출력: {'action': 'SEARCH', 'input': 'author of Hamlet'}
print(get_routing_decision("How are you today?"))
# 출력: {'action': 'CHAT', 'input': 'How are you today?'}이 프롬프트는 의도적으로 제한적입니다. LLM에게 창의적이 되라고 요청하는 것이 아니라, 입력을 분류하고 관련 정보를 추출하여 구조화된 형식으로 만들라고 요청하는 것입니다.
여기서 무슨 일이 일어나는지 주목하세요:
- LLM이 사용자의 질문을 읽습니다
- 의도를 판단합니다 (계산, 사실 검색, 대화)
- 필수 정보를 추출하고 정제합니다
- 행동과 정제된 입력이 모두 포함된 JSON 객체를 출력합니다
- 우리 코드가 이 구조화된 데이터를 받아 라우팅합니다
LLM은 질문에 답하는 것이 아니라—무엇이 질문에 답해야 할지 결정하고 있습니다.
브리지: LLM 결정을 도구 실행에 연결
이제 LLM의 결정을 실제 도구 실행에 연결해야 합니다. 다음은 최소한의 브리지입니다:
def safe_calculator(expression: str) -> float:
try:
# 안전을 위해 제한된 네임스페이스로 eval 사용
# 참고: eval()은 보안 위험이 있습니다.
# 프로덕션에서는 sympy나 ast 기반 평가 같은 수학 파싱 라이브러리를 사용하세요.
result = eval(expression, {"__builtins__": {}}, {})
return float(result)
except:
raise ValueError(f"Could not calculate: {expression}")
def search(query: str) -> str:
"""플레이스홀더 검색 함수"""
# 실제로는 검색 API를 호출할 것입니다
# 이제 "author of Hamlet" 같은 정제된 쿼리를 받습니다
return f"Search results for: {query}"
def route_and_execute(user_input: str) -> str:
"""LLM 결정을 받아 적절한 도구를 실행"""
decision = get_routing_decision(user_input)
action = decision["action"]
tool_input = decision["input"] # LLM이 정제한 입력
if action == "CALC":
try:
result = safe_calculator(tool_input)
return f"Calculation result: {result}"
except ValueError as e:
return f"Could not perform calculation: {e}"
elif action == "SEARCH":
result = search(tool_input)
return result
else: # CHAT
# 대화형 쿼리의 경우 LLM이 직접 응답
response = llm.invoke([HumanMessage(content=tool_input)])
return response.content
# 전체 흐름 테스트
print(route_and_execute("What's 25 * 48?"))
# LLM이 "25 * 48"을 추출 → 계산기가 깨끗한 입력을 받음
# 출력: Calculation result: 1200.0
print(route_and_execute("Who wrote Hamlet?"))
# LLM이 "author of Hamlet"로 재포맷 → 더 나은 검색 쿼리
# 출력: Search results for: author of Hamlet
print(route_and_execute("How are you today?"))
# 출력: I'm doing well, thank you for asking!이것이 가장 간단한 형태의 최소 라우팅 패턴입니다:
- LLM 결정: LLM으로부터 라우팅 결정 받기
- 코드 실행: Python 코드가 결정 확인
- 도구 호출: 적절한 함수 호출
- 결과 반환: 출력을 포맷하여 반환
핵심 통찰: LLM의 출력은 더 이상 사용자를 위한 텍스트가 아닙니다—프로그램의 동작을 제어하는 구조화된 데이터입니다. action 필드는 코드가 어떤 경로를 택할지 알려주고, input 필드는 해당 도구에 정제된 데이터를 제공합니다. 이 JSON 응답은 실행 가능한 로직이 됩니다.
이것은 챗봇과 근본적으로 다릅니다. 챗봇에서는 LLM의 출력이 사용자에게 직접 전달됩니다. 여기서는 LLM의 출력이 코드로 먼저 전달되고, 코드가 무엇을 실행할지 결정합니다.
이 섹션에서 우리가 만든 것은 라우터입니다—완전한 에이전트의 전신입니다. 이것은 기본 원리(LLM이 흐름을 제어)를 보여주지만, 진정한 에이전트 루프를 정의하는 반복과 자기 교정은 없습니다.
이 간단한 라우팅 시스템에는 큰 한계가 있습니다: 자신의 실수를 교정할 수 없습니다. 이것이 왜 중요한지, 그리고 다음에 무엇이 올지 살펴봅시다.
5.3) 용어 이해하기: 라우터 vs 에이전트
우리가 방금 만든 것은 라우터입니다. 그런데 완전한 에이전트와 비교하면 어떨까요? 명확한 정의를 확립해봅시다:
라우터 (우리가 방금 만든 것)
- 요청당 한 번의 분류 결정을 내립니다
- 어떤 도구/경로를 실행할지 선택합니다
- 한 번 실행하고 반환합니다
- 반복 없음, 상태 없음, 자기 교정 없음
- 예시: 이메일 분류기, 의도 감지기, 도구 선택기
도구 호출 어시스턴트 (13장)
- LLM이 함수 호출 API를 통해 도구를 직접 호출할 수 있습니다
- 여전히 일반적으로 단일 턴입니다 (한 번의 요청 → 한 번의 응답)
- 라우팅보다 정교하지만, 반드시 반복적이지는 않습니다
- 예시: "날씨를 검색하고 요약해줘"를 한 번의 호출로 처리
에이전트 (완전한 루프) (14-17장)
- 생각 → 행동 → 관찰 사이클을 반복합니다
- 반복 간에 상태를 유지합니다
- 결과에 따라 결정을 수정할 수 있습니다
- 자기 교정을 구현합니다
- 예시: 코드가 작동할 때까지 수정을 시도하는 디버깅 어시스턴트
에이전틱 시스템 (포괄 용어)
- LLM 출력이 제어 흐름에 영향을 미치는 모든 시스템
- 라우터, 도구 호출 어시스턴트, 완전한 에이전트를 포함합니다
- "에이전틱"은 속성을 설명하고, "에이전트"는 특정 아키텍처를 설명합니다
- 예시: 위의 모든 것이 에이전틱 동작을 보입니다
5.2에서 우리가 만든 것은 라우터입니다—한 번 결정하고 실행합니다. 이것은 간단한 분류 작업에는 잘 작동하지만, 여러 단계, 검증 또는 방향 수정이 필요한 상황은 처리할 수 없습니다. 초기 결정이 최적이 아니거나 작업이 예상보다 복잡한 것으로 판명되면, 라우터는 적응할 메커니즘이 없습니다.
에이전트는 이 한계를 해결합니다 반복을 도입함으로써. 에이전트는 행동의 결과를 관찰하고, 접근 방식을 재고하고, 다시 시도할 수 있습니다. 이것이 에이전트를 해결책으로 가는 경로가 처음부터 명확하지 않은 복잡한 다단계 작업에 적합하게 만듭니다.
14-17장에서는 이러한 반복적 에이전트 루프를 구축하는 방법을 배우게 됩니다.
참고: 최근에는 o1과 o3 같은 추론 모델이 일부 다단계 작업을 내부적으로 처리할 수 있어, 특정 시나리오에서 명시적인 루프의 필요성을 줄여줍니다. Part IV에서 실제 에이전트를 구축할 때 루프와 추론 모델을 언제 사용할지 탐구하게 됩니다.
지금 이해해야 할 것:
- 에이전틱 시스템은 제어 흐름에서 시작합니다: LLM의 출력이 코드가 다음에 무엇을 할지 결정합니다
- 라우팅은 가장 간단한 형태입니다: 한 번의 결정, 한 번의 도구, 한 번의 결과—5.2에서 우리가 만든 것
- 진짜 에이전트는 루프가 필요합니다: 다단계 작업, 검증, 오류 복구를 처리하기 위해 (14-17장)
- 자기 교정이 핵심 차이점입니다: 에이전트는 결과를 관찰하고 접근 방식을 조정할 수 있습니다
아직 다루지 않은 것 (나중 챕터에서):
- 실행 파이프라인을 구성하는 방법 (6장)
- 구조화된 출력을 처리하는 방법 (7장)
- 대화 메모리를 처리하는 방법 (8장)
- 도구를 적절히 정의하는 방법 (12장)
- 자기 교정 기능을 가진 에이전트 루프를 구현하는 방법 (14-17장)
- 에이전트를 프로덕션에 적합하게 만드는 방법 (18-26장)
이번 챕터는 개념적 전환에 관한 것이었습니다—시스템을 에이전틱 동작을 보이게 만드는 것이 무엇인지 이해하는 것. 구현 세부사항은 나중에 나옵니다.
다음 챕터에서는 실용적인 기초를 계속 구축합니다: LangChain Expression Language (LCEL)를 사용한 실행 파이프라인 구성. 이것들은 Part IV에서 실제 에이전트를 구현할 때 사용할 구성 요소입니다.
"아하!" 순간이 완성되었습니다. 이제 여러분은 에이전틱 시스템이 LLM이 결정하고, 코드가 실행하는 시스템이라는 것을 이해합니다. 그 외 모든 것—도구, 루프, 상태 관리—은 이 결정-실행 사이클을 더 견고하고 유능하게 만드는 것에 관한 것입니다.
기초를 계속 구축해 나갑시다.