본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 4. 13. · 0 Views
LLM 핵심 원리 함수 호출 환각 임베딩 완벽 가이드
LLM의 세 가지 핵심 개념인 함수 호출(Function Calling), 환각(Hallucination), 임베딩(Embedding)을 중급 개발자 관점에서 실무 중심으로 설명합니다. 에이전트 AI 엔지니어가 반드시 알아야 할 원리와 실전 팁을 담았습니다.
목차
- 함수 호출_Function_Calling
- 환각_Hallucination
- 임베딩_Embedding
- 함수 호출과_임베딩의_결합_RAG_파이프라인
- 환각_감지와_방어_전략
- 답변 끝에 신뢰도를 [높음/중간/낮음]으로 표시하세요"""},
- 임베딩_실전_벡터_데이터베이스와_청킹_전략
- 함수_호출_고급_패턴_다중_도구와_에러_처리
- 다음_단계_프레임워크로_에이전트를_만들어봅시다
1. 함수 호출 Function Calling
김개발 씨가 팀장님께 새로운 기능 시연을 하던 날이었습니다. "LLM에게 날씨를 물어보면, 실시간 날씨 API를 호출해서 알려주는 거예요." 팀장님이 고개를 갸웃했습니다.
"LLM이 어떻게 외부 API를 호출하는 건데?"
**함수 호출(Function Calling)**은 LLM이 텍스트를 생성하는 것을 넘어, 미리 정의된 함수를 실행하고 그 결과를 바탕으로 답변을 구성하는 메커니즘입니다. 마치 레스토랑에서 주방장이 직접 재료를 구하지 않고, 웨이터를 통해 식자재를 주문받는 것과 같습니다.
이것을 활용하면 LLM이 실시간 데이터, 데이터베이스 조회, 외부 서비스 연동 등 정적 지식을 넘어선 동적인 작업을 수행할 수 있습니다.
다음 코드를 살펴봅시다.
import openai
import json
# 함수 정의 - LLM이 이 함수를 호출할 수 있음
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "도시의 현재 날씨를 반환합니다",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "도시 이름"}
},
"required": ["city"]
}
}
}]
# 사용자 질문과 함께 tools를 전달
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "서울 날씨 어때?"}],
tools=tools,
tool_choice="auto" # LLM이 필요시 자동 호출
)
# LLM이 함수 호출을 요청했는지 확인
if response.choices[0].message.tool_calls:
function_args = json.loads(
response.choices[0].message.tool_calls[0].function.arguments
)
print(f"호출할 함수: get_weather('{function_args['city']}')")
김개발 씨는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스를 따라 열심히 공부하고 있습니다. 지난 4화에서는 토큰, 컨텍스트 윈도우, 프롬프트 설계의 기초를 배웠죠.
이번에는 그다음 단계로, LLM이 단순히 텍스트를 생성하는 것을 넘어 실제로 "행동"하는 방법을 알아보려 합니다. 어느 날 김개발 씨가 팀장님께 새로운 기능을 시연하겠다며 자신만만하게 나섰습니다.
"LLM에게 날씨를 물어보면, 실시간으로 API를 호출해서 정확한 날씨를 알려주는 거예요!" 팀장님이 커피를 한 모금 마시며 물었습니다. "LLM이 어떻게 외부 API를 호출하는 건데?
LLM은 텍스트만 생성하는 모델 아닌가?" 김개발 씨는 순간 말문이 막혔습니다. 맞습니다.
LLM 자체는 텍스트를 생성하는 모델입니다. 코드를 직접 실행할 수 없죠.
그런데 어떻게 외부 API를 호출할 수 있는 걸까요? 선배 박시니어 씨가 옆에서 설명을 시작했습니다.
"함수 호출, 즉 Function Calling은 말 그대로 LLM이 함수를 호출하도록 요청하는 메커니즘이에요. LLM이 직접 코드를 실행하는 게 아니라, '이 함수를 이렇게 호출해줘'라고 JSON 형식으로 알려주는 거죠." 쉽게 비유하자면, 함수 호출은 마치 호텔 프론트 데스크와 객실 서비스의 관계와 같습니다.
투숙객(사용자)이 프론트(LLM)에 "방에서 저녁을 먹고 싶어"라고 말하면, 프론트는 주방(외부 API)에 주문을 전달합니다. 프론트가 직접 요리를 만드는 게 아니라, 주문서를 작성해서 전달하는 것이죠.
함수 호출의 동작 과정을 단계별로 살펴보겠습니다. 첫 번째 단계는 함수 정의입니다.
개발자가 LLM에게 "이런 함수들이 있다"라고 알려줍니다. 함수 이름, 설명, 파라미터의 형식을 JSON Schema로 정의하죠.
위 코드에서 tools 배열이 바로 이 역할을 합니다. 두 번째 단계는 LLM의 판단입니다.
사용자의 질문을 보고, LLM이 "아, 이건 get_weather 함수를 호출해야겠다"라고 판단합니다. tool_choice="auto"로 설정하면 LLM이 스스로 필요한지 여부를 결정합니다.
세 번째 단계는 함수 인자 추출입니다. LLM이 사용자의 질문에서 "서울"이라는 정보를 추출해, {"city": "서울"} 형태의 JSON 인자를 생성합니다.
이 과정에서 자연어 처리 능력이 핵심 역할을 합니다. 네 번째 단계는 실제 실행입니다.
애플리케이션 코드가 LLM의 요청을 받아 실제 함수를 실행하고, 결과를 다시 LLM에게 전달합니다. 이 부분은 개발자가 직접 구현해야 합니다.
다섯 번째 단계는 최종 답변 생성입니다. LLM이 함수 실행 결과를 바탕으로 사용자에게 자연스러운 답변을 만들어냅니다.
"현재 서울의 기온은 12도이고, 흐린 하늘입니다." 처럼요. 실제 현업에서는 이 메커니즘을 훨씬 복잡하게 활용합니다.
데이터베이스 조회, 이메일 발송, 파일 생성, 캘린더 일정 추가 등 수십 개의 함수를 등록해두고 LLM이 상황에 맞게 선택하도록 구성합니다. 이것이 바로 에이전트의 핵심이 되는 "도구 사용(Tool Use)" 능력입니다.
하지만 주의할 점이 있습니다. LLM이 항상 올바른 함수를 선택하는 것은 아닙니다.
때로는 존재하지 않는 파라미터를 넣거나, 필요 없는 함수를 호출하기도 합니다. 따라서 함수 호출 결과에 대한 검증 로직을 반드시 구현해야 합니다.
또한 함수의 description을 명확하게 작성하는 것이 매우 중요합니다. "날씨 정보를 가져온다"보다 "특정 도시의 현재 기온, 습도, 날씨 상태를 반환한다"가 훨씬 정확한 호출을 유도합니다.
김개발 씨는 박시니어 씨의 설명을 듣고 눈을 반짝였습니다. "그러면 LLM은 뇌 역할을 하고, 함수들은 손발 역할을 하는 거군요!" 박시니어 씨가 웃으며 고개를 끄덕였습니다.
"정확해요. 이 개념을 제대로 이해하면, 다음에 배울 에이전트 아키텍처가 훨씬 쉽게 이해될 거예요."
실전 팁
💡 - 함수의 description은 구체적으로 작성하세요. LLM이 어떤 함수를 선택할지 이 필드가 결정합니다.
- 항상 함수 호출 결과를 검증하는 로직을 구현하세요. LLM의 판단이 항상 옳지는 않습니다.
- 여러 함수를 동시에 호출할 수 있습니다. 병렬 호출을 지원하면 응답 속도를 크게 개선할 수 있습니다.
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
2. 환각 Hallucination
김개발 씨가 LLM으로 자동화된 뉴스 요약 봇을 만들고 팀에 공유했습니다. 그런데 다음 날 팀원이 보고했습니다.
"어제 뉴스 요약에서 없는 기사 내용을 포함하고 있어요." 김개발 씨는 식은땀을 흘렸습니다. LLM이 거짓말을 한 겁니다.
**환각(Hallucination)**은 LLM이 학습 데이터에 없는 정보를 마치 사실인 것처럼 자연스럽게 생성하는 현상입니다. 마치 자신감 넘치는 퀴즈 참가자가 정답을 모르면서도 그럴듯한 답을 지어내는 것과 같습니다.
에이전트 AI 엔지니어라면 환각의 원인을 이해하고, 이를 최소화하는 전략을 반드시 알아야 합니다.
다음 코드를 살펴봅시다.
import openai
# 환각이 발생하기 쉬운 프롬프트
user_prompt = "2024년 한국의 GDP 성장률은 몇%인가요?"
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_prompt}],
temperature=0.0 # 낮은 온도도 완전한 환각 방지는 불가
)
# 환각 방지: RAG 기반 접근법
def answer_with_sources(query, documents):
# 관련 문서만 컨텍스트로 제공
relevant_docs = retrieve_relevant(query, documents)
context = "\n".join(relevant_docs)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "주어진 문서만 기반으로 답변하세요. "
"문서에 없는 정보면 '모르겠습니다'라고 답하세요."},
{"role": "user", "content": f"문서: {context}\n\n질문: {query}"}
],
temperature=0.0
)
return response.choices[0].message.content
김개발 씨의 뉴스 요약 봇 사건 이후, 팀 전체 회의가 열렸습니다. "LLM이 만들어낸 가짜 뉴스가 사용자에게 전달될 뻔했어요." 팀장님의 목소리가 무거웠습니다.
"이런 문제를 어떻게 방지할 수 있죠?" 박시니어 씨가 화면을 켜며 설명을 시작했습니다. "이것이 바로 **환각(Hallucination)**이에요.
LLM이 가장 조심해야 할 현상이죠." 환각이란 무엇일까요? 쉽게 비유하자면, 환각은 마치 자신감 넘치는 퀴즈 참가자와 같습니다.
정답을 모르면서도 그럴듯한 톤으로 말을 지어내죠. LLM은 "다음에 올 확률이 높은 단어"를 예측하는 모델입니다.
사실 여부를 확인하는 게 아니라, 문맥상 가장 자연스러운 문장을 생성하는 것이 목적이거든요. 환각은 크게 세 가지 유형으로 나뉩니다.
첫 번째는 사실 환각입니다. 존재하지 않는 사실을 마치 진짜인 것처럼 말합니다.
"2024년 한국 GDP 성장률은 3.7%입니다"라고 말했는데, 실제로는 다른 값일 수 있죠. 김개발 씨의 뉴스 봇이 바로 이 유형의 환각을 보여준 것입니다.
두 번째는 추론 환각입니다. 주어진 정보에서 논리적으로 도출할 수 없는 결론을 내립니다.
"A 때문에 B가 발생했다"고 확신하며 말하지만, 실제로는 인과관계가 전혀 없을 수 있습니다. 세 번째는 존재 환각입니다.
존재하지 않는 문서, API, 논문, 사람을 마치 실제 존재하는 것처럼 언급합니다. 특히 LLM은 실제 있는 것과 없는 것의 구분이 애매한 경우가 많습니다.
왜 환각이 발생할까요? 근본적인 원인은 LLM의 학습 방식에 있습니다.
LLM은 텍스트 데이터에서 통계적 패턴을 학습합니다. 세상의 모든 사실을 데이터베이스처럼 저장하는 게 아니라, "이 단어 다음에는 저 단어가 올 확률이 높다"는 패턴을 학습하는 것이죠.
그렇다면 환각을 어떻게 방지할 수 있을까요? 첫 번째 전략은 **RAG(Retrieval-Augmented Generation)**입니다.
관련 문서를 검색해서 LLM에게 컨텍스트로 제공하고, "이 문서만 기반으로 답변하세요"라고 지시하는 방식입니다. 위 코드의 answer_with_sources 함수가 이 패턴을 보여줍니다.
두 번째 전략은 체인 오브 쏘(Chain of Thought) 프롬프팅입니다. "단계별로 생각해서 답변하세요"라고 지시하면, LLM이 답변을 도출하는 과정을 거치면서 환각이 줄어듭니다.
즉흥적인 답변보다 논리적인 사고 과정을 거치는 것이죠. 세 번째 전략은 온도(Temperature) 조절입니다.
낮은 온도(0.0~0.3)로 설정하면 더 결정론적인 답변을 생성합니다. 하지만 이것만으로 환각을 완전히 막을 수는 없습니다.
네 번째 전략은 시스템 프롬프트 제약입니다. "모르는 정보면 '모르겠습니다'라고 답하세요"처럼 명확한 지침을 주면 환각 발생을 줄일 수 있습니다.
다섯 번째 전략은 **검증(Verification)**입니다. LLM의 답변을 또 다른 LLM이나 규칙 기반 시스템으로 검증하는 방법도 있습니다.
특히 의료, 금융 등 민감한 도메인에서는 이중 검증이 필수입니다. 박시니어 씨가 마무리했습니다.
"환각을 완전히 제거하는 건 불가능해요. 하지만 체계적인 전략으로 확률을 크게 낮출 수는 있죠.
에이전트를 만들 때는 항상 '이 답변이 틀릴 수 있다'고 가정하고 설계해야 합니다." 김개발 씨는 고개를 끄덕이며 노트에 적었습니다. "검증 없는 LLM 출력은 믿지 마라." 이것이 에이전트 AI 엔지니어의 첫 번째 원칙이었습니다.
실전 팁
💡 - "모르면 모른다고 답하세요"라는 시스템 프롬프트를 기본으로 설정하세요.
- RAG를 적용할 때는 출처(source)를 함께 반환하도록 구성하면 검증이 쉽습니다.
- 민감한 도메인에서는 LLM 단일 모델 대신 여러 모델을 교차 검증하는 구조를 고려하세요.
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
3. 임베딩 Embedding
김개발 씨가 회사 지식 베이스 검색 시스템을 만들고 있었습니다. "사용자가 질문하면 관련 문서를 찾아줘야 하는데, 단순히 키워드 매칭만으로는 부족해요." 박시니어 씨가 스케치북을 꺼내며 말했습니다.
"텍스트를 숫자로 변환하는 임베딩을 배워볼까요?"
**임베딩(Embedding)**은 텍스트, 이미지, 오디오 같은 데이터를 고차원 벡터 공간의 숫자 배열로 변환하는 기술입니다. 마치 언어의 의미를 지도 위의 좌표로 표현하는 것과 같습니다.
의미가 비슷한 텍스트는 벡터 공간에서 가까운 위치에 배치되므로, 의미 기반 검색과 유사도 계산이 가능해집니다.
다음 코드를 살펴봅시다.
from openai import OpenAI
client = OpenAI()
# 텍스트를 임베딩 벡터로 변환
response = client.embeddings.create(
model="text-embedding-3-small",
input="에이전트 AI는 자율적으로 작업을 수행하는 시스템입니다"
)
vector = response.data[0].embedding
print(f"벡터 차원: {len(vector)}") # 1536차원
print(f"처음 5개 값: {vector[:5]}")
# 두 문장의 유사도 계산
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
sentences = [
"에이전트 AI는 자율적으로 작업을 수행합니다",
"AI 비서가 스스로 업무를 처리합니다",
"오늘 점심 메뉴는 불고기입니다"
]
vectors = [client.embeddings.create(
model="text-embedding-3-small", input=s
).data[0].embedding for s in sentences]
print(f"문장1-문장2 유사도: {cosine_similarity(vectors[0], vectors[1]):.4f}")
print(f"문장1-문장3 유사도: {cosine_similarity(vectors[0], vectors[2]):.4f}")
김개발 씨는 팀장님께 받은 과제가 있었습니다. 회사의 모든 기술 문서를 검색할 수 있는 시스템을 만드는 것.
단순한 키워드 검색이 아니라, 의미를 이해하고 관련 문서를 찾아주는 "스마트한" 검색이어야 했습니다. "전통적인 검색은 키워드 매칭이에요." 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.
"사용자가 '에이전트'라고 검색하면, 문서에 '에이전트'라는 단어가 있어야만 찾아줍니다. 하지만 'AI 비서'나 '자율 시스템'이라는 글에는 '에이전트'라는 단어가 없어서 검색이 안 되죠." 김개발 씨가 고개를 끄덕였습니다.
맞습니다. 의미는 비슷하지만 단어가 다르면 찾을 수 없는 것이 기존 검색의 한계였습니다.
"바로 이 문제를 해결하는 것이 **임베딩(Embedding)**이에요." 박시니어 씨가 말했습니다. 임베딩이란 무엇일까요?
쉽게 비유하자면, 임베딩은 마치 의미의 지도를 만드는 것과 같습니다. 세상의 모든 단어와 문장을 거대한 지도 위에 점으로 찍는데, 의미가 비슷한 것들은 서로 가까운 위치에, 의미가 다른 것들은 멀리 떨어진 위치에 배치합니다.
예를 들어 "강아지"와 "고양이"는 지도에서 가까이 있고, "강아지"와 "자동차"는 멀리 있겠죠. 이 지도에서 각 점의 위치를 숫자로 표현한 것이 바로 **벡터(Vector)**입니다.
보통 1536개나 3072개의 숫자로 하나의 문장을 표현합니다. 왜 이런 변환이 필요할까요?
컴퓨터는 텍스트를 직접 이해하지 못합니다. 하지만 숫자는 계산할 수 있습니다.
두 벡터 사이의 거리를 계산하면, 두 문장이 얼마나 의미적으로 비슷한지 수학적으로 판단할 수 있습니다. 이것이 바로 **의미 기반 검색(Semantic Search)**의 핵심입니다.
임베딩의 동작 과정을 살펴보겠습니다. 먼저 텍스트를 임베딩 모델에 입력합니다.
OpenAI의 text-embedding-3-small 같은 전용 모델을 사용합니다. 일반 챗봇 모델과는 다른, 임베딩 전용으로 학습된 모델이죠.
모델은 텍스트를 분석하여 고차원 벡터를 생성합니다. 위 코드에서 볼 수 있듯이, 하나의 문장이 1536개의 실수 배열로 변환됩니다.
이 숫자들 각각이 문장의 다양한 의미적 특징을 담고 있습니다. 그다음 코사인 유사도(Cosine Similarity) 같은 수학적 방법으로 두 벡터의 유사도를 계산합니다.
코사인 유사도는 -1에서 1 사이의 값을 가지며, 1에 가까울수록 두 문장이 의미적으로 유사함을 의미합니다. 위 코드의 실행 결과를 상상해보세요.
"에이전트 AI는 자율적으로 작업을 수행합니다"와 "AI 비서가 스스로 업무를 처리합니다"의 유사도는 0.85 정도로 높게 나올 것입니다. 반면 "오늘 점심 메뉴는 불고기입니다"와의 유사도는 0.1 정도로 낮겠죠.
실제 현업에서 임베딩은 어떻게 활용될까요? 가장 대표적인 활용처가 RAG 시스템입니다.
수많은 문서를 미리 임베딩해서 벡터 데이터베이스(Pinecone, Weaviate, ChromaDB 등)에 저장합니다. 사용자가 질문하면 질문도 임베딩으로 변환하고, 벡터 데이터베이스에서 가장 유사한 문서를 검색한 뒤, 그 문서를 바탕으로 LLM이 답변을 생성합니다.
또한 추천 시스템에서도 활용됩니다. 사용자가 본 문서와 임베딩이 비슷한 다른 문서를 추천하는 것이죠.
문서 분류, 클러스터링, 중복 제거 등 다양한 NLP 작업의 기반이 되기도 합니다. 주의할 점도 있습니다.
임베딩 모델의 선택이 결과에 큰 영향을 미칩니다. OpenAI 임베딩, 코히어 임베딩, 오픈소스 모델 등 다양한 선택지가 있고, 각각 장단점이 다릅니다.
벡터 차원이 클수록 더 정밀한 표현이 가능하지만, 저장 공간과 연산 비용도 증가합니다. 또한 임베딩은 문맥 길이에 제한이 있습니다.
text-embedding-3-small은 최대 8191 토큰까지 처리할 수 있습니다. 긴 문서는 청크(Chunk)로 나누어 각각 임베딩해야 합니다.
이때 청크의 크기와 겹침(Overlap) 설정이 검색 품질에 큰 영향을 미칩니다. 김개발 씨는 화이트보드의 벡터 지도를 바라보며 감탄했습니다.
"텍스트가 숫자가 되고, 숫자가 의미가 되는 거군요. 마치 세상의 모든 언어가 하나의 수학 공간에서 만나는 것 같아요." 박시니어 씨가 미소를 지었습니다.
"맞아요. 임베딩을 이해하면 RAG, 시맨틱 검색, 추천 시스템 등 수많은 응용 분야의 문이 열립니다.
다음 장에서는 이 임베딩을 활용한 벡터 데이터베이스까지 다룰 거예요."
실전 팁
💡 - 임베딩 모델은 용도에 맞게 선택하세요. 다국어 지원이 필요하면 multilingual 모델을 고려하세요.
- 벡터 데이터베이스를 선택할 때는 규모, 지연 시간, 비용을 종합적으로 고려해야 합니다.
- 문서를 청크로 나눌 때는 256
512 토큰, 1020% 겹침을 기본 설정으로 시작해보세요. - 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
4. 함수 호출과 임베딩의 결합 RAG 파이프라인
김개발 씨가 두 가지를 배운 후 궁금해졌습니다. "함수 호출로 도구를 쓰고, 임베딩으로 검색을 하는데, 이 둘을 같이 쓰면 더 강력한 시스템이 되지 않을까요?" 박시니어 씨가 눈을 빛내며 대답했습니다.
"그게 바로 실전 에이전트의 핵심이죠."
RAG 파이프라인은 임베딩 기반의 문서 검색과 함수 호출 기반의 도구 실행을 결합하여, 지식 기반 답변과 실시간 데이터 처리를 동시에 수행하는 시스템 아키텍처입니다. 마치 도서관에서 필요한 책을 찾고(임베딩 검색), 동시에 최신 뉴스도 확인하는(함수 호출) 것과 같습니다.
다음 코드를 살펴봅시다.
import openai
import numpy as np
# RAG + Function Calling 결합 파이프라인
tools = [{
"type": "function",
"function": {
"name": "search_documents",
"description": "회사 문서에서 관련 내용을 검색합니다",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "검색어"}
},
"required": ["query"]
}
}
}, {
"type": "function",
"function": {
"name": "get_current_time",
"description": "현재 날짜와 시간을 반환합니다",
"parameters": {"type": "object", "properties": {}}
}
}]
# 사용자 질문: 문서 검색 + 실시간 정보 필요
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "문서 검색 결과와 실시간 정보를 "
"결합하여 정확한 답변을 제공하세요."},
{"role": "user", "content": "우리 서비스 장애 복구 가이드를 알려줘. "
"현재 몇 시인지도 같이 알려줘."}
],
tools=tools,
tool_choice="auto"
)
# LLM이 두 함수를 모두 호출하도록 유도
for tool_call in response.choices[0].message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"호출: {func_name}({func_args})")
김개발 씨는 지난 시간에 함수 호출과 임베딩을 각각 배웠습니다. 두 가지가 각각 강력하다는 것은 이해했지만, 실무에서는 어떻게 함께 사용되는지 궁금했습니다.
"박시니어 씨, 함수 호출은 외부 도구를 쓰고, 임베딩은 문서를 검색하는 건가요? 그러면 두 개를 합치면 뭐가 되는 건데요?" 박시니어 씨가 자신의 모니터를 돌려 보여주었습니다.
화면에는 복잡해 보이지만 논리적인 흐름도가 그려져 있었습니다. "이게 바로 실전 에이전트의 기본 아키텍처, RAG 파이프라인이에요." 쉽게 비유하자면, RAG 파이프라인은 마치 스마트한 비서와 같습니다.
비서에게 질문하면, 먼저 사내 문서를 뒤져 관련 자료를 찾고(임베딩 검색), 필요하면 외부에 연락해 최신 정보도 확인한 뒤(함수 호출), 모든 정보를 종합해서 답변을 줍니다. 이 아키텍처가 강력한 이유는 세 가지 기능이 결합되기 때문입니다.
첫 번째는 지식 검색입니다. 임베딩 기반의 벡터 데이터베이스에서 관련 문서를 검색합니다.
이것은 회사의 고유한 지식, 과거 프로젝트 기록, 기술 문서 등을 활용하는 데 필수적입니다. 두 번째는 도구 실행입니다.
함수 호출을 통해 실시간 정보를 가져오거나 외부 시스템과 상호작용합니다. 날씨, 주식, 데이터베이스 조회, 이메일 발송 등 다양한 작업이 가능합니다.
세 번째는 지능적 판단입니다. LLM이 사용자의 질문을 분석하여, 어떤 도구를 사용할지, 검색 결과를 어떻게 종합할지 스스로 결정합니다.
이것이 단순한 자동화와 에이전트의 차이점입니다. 위 코드를 보면 이 구조가 잘 드러납니다.
tools 배열에 문서 검색 함수와 시간 확인 함수를 모두 등록했습니다. 사용자가 "장애 복구 가이드"와 "현재 시간"을 동시에 요청하면, LLM은 두 함수를 각각 호출합니다.
파이프라인의 전체 흐름을 단계별로 살펴보겠습니다. 1단계: 사용자가 질문을 입력합니다.
2단계: LLM이 질문을 분석하고 필요한 도구를 선택합니다. 3단계: 애플리케이션이 도구를 실행하고 결과를 수집합니다.
4단계: 도구 실행 결과를 LLM에게 다시 전달합니다. 5단계: LLM이 모든 정보를 종합하여 최종 답변을 생성합니다.
이 과정에서 중요한 것은 도구 선택의 정확성입니다. LLM이 "장애 복구 가이드"라는 질문에 search_documents를 선택하고, "현재 시간"에 get_current_time을 선택할 수 있어야 합니다.
이것이 바로 함수의 description이 중요한 이유입니다. 실무에서는 더 복잡한 구성이 일반적입니다.
10개 이상의 도구를 등록하고, 벡터 데이터베이스에 수만 개의 문서를 저장하며, 사용자 권한에 따라 접근 가능한 도구를 다르게 구성합니다. 하지만 복잡해질수록 고려해야 할 것도 많아집니다.
도구가 너무 많으면 LLM이 올바른 도구를 선택하기 어려워집니다. 이럴 때는 도구를 카테고리별로 그룹화하거나, 먼저 어떤 카테고리의 도구가 필요한지 선택하게 하는 다단계 라우팅을 적용합니다.
또한 각 도구 실행에 대한 시간 제한(Timeout) 설정도 필수입니다. 외부 API 응답이 지연되면 전체 파이프라인이 멈출 수 있기 때문입니다.
김개발 씨는 화면의 흐름도를 따라가며 이해했습니다. "그러면 에이전트란 이런 파이프라인을 자동으로 orchestration 하는 시스템이라고 볼 수 있겠네요!" 박시니어 씨가 엄지를 치켜세웠습니다.
"정확합니다. 이 구조가 다음에 배울 프레임워크들(LangGraph, CrewAI, AutoGen)의 기반이 될 거예요."
실전 팁
💡 - 도구가 많을 때는 관련 도구끼리 그룹화하고, LLM이 단계적으로 선택하도록 구성하세요.
- 각 도구에 적절한 시간 제한과 에러 처리를 반드시 구현하세요.
- 도구 실행 로그를 구조화하여 저장하면, 문제 발생 시 원인 추적이 쉽습니다.
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
5. 환각 감지와 방어 전략
김개발 씨가 드디어 첫 에이전트 시스템을 프로덕션에 배포했습니다. 며칠 동안 잘 돌아가더니, 어느 날 사용자에게 완전히 틀린 정보를 전달한 사건이 발생했습니다.
"어떻게 환각을 사전에 감지할 수 있을까요?" 김개발 씨는 이제 방어가 필요하다는 것을 깨달았습니다.
환각 감지와 방어 전략은 LLM이 생성한 답변의 신뢰성을 평가하고, 잘못된 정보가 사용자에게 전달되는 것을 막는 체계적인 접근법입니다. 마치 식당에서 위생 검사관이 음식을 내기 전에 검사하는 것과 같습니다.
에이전트 시스템을 프로덕션에 배포하려면 이 방어 계층이 반드시 필요합니다.
다음 코드를 살펴봅시다.
import openai
def generate_with_guardrails(query, context):
# 1단계: RAG로 관련 문서 검색 후 답변 생성
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": """다음 규칙을 반드시 따르세요:
3. 답변 끝에 신뢰도를 [높음/중간/낮음]으로 표시하세요"""},
김개발 씨의 첫 프로덕션 배포. 처음 며칠은 성공적이었습니다.
사용자들로부터 "정말 편하다"는 피드백이 들어왔습니다. 하지만 일주일째, 한 사용자가 불만을 접수했습니다.
"이 시스템이 알려준 API 엔드포인트가 존재하지 않아요. 개발팀에서 확인해봤는데, 그런 API는 없다고 하네요." 김개발 씨의 얼굴이 하얘졌습니다.
LLM이 존재하지 않는 API를 만들어낸 것이었습니다. 전형적인 환각이었습니다.
"프로덕션에서 환각은 버그가 아니라 재앙이에요." 박시니어 씨가 회의실에서 진지하게 말했습니다. "LLM의 답변을 그대로 사용자에게 전달하면 안 됩니다.
반드시 **가드레일(Guardrail)**을 설치해야 합니다." 가드레일이란 무엇일까요? 쉽게 비유하자면, 가드레일은 마치 자동차의 안전벨트와 에어백과 같습니다.
사고가 나더라도 탑승자를 보호하는 시스템이죠. 에이전트 시스템에서 가드레일은 LLM의 답변을 사용자에게 전달하기 전에 검사하는 방어 계층입니다.
환각 방어 전략은 크게 세 가지 레이어로 구성됩니다. 첫 번째 레이어는 프롬프트 레벨 방어입니다.
시스템 프롬프트에 명확한 제약을 설정합니다. "문서에 없는 내용은 포함하지 마세요", "모르면 모른다고 답하세요" 같은 지침이 여기에 해당합니다.
위 코드에서도 첫 번째 응답 생성 시 세 가지 규칙을 부여하고 있습니다. 두 번째 레이어는 검증 레벨 방어입니다.
LLM이 생성한 답변을 별도의 모델이나 규칙 기반 시스템으로 검증합니다. 위 코드의 두 번째 단계가 바로 이 역할을 합니다.
더 작고 빠른 모델(gpt-4o-mini)을 사용해 비용을 절감하면서도 검증 효과를 얻을 수 있습니다. 세 번째 레이어는 아키텍처 레벨 방어입니다.
RAG를 통해 항상 문서 기반으로 답변하게 하거나, 답변에 출처를 표시하거나, 신뢰도가 낮은 답변은 자동으로 에스컬레이션하는 구조를 만듭니다. 실무에서 효과적인 환전 방어 패턴 몇 가지를 소개하겠습니다.
신뢰도 자가 평가(Self-Evaluation) 패턴이 있습니다. LLM에게 답변과 함께 자신의 신뢰도를 평가하게 하는 방식입니다.
위 코드에서 답변 끝에 [높음/중간/낮음]을 표시하도록 한 것이 이 패턴의 적용입니다. 신뢰도가 낮으면 사용자에게 "확인이 필요합니다"라고 안내합니다.
다중 모델 교차 검증 패턴도 있습니다. 두 개 이상의 LLM에 같은 질문을 하고, 답변이 일치하는지 비교합니다.
일치하지 않으면 검토가 필요한 것으로 표시합니다. 비용은 높아지지만, 민감한 도메인에서는 효과적입니다.
출처 추적(Attribution) 패턴도 중요합니다. 답변의 각 부분이 어떤 문서에서 왔는지 출처를 표시하면, 사용자가 직접 검증할 수 있습니다.
또한 나중에 환각이 발견되었을 때 원인을 추적하기도 쉽습니다. 주의할 점이 있습니다.
가드레일을 너무 엄격하게 설정하면 정상적인 답변까지 차단될 수 있습니다. "확실하지 않으면 모른다고 답하세요"라고 하면, 실제로는 아는 것도 모른다고 답하는 과도한 보수성이 나타납니다.
이 정확도-커버리지 트레이드오프를 적절히 조절하는 것이 에이전트 엔지니어의 역량입니다. 또한 검증 모델도 완벽하지 않습니다.
검증 모델 자체가 환각을 일으키거나, 오탐지(False Positive)를 할 수 있습니다. 따라서 검증 결과에 대해서도 로그를 남기고, 주기적으로 검증 품질을 모니터링해야 합니다.
김개발 씨는 노트에 적으며 말했습니다. "가드레일은 선택이 아니라 필수.
그리고 가드레일 자체도 검증해야 한다." 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
에이전트 시스템의 신뢰성은 한 번에 확보되는 게 아니에요. 지속적인 모니터링과 개선으로 쌓아가는 거죠."
실전 팁
💡 - 프로덕션 배포 전 반드시 가드레일 계층을 구현하세요. LLM 답변을 직접 사용자에게 전달하는 것은 위험합니다.
- 신뢰도를 수치화하여 로그에 기록하면, 시간에 따른 시스템 품질 변화를 추적할 수 있습니다.
- 사용자에게 "이 답변은 AI가 생성한 것으로, 사실 확인이 필요할 수 있습니다"라는 안내를 포함하는 것이 좋습니다.
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
6. 임베딩 실전 벡터 데이터베이스와 청킹 전략
김개발 씨가 RAG 시스템을 만들려고 보니, 임베딩만으로는 부족했습니다. "문서가 수천 개인데, 매번 전체를 임베딩해서 비교할 수는 없잖아요." 박시니어 씨가 데이터베이스 설계서를 펼치며 말했습니다.
"벡터 전용 데이터베이스와 문서 분할 전략을 알려드릴게요."
**벡터 데이터베이스와 청킹(Chunking)**은 대규모 문서를 임베딩하여 효율적으로 검색하기 위한 핵심 인프라와 기술입니다. 벡터 데이터베이스는 수백만 개의 벡터를 저장하고 유사도 검색을 밀리초 단위로 수행합니다.
청킹은 긴 문서를 의미 있는 단위로 분할하여 검색 정확도를 높이는 전략입니다.
다음 코드를 살펴봅시다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 긴 문서를 의미 있는 청크로 분할
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 각 청크의 최대 길이
chunk_overlap=50, # 청크 간 겹침 (문맥 보존)
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""] # 우선 순위별 분할 기준
)
long_document = """
에이전트 AI는 대규모 언어 모델(LLM)의 능력을 확장하는 시스템입니다.
에이전트는 도구를 사용하고, 계획을 세우며, 복잡한 작업을 수행합니다.
함수 호출은 에이전트가 외부 시스템과 상호작용하는 핵심 메커니즘입니다.
개발자는 함수를 정의하고, LLM이 적절한 시점에 호출하도록 합니다.
RAG는 검색 증강 생성으로, 문서 검색과 LLM 생성을 결합합니다.
벡터 데이터베이스에 문서를 저장하고 유사도 검색으로 관련 문서를 찾습니다.
"""
chunks = text_splitter.split_text(long_document)
for i, chunk in enumerate(chunks):
print(f"청크 {i+1}: {chunk[:80]}...")
# ChromaDB에 벡터 저장
import chromadb
client = chromadb.PersistentClient(path="./vector_db")
collection = client.get_or_create_collection("documents")
# 문서 저장 (자동 임베딩)
collection.add(
documents=chunks,
ids=[f"doc_{i}" for i in range(len(chunks))],
metadatas=[{"source": "agent_ai_guide", "chunk": i}
for i in range(len(chunks))]
)
# 유사도 검색
results = collection.query(
query_texts=["에이전트가 도구를 사용하는 방법"],
n_results=3
)
print(f"검색 결과: {results['documents'][0][0][:100]}")
김개발 씨는 이론으로 임베딩을 이해했지만, 실제 구현에 부딪혔습니다. 회사 기술 문서가 5,000개가 넘었고, 각 문서의 평균 길이는 20페이지였습니다.
이걸 어떻게 검색 가능하게 만들까요? "먼저 **청킹(Chunking)**부터 알아야 해요." 박시니어 씨가 화이트보드에 긴 문서를 여러 조각으로 나누는 그림을 그렸습니다.
청킹이란 무엇일까요? 쉽게 비유하자면, 청킹은 마치 긴 영화를 예고편, 장면별 하이라이트로 나누는 것과 같습니다.
2시간짜리 영화 전체를 한 번에 보여주는 것보다, 관련 장면만 빠르게 찾아 보여주는 것이 효율적이죠. 문서도 마찬가지입니다.
20페이지짜리 문서 전체를 하나의 임베딩으로 처리하면, 세부 내용이 희석되어 검색 정확도가 떨어집니다. 그렇다고 무작정 자르면 안 됩니다.
문장 중간이 잘리면 의미가 파괴되기 때문입니다. 따라서 의미 있는 단위로 자르는 전략이 필요합니다.
가장 일반적인 전략이 RecursiveCharacterTextSplitter입니다. 위 코드에서 사용한 방식인데, 우선 순위별로 분할 기준을 정합니다.
먼저 단락(\n\n)으로 나누고, 그다음 줄바꿈(\n), 문장(. ), 단어( ) 순서로 시도합니다.
문단 단위로 먼저 나누려고 하고, 문단이 너무 길면 문장 단위로 나누는 것이죠. 여기서 핵심 파라미터 두 가지가 있습니다.
chunk_size는 각 청크의 최대 길이를 결정합니다. 너무 작으면 문맥이 부족하고, 너무 크면 검색 정확도가 떨어집니다.
일반적으로 256~1024 토큰 사이에서 시작하며, 도메인에 따라 조정합니다. 기술 문서는 500토큰, 법률 문서는 1000토큰 정도가 적절한 경우가 많습니다.
chunk_overlap은 인접한 청크 간에 겹치는 부분의 길이입니다. 이것이 중요한 이유는, 문서를 자를 때 의미가 분절되는 것을 방지하기 위해서입니다.
예를 들어 "에이전트는 도구를 사용합니다. 도구에는 API 호출, 파일 읽기, 데이터베이스 조회가 있습니다."라는 문장이 두 청크로 나뉘면, "도구에는 API 호출"이라는 부분이 앞 청크의 끝과 뒤 청크의 시작에 겹치도록 해야 문맥이 유지됩니다.
보통 chunk_size의 10~20%를 겹침으로 설정합니다. 청킹을 마쳤으면 이제 벡터 데이터베이스에 저장할 차례입니다.
벡터 데이터베이스는 일반 관계형 데이터베이스와 다릅니다. 일반 데이터베이스는 "이름이 '김개발'인 행을 찾아줘" 같은 정확한 매칭에 특화되어 있습니다.
반면 벡터 데이터베이스는 "이 벡터와 가장 비슷한 벡터 5개를 찾아줘" 같은 근사 최근접 이웃(ANN) 검색에 최적화되어 있습니다. 대표적인 벡터 데이터베이스로는 ChromaDB, Pinecone, Weaviate, Qdrant, Milvus 등이 있습니다.
위 코드에서는 로컬 개발에 적합한 ChromaDB를 사용했습니다. 파일 기반으로 동작하므로 설치가 간단하고, 프로토타입 개발에 적합합니다.
벡터 데이터베이스를 선택할 때 고려해야 할 요소는 다음과 같습니다. 데이터 규모: 수만 개 이하면 ChromaDB, 수십만 개 이상이면 Pinecone이나 Weaviate를 고려합니다.
지연 시간: 실시간 응답이 필요하면 인메모리 기반 Qdrant가 유리할 수 있습니다. 비용: 오픈소스(ChromaDB, Qdrant)는 자체 호스팅, 상용(Pinecone)은 관리형 서비스입니다.
메타데이터 필터링: 문서의 출처, 날짜, 카테고리 등으로 검색 결과를 필터링해야 하는 경우가 많습니다. 위 코드에서 metadatas 파라미터에 {"source": "agent_ai_guide"}를 추가한 것이 이를 위한 것입니다.
나중에 "agent_ai_guide에서 나온 문서만 검색" 같은 조건을 걸 수 있습니다. 청킹과 벡터 데이터베이스의 조합이 RAG 시스템의 성능을 결정한다고 해도 과언이 아닙니다.
청킹이 잘못되면 관련 문서를 찾지 못하고, 벡터 데이터베이스가 느리면 사용자 경험이 저하됩니다. 김개발 씨는 화이트보드의 청킹 다이어그램을 사진으로 찍으며 말했습니다.
"문서 분할 하나가 이렇게 중요한 거군요. 단순히 자르는 게 아니라, 의미를 보존하면서 자르는 거구나." 박시니어 씨가 덧붙였습니다.
"그리고 이걸 실제 서비스에 넣으려면 모니터링도 필요해요. 어떤 청크가 검색에 잘 안 걸리는지, 검색 결과의 관련성 점수는 얼마인지 지속적으로 추적해야죠."
실전 팁
💡 - 청킹 전략은 도메인에 맞게 실험으로 최적화하세요. 기본값(500토큰, 10% 겹침)에서 시작해보세요.
- 메타데이터를 풍부하게 저장하세요. 문서 출처, 작성일, 버전 등은 검색 필터링에 필수적입니다.
- 프로덕션에서는 벡터 데이터베이스의 인덱스를 정기적으로 재구축하여 검색 성능을 유지하세요.
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
7. 함수 호출 고급 패턴 다중 도구와 에러 처리
김개발 씨의 에이전트가 점점 정교해지고 있었습니다. 이제 날씨 조회, 문서 검색, 이메일 발송까지 10개의 도구를 등록했습니다.
그런데 어느 날 시스템이 멈춰버렸습니다. 이메일 발송 API가 응답하지 않자, 전체 파이프라인이 함께 멈춘 것이었습니다.
다중 도구 관리와 에러 처리는 여러 함수 호출을 동시에 조율하고, 각 도구의 실패가 전체 시스템의 장애로 이어지지 않도록 방어적으로 설계하는 고급 패턴입니다. 마치 항공 관제사가 여러 비행기를 동시에 관리하면서도 한 비행기의 문제가 공항 전체를 멈추게 하지 않는 것과 같습니다.
다음 코드를 살펴봅시다.
import asyncio
import json
from openai import OpenAI
client = OpenAI()
async def safe_tool_call(tool_name, tool_args, timeout=5.0):
"""도구 호출 래퍼 - 타임아웃과 에러 처리 포함"""
tool_registry = {
"get_weather": lambda args: fetch_weather(args["city"]),
"send_email": lambda args: send_email(args["to"], args["body"]),
"search_docs": lambda args: search_documents(args["query"])
}
try:
result = await asyncio.wait_for(
asyncio.to_thread(tool_registry[tool_name], tool_args),
timeout=timeout
)
return {"status": "success", "data": result}
except asyncio.TimeoutError:
return {"status": "error", "message": f"{tool_name} 타임아웃 발생"}
except Exception as e:
return {"status": "error", "message": str(e)}
# 병렬 도구 실행
async def execute_tools_parallel(tool_calls):
tasks = [
safe_tool_call(
tc.function.name,
json.loads(tc.function.arguments)
) for tc in tool_calls
]
results = await asyncio.gather(*tasks)
return results
# LLM 응답에서 도구 호출 추출 후 병렬 실행
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user",
"content": "서울 날씨 알려주고, 팀에 메일도 보내줘"}],
tools=weather_and_email_tools,
tool_choice="auto"
)
if response.choices[0].message.tool_calls:
results = asyncio.run(
execute_tools_parallel(response.choices[0].message.tool_calls)
)
for r in results:
print(f"결과: {r['status']} - {r.get('message', '')}")
김개발 씨의 에이전트가 사용자에게 인기를 끌고 있었습니다. "이거 하나로 날씨도 확인하고, 문서도 검색하고, 이메일도 보내고, 일정도 잡아줘요!" 팀원들의 반응이 좋았습니다.
하지만 성공이 문제를 낳았습니다. 도구가 10개로 늘어나자, 관리가 복잡해졌습니다.
그리고 그날, 이메일 발송 API 서버가 다운되면서 전체 시스템이 멈춰버렸습니다. "이메일 하나 못 보내는데 왜 시스템 전체가 멈춰요?" 팀장님의 항의에 김개발 씨는 할 말이 없었습니다.
박시니어 씨가 코드 리뷰를 시작했습니다. "현재 구조의 문제는 도구 호출이 순차적이라는 거예요.
그리고 하나라도 실패하면 전체가 실패하죠. 이걸 해결하려면 병렬 실행과 에러 격리가 필요해요." 쉽게 비유하자면, 현재 시스템은 마치 한 줄로 서 있는 식당과 같습니다.
앞사람이 주문을 망설이면 뒤사람은 전부 기다려야 합니다. 하지만 식당이 여러 창구를 열면(병렬 실행), 각 창구에서 독립적으로 주문을 처리할 수 있습니다.
그리고 한 창구가 문제가 생겨도 다른 창구는 정상적으로 동작하죠(에러 격리). 해결의 핵심은 세 가지입니다.
첫 번째는 병렬 도구 실행입니다. Python의 asyncio를 활용하면 여러 도구 호출을 동시에 실행할 수 있습니다.
위 코드의 execute_tools_parallel 함수가 이 역할을 합니다. LLM이 여러 도구를 호출하도록 요청하면, asyncio.gather로 모두 동시에 실행합니다.
날씨 조회와 이메일 발송이 동시에 진행되므로 전체 응답 시간이 크게 단축됩니다. 두 번째는 타임아웃 설정입니다.
각 도구 호출에 제한 시간을 설정합니다. safe_tool_call 함수에서 timeout=5.0으로 설정한 것이죠.
외부 API가 5초 안에 응답하지 않으면 TimeoutError가 발생하고, 해당 도구만 실패 처리됩니다. 시스템 전체가 멈추지 않습니다.
세 번째는 에러 격리입니다. try-except로 각 도구의 에러를 개별적으로 처리합니다.
이메일 발송이 실패해도 날씨 조회 결과는 정상적으로 반환됩니다. 그리고 실패한 도구의 정보를 LLM에게 전달하여, "이메일 발송은 실패했지만 날씨 정보는 여기 있습니다"라고 사용자에게 안내할 수 있습니다.
이 패턴을 실제 코드로 살펴보겠습니다. tool_registry 딕셔너리는 도구 이름과 실제 실행 함수를 매핑합니다.
이 레지스트리 패턴을 사용하면 새로운 도구를 추가하기 쉽습니다. 딕셔너리에 한 줄만 추가하면 되니까요.
asyncio.to_thread는 동기 함수를 비동기로 실행하는 래퍼입니다. 기존에 작성된 동기 함수(예: requests 기반 API 호출)를 async 환경에서도 사용할 수 있습니다.
asyncio.wait_for는 타임아웃 기능을 추가합니다. 지정된 시간 안에 완료되지 않으면 TimeoutError를 발생시킵니다.
asyncio.gather는 여러 비동기 작업을 동시에 실행하고 모든 결과를 수집합니다. 하나의 작업이 실패해도 다른 작업은 계속 진행됩니다.
실무에서 추가로 고려해야 할 점이 있습니다. 재시도(Retry) 전략입니다.
일시적인 네트워크 오류 같은 경우에는 자동 재시도가 효과적입니다. tenacity 같은 라이브러리를 사용하면 지수 백오프(Exponential Backoff)와 함께 재시도를 구현할 수 있습니다.
**속도 제한(Rate Limiting)**도 중요합니다. 외부 API의 호출 제한을 초과하면 차단될 수 있습니다.
asyncio.Semaphore를 사용하면 동시에 실행되는 도구 호출 수를 제한할 수 있습니다. 결과 캐싱도 고려해야 합니다.
같은 질문에 대해 같은 도구를 반복 호출하는 것을 방지하기 위해, 이전 결과를 캐시하면 응답 속도와 비용을 절감할 수 있습니다. 김개발 씨는 리팩토링을 마치고 테스트를 실행했습니다.
이번에는 이메일 API가 다운되어도, 날씨 조회와 문서 검색은 정상적으로 동작했습니다. "실패한 도구만 에러를 표시하고 나머지는 정상 결과를 보여주네요." 팀장님이 만족스러운 표정을 지었습니다.
"좋아요. 이제 여러 도구를 안전하게 관리할 수 있겠네요."
실전 팁
💡 - 모든 도구 호출에 타임아웃을 설정하세요. 외부 API는 언제든 응답이 지연될 수 있습니다.
- 실패한 도구의 에러 메시지를 LLM에게 전달하세요. LLM이 대안을 제시하거나 사용자에게 적절히 안내할 수 있습니다.
- 도구 레지스트리 패턴을 사용하면 새 도구 추가와 관리가 체계적으로 이루어집니다.
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
8. 다음 단계 프레임워크로 에이전트를 만들어봅시다
김개발 씨는 지난 시간에 걸쳐 LLM의 핵심 원리를 모두 배웠습니다. 함수 호출로 도구를 연결하고, 환각을 방어하고, 임베딩으로 지식을 검색하고, 이 모든 것을 파이프라인으로 결합하는 방법까지.
이제 직접 손으로 코딩한 모든 것을 이해했지만, 박시니어 씨가 말했습니다. "이제 프레임워크를 쓸 차례예요."
지금까지 배운 함수 호출, 환각 방어, 임베딩, RAG 파이프라인의 원리를 바탕으로, 다음 단계에서는 LangGraph, CrewAI, AutoGen 같은 에이전트 프레임워크를 배우게 됩니다. 이 프레임워크들은 우리가 직접 구현한 파이프라인을 더 구조화되고 확장 가능한 형태로 제공합니다.
마치 자전거를 직접 만들어 타본 사람이 자동차를 다루기 쉬운 것과 같습니다.
다음 코드를 살펴봅시다.
# 지금까지 배운 것을 한눈에 복습
core_concepts = {
"함수 호출": "LLM이 외부 도구를 사용하는 메커니즘",
"환각": "LLM이 존재하지 않는 정보를 사실처럼 생성하는 현상",
"임베딩": "텍스트를 벡터로 변환하여 의미를 수학적으로 표현",
"RAG": "문서 검색과 LLM 생성을 결합한 아키텍처",
"가드레일": "LLM 답변을 검증하는 방어 계층",
"벡터 DB": "임베딩을 저장하고 초고속 유사도 검색 제공",
"청킹": "긴 문서를 의미 있는 단위로 분할하는 전략"
}
# 다음 단계: 프레임워크로의 진화
frameworks = {
"LangGraph": "상태 기반 그래프로 에이전트 워크플로우 정의",
"CrewAI": "역할 기반 다중 에이전트 협업 시스템",
"AutoGen": "대화 기반 에이전트 간 통신 프레임워크"
}
# 이 원리들이 프레임워크에서 어떻게 추상화되는지
for concept, desc in core_concepts.items():
print(f"직접 구현: {concept} - {desc}")
print("\n다음 장에서 배울 프레임워크:")
for fw, desc in frameworks.items():
print(f" {fw}: {desc}")
김개발 씨는 지난 시간 동안 LLM의 핵심 원리를 깊이 있게 배웠습니다. 함수 호출의 메커니즘부터 환각 방어 전략, 임베딩과 벡터 데이터베이스, RAG 파이프라인까지.
원리를 이해하고 직접 코드로 구현해보니, LLM이 어떻게 작동하는지 투명하게 보이기 시작했습니다. "근데 박시니어 씨, 이걸 매번 이렇게 직접 구현해야 하나요?" 김개발 씨가 물었습니다.
"함수 호출도 직접 파싱하고, 도구 레지스트리도 직접 만들고, 에러 처리도 다 손으로..." 박시니어 씨가 웃으며 대답했습니다. "아니요.
원리를 이해한 다음에는 프레임워크를 쓰면 돼요. 원리를 먼저 배운 덕분에 프레임워크가 어떻게 동작하는지 이해할 수 있을 거예요." 쉽게 비유하자면, 지금까지의 학습은 마치 자전거의 원리를 이해하고 직접 조립해본 것과 같습니다.
페달이 어떻게 동력을 전달하고, 브레이크가 어떻게 제동하는지 이해했죠. 이제 자동차를 배우러 가는 겁니다.
엔진, 변속기, 브레이크의 원리를 이미 알고 있으니, 운전대를 잡고 주행하는 것을 배우기만 하면 됩니다. 에이전트 프레임워크가 우리가 직접 구현한 것에 비해 제공하는 이점은 명확합니다.
첫 번째는 구조화된 워크플로우입니다. 우리가 직접 구현한 파이프라인은 선형적이었습니다.
질문 -> 도구 호출 -> 답변. 하지만 실제 에이전트는 더 복잡한 흐름이 필요합니다.
조건 분기, 반복, 병렬 실행, 롤백 등. 프레임워크는 이런 복잡한 흐름을 선언적으로 정의할 수 있게 해줍니다.
두 번째는 상태 관리입니다. 에이전트가 여러 단계를 거치면서 중간 결과를 저장하고, 이전 단계의 결과를 다음 단계에서 참조해야 합니다.
프레임워크는 이 상태 관리를 내장하고 있습니다. 세 번째는 재사용 가능한 컴포넌트입니다.
프롬프트 템플릿, 도구 정의, 검증 로직 등을 재사용 가능한 컴포넌트로 만들 수 있습니다. 다음 장에서 배울 세 가지 프레임워크를 미리 살펴보겠습니다.
LangGraph는 상태 기반(State-based) 그래프 구조로 에이전트를 정의합니다. 노드는 각 처리 단계를 나타내고, 엣지는 단계 간의 전환을 나타냅니다.
복잡한 워크플로우를 시각적으로 설계하고 구현할 수 있어, 제어 흐름이 복잡한 에이전트에 적합합니다. CrewAI는 역할(Role) 기반 다중 에이전트 시스템입니다.
"연구원 에이전트", "작가 에이전트", "검토자 에이전트"처럼 각 에이전트에게 역할을 부여하고, 협업하여 작업을 수행합니다. 팀 프로젝트를 구성하는 것과 비슷한 개념입니다.
AutoGen은 대화(Conversation) 기반 에이전트 통신 프레임워크입니다. 에이전트들이 서로 대화하면서 문제를 해결합니다.
Microsoft에서 개발했으며, 다중 에이전트 협업에 특화되어 있습니다. 각 프레임워크의 장단점은 다릅니다.
LangGraph는 제어 흐름에 강하고, CrewAI는 역할 분담에 강하고, AutoGen은 대화형 협업에 강합니다. 프로젝트의 요구사항에 따라 적절한 프레임워크를 선택해야 합니다.
중요한 것은, 프레임워크가 마법이 아니라는 점입니다. 우리가 지금까지 배운 원리들을 더 편리하게 사용할 수 있게 해주는 도구일 뿐입니다.
프레임워크 내부에서는 여전히 함수 호출이 일어나고, 임베딩이 생성되고, 환각이 발생할 수 있습니다. 따라서 우리가 배운 가드레일, 에러 처리, 검증 전략은 프레임워크를 사용하더라도 그대로 적용됩니다.
프레임워크가 안전성을 보장해주는 것이 아니라, 우리가 설계한 안전 전략을 더 쉽게 구현할 수 있게 해주는 것입니다. 김개발 씨는 다음 장이 기대되기 시작했습니다.
"원리를 알면 프레임워크의 선택 기준도 명확해지겠네요. 어떤 문제를 해결하느냐에 따라 도구를 선택하는 거죠." 박시니어 씨가 마지막으로 당부했습니다.
"프레임워크를 배우면서도 항상 '이게 내부적으로 어떻게 동작하지?'라고 질문하세요. 원리를 이해한 개발자가 그렇지 않은 개발자보다 훨씬 더 좋은 에이전트를 만들 수 있거든요."
실전 팁
💡 - 프레임워크를 선택할 때는 프로젝트의 복잡도, 팀의 경험, 커뮤니티 지원을 종합적으로 고려하세요.
- 하나의 프레임워크에 종속되지 마세요. 각 프레임워크의 장점을 이해하면 적절한 상황에 맞게 선택할 수 있습니다.
- 프레임워크를 사용하더라도 기본 원리(함수 호출, 임베딩, 환각 방어)에 대한 이해는 항상 유지하세요.
- 다음 장에서는 LangGraph, CrewAI, AutoGen 프레임워크를 비교 분석하고, 각각의 적합한 사용 사례를 배웁니다.
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 5/16편입니다.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
프레임워크 선택 LangGraph vs CrewAI vs AutoGen 완벽 가이드
AI 에이전트 개발을 위한 세 가지 핵심 프레임워크를 비교 분석합니다. 각 프레임워크의 특징, 장단점, 실무 선택 기준을 초급 개발자도 이해할 수 있도록 설명합니다.
Day 6 학습 루프 이해하기
LLM이 실제로 어떻게 학습하는지 학습 루프의 핵심 원리를 단계별로 살펴봅니다. Forward Pass, Loss 계산, Backward Pass, 파라미터 업데이트까지 한 사이클의 전 과정을 이해합니다.
Day 5 Baseline 모델 만들기
복잡한 모델에 앞서 가장 단순한 Baseline 모델을 직접 만들어봅니다. 아무런 기교 없이 순수하게 다음 토큰을 예측하는 모델을 구현하면서, 언어모델의 가장 기본 구조를 이해합니다.
Day 4 학습용 샘플 데이터 만들기
LLM을 학습시키기 위한 샘플 데이터를 직접 만들어봅니다. 작은 텍스트 말뭉치를 준비하고, 토크나이저로 변환한 뒤 PyTorch 텐서로 만드는 전체 과정을 단계별로 배웁니다.
Day 2 PyTorch 기본기 정리
LLM을 직접 만들기 위해 꼭 알아야 할 PyTorch의 핵심 개념을 정리합니다. 텐서, 자동 미분, 옵티마이저까지 모델 학습의 기초를 다집니다.