Basic 코스에서 에이전트의 개념과 프롬프트 설계를 배웠다면, Advanced는 에이전트의 내부로 들어간다. 에이전트가 어떻게 생각하고, 어떻게 도구를 호출하고, 그 과정이 어떻게 표준화되는지를 다뤄보려 한다.
1주차에서 다루는 내용은 크게 네 가지다.
- 에이전트의 정의와 ReAct 루프 — 에이전트가 생각하는 방식
- Function Calling — 에이전트가 도구를 부르는 원리
- Raw Function Calling 구현 — 직접 파이썬으로 에이전트를 만들어보기
- MCP (Model Context Protocol) — 도구 연결을 표준화하는 프로토콜
에이전트의 정의
일반적인 챗봇은 사용자의 질문에 대해 학습된 데이터를 바탕으로 텍스트를 생성하는 데 그친다. 에이전트는 다르다. 특정 목표(Goal)가 주어지면, 그 목표를 달성하기 위해 자신이 가진 도구(Tool) 중 무엇을 사용할지 스스로 결정하고 실행하는 대리인이다.
“오늘 삼성전자 주가 알려줘”라는 요청에 대해:
- 챗봇: “제가 실시간 정보를 알 수 없습니다”
- 에이전트: 주가 검색 도구를 선택해서 직접 데이터를 가져온다
에이전트를 보며 가장 크게 느낀 건 텍스트 생성에서 행동(Action)으로 넘어가는 확장이었다.
ReAct 루프
에이전트를 설계하는 아키텍처는 다양하다. 계획부터 다 짜놓고 움직이는 Plan-and-Execute, 자신의 답변을 스스로 검토하는 Reflection 등이 있다. 하지만 현대 에이전트의 가장 기초가 되는 표준 모델은 ReAct(Reasoning + Acting) 다. 대부분의 복잡한 구조도 결국 이 ReAct의 변형이다.
ReAct는 세 단계의 루프로 구성된다.
1. Thought (추론) — 모델이 현재 상황을 분석하고, 목표 달성을 위해 다음에 무엇을 해야 할지 스스로 계획을 세우는 단계.
2. Action (행동) — 세워진 계획에 따라 외부 도구(Function)를 실제로 호출하는 단계.
3. Observation (관찰) — 실행된 결과값을 확인하는 단계. 모델은 이 결과가 목표에 부합하는지, 추가 작업이 필요한지 관찰한다.
모델은 이 추론-행동-관찰의 루프를 목표가 달성될 때까지 반복한다. 여기서 인상적이었던 건 에이전트가 단번에 정답을 맞히는 구조가 아니라, 루프를 돌며 시행착오로 정답에 가까워진다는 점이었다. 이 유연함이 고정된 시나리오 기반 챗봇과 에이전트를 가르는 가장 큰 차이다.
Function Calling
What, not How
지금까지의 프로그래밍은 How의 세계였다. 개발자가 if-else 문을 촘촘하게 짜서 모든 경로를 미리 정해두는 결정론적 경로.
에이전트의 세계에서는 이 제어권이 개발자로부터 모델의 추론(Reasoning)으로 넘어간다. 개발자는 모델에게 어떤 도구가 있는지와 최종 목적이 무엇인지(What)만 알려준다. 그러면 모델이 실시간으로 상황을 판단해서 실행 경로를 스스로 결정한다.
소프트웨어가 정해진 레일을 달리는 기차에서, 목적지를 보고 스스로 길을 찾아가는 자율주행 자동차가 되는 것이다.
Constrained Decoding
모델은 본래 확률에 따라 다음 단어를 예측하는 통계적 모델이다. 그냥 두면 JSON을 쓰다가도 중간에 불필요한 말을 섞거나 괄호를 빼먹는 실수를 한다.
이때 필요한 기술이 Constrained Decoding(제약된 디코딩) 이다. 다음 단어를 선택할 때 확률 값을 강제로 조정해서, 토큰의 확률 분포를 JSON Schema라는 틀에 가두는 것이다. “다음 단어는 반드시 {여야 해”, “그다음엔 반드시 name이 와야 해”처럼. 이를 통해 자유로운 추론 모델이 공학적으로 즉시 사용 가능한 구조화 데이터를 출력하게 된다.
Semantic Matching
모델이 수많은 함수 중 왜 특정 함수를 선택하는가? Semantic Matching 때문이다.
모델은 우리가 작성한 함수의 이름과 description(설명)을 텍스트로 읽는다. 그리고 사용자의 질문과 가장 의미론적으로 가까운 도구를 수학적으로 계산해서 찾아낸다.
그래서 Function Calling에서 가장 중요한 코딩은 파이썬 코드가 아니라, 함수가 무엇을 하는지 모델이 이해하기 쉽게 설명하는 글이 된다.
에이전트적 사고와 프롬프트 실습
모델에게 제어권이 넘어갔다면, 개발자의 역할은 에이전트가 올바른 판단을 내릴 수 있도록 사고의 가드레일을 설계하는 것이다.
에이전트 설계의 5대 기둥
| 기둥 | 설명 |
|---|---|
| Persona | 누구인지 정의 |
| Context | 어떤 상황인지 알려주기 |
| Task | 무엇을 해야 하는지 명시 |
| Constraint | 절대 해서는 안 될 행동 규정 |
| Output | 어떤 형태로 결과를 내놓을지 |
이 5가지가 갖춰졌을 때 비로소 모델은 단순한 챗봇이 아닌 책임감 있는 에이전트처럼 행동한다.
Hallucination과 Self-Correction
에이전트도 실수를 한다. 존재하지 않는 함수를 부르거나, 숫자가 들어가야 할 곳에 문자를 넣는 할루시네이션이 발생한다. 이때 중요한 것이 ReAct 루프의 Observation(관찰) 단계다.
에이전트는 실행 에러 메시지를 보고 “아, 내가 인자를 잘못 넣었구나”라고 스스로 판단(Thought)한다. 그리고 올바른 값을 넣어 재시도(Self-Correction)한다. 에이전트적 사고는 한 번에 성공하는 것이 아니라, 오류를 스스로 교정하며 목표를 향해 나아가는 과정임을 이해해야 한다.
Raw Function Calling 구현
이론을 배웠으니, 직접 파이썬 코드로 ReAct 루프를 구현해보는 실습이다. Gemini 1.5 Flash 모델을 활용한다.
환경 설정
# Google AI Studio에서 API 키 발급
# .env 파일에 키 저장
pip install -r requirements.txt # (Mac은 pip3)
Schema Design
코드의 FUNCTIONS_SCHEMA 부분이 핵심이다. 여기가 앞서 배운 Semantic Matching이 일어나는 곳이다.
description필드에 공을 들여야 한다- 모델은 파이썬 코드를 읽는 게 아니라, 이 설명을 읽고 어떤 함수를 쓸지 판단한다
- 설명을 조금씩 수정하면 모델의 반응이 달라진다
The Loop
raw_function_calling.py의 메인 루프가 에이전트가 살아 움직이는 구간이다.
- 모델에게 물어보기: 사용자의 질문을 전달
- 도구 호출 확인: 모델이
tool_calls신호를 주는지 파싱 - Action: 실제 파이썬 코드로 API나 계산기 함수를 실행
- Observation: 결과값을 모델에게 피드백 — “이게 결과야, 다음엔 뭐 할까?”
이 루프는 모델이 Final Answer(최종 답변)를 낼 때까지 반복된다. 이것이 ReAct 패턴의 실체다.
실행 및 관찰
코드를 실행하면 터미널 로그에서 다음을 확인할 수 있다.
- Thought: 모델이 “주가 정보 도구를 사용해야겠다”라고 생각하는 과정
- Action: 실제로
get_stock_price가 호출되는 순간 - Observation: API에서 받아온 실시간 데이터
모델이 인자값을 잘못 넣으면? 에러 메시지가 피드백으로 들어가고, 모델이 스스로 수정하며 다시 시도하는 Self-Correction 과정도 로그에서 직접 확인할 수 있다.
MCP (Model Context Protocol)
방금 직접 스키마를 짜고 루프를 관리했다. 그런데 연동할 도구가 100개라면? 모델을 바꿀 때마다 이걸 다시 해야 한다면? 이 혼란을 해결하기 위해 등장한 것이 MCP다.
Before vs After
Before:
- 앱마다 커넥터를 따로 만든다
- 인증, 형식, 에러, 권한, UX가 다 다르다
After (MCP):
- 앱은 MCP Client를 한 번 구현
- 데이터/툴은 MCP Server로 표준 노출
- 연결이 늘어도 구조가 유지된다
MCP는 기능을 표준화하는 게 아니라, 연결 방식을 표준화하는 프로토콜이다.
아키텍처
MCP 시스템은 세 부분으로 나뉜다.
Host (호스트) — 사용자가 직접 사용하는 최종 애플리케이션이다. Claude Desktop, Cursor, 또는 직접 만든 Python 스크립트 등. 사용자 인터페이스를 제공하고, 어떤 서버를 신뢰하고 연결할지 결정하는 결정권자다.
Client (클라이언트) — 호스트 앱 내부에 존재하는 인터페이스 코드다. AI 모델이 “파일을 읽어줘”라고 하면, 이를 MCP 규격에 맞는 JSON-RPC 요청으로 변환한다. 서버가 보내준 데이터를 AI가 이해하기 쉬운 형태로 가공하는 통역사 역할이다.
Server (서버) — 특정 기능이나 데이터를 가진 개별 프로그램이다. 클라이언트가 요청하면 실제로 그 일을 수행하고 결과를 돌려주는 전문가 역할이다.
Transport
로컬 (STDIO)
- 서버가 내 컴퓨터 안에서 같이 돌아갈 때
- 별도의 네트워크 설정 없이 호스트가 서버 프로세스를 직접 실행
- 가장 빠르고 보안이 강력하다
원격 (HTTP / SSE)
- MCP 서버가 AWS나 다른 서버에 떠 있을 때
- 인터넷 망을 통해 어디서든 서버에 접속 가능
여기서 중요한 건 “어떻게 연결하느냐(Transport)”와 “무엇을 주고받느냐(Data layer)”를 분리한다는 점이다.
서버가 제공하는 3가지
| Primitive | 설명 | 제어권 |
|---|---|---|
| Prompts | 사용자가 고르는 템플릿 | User-controlled |
| Resources | 앱이 붙이는 데이터 | App-controlled |
| Tools | 모델이 호출하는 실행 기능 | Model-controlled |
MCP를 보며 남은 포인트는 제어권 분리였다. 누가 무엇을 제어하는지가 명확하게 나뉜다.
Lifecycle
- Initialize: 버전과 기능(capabilities) 합의
- Operate: 정상 동작
- Shutdown: 세션 종료
시작에 합의가 있어서, 서로 업데이트해도 덜 깨진다.
운영/보안
- 동의(Consent): 사용자 승인
- 권한/범위(Access): 접근 제어
- 로그/감사(Audit): 추적 가능성
- 민감 데이터(Resources): 데이터 보호
- 위험 행동(Tools): 행동 제한
1주차를 마치며
Advanced 1주차는 에이전트의 표면이 아니라 내부를 파헤친 시간이었다.
직접 raw_function_calling.py를 작성하면서 ReAct 루프가 코드로 어떻게 구현되는지 확인했고, 그 과정이 얼마나 번거로운지도 체감했다. 도구가 100개라면? 모델을 바꿀 때마다? 이 고통을 해결하기 위해 MCP가 등장했다는 것이 자연스럽게 이해된다.
돌아보면 이렇게 남는다.
- 에이전트: 목표를 이해하고, 도구를 선택하고, 행동하는 AI
- ReAct 루프: Thought → Action → Observation의 반복
- Function Calling: 제어권이 개발자에서 모델로 넘어가는 패러다임 시프트
- MCP: 도구 연결을 표준화해서, 확장과 운영을 쉽게 만드는 프로토콜
다음 시간에는 MCP를 직접 구현하고 붙여보는 실습이 이어지는데, 오늘 이해한 구조가 어디서부터 실제 코드로 바뀌는지 집중해서 보려 한다.
다시 볼 메모
- ReAct (Reasoning + Acting): Thought → Action → Observation의 무한 루프. 시행착오를 통해 정답에 가까워지는 구조
- Function Calling: 개발자가 What만 알려주면, 모델이 How를 스스로 결정. Semantic Matching으로 함수를 선택
- Constrained Decoding: 통계적 모델이 완벽한 JSON을 출력하도록 확률 분포를 강제 조정하는 기술
- MCP 아키텍처: Host(결정권자) / Client(통역사) / Server(전문가)의 3계층 구조
- MCP의 3대 Primitive: Prompts(User), Resources(App), Tools(Model) — 제어권 분리가 핵심
- Transport 분리: 로컬(STDIO)과 원격(HTTP/SSE)을 나눠서 어디서든 연결 가능