1. 이번 단계에서 달라지는 것
LLM이 모르는 "사내·최신 데이터"를 RAG로 끌어와 추론에 반영하는 식사 추천 에이전트를 만듦.
③편까지의 에이전트는 곱셈처럼 자체적으로 풀 수 있는 도구만 썼음. 이번에는 LLM이 모르는 정보를 다룸. RAG(Retrieval-Augmented Generation)를 도구로 차용해 다음 문제를 해결함.
- 할루시네이션 방지 — 존재하지 않는 식당을 지어내지 않도록, 실제 데이터를 검색해 근거로 제공함.
- 내부 데이터 접근 — LLM이 모르는 private 데이터(보안 이슈로 학습되지 않은 정보)를 보완함.
- 최신 정보 부재 해소 — 학습 시점 이후의 정보를 외부에서 가져옴.
설계 방향은 다음과 같음.
- 전체 의사결정 — LangGraph가 담당함.
- 부분 체인 — 프롬프트 → LLM 추론은 LangChain으로 묶어 하나의 노드로 구성함.
- 프롬프트 기법 — FewShot으로 답변 톤과 형식을 잡음.
②~③편이 "계산 도구"였다면, ④편의 도구는 "지식 검색"이다. 도구의 성격이 행동에서 지식 보강으로 확장된다.
2. RAG 저장소 구성 (rag_store.py)
더미 식당 데이터를 임베딩해 FAISS 벡터 DB에 적재하고, 유사도 검색 함수를 제공함.
from langchain_community.vectorstores import FAISS
from langchain_aws import BedrockEmbeddings
import boto3, os
from dotenv import load_dotenv
load_dotenv()
# 임베딩 모델(토크나이저) 구성
tokenizer = BedrockEmbeddings(
model_id="amazon.titan-embed-text-v2:0",
region_name=os.getenv('AWS_REGION')
)
# 더미 데이터 — LLM이 모르는 사내/최신 데이터 가정
data = [
"가게명: 스파이시 웍, 메뉴: 마라탕, 꿔바로우, 특징: 아주 매움, 스트레스 풀림, 가격: 15000원",
"가게명: 헬시 샐러드, 메뉴: 닭가슴살 샐러드, 샌드위치, 특징: 다이어트, 가벼움, 신선함, 가격: 9000원",
"가게명: 엄마손 백반, 메뉴: 김치찌개, 제육볶음, 특징: 집밥 스타일, 가성비, 든든함, 가격: 8000원",
"가게명: 골든 스시, 메뉴: 초밥 세트, 우동, 특징: 고급스러움, 깔끔함, 월급날 추천, 가격: 25000원",
"가게명: 해장국 천국, 메뉴: 뼈해장국, 순대국, 특징: 국물 진함, 비 오는 날 추천, 가격: 10000원"
]
# 벡터화 -> 벡터 DB 세팅
vector_db = FAISS.from_texts(data, embedding=tokenizer)
# 검색 함수: 질의 -> 유사도 검색 -> 상위 k개 반환
def search_stores(query: str, k: int = 2):
docs = vector_db.similarity_search(query, k)
print(f'==== [RAG 검색 결과] : {docs}')
return '\n'.join([doc.page_content for doc in docs])
현재는 인메모리 FAISS로 1회성 구성임. 향후 벡터 DB는 외부에 영속 구성하는 것이 실서비스 방향임.
3. 도구 래핑 (tools.py)
검색 함수를 `@tool`로 감싸 LLM이 호출 가능한 도구로 만듦.
from langchain_core.tools import tool
from rag_store import search_stores
@tool
def rag_search(cate: str) -> str:
'''
가격, 특징, 메뉴, 카테고리 등을 입력받아 벡터 유사도 검색 ->
실제 식당 정보 제공(할루시네이션 회피)
'''
res = search_stores(cate) # 상위 2개 반환
return res if res else '관련 식당 정보를 찾을 수 없습니다.'
도구를 별도 모듈로 분리해, 도구가 늘어나도 에이전트 본체가 비대해지지 않게 했음.
4. 에이전트 본체 (lg_rag_agent.py)
4.1 모델·도구 준비
온도·토큰 한도를 지정한 Bedrock 모델에 RAG 도구를 바인딩함.
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, BaseMessage
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_aws import ChatBedrockConverse
from langgraph.prebuilt import ToolNode, tools_condition
from dotenv import load_dotenv
import os, boto3
from tools import rag_search # 외부에 구성한 커스텀 RAG 도구
load_dotenv()
llm = ChatBedrockConverse(
model=os.getenv('MODEL_ID'),
max_tokens=1000,
temperature=0.5,
region_name=os.getenv('AWS_REGION')
)
tools = [rag_search]
llm_with_tools = llm.bind_tools(tools)
4.2 FewShot 프롬프트 구성
예시 입력-출력 쌍을 주어 답변 톤(센스 있는 추천)을 학습시킴.
# 샘플 형태는 다양하게 구성 가능
examples = [
{"input": "비 오는 날 국밥이 땡겨", "output": "국룰이죠. 칼국수와 잔치국수가 좋습니다."},
{"input": "다이어트를 위해서 오늘 칼로리가 낮은 걸로 추천해줘.", "output": "관리하시는군요. 닭가슴 샐러드 드세요."}
]
examples_prompt = ChatPromptTemplate.from_messages([
('human', '{input}'),
('ai', '{output}')
])
few_shot_prompt = FewShotChatMessagePromptTemplate(
examples=examples,
example_prompt=examples_prompt
)
final_prompt = ChatPromptTemplate.from_messages([
# 1. 페르소나
('system', '당신은 센스있는 식사 메뉴 추천 전문가입니다. 사용자의 상황에 맞춰 메뉴를 추천하고, 필요하면 도구를 사용해 실제 식당을 찾으세요.'),
# 2. 퓨샷 샘플
few_shot_prompt,
# 3. 사용자 질의
('human', '{query}')
])
`시스템(페르소나) → FewShot 예시 → 사용자 질의` 순서로 프롬프트가 조립됨.
FewShot은 "이런 식으로 답하라"는 모범 답안을 보여주는 기법이다. 규칙을 길게 설명하는 것보다 예시 한두 개가 톤을 더 정확히 잡아준다.
4.3 상태 정의
메시지 리스트를 담는 커스텀 상태를 ①편처럼 직접 정의함.
class AgentState(TypedDict):
messages: List[BaseMessage]
5. 노드 3종 구성
"생각 → (필요 시)도구 → 최종 답변" 세 노드로 의사결정을 나눔.
5.1 thinking_node — 1차 추론
사용자 질의를 받아 직접 답할지, 도구를 쓸지 판단함.
def thinking_node(state: AgentState):
# 사용자 입력 추출 (UI 입력 -> 서버 -> 그래프.invoke(프롬프트) -> state['messages'])
msg = state['messages'][-1].content
# 랭체인 단방향 구성: FewShot 프롬프트(페르소나+샘플) | LLM
chain = final_prompt | llm_with_tools
# 1차 추론
res = chain.invoke({"query": msg})
return {'messages': [res]}
프롬프트와 LLM을 `|`로 묶은 LangChain 체인을 하나의 노드 안에 넣은 점이 포인트임.
5.2 tool_node — RAG 검색 수행
LLM이 도구 사용을 결정했을 때, 실제 RAG 검색을 실행하고 결과를 메시지로 되돌림.
def tool_node(state: AgentState):
last_msg = state['messages'][-1]
print('tool_node 호출 : 툴 사용 LLM이 응답', last_msg.tool_calls)
if last_msg.tool_calls:
# 등록 도구가 1개라 0으로 고정. 도구가 늘면 상황별 선택 부여
tool = last_msg.tool_calls[0]
# tool_calls 예시 형태:
# [{'name':'rag_search', 'args':{'cate':'가벼운 식사'},
# 'id':'tooluse-...', 'type':'tool_call'}]
result = rag_search.invoke(tool['args']) # RAG 수행
print('RAG 호출 결과 :', result)
return {"messages": [
HumanMessage(content=f'[사내데이터 검색결과]: {result}\n 제공된 정보를 기반으로 최종 답변을 해주세요.')
]}
검색 결과를 다시 사람 메시지처럼 주입해, 다음 노드가 그 근거로 답변하도록 유도함.
5.3 final_answer_node — 근거 기반 최종 답변
검색 결과가 포함된 전체 맥락으로 LLM이 마무리 답변을 생성함.
def final_answer_node(state: AgentState):
msg = state['messages']
res = llm.invoke(msg)
return {'messages': [res]}
여기서는 별도 노드로 분리했지만, 실제로는 다시 `thinking_node`로 돌려보내 마무리해도 무방함.
6. 조건부 엣지와 흐름
커스텀 분기 함수로 "도구가 필요한지"를 직접 판정함.
②~③편은 라이브러리 제공 tools_condition을 썼음. 이번에는 분기 로직을 직접 작성해 동작을 명시적으로 드러냄.
def custom_check_tool_node(state: AgentState):
# 마지막 AI 응답에서 tool_calls 확인
last_msg = state['messages'][-1]
if last_msg.tool_calls:
print('툴 사용 필요')
return 'tool'
else: # 도구 불필요 -> 답변 완성 -> END
return END
workflow = StateGraph(AgentState)
workflow.add_node('thinking', thinking_node)
workflow.add_node('tool', tool_node)
workflow.add_node('final_answer', final_answer_node)
workflow.set_entry_point('thinking')
workflow.add_conditional_edges('thinking', custom_check_tool_node)
workflow.add_edge('tool', 'final_answer') # 도구 사용 -> 최종 답변
workflow.add_edge('final_answer', END)
랭그래프객체 = workflow.compile()
흐름 시나리오는 다음과 같음.
| 시나리오 | 흐름 |
| 베스트 | 프롬프트 → thinking → END |
| RAG 경유 | 프롬프트 → thinking → (부족) → tool → RAG → final_answer → END |
②편의 분기가 라이브러리에 맡긴 자동 분기였다면, 여기서는 분기 기준을 직접 코딩했다. 동작 원리를 손으로 짚어보는 단계다.
7. 실행
상황과 위치를 담은 질의를 던지면, RAG로 실제 식당을 찾아 식단까지 구성함.
if __name__ == '__main__':
res = 랭그래프객체.invoke({
"messages": [HumanMessage(content="근처에서 점심 추천 식당 세 군데 해주고 식단까지 그 식당 메뉴에 맞춰 짜줘. 영등포역이야")]
})
print(res)
LLM이 추천만으로 부족하다고 판단하면 `rag_search`를 호출해 더미 DB의 실제 식당 정보를 근거로 답변을 마무리함.
요약
RAG를 도구로 결합하고 노드를 3단계로 나눠, 근거 기반으로 답하는 식사 추천 에이전트를 완성했음.
| 항목 | LangGraph 3 | LangGraph 4 |
| 도구 성격 | 계산(행동) | 지식 검색(RAG) |
| 데이터 출처 | LLM 내부 지식 | 외부 벡터 DB(FAISS) |
| 프롬프트 | 기본 | FewShot + 페르소나 |
| 노드 수 | 2개(chatbot/tools) | 3개(thinking/tool/final) |
| 분기 | tools_condition | 커스텀 분기 함수 |
| 모듈 구성 | 단일 파일 | 본체/도구/저장소 분리 |
4단계를 거치며 그래프는 "고정 흐름 → 자율 분기 → 기억 → 외부 지식 보강"으로 발전했다. 다음 확장 지점은 외부 영속 벡터 DB, 다중 도구 선택, 그리고 MCP 연계다.
'SK플래닛 ai활용 데이터엔지니어 과정 2기 > ML & DL' 카테고리의 다른 글
| LangGraph 6 - MCP 도구 연계 (0) | 2026.06.05 |
|---|---|
| LangGraph 5 - 멀티 에이전트 협업 (0) | 2026.06.04 |
| LangGraph 3 - 단기기억 (0) | 2026.06.02 |
| LangGraph 2 - Tool + LLM (0) | 2026.06.02 |
| LangGraph 1 - 상태 그래프 기초 (0) | 2026.06.02 |