1. sqlite

SQLite는 서버가 필요 없는 가볍고(self-contained) 독립적인 관계형 데이터베이스 관리 시스템(RDBMS)으로, 애플리케이션 내부에 파일 형태로 데이터베이스를 저장해 별도의 설치나 설정 없이 쉽게 사용할 수 있다는 특징이 있습니다. ANSI-SQL 표준을 대부분 지원하며, 트랜잭션, 인덱스, 뷰, 트리거 같은 기능도 제공하지만 MySQL이나 PostgreSQL 같은 대형 서버형 DBMS에 비해 동시성 처리나 고성능 분산 처리에는 한계가 있습니다. 작은 규모의 웹·모바일 앱, 프로토타입 개발, 내장형 기기 등에 널리 활용되며, 파일 하나만 복사하면 전체 데이터베이스를 옮길 수 있을 만큼 간편성이 뛰어납니다.

import requests

url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db"

response = requests.get(url)

if response.status_code == 200:
    with open("Chinook.db", "wb") as file:
        file.write(response.content)
    print("Chinook.db 다운로드")
else:
    print(f"다운로드 실패: {response.status_code}")

 

!pip install langchain_community

 

from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db")
print(f"Dialect: {db.dialect}")
print(f"Available tables: {db.get_usable_table_names()}")
print(f'Sample output: {db.run("SELECT * FROM Artist LIMIT 5;")}')

 

from langchain_community.utilities import SQLDatabase
import os

db_path = "./Chinook.db"
print("경로 존재 여부:", os.path.exists(db_path))

db = SQLDatabase.from_uri(f"sqlite:///{db_path}")
print(f"Dialect: {db.dialect}")
print(f"Available tables: {db.get_usable_table_names()}")
print(f'Sample output: {db.run("SELECT * FROM Artist LIMIT 5;")}')

 

import getpass
import os

def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")

_set_env("OPENAI_API_KEY")

 

!pip install langchain_openai

 

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o", temperature=0, max_retries=3)

 

  • sql_db_query: SQL 쿼리를 입력하면, 데이터베이스 조회 결과를 출력하는 도구로, 쿼리가 올바르지 않으면 오류 메시지가 반환됨.
  • sql_db_schema: 테이블 목록을 입력하면 해당 테이블의 스키마와 샘플 행이 출력되는 도구.
  • sql_db_list_tables: 데이터베이스 테이블 목록을 반환하는 도구.
  • sql_db_query_checker: 쿼리가 올바른지 확인하는 도구.
from langchain_community.agent_toolkits import SQLDatabaseToolkit

toolkit = SQLDatabaseToolkit(db=db, llm=llm)

tools = toolkit.get_tools()

for tool in tools:
    print(f"{tool.name}: {tool.description}\n")

 

!pip install langgraph

 

from langgraph.prebuilt import create_react_agent

system_prompt = f"""
You are an agent designed to interact with a SQL database.
Given an input question, create a syntactically correct {db.dialect} query to run,
then look at the results of the query and return the answer. Unless the user
specifies a specific number of examples they wish to obtain, always limit your
query to at most 5 results.

You can order the results by a relevant column to return the most interesting
examples in the database. Never query for all the columns from a specific table,
only ask for the relevant columns given the question.

You MUST double check your query before executing it. If you get an error while
executing a query, rewrite the query and try again.

DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the
database.

To start you should ALWAYS look at the tables in the database to see what you
can query. Do NOT skip this step.

Then you should query the schema of the most relevant tables.
"""

agent = create_react_agent(
    llm,
    tools,
    prompt=system_prompt,
)

agent

question = "2009년에 가장 많은 매출을 올린 영업 사원은 누구인가요?"

for step in agent.stream(
    {"messages": [{"role": "user", "content": question}]},
    stream_mode="values",
):
    step["messages"][-1].pretty_print()

 

tools

 

 

2. Agent 구축

 

1. ToolNode 생성하기

from langgraph.prebuilt import ToolNode

list_tables_tool = next(tool for tool in tools if tool.name == "sql_db_list_tables")
list_tables_node = ToolNode([list_tables_tool], name="list_tables")

get_schema_tool = next(tool for tool in tools if tool.name == "sql_db_schema")
get_schema_node = ToolNode([get_schema_tool], name="get_schema")

run_query_tool = next(tool for tool in tools if tool.name == "sql_db_query")
run_query_node = ToolNode([run_query_tool], name="run_query")

 

2. DB 조회 시작을 결정할 첫 노드 생성하기

from langgraph.graph import END, START, MessagesState, StateGraph


def chatbot(state: MessagesState):
    llm_with_tools = llm.bind_tools([list_tables_tool])
    response = llm_with_tools.invoke(state["messages"]) # tool_calls : DB 조회가 필요 / AIMessage : DB 조회가 필요X
    return {"messages": [response]}

 

3. 스키마 조회를 위한 도구 호출

def call_get_schema(state: MessagesState):
    llm_with_tools = llm.bind_tools([get_schema_tool], tool_choice="any")
    response = llm_with_tools.invoke(state["messages"]) # tool_calls : 필요한 테이블에 대한 스키마 요청

    return {"messages": [response]}

 

4. 사용자 질문 기반 쿼리문 생성하기

from langchain_core.prompts import ChatPromptTemplate

generate_query_system_prompt = f"""
You are an agent designed to interact with a SQL database.
Given an input question, create a syntactically correct {db.dialect} query to run,
then look at the results of the query and return the answer. Unless the user
specifies a specific number of examples they wish to obtain, always limit your
query to at most 5 results.

You can order the results by a relevant column to return the most interesting
examples in the database. Never query for all the columns from a specific table,
only ask for the relevant columns given the question.

DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.
DO NOT wrap the response in any backticks or anything else(```). Respond with a SQL statement only!
"""

generate_query_user_prompt = """
User input: {question}
Schema: {schema}

If an error message is given, regenerate the query based on the error message.
History: {history}

SQL query:"""


def generate_query(state: MessagesState):
    print("##### GENERATE QUERY #####")
    print(state["messages"])
    history = ""
    for message in state["messages"][2:]:
        history += message.content + "\n"

    
    generate_query_msgs = [
        ("system", generate_query_system_prompt),
        ("user", generate_query_user_prompt),
    ]
    generate_propmt = ChatPromptTemplate.from_messages(generate_query_msgs)

    response = llm.invoke(generate_propmt.format_messages(
        question=state["messages"][0].content, 
        schema=state["messages"][1].content,
        history=history
    ))
    print("generate_query", response)

    return {"messages": [response]}

 

5. 생성한 쿼리문 실행하기

from langchain_core.messages import AIMessage
import re  # 파일 상단 임포트

def _sanitize_sql(text: str) -> str:
    m = re.search(r"```(?:sql)?\s*(.*?)```", text, flags=re.DOTALL | re.IGNORECASE)
    return (m.group(1) if m else text).strip()

def check_query(state: MessagesState):
    print("##### CHECK QUERY #####")
    raw = state["messages"][-1].content
    query = _sanitize_sql(raw)                 # <-- 코드펜스 제거 포인트
    print("check_query", query)
    response = run_query_tool.invoke({"query": query})
    return {"messages": [AIMessage(content=str(response))]}

 

6. 질문 + 쿼리 실행 결과 기반 답변 생성하기

answer_system_prompt = """
You are a highly intelligent assistant trained to provide concise and accurate answers.
You will be given a context that has been retrieved from a database using a specific SQL query.
Your task is to analyze the context and answer the user’s question based on the information provided in the context.
ANSWER IN KOREAN.
"""

def answer(state: MessagesState):
    print("##### ANSWER #####")
    question = state["messages"][0].content
    context = state["messages"][-1].content
    generated_query = state["messages"][-2].content
    print("context", context)

    answer_msgs = [
        ("system", answer_system_prompt),
        ("user", "User Question: {question} SQL Query: {generated_query} Context: {context}"),
    ]
    answer_propmt = ChatPromptTemplate.from_messages(answer_msgs)
    
    response = llm.invoke(answer_propmt.format_messages(question=question, generated_query=generated_query, context=context))
    print("response", response)

    return {"messages": [response]}

 

7. Graph Compile

from langgraph.prebuilt import tools_condition

graph_builder = StateGraph(MessagesState)
graph_builder.add_node("chatbot", chatbot)

graph_builder.add_edge(START, "chatbot")

graph_builder.add_node("list_tables", list_tables_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {
        "tools": "list_tables",
        END: END,
    },
)

 

graph_builder.add_node("call_get_schema", call_get_schema)
graph_builder.add_node("get_schema", get_schema_node)
graph_builder.add_node("generate_query", generate_query)
graph_builder.add_node("check_query", check_query)
graph_builder.add_node("answer", answer)

graph_builder.add_edge("list_tables", "call_get_schema")
graph_builder.add_edge("call_get_schema", "get_schema")
graph_builder.add_edge("get_schema", "generate_query")
graph_builder.add_edge("generate_query", "check_query")


def should_correct(state):
    print(state["messages"][-1].content)
    txt = state["messages"][-1].content
    if "Error:" in txt or "error" in txt.lower(): # DB 조회 결과가 [-1] 에 저장
        return "generate_query"
    else:
        return "answer"

graph_builder.add_conditional_edges(
    "check_query",
    should_correct,
    {
        "generate_query": "generate_query",
        "answer": "answer",
    },
)


graph_builder.add_edge("answer", END)

graph = graph_builder.compile()
graph

question = "2009년에 가장 많은 매출을 올린 영업 사원은 누구인가요?"

for step in graph.stream(
    {"messages": [{"role": "user", "content": question}]},
    stream_mode="values",
):
    step["messages"][-1].pretty_print()

 

question = "DB 조회를 기반으로 2009년 가장 많은 양의 음반을 판매한 아티스트의 해당 앨범 판매 기간을 알려주세요. "

for step in graph.stream(
    {"messages": [{"role": "user", "content": question}]},
    stream_mode="values",
):
    step["messages"][-1].pretty_print()

 

'AI > AI Agent' 카테고리의 다른 글

벡터 데이터베이스  (0) 2025.09.18
LangGraph Reflection  (0) 2025.09.15
랭그래프를 이용한 간단한 챗봇  (0) 2025.09.10
LangGraph basic  (0) 2025.09.09
LangGraph  (1) 2025.09.08

1. 벡터 데이터베이스

벡터 데이터베이스(Vector Database)는 텍스트, 이미지, 오디오와 같은 데이터를 고차원 벡터 형태로 변환해 저장하고, 이 벡터 간의 유사도를 빠르게 검색할 수 있도록 최적화된 데이터베이스입니다. 일반적인 관계형 데이터베이스가 정확한 값 기반 검색(SQL 쿼리 등)에 적합하다면, 벡터 데이터베이스는 의미적 유사성(semantic similarity)에 기반한 검색을 지원하여 예를 들어 "강아지"와 "개"처럼 다른 표현이라도 비슷한 의미의 데이터를 찾아낼 수 있습니다. 이를 위해 코사인 유사도, 내적(dot product), 유클리드 거리와 같은 수학적 거리 계산을 활용하며, 대규모 임베딩(embedding) 데이터를 효율적으로 관리하고 검색할 수 있어 추천 시스템, 검색 엔진, 생성형 AI의 RAG(Retrieval-Augmented Generation) 등에 널리 활용됩니다.

 

크로마디비

크로마디비(ChromaDB)는 대표적인 오픈소스 벡터 데이터베이스로, 문서·이미지·코드 등 다양한 데이터를 임베딩 벡터로 변환해 저장하고, 이를 빠르게 검색할 수 있도록 설계된 시스템입니다. 파이썬 기반으로 사용이 간편하며, LangChain 같은 LLM 프레임워크와 잘 통합되어 RAG(Retrieval-Augmented Generation) 구조를 쉽게 구축할 수 있습니다. 내부적으로는 벡터 인덱싱과 메타데이터 저장을 함께 지원하여, 단순히 유사도 검색뿐 아니라 조건 필터링과 결합된 검색도 가능합니다. 무료로 가볍게 실행할 수 있고, 로컬 환경부터 클라우드까지 유연하게 확장할 수 있어 학습용이나 실무용 AI 검색 엔진 구축에 많이 활용됩니다.

import getpass
import os

def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")

_set_env("OPENAI_API_KEY")

 

!pip install langchain_community

!pip install langchain_experimental

!pip install langchain_openai

!pip install pypdf

 

 

2. 청크

청크(Chunk)는 긴 텍스트나 문서를 작은 단위로 나눈 조각을 의미하며, 주로 자연어 처리와 RAG(Retrieval-Augmented Generation) 같은 작업에서 사용됩니다. 대형 언어모델은 한 번에 처리할 수 있는 토큰 수에 한계가 있기 때문에 문서를 일정한 길이로 분할하여 임베딩 벡터로 변환하고, 이후 검색이나 질의 응답 시 필요한 청크만 불러와 모델에 전달하는 방식으로 효율성과 정확성을 높입니다. 청크는 단순히 일정 글자 수나 토큰 수로 나누기도 하지만, 문단·문장 단위 등 의미 단위로 나누어야 검색 품질이 좋아지며, 결국 청크는 방대한 데이터를 모델이 다룰 수 있는 크기로 잘게 나눈 최소 단위라고 할 수 있습니다.

 

SemanticChunker

SemanticChunker는 텍스트를 단순히 일정한 길이로 자르는 방식이 아니라, 문장의 의미적 맥락을 고려해 자연스럽게 분할하는 청크 생성 기법입니다. 즉, 문장이나 문단의 의미가 단절되지 않도록 문맥 단위로 텍스트를 나누어 임베딩과 검색의 정확도를 높여줍니다. 이를 통해 RAG(Retrieval-Augmented Generation) 구조에서 모델이 보다 관련성 높은 정보를 검색할 수 있으며, 불필요하게 잘려나간 조각이나 중복된 정보 전달을 줄일 수 있습니다. 따라서 SemanticChunker는 의미 기반 검색과 대규모 문서 처리에서 효율성과 정밀도를 동시에 향상시키는 중요한 도구로 활용됩니다.

from langchain_community.document_loaders import PyPDFLoader
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings

file_path = "SPRi AI Brief_4월호_산업동향_250407_F.pdf"

loader = PyPDFLoader(file_path)
pages = []

async for page in loader.alazy_load():
    pages.append(page)

text_splitter = SemanticChunker(OpenAIEmbeddings())

docs = text_splitter.split_documents(pages)

 

print(f"총 {len(docs)}개 만큼의 문서로 청킹되었습니다.")
print([len(i.page_content) for i in docs])

# 각 청크의 메타데이터 및 내용 출력
for i in docs:
    print(i.metadata)       # 문서의 메타데이터 출력 (예: 페이지 번호 등)
    print(i.page_content)   # 분할된 청크의 내용 출력
    print("-" * 100)        # 구분선 출력

 

 

3. 벡터 리트리버

벡터 리트리버(Vector Retriever)는 사용자의 질의(Query)를 임베딩 벡터로 변환한 뒤, 벡터 데이터베이스에 저장된 청크(Chunk) 벡터들과의 유사도를 계산하여 가장 관련성 높은 결과를 찾아주는 구성 요소입니다. 즉, “검색기” 역할을 하는데, 단순히 키워드 일치를 찾는 것이 아니라 의미적 유사성을 기반으로 정보를 불러옵니다. 이를 위해 코사인 유사도, 내적(dot product), 유클리드 거리 등의 수학적 방법을 활용하며, 검색된 결과는 LLM과 결합되어 RAG(Retrieval-Augmented Generation) 같은 구조에서 모델의 답변 품질을 높이는 데 쓰입니다. 쉽게 말해, 벡터 리트리버는 “의미를 이해하는 검색 엔진”이라고 할 수 있습니다.

 

from_documents

  • documents (List[Document]): 벡터 저장소에 추가할 문서 리스트
  • embedding (Optional[Embeddings]): 임베딩 함수. 기본값은 None
  • ids (Optional[List[str]]): 문서 ID 리스트. 기본값은 None
  • collection_name (str): 생성할 컬렉션 이름.
  • persist_directory (Optional[str]): 컬렉션을 저장할 디렉토리. 기본값은 None
  • client_settings (Optional[chromadb.config.Settings]): Chroma 클라이언트 설정
  • client (Optional[chromadb.Client]): Chroma 클라이언트 인스턴스
  • collection_metadata (Optional[Dict]): 컬렉션 구성 정보. 기본값은 None
!pip install langchain_chroma

!pip install -U langchain langchain-chroma langchain-openai chromadb

!pip install -U "opentelemetry-api==1.26.0" \
              "opentelemetry-sdk==1.26.0" \
              "opentelemetry-exporter-otlp-proto-grpc==1.26.0"

 

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

vectorstore = Chroma.from_documents(documents=docs, embedding=OpenAIEmbeddings())

 

# query = "카나나의 테크니컬 리포트는 어떤 내용인가요?"
# query = "에이전트 SDK는 어떤 기능을 제공하나요?"
query = "딥마인드가 발표한 로봇AI 모델은?"

results = vectorstore.similarity_search(query, k=1)

 

print(results[0].page_content)

 

vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 1})

 

relevant_doc = vector_retriever.invoke(query)
print(relevant_doc)

 

print(relevant_doc[0].page_content)

 

앙상블 리트리버

앙상블 리트리버(Ensemble Retriever)는 하나의 검색 방식에만 의존하지 않고, 여러 종류의 리트리버를 조합해 더 정확하고 풍부한 검색 결과를 제공하는 방법입니다. 예를 들어 키워드 기반의 전통적 BM25 리트리버와 의미 기반의 벡터 리트리버를 함께 사용하면, 단어가 정확히 일치하는 문서뿐 아니라 의미적으로 관련 있는 문서도 함께 찾아낼 수 있습니다. 이렇게 서로 다른 리트리버의 강점을 결합하면 검색 누락을 줄이고, 다양한 관점에서 문서를 확보할 수 있어 RAG(Retrieval-Augmented Generation) 구조에서 더욱 신뢰도 높은 응답을 생성하는 데 유용합니다.

 

BM25 리트리버

BM25 리트리버는 전통적인 정보 검색 기법 중 하나로, 사용자의 질의(Query)와 문서 간의 키워드 일치 정도를 계산해 관련성이 높은 문서를 찾아주는 방식입니다. 기본적으로 단어 빈도(Term Frequency), 역문서 빈도(Inverse Document Frequency), 그리고 문서 길이를 고려해 점수를 매기며, 특정 단어가 질의에 많이 등장하거나 드문 단어일수록 가중치를 높게 주어 검색 정확도를 높입니다. 벡터 리트리버처럼 의미적 유사성을 직접 파악하지는 못하지만, 빠르고 해석 가능한 결과를 제공하기 때문에 대규모 문서 검색이나 키워드 중심 검색에서 여전히 많이 활용되며, 종종 벡터 리트리버와 결합해 앙상블 리트리버로 사용됩니다.

!pip install rank_bm25

 

from langchain.retrievers import BM25Retriever, EnsembleRetriever

bm25_retriever = BM25Retriever.from_documents(
    docs,
)
bm25_retriever.k = 1

 

ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.7, 0.3],
)

 

query = "깊이 업스케일링 (Depth UP-Scaling)"

 

ensemble_result = ensemble_retriever.invoke(query)
bm25_result = bm25_retriever.invoke(query)
vector_result = vector_retriever.invoke(query)

print("[Ensemble Retriever]")
for doc in ensemble_result:
    print(f"Content: {doc.page_content}")
    print()

print("[BM25 Retriever]")
for doc in bm25_result:
    print(f"Content: {doc.page_content}")
    print()

print("[Vector Retriever]")
for doc in vector_result:
    print(f"Content: {doc.page_content}")
    print()

 

!pip install langgraph

 

from langgraph.graph import StateGraph, MessagesState

class State(MessagesState):
    context: str

graph_builder = StateGraph(State)

 

from langchain_core.messages import HumanMessage

def retriever(state: State):
    """
    Retrieve the relevant document and return the content.
    """
    print("##### RETRIEVER #####")
    query = state["messages"][0].content
    ensemble_result = ensemble_retriever.invoke(query)

    content = ensemble_result[0].page_content
    print("[CONTEXT]\n", content)

    return {"context" : content, "messages": [HumanMessage(content=content)]}

 

 

4. 랭체인 허브

랭체인 허브(LangChain Hub)는 개발자와 연구자들이 프롬프트(Prompt), 체인(Chain), 에이전트(Agent) 같은 LLM 관련 리소스를 공유하고 재사용할 수 있도록 만든 오픈 플랫폼입니다. 사용자는 자신이 만든 프롬프트 템플릿을 업로드해 다른 사람과 공유할 수 있고, 다른 사람이 만든 검증된 프롬프트를 hub.pull() 같은 방식으로 손쉽게 불러와 활용할 수 있습니다. 이를 통해 매번 새롭게 프롬프트를 설계할 필요 없이 빠르게 실험하고 협업할 수 있으며, 다양한 검색 기능을 제공해 특정 목적(Q&A, 요약, RAG 등)에 맞는 템플릿을 쉽게 찾을 수 있습니다. 결국 랭체인 허브는 LLM 애플리케이션 개발을 가속화하는 중앙 저장소 역할을 합니다.

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain import hub

llm = ChatOpenAI(model="gpt-5-nano", temperature=0)

def answer(state: State):
    """
    Answer the question based on the retrieved document.
    """
    print("##### ANSWER #####")
    query = state["messages"][0].content
    context = state["messages"][-1].content
    # context = state["context"]

    # prompt = ChatPromptTemplate.from_messages(
    #     [
    #         ("system",
    #          """
    #             You are an assistant for answering questions based on retrieved document context.
    #             Answer in Korean.

    #             Context: {context}"""
    #          ),
    #         ("human", "{question}"),
    #     ]
    # )
    prompt = hub.pull("rlm/rag-prompt")
    # You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
    # Question: {question}
    # Context: {context}
    # Answer:

    response = llm.invoke(
        prompt.format_messages(context=context, question=query)
    )

    return {"messages": [response]}

 

hub.pull("rlm/rag-prompt").pretty_print()

 

from langgraph.graph import START, END

graph_builder.add_sequence([retriever, answer])
graph_builder.add_edge(START, "retriever")
graph_builder.add_edge("answer", END)
graph = graph_builder.compile()
graph

 

response = graph.invoke({"messages": "카나나의 테크니컬 리포트는 어떤 내용인가요?"})

for mes in response['messages']:
    mes.pretty_print()

 

from langchain.tools.retriever import create_retriever_tool

retriever_tool = create_retriever_tool(
    ensemble_retriever,
    "retrieve_AI_brief",
    "Search and return information about AI Technology and Industry.",
)

tools = [retriever_tool]

 

from langgraph.graph import StateGraph, MessagesState

graph_builder = StateGraph(MessagesState)

 

from langgraph.prebuilt import ToolNode, tools_condition

tool_node = ToolNode(tools=tools)
graph_builder.add_node("retriever", tool_node)

 

llm_with_tools = llm.bind_tools(tools)

def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]} # 1) 도구 호출(tool_calls) 2) AI Message

graph_builder.add_node("chatbot", chatbot)

 

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {
        "tools" : "retriever",
        END: END
    }
)
graph_builder.add_node("answer", answer)

graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("retriever", "answer")
graph_builder.add_edge("answer", END)
graph = graph_builder.compile()
graph

 

response = graph.invoke({"messages": "카나나의 테크니컬 리포트는 어떤 내용인가요?"})

for mes in response['messages']:
    mes.pretty_print()

 

 

5. 환각 여부를 평가하는 RAG

import getpass
import os


def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")


_set_env("OPENAI_API_KEY")
_set_env("TAVILY_API_KEY")

 

1. Vector DB (문서 검색을 위한 Retriever 생성하기)

!pip install langchain_community langchain_experimental langchain_openai pypdf

 

from langchain_community.document_loaders import PyPDFLoader
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings

file_path = "/content/SPRi AI Brief_4월호_산업동향_250407_F.pdf"

loader = PyPDFLoader(file_path)
pages = []

async for page in loader.alazy_load():
    pages.append(page)

text_splitter = SemanticChunker(OpenAIEmbeddings())

docs = text_splitter.split_documents(pages)

 

!pip install langchain_chroma

!pip install -U "opentelemetry-api==1.26.0" \
              "opentelemetry-sdk==1.26.0" \
              "opentelemetry-exporter-otlp-proto-grpc==1.26.0"

 

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

vectorstore = Chroma.from_documents(documents=docs, embedding=OpenAIEmbeddings())

 

retriever = vectorstore.as_retriever(search_kwargs={"k": 1})

 

!pip install langgraph

 

from langgraph.graph import MessagesState


class State(MessagesState):
    """
    Represents the state of our graph.

    Attributes:
        question: question
        generation: LLM generation
        document: retrieved document
    """

    question: str
    generation: str
    document: str

 

2. Retriever 호출을 위한 Agent 와 Retriever

from langchain.tools.retriever import create_retriever_tool

retriever_tool = create_retriever_tool(
    retriever,
    "retrieve_ai_policy_april_2025",
    "Search and return information from the April 2025 AI Policy Report, including global AI policies, legal regulations, major industry updates, technical research, education trends, and government strategies related to AI development.",
)

 

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def agent(state: State):
    """
    Invokes the agent model to generate a response based on the current state. Given
    the question, it will decide to retrieve using the retriever tool, or simply end.
    """
    print("##### HI ! #####")
    messages = state["messages"]
    llm_with_tools = llm.bind_tools([retriever_tool]) # , tool_choice="any"
    response = llm_with_tools.invoke(messages)
    print("response", response)

    return {"messages": [response], "question" : messages[0].content}

 

def retrieve(state: State):
    """
    Retrieve documents
    """
    print("##### RETRIEVE #####")
    question = state["question"]

    document = retriever.invoke(question)


    return {"document": document[0].page_content, "question": question}

 

3. 관련성 평가

from pydantic import BaseModel, Field

class Grade(BaseModel):
    """Binary score for relevance check."""

    binary_score: str = Field(description="Relevance score 'yes' or 'no'")

 

from langchain import hub
from langchain_core.prompts import ChatPromptTemplate


def grade_documents(state: State):
    """
    Determines whether the retrieved document is relevant to the question.
    """

    print("##### CHECK RELEVANCE #####")

    grader = llm.with_structured_output(Grade)

    grader_prompt = ChatPromptTemplate.from_template(
        """
        You are a grader assessing relevance of a retrieved document to a user question. \n 
        Here is the retrieved document: \n\n {context} \n\n
        Here is the user question: {question} \n
        If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n
        It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \n
        Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.
        """
    )

    chain = grader_prompt | grader

    question = state["question"]
    print("state[messages]", state["messages"])
    
    document = state["document"]

    print("question", question)
    print("context", document)

    score = chain.invoke({"question": question, "context": document})
    print("context", document)
    grade = score.binary_score
    print("grade", grade)
    if grade == "yes":
        print("---GRADE: DOCUMENT RELEVANT---")
        return {"document": document, "question": question}
    else:
        print("---GRADE: DOCUMENT NOT RELEVANT---")
        return {"document": "", "question": question}

 

4. 답변 생성

def generate(state: State):
    """
    Generate answer based on the retrieved document and the question.
    """
    print("##### GENERATE #####")
    question = state["question"]
    document = state["document"]
    print("question", question)
    print("context", document)
    prompt = hub.pull("rlm/rag-prompt")

    response = llm.invoke(
        prompt.format_messages(context=document, question=question)
    )
    print("response", response)
    return {"documents": document, "question": question, "generation": response.content, "messages": [response]}

 

5. 질문 재작성

from langchain_core.messages import HumanMessage

def transform_query(state):
    """
    Transform the query to produce a better question.

    Args:
        state (dict): The current graph state

    Returns:
        state (dict): Updates question key with a re-phrased question
    """

    print("##### TRANSFORM QUERY #####")
    question = state["question"]

    system = """You a question re-writer that converts an input question to a better version that is optimized \n 
     for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning."""
    re_write_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            (
                "user",
                "Here is the initial question: \n\n {question} \n Formulate an improved question In Korean. ",
            ),
        ]
    )

    question_rewriter = re_write_prompt | llm

    better_question = question_rewriter.invoke({"question": question})
    print("question", question)
    print("better_question", better_question.content)
    return {"question": better_question.content, "messages": [better_question]}

 

6. Web Search Agent

from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
web_search_agent = create_react_agent(llm, tools=[TavilySearchResults(max_results=3)])

 

7. 답변 생성 여부 결정

def decide_to_generate(state):
    """
    Determines whether to generate an answer, or re-generate a question.
    """

    print("##### ASSESS GRADED DOCUMENTS #####")

    if state["document"] == "":
        print(
            "---DECISION: RETRIEVED DOCUMENT ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---"
        )
        return "transform_query"
    else:
        print("---DECISION: GENERATE---")
        return "generate"

 

8. 답변 평가

  • 답변 해결성 평가 (답변과 질문을 비교)
class GradeAnswer(BaseModel):
    """Binary score to assess answer addresses question."""

    binary_score: str = Field(
        description="Answer addresses the question, 'yes' or 'no'"
    )


structured_llm_grader = llm.with_structured_output(GradeAnswer)

system = """You are a grader assessing whether an answer addresses / resolves a question \n 
    If ambiguous, return no. \n
    Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question."""
answer_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("user", "User question: \n\n {question} \n\n LLM generation: {generation}"),
    ]
)

answer_grader = answer_prompt | structured_llm_grader

 

  • 환각 평가 (문서와 답변을 비교하여 팩트 체크)
class GradeHallucinations(BaseModel):
    """Binary score for hallucination present in generation answer."""

    binary_score: str = Field(
        description="Answer is grounded in the facts, 'yes' or 'no'"
    )

structured_llm_grader = llm.with_structured_output(GradeHallucinations)

system = """You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \n 
     Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts."""
hallucination_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("user", "Set of facts: \n\n {document} \n\n LLM generation: {generation}"),
    ]
)

hallucination_grader = hallucination_prompt | structured_llm_grader
  • 환각 발생 : not supported
  • 환각 X, 답변이 질문을 해결 X : not useful
  • 환각 X, 답변이 질문을 해결 O : useful
def grade_generation_v_documents_and_question(state):
    """
    Determines whether the generation is grounded in the document and answers question.
    """

    print("##### CHECK HALLUCINATIONS #####")
    question = state["question"]
    document = state["document"]
    generation = state["generation"]

    score = hallucination_grader.invoke(
        {"document": document, "generation": generation}
    )
    grade = score.binary_score

    
    if grade == "yes": # 환각 없음
        print("---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---")
        score = answer_grader.invoke({"question": question, "generation": generation})
        grade = score.binary_score
        if grade == "yes": # 답변이 질문을 해결함
            print("---DECISION: GENERATION ADDRESSES QUESTION---")
            return "useful"
        else: # 답변이 질문을 해결하지 않음
            print("---DECISION: GENERATION DOES NOT ADDRESS QUESTION---")
            return "not useful"
    else: # 환각 있음
        print("---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---")
        return "not supported"

 

from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode, tools_condition

graph_builder = StateGraph(State)

graph_builder.add_node("agent", agent)
graph_builder.add_node("web_search_agent", web_search_agent)
graph_builder.add_node("retrieve", retrieve)

graph_builder.add_conditional_edges(
    "agent",
    tools_condition,
    {
        "tools": "retrieve",
        END: "web_search_agent",
    },
)
graph_builder.add_edge(START, "agent")
graph_builder.add_edge("web_search_agent", END)

 

graph_builder.add_node("grade_documents", grade_documents)
graph_builder.add_node("generate", generate)
graph_builder.add_node("transform_query", transform_query)

graph_builder.add_edge("retrieve", "grade_documents")
graph_builder.add_conditional_edges(
    "grade_documents",
    decide_to_generate,
    {
        "transform_query": "transform_query",
        "generate": "generate",
    },
)
graph_builder.add_edge("transform_query", "retrieve")
graph_builder.add_conditional_edges(
    "generate",
    grade_generation_v_documents_and_question,
    {
        "not supported": "generate",
        "useful": END,
        "not useful": "transform_query",
    },
)

graph = graph_builder.compile()

 

from IPython.display import Image, display

try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
    pass

 

try:
    display(Image(graph.get_graph(xray=True).draw_mermaid_png()))
except Exception:
    pass

 

9. 테스트

  • case 1: 내부 문서 검색이 필요하지 않은 경우
from langgraph.errors import GraphRecursionError

inputs = {
    "messages": "2024년 노벨문학상 수상자는 누구인가요?"
}
try: 
    response = graph.invoke(inputs)
except GraphRecursionError as e:
        print("Recursion Error")

for msg in response["messages"]:
    msg.pretty_print()

 

  • case 2: 내부 문서 검색이 필요한 경우
inputs = {
    "messages": "카카오가 개발한 모델에 대해 알려주세요."
}
try: 
    response = graph.invoke(inputs)
except GraphRecursionError as e:
        print("Recursion Error")

for msg in response["messages"]:
    msg.pretty_print()

 

inputs = {
    "messages": "마누스 AI 에이전트에 대한 장단점"
}

try: 
    response = graph.invoke(inputs)
except GraphRecursionError as e:
        print("Recursion Error")
        
for msg in response["messages"]:
    msg.pretty_print()

 

  • case 3: 부실한 사용자 쿼리를 재작성한 후 재 검색 및 답변
inputs = {
    "messages": "구글 AI AI AI"
}

try: 
    response = graph.invoke(inputs)
except GraphRecursionError as e:
        print("Recursion Error")

for msg in response["messages"]:
    msg.pretty_print()

'AI > AI Agent' 카테고리의 다른 글

쿼리문을 작성하는 RAG  (0) 2025.09.19
LangGraph Reflection  (0) 2025.09.15
랭그래프를 이용한 간단한 챗봇  (0) 2025.09.10
LangGraph basic  (0) 2025.09.09
LangGraph  (1) 2025.09.08

1. Reflection

Reflection은 에이전트가 스스로 결과를 평가·비판 한 뒤 그 피드백을 상태(state)에 기록하고, 필요하면 수정 루프로 되돌아가 답을 개선하는 설계 패턴입니다. 보통 “작성 노드(답 생성) → 리플렉션 노드(자기평가) → 라우팅(조건부 엣지)”로 구성되며, 리플렉션 노드는 품질 기준(예: 정확성, 근거, 형식)을 점수·코멘트(score, critique)로 남깁니다. 라우터는 이 정보를 읽어 임계값 미달이면 작성 노드로 되감기, 충족하면 종료 노드로 이동합니다. 무한 루프를 막기 위해 max_iters 같은 반복 한도를 두며, 툴 호출과는 별개로 LLM의 자기검토 능력을 활용해 코드 생성, 질의응답, 체인드 리저닝 등의 정확도·일관성을 높이는 데 쓰입니다.

 

import getpass
import os

def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")

_set_env("OPENAI_API_KEY")

 

!pip install langchain_openai

 

1. 가사 생성

from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "당신은 5단락 노래가사를 훌륭하게 작성하는 작사 도우미입니다."
            "사용자의 요청에 따라 최고의 가사를 작성하세요."
            "사용자가 피드백을 제공할 경우, 이전 시도에서 개선된 수정본을 작성해 응답하세요.",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

llm = ChatOpenAI(model="gpt-5-nano")
generate = prompt | llm

 

lyric = ""
request = HumanMessage(
    content="코딩에 대한 가사를 작성해주세요."
)
for chunk in generate.stream({"messages": [request]}):
    print(chunk.content, end="")
    lyric += chunk.content

 

2. 가사 개선

reflection_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "당신은 가사를 채점하는 작사가입니다. 사용자가 제출한 작사에 대한 비평과 개선 사항을 작성하세요."
            "가사의 길이, 깊이, 문체 등을 포함해 구체적인 개선 요청을 제공하세요.",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)
reflect = reflection_prompt | llm

 

reflection = ""
for chunk in reflect.stream({"messages": [request, HumanMessage(content=lyric)]}):
    print(chunk.content, end="")
    reflection += chunk.content

for chunk in generate.stream(
    {"messages": [request, AIMessage(content=lyric), HumanMessage(content=reflection)]}
):
    print(chunk.content, end="")

3. Graph로 Reflection 구현

!pip install langgraph

 

from typing import Annotated
from typing_extensions import TypedDict

from langgraph.graph import END, StateGraph, START
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver


class State(TypedDict):
    messages: Annotated[list, add_messages]

 

def generation_node(state: State) -> State:
    return {"messages": [generate.invoke(state["messages"])]}

 

def reflection_node(state: State) -> State:
    cls_map = {"ai": AIMessage, "human": HumanMessage}

    # 첫번째 사용자 요청 + 생성메시지 (reflection_node's input)
    # 첫번째 사용자 요청 + 생성메시지 + 피드백메시지 (generation_node's input)
    # 첫번째 사용자 요청 + 생성메시지 + 피드백메시지 + 수정된 생성메시지 (reflection_node's input)
    # 첫번째 사용자 요청 + 생성메시지 + 피드백메시지 + 수정된 생성메시지 + 피드백메시지 (generation_node's input)
    # ...
    translated = [state["messages"][0]] + [
        cls_map[msg.type](content=msg.content) for msg in state["messages"][1:]
    ]
    # translated = [state["messages"][0]] + [
    #     cls_map[msg.type](content=msg.content) for msg in state["messages"][-2:]
    # ]
    res = reflect.invoke(translated)

    return {"messages": [HumanMessage(content=res.content)]}

 

graph_builder = StateGraph(State)
graph_builder.add_node("generate", generation_node)
graph_builder.add_node("reflect", reflection_node)
graph_builder.add_edge(START, "generate")

 

from typing import Literal
from langgraph.graph import END

def should_continue(state: State) -> Literal["reflect", END]:
    if len(state["messages"]) > 6:
        return END
    return "reflect"


graph_builder.add_conditional_edges("generate", should_continue)

 

graph_builder.add_edge("reflect", "generate")

 

memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
graph

config = {"configurable": {"thread_id": "1"}}

 

for event in graph.stream(
    {
        "messages": [
            HumanMessage(
                content="코딩에 대한 가사를 작성해주세요."
            )
        ],
    },
    config,
):
    print(event)
    print("---")

첫번째 사용자 요청 + 생성메시지 + 피드백메시지 + 수정된 생성메시지

 

state = graph.get_state(config)

 

ChatPromptTemplate.from_messages(state.values["messages"]).pretty_print()

 

2. Reflextion 구현

“Reflexion: Language Agents with Verbal Reinforcement Learning”은, 2023년 3월 20일 최초 제출, 2023년 10월 10일 v4로 개정된 논문입니다. 저자는 Noah Shinn 외 5명이고, 핵심 내용은 언어 에이전트가 스스로 언어적 피드백(반성문)을 생성·메모리에 저장해 다음 시도에 반영함으로써 성능을 높이는 프레임워크를 제안했다는 점입니다. HumanEval 등에서 유의미한 향상을 보고합니다.

https://arxiv.org/abs/2303.11366

 

Reflexion: Language Agents with Verbal Reinforcement Learning

Large language models (LLMs) have been increasingly used to interact with external environments (e.g., games, compilers, APIs) as goal-driven agents. However, it remains challenging for these language agents to quickly and efficiently learn from trial-and-

arxiv.org

 

 

  • Actor (LM): 실제 행동(답안 작성, 코드 생성 등)을 내는 언어모델입니다.
  • Evaluator (LM): Actor가 낸 결과를 내부적으로 평가합니다(정확성·형식·테스트 통과 여부 판단 등).
  • Self-reflection (LM): 평가 결과를 바탕으로 “다음에는 이렇게 고치자” 같은 반성문(Reflective text)을 만들어 냅니다.
  • Trajectory (short-term memory): 이번 시도에서의 행동/관찰 기록(a₀, o₀, …)을 담는 단기 메모리입니다.
  • Experience (long-term memory): 누적된 반성문을 쌓아두는 장기 메모리(mem)입니다. 이후 시도에서 프롬프트에 이 기억을 넣어 같은 실수를 반복하지 않게 합니다.
  • Environment: 외부에서 관찰/보상(예: 유닛 테스트의 통과/실패, 웹툴의 응답 등)을 제공합니다. 외부 피드백이 있으면 Evaluator의 판단과 함께 사용됩니다.

def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")

_set_env("TAVILY_API_KEY")

 

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-5-nano")

 

!pip install langchain_community

 

!pip install langchain-tavily

 

from langchain_tavily import TavilySearch

tavily_tool = TavilySearch(max_results=5)

 

1. 필요한 데이터 클래스 정의

  • Reflection - 놓친것 / 불필요한 것
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from pydantic import BaseModel, Field

class Reflection(BaseModel):
    missing: str = Field(description="누락되거나 부족한 부분에 대한 비평")
    superfluous: str = Field(description="불필요한 부분에 대한 비평")

 

  • AnswerQuestion - 답변 / 답변에 대한 반성 / 개선하기 위한 검색 쿼리
class AnswerQuestion(BaseModel):
    answer: str = Field(description="질문에 대한 10문장 이내의 자세한 답변")
    search_queries: list[str] = Field(
        description="현재 답변에 대한 비평을 해결하기 위한 추가 조사를 위한 1~3개의 웹 검색 쿼리"
    )
    reflection: Reflection = Field(description="답변에 대한 자기반성 내용")

 

  • Responder - 구조화된 출력을 위한 답변기
class Responder:
    def __init__(self, runnable):
        self.runnable = runnable # Chain

    def respond(self, state: dict):
        response = self.runnable.invoke(
            {"messages": state["messages"]}
        )
        return {"messages": response}

 

2. 초기 답변기 만들기 (Initial responder)

  • 초기 답변을 위한 Chain 생성 -출력 스키마를 도구로 사용
import datetime

actor_prompt_template = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """당신은 전문 연구자입니다.

            1. {first_instruction}
            2. <Reflect> 생성한 답변을 다시 되돌아보고 개선할 수 있도록 비판하세요.
            3. <Recommend search queries> 답변의 질을 높이기 위해 추가적으로 조사해야 할 정보에 대한 웹 검색 쿼리를 추천하세요.""",
        ),
        MessagesPlaceholder(variable_name="messages"),
        (
            "user",
            "\n\n<Reflect> 사용자 원래 질문과 지금까지의 행동을 되돌아보세요."
        ),
    ]
)

initial_answer_chain = actor_prompt_template.partial(
    first_instruction="질문에 대한 10문장 이내의 자세한 답변을 제공해주세요.", # 초기 답변
) | llm.bind_tools(tools=[AnswerQuestion], tool_choice="any")

 

llm_with_tool = llm.bind_tools(tools=[AnswerQuestion], tool_choice="any")
response = llm_with_tool.invoke([HumanMessage(content="AI Agent가 무엇인가요?")])
print(response)

 

response.tool_calls[0]['args']

first_responder = Responder(runnable=initial_answer_chain)

 

example_question = "AI Agent가 무엇인가요?"
initial = first_responder.respond(
    {"messages": [HumanMessage(content=example_question)]}
)

 

initial

  • tool 호출 결과 확인 (AnswerQuestion 에 맞춰 출력 생성)
initial["messages"].tool_calls[0]["args"]

 

3. 수정 단계(Revision)

class ReviseAnswer(AnswerQuestion):
    """Revise your original answer to your question. Provide an answer, reflection,

    cite your reflection with references, and finally
    add search queries to improve the answer."""

    references: list[str] = Field(
        description="업데이트된 답변에 사용된 인용 출처"
    )

 

revise_instructions = """이전 답변을 새로운 정보를 바탕으로 수정하세요.
- 이전 비평 내용을 활용해 중요한 정보를 추가해야 합니다.  
  - 수정된 답변에는 반드시 숫자로 된 인용 표시를 포함하여 검증 가능하도록 해야 합니다.  
  - 답변 하단에 "참고문헌" 섹션을 추가하세요 (이 부분은 단어 수 제한에 포함되지 않습니다). 형식은 다음과 같습니다:  
    - [1] https://example.com  
    - [2] https://example.com  

- 이전 비평 내용을 바탕으로 불필요한 정보를 제거하고, 최종 답변은 반드시 200자를 넘지 않도록 하세요.
"""


revision_chain = actor_prompt_template.partial(
    first_instruction=revise_instructions,
) | llm.bind_tools(tools=[ReviseAnswer], tool_choice="any")


revisor = Responder(runnable=revision_chain)

 

  • 초기답변에서 생성한 웹검색 쿼리를 Tool 실행한 결과를 함께 입력
import json

revised = revisor.respond(
    {
        "messages": [
            HumanMessage(content=example_question),
            initial["messages"],
            ToolMessage(
                tool_call_id=initial['messages'].additional_kwargs['tool_calls'][0]['id'],
                content=json.dumps(
                    tavily_tool.invoke(
                        {
                            "query": initial["messages"].tool_calls[0]["args"]['search_queries'][0]
                        }
                    )
                ),
            ),
        ]
    }
)

 

 

revised["messages"]

revised["messages"].tool_calls

 


4. 웹검색을 위한 툴 노드 생성

tavily_tool.batch(
    [
        {"query": initial["messages"].tool_calls[0]["args"]['search_queries'][0]}
    ]
)

from langchain_core.tools import StructuredTool

from langgraph.prebuilt import ToolNode


def run_queries(search_queries: list[str], **kwargs):
    """Run the generated queries."""
    return tavily_tool.batch([{"query": query} for query in search_queries])


tool_node = ToolNode(
    [
        StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),
        StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),
    ]
)

 

 

5. 그래프 생성하기

from langgraph.graph import END, StateGraph, START
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict


class State(TypedDict):
    messages: Annotated[list, add_messages]

 

MAX_ITERATIONS = 5
graph_builder = StateGraph(State)
graph_builder.add_node("draft", first_responder.respond)

graph_builder.add_node("execute_tools", tool_node) # 웹 검색 진행
graph_builder.add_node("revise", revisor.respond)

graph_builder.add_edge("draft", "execute_tools")
graph_builder.add_edge("execute_tools", "revise")

 

def _get_num_iterations(state: list):
    i = 0
    for m in state[::-1]:
        if m.type not in {"tool", "ai"}:
            break
        i += 1
    return i


def event_loop(state: list):
    num_iterations = _get_num_iterations(state["messages"])
    if num_iterations > MAX_ITERATIONS:
        return END
    return "execute_tools"


graph_builder.add_conditional_edges("revise", event_loop, ["execute_tools", END])
graph_builder.add_edge(START, "draft")
graph = graph_builder.compile()
graph

events = graph.stream(
    {"messages": [HumanMessage(content="AI Agent가 무엇인가요?")]},
    stream_mode="values",
)
for i, step in enumerate(events):
    print(f"Step {i}")
    step["messages"][-1].pretty_print()

 


3. Plan & Execute

import getpass
import os


def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")


_set_env("OPENAI_API_KEY")
_set_env("TAVILY_API_KEY")
!pip install langchain_community
from langchain_community.tools.tavily_search import TavilySearchResults
search_tool = TavilySearchResults(max_results=3)
tools = [search_tool]
# search_tool = TavilySearchResults(max_results=3)
!pip install langchain_openai
!pip install langgraph
# ReAct 패턴
# 추론 -> 행동 -> 관찰 -> 답변, 자동으로 생성
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent   

llm = ChatOpenAI(model="gpt-5-nano")
prompt = "You are a helpful assistant."
plan_executor = create_react_agent(llm, tools, prompt=prompt)
plan_executor

plan_executor.invoke({'messages':[('user','2025년 한국의 최저 시급은 얼마입니까?')]})

from pydantic import BaseModel, Field
from typing import List
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
class Plan(BaseModel):
    """Plan to follow in future"""
    steps: List[str] = Field(
        # 단계별 진행 예정
        description="different steps to follow, should be in sorted order"
    )
planner_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """For the given objective, come up with a simple step by step plan. \
            This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \
            The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.""",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)
planner = planner_prompt | llm.with_structured_output(Plan)
plan_result = planner.invoke(
    {
        "messages": [HumanMessage(
            content="2025년 한국에서 개봉한 영화 중 가장 흥행한 영화는 무엇인가요?",
        )]
    }
)
plan_result.steps

 

replanner_prompt = ChatPromptTemplate.from_template(
    """For the given objective, come up with a simple step by step plan. \
    This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \
    The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.
    
    Your objective was this:
    {input}
    
    Your original plan was this:
    {plan}
    
    You have currently done the follow steps:
    {past_steps}
    
    Update your plan accordingly.
    If no more steps are needed and you can return to the user, then respond with that.
    Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done.
    Do not return previously done steps as part of the plan."""
)
from typing import Union

class Response(BaseModel):
    """Response to user."""
    response: str

class Act(BaseModel):
    """Action to perform."""
    action: Union[Response, Plan] = Field(
        description="Action to perform. If you want to respond to user, use Response. "
        "If you need to further use tools to get the answer, use Plan."
    )
replanner = replanner_prompt | llm.with_structured_output(Act)
import operator
from typing import Annotated, List, Tuple
from typing_extensions import TypedDict

class PlanExecute(TypedDict):
    input: str
    plan: List[str]
    past_steps: Annotated[List[Tuple], operator.add]
    response: str
# 계획 생성
def plan_step(state: PlanExecute):  # 계획을 생성하는 노드
    plan = planner.invoke({"messages": [("user", state["input"])]})
    return {"plan": plan.steps}
# 계획 실행
def execute_step(state: PlanExecute):
    plan = state["plan"]
    plan_str = "\n".join(f"{i+1}. {step}" for i, step in enumerate(plan))
    task = plan[0]
    task_formatted = f"""For the following plan:
    {plan_str}\n\nYou are tasked with executing step {1}, {task}."""
    agent_response = plan_executor.invoke(
        {"messages": [("user", task_formatted)]}
    )
    return {
        "past_steps": [(task, agent_response["messages"][-1].content)], # 실행 완료한 계획과 결과 저장
    }
# 계획 수정
def replan_step(state: PlanExecute):
    output = replanner.invoke(state)
    if isinstance(output.action, Response): # 답변이 바로 가능한 상태
        return {"response": output.action.response}
    else:
        return {"plan": output.action.steps}
from langgraph.graph import END

def should_end(state: PlanExecute):
    if "response" in state and state["response"]:
        return END
    else:
        return "agent"
from langgraph.graph import StateGraph, START

graph_builder = StateGraph(PlanExecute)
graph_builder.add_node("planner", plan_step)
graph_builder.add_node("agent", execute_step)
graph_builder.add_node("replan", replan_step)
graph_builder.add_edge(START, "planner")
graph_builder.add_edge("planner", "agent")
graph_builder.add_edge("agent", "replan")
graph_builder.add_conditional_edges(
    "replan",
    should_end,
    ["agent", END],
)

graph = graph_builder.compile()
graph

 

config = {'recursion_limit':50}
inputs = {'input' :'2024년 노벨 문학상 수상자의 출신국가는 어디인가요?'}

for event in graph.stream(inputs, config=config, stream_mode='values'):
    for k,v in event.items():
        print(k,v)
    print('*'*50)

 

4. 코드 수정을 반복하는 데이터 전처리 Agent

1. 클로드

클로드(Claude)는 앤트로픽(Anthropic)에서 개발한 대규모 언어 모델(LLM) 기반의 인공지능 챗봇으로, 사람과의 대화, 글쓰기, 요약, 코드 작성 등 다양한 작업을 수행할 수 있는 생성형 AI입니다. 이름은 인공지능의 선구자 클로드 섀넌(Claude Shannon)에서 따왔으며, “헌宪법 기반 AI(constitutional AI)” 접근법을 적용해 안전성과 투명성을 강화한 것이 특징입니다. 즉, 인간의 직접적인 지시보다는 미리 정해둔 원칙과 가이드라인을 통해 스스로 출력을 조율하도록 설계되었기 때문에, 사용자가 안심하고 활용할 수 있는 대화형 AI라는 점에서 주목받고 있습니다.

 

titanic.csv
0.06MB

 

from langchain_core.tools import tool
import pandas as pd
@tool
def describe_data(csv: str) -> str:
    """Describe the date column in the dataframe.
    Args:
        csv: csv data path string
    """
    df = pd.read_csv(csv)
    describe_str = f"""Data: {csv}""" + df.describe(include='all').to_string()
    return describe_str
tools=[describe_data]
!pip install langchain_openai		# 세션 다시시작 취소해주세요
!pip install langchain_anthropic
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic

llm_gpt = ChatOpenAI(model="gpt-5-nano")
llm_with_tools = llm_gpt.bind_tools(tools, tool_choice="any")
response = llm_with_tools.invoke(
    "https://raw.githubusercontent.com/pycaret/pycaret/master/datasets/diabetes.csv 이 데이터의 전처리를 해주세요."
)
response.tool_calls[0]['args']

# {'csv': 'https://raw.githubusercontent.com/pycaret/pycaret/master/datasets/diabetes.csv'}
from pydantic import BaseModel, Field

class code(BaseModel):
    """Schema for code solutions."""
    # 코드에 대한 접근방식
    prefix: str = Field(description="Description of the problem and approach")
    # 코드의 import 영역
    imports: str = Field(description="Code block import statements")
    # 코드 자체에 대한 
    code: str = Field(description="Code block not including import statements")
from langchain_core.prompts import ChatPromptTemplate

GENERATE_CODE_TEMPLATE = """
Given the following pandas `describe()` output of a dataset,
write a **directly executable Python code** to:
1. handle missing values,
2. convert categorical columns,
3. ...any additional preprocessing needed,
4. prepare the dataset for machine learning.
Here is the describe result of the dataset:
\n ------- \n  {context} \n ------- \n
Do not wrap the code in a function and the response in any backticks or anything else. The code should be written as a flat script, so that it can be run immediately and any errors will be visible during execution.
Ensure any code you provide can be executed \n
with all required imports and variables defined. Structure your answer with a description of the code solution. \n
Then list the imports. And finally list the functioning code block.
"""

code_gen_prompt = ChatPromptTemplate.from_messages(
    [
        ("user", GENERATE_CODE_TEMPLATE),
    ]
)
from langchain_anthropic import ChatAnthropic

llm_claude= ChatAnthropic(model="claude-sonnet-4-20250514")
tool_result = describe_data.invoke(response.tool_calls[0]['args'])
print(tool_result)

generated_code = llm_claude.invoke(
    code_gen_prompt.format_messages(context=tool_result)
)
print("generated_code", generated_code)
# 클라드 결제 안되서 키 에러남 

code_structurer = llm_gpt.with_structured_output(code)
code_solution = code_structurer.invoke(generated_code.content)
print("code_solution", code_solution)
!pip install langgraph
from langgraph.graph import StateGraph, MessagesState

class State(MessagesState): # messages
    """
    Represents the state of our graph.
    Attributes:
        error : Binary flag for control flow to indicate whether test error was tripped
        context: Data summary
        generation : Code solution
        iterations : Number of tries
    """
    error: str # yes or no
    context: str
    generation: str
    iterations: int
graph_builder = StateGraph(State)
llm_with_tools = llm_gpt.bind_tools(tools=[describe_data])  # llm이 알아서 하도록
def chatbot(state: State):
    print("##### HI ! #####")
    response = llm_with_tools.invoke(state["messages"])
    print("첫번째 LLM 호출 결과 : ", response)
    return {"messages": [response]}
graph_builder.add_node("chatbot", chatbot)
def add_context(state: State):
    print("##### ADD CONTEXT #####")
    if messages := state.get("messages", []):
        message = messages[-1] # 마지막 message 꺼내서 저장
    else:
        raise ValueError("No message found in input")
    for tool_call in message.tool_calls:
        for tool in tools:
            if tool.name == tool_call['name']:
                describe_str = tool.invoke(tool_call['args'])
    # Get context from describe_data tool
    print("데이터 통계 (context) : ", describe_str[:100])
    return {"context": describe_str}

graph_builder.add_node("add_context", add_context)
from langgraph.graph import END

def guardrail_route(
    state: State,
):
    """
    Use in the conditional_edge to route to the ToolNode if the last message
    has tool calls. Otherwise, route to the end.
    """
    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "add_context"
    return END
    
graph_builder.add_conditional_edges(
    "chatbot",
    guardrail_route,
    {"add_context": "add_context", END: END},
)
from pydantic import BaseModel, Field

class code(BaseModel):
    """Schema for code solutions."""
    prefix: str = Field(description="Description of the problem and approach")
    imports: str = Field(description="Code block import statements")
    code: str = Field(description="Code block not including import statements")
from langchain_core.prompts import ChatPromptTemplate

GENERATE_CODE_TEMPLATE = """
Given the following pandas `describe()` output of a dataset,
write a **directly executable Python code** to:
1. handle missing values,
2. convert categorical columns,
3. ...any additional preprocessing needed,
4. prepare the dataset for machine learning.
Here is the describe result of the dataset:
\n ------- \n  {context} \n ------- \n
Do not wrap the code in a function and the response in any backticks or anything else. The code should be written as a flat script, so that it can be run immediately and any errors will be visible during execution.
Ensure any code you provide can be executed \n
with all required imports and variables defined. Structure your answer with a description of the code solution. \n
Then list the imports. And finally list the functioning code block.
"""

code_gen_prompt = ChatPromptTemplate.from_messages(
    [
        ("user", GENERATE_CODE_TEMPLATE),
    ]
)
def generate(state: State):
    print("##### GENERATING CODE SOLUTION #####")
    context = state["context"]
    generated_code = llm_claude.invoke(
        code_gen_prompt.format_messages(context=context)
    )
    code_structurer = llm_gpt.with_structured_output(code)
    code_solution = code_structurer.invoke(generated_code.content)
    messages = [
        (
            "assistant",
            f"{code_solution.prefix} \n Imports: {code_solution.imports} \n Code: {code_solution.code}",
        )
    ]
    return {"generation": code_solution, "messages": messages}
graph_builder.add_node("generate", generate)
def code_check(state: State):
    print("##### CHECKING CODE #####")
    code_solution = state["generation"]
    imports = code_solution.imports
    code = code_solution.code
    # Check imports
    try:
        exec(imports)
    except Exception as e:
        print("---CODE IMPORT CHECK: FAILED---")
        error_message = [("user", f"Your solution failed the import test: {e}")]
        print("에러 메시지 : ", error_message)
        return {
            "generation": code_solution,
            "messages": error_message,
            "error": "yes",
        }
    # Check execution
    try:
        exec(imports + "\n" + code)
    except Exception as e:
        print("---CODE BLOCK CHECK: FAILED---")
        error_message = [("user", f"Your solution failed the code execution test: {e}")]
        print("에러 메시지 : ", error_message)
        return {
            "generation": code_solution,
            "messages": error_message,
            "error": "yes",
        }
    # No errors
    print("---NO CODE TEST FAILURES---")
    return {
        "generation": code_solution,
        "error": "no",
    }
graph_builder.add_node("code_check", code_check)
from langchain_core.prompts import ChatPromptTemplate

reflect_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """
            You are given an error message that occurred while running a Python script, along with the original code that produced the error.
            Provide a corrected version of the original code that resolves the issue.
            Ensure the code runs without errors and maintains the intended functionality."""
        ),
        (
            "user",
            """
            --- ERROR MESSAGE ---
            {error}
            --- ORIGINAL CODE ---
            {code_solution}
            ----------------------
            Ensure any code you provide can be executed \n
            with all required imports and variables defined. Structure your answer with a description of the code solution. \n
            Then list the imports. And finally list the functioning code block.""",
        )
    ]
)
from langgraph.graph import START, END
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("add_context", "generate")
graph_builder.add_edge("generate", "code_check")
graph_builder.add_conditional_edges(
    "code_check",
    decide_to_finish,
    {
        "end": END,
        "reflect": "reflect"
    },
)
graph_builder.add_edge("reflect", "code_check")
graph = graph_builder.compile()
graph

'AI > AI Agent' 카테고리의 다른 글

쿼리문을 작성하는 RAG  (0) 2025.09.19
벡터 데이터베이스  (0) 2025.09.18
랭그래프를 이용한 간단한 챗봇  (0) 2025.09.10
LangGraph basic  (0) 2025.09.09
LangGraph  (1) 2025.09.08

1. Tool Calling Agent

Tool Calling Agent는 자신이 가진 지식만 사용하는 것이 아니라, 외부 도구(API, 데이터베이스, 코드 실행기 등)를 호출해 문제를 해결하는 에이전트입니다. 사용자의 질문을 이해한 뒤 필요한 경우 적절한 툴을 선택하고, 입력값을 구성해 호출하며, 반환된 결과를 다시 가공해 최종 답변을 만듭니다. 쉽게 말해, 단순히 대화만 하는 AI가 아니라 “필요할 때 계산기, 검색엔진, 데이터 조회 도구 같은 도구를 직접 쓸 수 있는 AI”가 Tool Calling Agent입니다.

 

 

2. 웹 검색을 하는 챗봇

1. Tavily

Tavily는 웹을 실시간으로 검색해 AI가 최신·정확한 정보를 답변할 수 있도록 돕는 AI용 검색·브라우징 API 플랫폼입니다.

 

!pip install tavily-python

 

API 키발급(https://app.tavily.com/home)

import getpass
import os

def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")


_set_env("OPENAI_API_KEY")
_set_env("TAVILY_API_KEY")

 

 

from tavily import TavilyClient

tavily_client = TavilyClient()

 

response = tavily_client.search("What is AI Agent?", max_results=3) # , topic="news", days = 10
response
  • max_results=3  → 검색 결과의 최대 개수를 지정합니다. 3이므로, 검색 결과를 최대 3개까지 가져옵니다.
  • topic="news" → 뉴스 기사 위주로 검색
  • days=10 → 최근 10일 이내의 자료만 검색

 

response['results']

 

# get_search_context: 보통 문자열(string) 형태이며, 여러 개의 검색 결과에서 중요한 내용만 추려서 한 덩어리의 텍스트로 제공합니다.
context = tavily_client.get_search_context(query="What is AI Agent?")
context

 

# qna_search: Tavily가 반환한 최종 답변. 보통 문자열(string) 형태이며, 한두 문장 정도로 정리된 응답을 제공합니다.
answer = tavily_client.qna_search(query="What is AI Agent?")
answer

 

2. TavilySearch

파라미터 :

  • max_results (optional, int): 검색 결과 반환 수
  • topic (optional, str): 검색 카테고리 / "general"(Default), "news", "finance"
  • include_answer (optional, bool): 쿼리에 대한 답변 포함 여부
  • include_raw_content (optional, bool): 결과 HTML 포함 여부
  • include_images (optional, bool): 쿼리 관련 이미지 목록 포함 여부
  • include_image_descriptions (optional, bool): 각 이미지에 대한 설명 텍스트 포함 여부
  • search_depth (optional, str): 검색 깊이 / "basic"(Default),"advanced"
  • time_range (optional, str): 필터링 날짜 범위 - "day", "week", "month", "year"
  • include_domains (optional, List[str]): 구체적으로 포함할 도메인 목록
  • exclude_domains (optional, List[str]): 구체적으로 제외할 도메인 목록
!pip install langchain_tavily

 

from langchain_tavily import TavilySearch

tool = TavilySearch(max_results=3)
tool.invoke("What's a 'node' in LangGraph?")

 

invoke_with_toolcall = tool.invoke({"args": {'query': "What's a 'node' in LangGraph?"}, "type": "tool_call", "id": "foo", "name": "tavily_search"})
invoke_with_toolcall
  • .invoke()는 LangChain Tool 호출 형식으로 입력을 받을 수 있습니다.
  • 단순 문자열을 넣는 대신, JSON(딕셔너리) 구조로 툴 호출 이벤트(tool call event)처럼 전달하는 방식입니다.

 

invoke_with_toolcall.content

 

!pip install langchain_community

 

from langchain_community.tools.tavily_search import TavilySearchResults

tool = TavilySearchResults(max_results=2)
tool.invoke("What's a 'node' in LangGraph?")

 

 

invoke_with_toolcall = tool.invoke({"args": {'query': "What's a 'node' in LangGraph?"}, "type": "tool_call", "id": "foo", "name": "tavily"})
invoke_with_toolcall

 

# results에 들어 있는 정보
invoke_with_toolcall.content

 

# 모델의 모든 실행결과
invoke_with_toolcall.artifact

 

3. 도구 바인딩

from langchain_core.tools import tool

# 데코레이터
@tool
def add(a: int, b: int) -> int:
    # 툴에 대한 설명이 없으면 에러난다.
    """Adds a and b
    
    Arg:
        a: first int
        b: second int
    """
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """Multiply a and b
    
    Arg:
        a: first int
        b: second int
    """
    return a * b

tools = [add, multiply]

 

!pip install langchain_openai

 

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-5-nano") # model="gpt-4o"
llm_with_tools = llm.bind_tools(tools)

 

query = "What is 3 * 12? Also, what is 11 + 49?"

# gpt모델은 tool이 필요없어서 tool을 호출하지않음
# 근데 gpt-4o모델로 바꾸면 toolcalling을 한다.
llm_with_tools.invoke(query).tool_calls

''' gpt-4o 모델 사용시 결과
[{'name': 'multiply',
  'args': {'a': 3, 'b': 12},
  'id': 'call_00yGRWusKGcGdJCESc8wGOfu',
  'type': 'tool_call'},
 {'name': 'add',
  'args': {'a': 11, 'b': 49},
  'id': 'call_NIp0bUiAlPJOeQbJdAWzuIWm',
  'type': 'tool_call'}]
'''

 

query = "What is 12 % 2?"

# tool에 나누기는 선언해주지 않았기때문에 나오지 않음
llm_with_tools.invoke(query).tool_calls

 

llm_with_tools = llm.bind_tools(tools,
    tool_choice={"type": "function", "function": {"name": "multiply"}}
)

resp = llm_with_tools.invoke("What is 3 * 12? Use tool.")
print(resp.tool_calls)  # 이제 비어있지 않음

# [{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_PF9QFtOiW9ybRxsEH0VRGS6P', 'type': 'tool_call'}]

 

from langchain_openai import ChatOpenAI

tool = TavilySearch(max_results=2)
tools = [tool]

llm = ChatOpenAI(model="gpt-5-nano")
llm_with_tools = llm.bind_tools(tools) # TavilySearch(tools) 을 호출할 수 있도록 함

 

llm_with_tools.invoke("안녕")

 

llm_with_tools.invoke("What is Langgraph?")

 

llm_with_tools.invoke("What is Langgraph?").tool_calls

'''
[{'name': 'tavily_search',
  'args': {'query': 'Langgraph'},
  'id': 'call_uOxsU8BqNsufmT3GeDks9QOq',
  'type': 'tool_call'}]
'''

 

!pip install langgraph

 

from typing import Annotated
from typing_extensions import TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]

graph_builder = StateGraph(State)

def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]} # 일반적인 질문에 대한 일반 답변 or tool_calls

graph_builder.add_node("chatbot", chatbot)

 

4. ToolNode

ToolNode는 LangGraph에서 도구 호출을 실제로 실행해 주는 노드입니다. LLM이 생성한 AIMessage.tool_calls를 읽어 각 호출의 도구 이름과 인자를 매칭해 실행하고, 결과를 ToolMessage로 반환하여 그래프의 상태(대화 기록)에 추가합니다. 보통 “LLM 노드 → ToolNode → LLM 노드” 형태의 루프에서 사용되며, tools_condition 같은 조건부 엣지와 함께 붙여 LLM이 도구를 요청할 때만 ToolNode가 동작하게 합니다. 요약하면, ToolNode는 LLM의 툴 호출 계획을 실제 코드 실행으로 연결하는 브리지로, 도구 레지스트리(이름→함수/툴)만 넘겨주면 호출·에러 처리·결과 전달까지 표준화된 방식으로 처리해줍니다.

import json
from langchain_core.messages import ToolMessage
from langgraph.prebuilt import ToolNode

class BasicToolNode:
    def __init__(self, tools: list) -> None:
        self.tools_by_name = {tool.name: tool for tool in tools} # ["tavily_search" : TavilySearch()]

    def __call__(self, inputs: dict):
        if messages := inputs.get("messages", []):
            message = messages[-1] # 마지막 message
        else:
            raise ValueError("No message found in input")
        
        outputs = []
        for tool_call in message.tool_calls: # 메시지에서 호출된 도구를 불러옴
            tool_result = self.tools_by_name[tool_call["name"]].invoke( # Tool 호출 실행
                tool_call["args"]
            )
            outputs.append( # Tool 호출 결과(ToolMessage) 추가
                ToolMessage(
                    content=json.dumps(tool_result),
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
        return {"messages": outputs}

tool_node = BasicToolNode(tools=[tool])
# tool_node = ToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)

 

def route_tools(
    state: State,
):

    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"
    return END


graph_builder.add_conditional_edges(
    "chatbot",
    route_tools,
    {"tools": "tools", END: END},
)

 

# 엣지 연결
graph_builder.add_edge("tools", "chatbot") # 도구가 호출될 때마다 챗봇으로 돌아가 다음 단계를 결정
graph_builder.add_edge(START, "chatbot")
graph = graph_builder.compile()
graph

def stream_graph_updates(user_input: str):
    for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}): # graph 노드 호출 결과 받아옴
        for value in event.values():
            print("Assistant:", value["messages"][-1].content) # AI 답변 출력

 

while True:
    try:
        user_input = input("User: ")
        if user_input.lower() in ["quit", "exit", "q"]:
            print("Goodbye!")
            break

        stream_graph_updates(user_input)
    except:
        user_input = "What do you know about LangGraph?"
        print("User: " + user_input)
        stream_graph_updates(user_input)
        break

 

 

5. create_react_agent

create_react_agent는 LangChain에서 ReAct 패턴(Reason+Act)을 따르는 에이전트를 손쉽게 구성하는 팩토리로, LLM과 사용할 도구 목록, 그리고 적절한 프롬프트를 결합해 “생각→도구 호출→관찰→최종 답변”의 반복 루프를 수행하는 Agent 객체를 만들어줍니다. 이 에이전트는 질문을 해석해 필요한 도구를 선택하고 인자를 구성해 호출한 뒤, 결과를 반영해 다음 행동을 결정하며, 보통 AgentExecutor와 함께 실행하여 다단계 추론과 복수의 툴 호출을 자동으로 오케스트레이션합니다. 핵심은 프롬프트(지침), LLM, 툴 레지스트리(이름→함수/API), 출력 파서를 표준화해 붙여주는 것이며, OpenAI 스타일의 툴콜을 포함한 다양한 LLM과 호환되어 실용적인 “생각하며 도구를 쓰는” 에이전트를 빠르게 구성할 수 있게 해주는 점입니다.

from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

tool = TavilySearch(max_results=2)
tools = [tool]

llm = ChatOpenAI(model="gpt-5-nano")
agent = create_react_agent(llm, tools)

 

response = agent.invoke({"messages": [{"role": "user", "content": "What is LangGraph?"}]})

 

 

3. 원하는 형태로 출력하는 챗봇

from pydantic import BaseModel, Field
from typing import Union

class MovieResponse(BaseModel):
    title: str = Field(description = '영화제목')
    director :  str = Field(description = '감독이름')
    genre :  str = Field(description = '장르')
    release_year :  int = Field(description = '개봉연도')
model = ChatOpenAI(model="gpt-5-nano")
model_with_structured_output = model.with_structured_output(MovieResponse)

model_with_structured_output.invoke('메멘토 영화에 대해 설명해주세요')

# MovieResponse(title='메멘토', director='크리스토퍼 놀런', genre='심리 스릴러, 미스터리', release_year=2000)
class MovieResponse(BaseModel):
    title: str = Field(description = '영화제목')
    director :  str = Field(description = '감독이름')
    genre :  str = Field(description = '장르')
    release_year :  int = Field(description = '개봉연도')

class ConversationalResponse(BaseModel):
    response: str = Field(description = '사용자의 질문에 대해 친절하게 대화하듯 답변하는 문장')

class FinalResponse(BaseModel):
    final_output:Union[MovieResponse, ConversationalResponse]
structured_llm = model.with_structured_output(FinalResponse)
structured_llm.invoke('메멘토 영화에 대해 설명해주세요')

 

from langchain_core.tools import tool		# 재정의
from langgraph.graph import MessagesState
from typing import Literal
class State(MessagesState):
    final_response:MovieResponse
# 내 메세지에 따라 분기하는 코드
@tool
def get_movieinfo(movie:Literal['메멘토','인터스텔라']):
    """ 아래 설명은 영화에 대한 내용이야. 참고해줘 """
    if movie == "메멘토":
        return "메멘토는 단기 기억 상실증을 가진 주인공이 아내 살해 사건의 진실을 찾기 위해 메모와 문신에 의존해 사건을 추적하는 독특한 구조의 스릴러 영화입니다. "
    elif movie == "인터스텔라":
        return "인터스텔라는 인류의 미래를 구하기 위해 우주로 떠난 탐사대가 사랑과 시간, 과학의 한계를 넘어서며 펼치는 감동적인 SF 영화입니다."
    else:
        raise AssertionError("알수없는 영화")
tools = [get_movieinfo] # tool로 등록
model_with_tool = model.bind_tools(tools)
def call_model(state:State):
    response = model_with_tool.invoke(state['messages'])
    return {'messages': [response]}
from langchain_core.messages import HumanMessage

model = ChatOpenAI(model="gpt-5-nano")
model_with_structured_output = model.with_structured_output(MovieResponse)

def respond(state:State):
    # [-1] : AIMessage , [-2]: ToolMessage
    response = model_with_structured_output.invoke(
    [HumanMessage(content=state['messages'][-2].content)]
    )
    return {'final_response': response}

def should_continue(state:State):
    messages = state['messages']
    last_message = messages[-1]
    if not last_message.tool_calls:
        return "respond"
    else:
        return "continue"
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode

graph_builder = StateGraph(State)
graph_builder.add_node("agent", call_model)
graph_builder.add_node("respond", respond)
graph_builder.add_node("tools", ToolNode(tools))

graph_builder.set_entry_point("agent")
graph_builder.add_conditional_edges(
    "agent",
    should_continue,
    {
        "continue":"tools",
        "respond":"respond",
    }
)

graph_builder.add_edge("tools","agent")
graph_builder.add_edge("respond",END)
graph

answer = graph.invoke(input={"messages":[("human","메멘토 영화에 대해 알려주세요")]})[
    "final_response"
]
answer

 


오늘의 과제

 

from langchain_community.utilities import ArxivAPIWrapper
!pip install arxiv
arxiv = ArxivAPIWrapper()
docs = arxiv.run("1706.03762")

 

'AI > AI Agent' 카테고리의 다른 글

벡터 데이터베이스  (0) 2025.09.18
LangGraph Reflection  (0) 2025.09.15
LangGraph basic  (0) 2025.09.09
LangGraph  (1) 2025.09.08
AI Agent  (0) 2025.09.08

1. 그래프의 상태 업데이트

  • HumanMessage : 사용자(사람)의 메시지
  • AIMessage : AI(LLM)의 메시지
  • AnyMessage : HumanMessage, AIMessage를 포함하는 메시지
!pip install langgraph

 

from langchain_core.messages import AnyMessage
from typing_extensions import TypedDict

class State(TypedDict):
    messages: list[AnyMessage]
    extra_field: int

 

from langchain_core.messages import AIMessage

def node(state: State):
    messages = state["messages"]
    new_message = AIMessage("안녕하세요! 무엇을 도와드릴까요?")

    # return {"messages": new_message, "extra_field": 10} 
    return {"messages": messages + [new_message], "extra_field": 10}

 

from langgraph.graph import StateGraph

graph_builder = StateGraph(State)
graph_builder.add_node("node", node)
# set_entry_point : 그래프의 시작 노드를 지정하는 엣지 (START -> "node")
graph_builder.set_entry_point("node")
graph = graph_builder.compile()

graph

from langchain_core.messages import HumanMessage

result = graph.invoke({"messages": [HumanMessage("안녕")]})
# 워크플로우가 끝날때까지 기다려
result

'''
{'messages': AIMessage(content='안녕하세요! 무엇을 도와드릴까요?', additional_kwargs={}, response_metadata={}),
 'extra_field': 10}
 '''

 

result["messages"]

 

 

2. 대화메시지 상태 누적 업데이트

add_messages 는 기존 메시지에서 추가 메시지를 병합하는 데 사용하는 함수로, 새로 들어온 메시지를 추가할 때 사용할 수 있는 리듀서 역할

from typing_extensions import Annotated
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    extra_field: int

 

def node(state: State):
    messages = state["messages"]
    new_message = AIMessage("안녕하세요! 무엇을 도와드릴까요?")

    return {"messages": new_message, "extra_field": 10}

 

from langgraph.graph import StateGraph

graph_builder = StateGraph(State)
graph_builder.add_node("node", node)
graph_builder.set_entry_point("node")
graph = graph_builder.compile()

graph

input_message = {"role": "user", "content": "안녕하세요."}

result = graph.invoke({"messages": [input_message]})

for message in result["messages"]:
# pretty_print()는 데이터나 객체를 보기 좋게(Pretty) 정리해서 출력하는 함수
    message.pretty_print()
    
'''
================================ Human Message =================================

안녕하세요.
================================== Ai Message ==================================

안녕하세요! 무엇을 도와드릴까요?
'''

 

result["messages"]

'''
[HumanMessage(content='안녕하세요.', additional_kwargs={}, response_metadata={}, id='8fa15d41-a075-4e19-9121-f8f02a9f6f1d'),
 AIMessage(content='안녕하세요! 무엇을 도와드릴까요?', additional_kwargs={}, response_metadata={}, id='0ee70963-e37a-40f3-9dfd-111e9bbd0cb6')]
'''

 

invoke : 하나의 요청에 대한 결과를 받을 때 까지 코드 실행 멈춤. 한번에 하나의 요청을 처리함

graph.invoke({"messages": [input_message]})

 

ainvoke : 비동기 처리로 여러 요청을 동시에 보낼 수 있음

await graph.ainvoke({"messages": [input_message]})

 

stream : 중간 결과를 실시간으로 반환함

  • stream_mode="values" 각 단계의 현재 상태 값 출력
  • Default) stream_mode="updates" 각 단계의 상태 업데이트만 출력
  • stream_mode="messages" 각 단계의 메시지 출력
# stream : 중간 결과를 실시간으로 반환함
# stream_mode="values" 각 단계의 현재 상태 값 출력

for chunk in graph.stream({"messages": [input_message]}, stream_mode="values"):
    print(chunk)
    for state_key, state_value in chunk.items():
        if state_key == "messages":
            state_value[-1].pretty_print()  # 최근걸로 가져오기 위해서

 

# Default) stream_mode="updates" 각 단계의 상태 업데이트만 출력

for chunk in graph.stream({"messages": [input_message]}, stream_mode="updates"):
    print(chunk)
    for node, value in chunk.items():
        if node:
            print(node)
        if "messages" in value:
            print(value['messages'].content)

 

# stream_mode="messages" 각 단계의 메시지 출력

for chunk_msg, metadata in graph.stream({"messages": [input_message]}, stream_mode="messages"):
    print(chunk_msg)
    print(chunk_msg.content)
    print(metadata)
    print(metadata["langgraph_node"])

 

astream : 비동기 방식으로 스트리밍 처리

async for chunk_msg, metadata in graph.astream({"messages": [input_message]}, stream_mode="messages"):
    print(chunk_msg)
    print(chunk_msg.content)
    print(metadata)
    print(metadata["langgraph_node"])

 

 

3. 노드와 엣지 연결

from typing_extensions import TypedDict

class State(TypedDict):
    value_1: str
    value_2: int

 

def step_1(state: State):
    return {"value_1": state["value_1"]}

def step_2(state: State):
    current_value_1 = state["value_1"]
    return {"value_1": f"{current_value_1} b"}

def step_3(state: State):
    return {"value_2": 10}

 

from langgraph.graph import START, StateGraph

graph_builder = StateGraph(State)

# 노드 추가
graph_builder.add_node(step_1)
graph_builder.add_node(step_2)
graph_builder.add_node(step_3)

# 엣지 추가
graph_builder.add_edge(START, "step_1") # START ->1
graph_builder.add_edge("step_1", "step_2") # 1-> 2
graph_builder.add_edge("step_2", "step_3") # 2->3

 

graph = graph_builder.compile()
graph

graph.invoke({"value_1": "apple"})
# {'value_1': 'apple b', 'value_2': 10}

 

 

4. 노드와 엣지를 한번에 연결

graph_builder = StateGraph(State).add_sequence([step_1, step_2, step_3])
graph_builder.add_edge(START, "step_1")

graph = graph_builder.compile()

graph.invoke({"value_1": "c"})

 

 

5. 병렬로 연결

import operator
from typing import Annotated, Any
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    aggregate: Annotated[list, operator.add] # 업데이트 값이 뒤에 추가되도록 하는 operator.add 리듀서

 

def a(state: State):
    print(f'Adding "A" to {state["aggregate"]}')
    return {"aggregate": ["A"]}

def b(state: State):
    print(f'Adding "B" to {state["aggregate"]}')
    return {"aggregate": ["B"]}

def c(state: State):
    print(f'Adding "C" to {state["aggregate"]}')
    return {"aggregate": ["C"]}

def d(state: State):
    print(f'Adding "D" to {state["aggregate"]}')
    return {"aggregate": ["D"]}

 

graph_builder = StateGraph(State)

# 노드 추가
graph_builder.add_node(a)
graph_builder.add_node(b)
graph_builder.add_node(c)
graph_builder.add_node(d)

# 엣지 추가
# graph_builder.add_edge(START, "a")
# graph_builder.add_edge("a", "b") # a -> b
# graph_builder.add_edge("b", "c") # a -> c
# graph_builder.add_edge("c", "d") # b -> d
# graph_builder.add_edge("d", END)
# graph = graph_builder.compile()

# graph

# 엣지 추가
graph_builder.add_edge(START, "a")
graph_builder.add_edge("a", "b") # a -> b
graph_builder.add_edge("a", "c") # a -> c
graph_builder.add_edge("b", "d") # b -> d
graph_builder.add_edge("c", "d") # c -> d
graph_builder.add_edge("d", END)
graph = graph_builder.compile()

graph

graph.invoke({"aggregate":[]})

'''
Adding "A" to []
Adding "B" to ['A']
Adding "C" to ['A']
Adding "D" to ['A', 'B', 'C']
{'aggregate': ['A', 'B', 'C', 'D']}
'''

 

 

6. 조건부 엣지 병렬 연결

 

import operator
from typing import Annotated, Sequence
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    aggregate: Annotated[list, operator.add]
    which: str

 

def a(state: State):
    print(f'Adding "A" to {state["aggregate"]}')
    return {"aggregate": ["A"]}

def b(state: State):
    print(f'Adding "B" to {state["aggregate"]}')
    return {"aggregate": ["B"]}

def c(state: State):
    print(f'Adding "C" to {state["aggregate"]}')
    return {"aggregate": ["C"]}

def d(state: State):
    print(f'Adding "D" to {state["aggregate"]}')
    return {"aggregate": ["D"]}

def e(state: State):
    print(f'Adding "E" to {state["aggregate"]}')
    return {"aggregate": ["E"]}

 

graph_builder = StateGraph(State)
graph_builder.add_node(a)
graph_builder.add_node(b)
graph_builder.add_node(c)
graph_builder.add_node(d)
graph_builder.add_node(e)
graph_builder.add_edge(START, "a")

 

# bc 혹은 cd 로 라우트를 결정하는 함수
def route_bc_or_cd(state: State) -> Sequence[str]:
    if state["which"] == "cd":
        return ["c", "d"]
    return ["b", "c"]   # 노드 이름

intermediates = ["b", "c", "d"]
graph_builder.add_conditional_edges(
    # 조건을 주는 add_conditional
    "a",
    route_bc_or_cd, # 콜백
    intermediates,  # 적용
)

 

for node in intermediates:
    graph_builder.add_edge(node, "e")

graph_builder.add_edge("e", END)
graph = graph_builder.compile()
graph

graph.invoke({"aggregate": [], "which": "bc"})

'''
Adding "A" to []
Adding "B" to ['A']
Adding "C" to ['A']
Adding "E" to ['A', 'B', 'C']
{'aggregate': ['A', 'B', 'C', 'E'], 'which': 'bc'}
'''

 

graph.invoke({"aggregate": [], "which": "cd"})

'''
Adding "A" to []
Adding "C" to ['A']
Adding "D" to ['A']
Adding "E" to ['A', 'C', 'D']
{'aggregate': ['A', 'C', 'D', 'E'], 'which': 'cd'}
'''

 

 

7. 조건과 반복

 

import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    aggregate: Annotated[list, operator.add]

 

def a(state: State):
    print(f'Node A 처리 중 현재 상태값 : {state["aggregate"]}')
    return {"aggregate": ["A"]}


def b(state: State):
    print(f'Node B 처리 중 현재 상태값 : {state["aggregate"]}')
    return {"aggregate": ["B"]}


graph_builder = StateGraph(State)
graph_builder.add_node(a)
graph_builder.add_node(b)

 

def route(state: State):
    if len(state["aggregate"]) < 7:
        return "b"
    else:
        return END


graph_builder.add_edge(START, "a")
graph_builder.add_conditional_edges("a", route)
graph_builder.add_edge("b", "a")
graph = graph_builder.compile()

https://kroki.io/

 

Kroki!

Kroki provides a unified API with support for BlockDiag (BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag, RackDiag), BPMN, Bytefield, C4 (with PlantUML), D2, DBML, Ditaa, Erd, Excalidraw, GraphViz, Mermaid, Nomnoml, Pikchr, PlantUML, Structurizr, SvgBob, S

kroki.io

import requests, zlib, base64
from IPython.display import Image

# Mermaid 코드 추출
code = graph.get_graph().draw_mermaid()

# 압축·인코딩 후 Kroki 요청
encoded = base64.urlsafe_b64encode(zlib.compress(code.encode())).decode()
url = f"https://kroki.io/mermaid/png/{encoded}"
resp = requests.get(url)
display(Image(resp.content))

 

graph.invoke({"aggregate": []})

'''
Node A 처리 중 현재 상태값 : []
Node B 처리 중 현재 상태값 : ['A']
Node A 처리 중 현재 상태값 : ['A', 'B']
Node B 처리 중 현재 상태값 : ['A', 'B', 'A']
Node A 처리 중 현재 상태값 : ['A', 'B', 'A', 'B']
Node B 처리 중 현재 상태값 : ['A', 'B', 'A', 'B', 'A']
Node A 처리 중 현재 상태값 : ['A', 'B', 'A', 'B', 'A', 'B']
{'aggregate': ['A', 'B', 'A', 'B', 'A', 'B', 'A']}
'''

 

from langgraph.errors import GraphRecursionError
# GraphRecursionError 로 에러를 반환하는 방법
try:
    graph.invoke({"aggregate": []}, config={"recursion_limit": 4})
except GraphRecursionError: # 반복 종료 조건에 도달할 수 없는 경우
    print("Recursion Error")
    
'''
Node A 처리 중 현재 상태값 : []
Node B 처리 중 현재 상태값 : ['A']
Node A 처리 중 현재 상태값 : ['A', 'B']
Node B 처리 중 현재 상태값 : ['A', 'B', 'A']
'''

 

 

8. 조건에 따른 반복 처리하기

import operator
from typing import Annotated, Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    aggregate: Annotated[list, operator.add]

 

def a(state: State):
    print(f'Node A 처리 중 현재 상태값 : {state["aggregate"]}')
    return {"aggregate": ["A"]}

def b(state: State):
    print(f'Node B 처리 중 현재 상태값 : {state["aggregate"]}')
    return {"aggregate": ["B"]}

def c(state: State):
    print(f'Node C 처리 중 현재 상태값 : {state["aggregate"]}')
    return {"aggregate": ["C"]}

def d(state: State):
    print(f'Node D 처리 중 현재 상태값 : {state["aggregate"]}')
    return {"aggregate": ["D"]}

graph_builder = StateGraph(State)
graph_builder.add_node(a)
graph_builder.add_node(b)
graph_builder.add_node(c)
graph_builder.add_node(d)

 

def route(state: State) -> Literal["b", END]:
    if len(state["aggregate"]) < 7:
        return "b"
    else:
        return END


graph_builder.add_edge(START, "a")
graph_builder.add_conditional_edges("a", route)
graph_builder.add_edge("b", "c")
graph_builder.add_edge("b", "d")
graph_builder.add_edge(["c", "d"], "a")
graph = graph_builder.compile()

 

result = graph.invoke({"aggregate": []})

'''
Node A 처리 중 현재 상태값 : []
Node B 처리 중 현재 상태값 : ['A']
Node C 처리 중 현재 상태값 : ['A', 'B']
Node D 처리 중 현재 상태값 : ['A', 'B']
Node A 처리 중 현재 상태값 : ['A', 'B', 'C', 'D']
Node B 처리 중 현재 상태값 : ['A', 'B', 'C', 'D', 'A']
Node C 처리 중 현재 상태값 : ['A', 'B', 'C', 'D', 'A', 'B']
Node D 처리 중 현재 상태값 : ['A', 'B', 'C', 'D', 'A', 'B']
Node A 처리 중 현재 상태값 : ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']
'''

 

 

9. 사용자 입력에 따른 반복 조건 설정

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.graph.message import add_messages

class State(TypedDict):
    human_messages: Annotated[list[HumanMessage], add_messages]
    ai_messages: Annotated[list[AIMessage], add_messages]
    retry_num : int

 

def chatbot(state:State):
    retry_num = state["retry_num"]
    user_input = input(f"(현재 {retry_num}번째 답변) 사용자 입력: ")
    ai_message = AIMessage(f"{retry_num}번째 답변중!")

    return {"human_messages": [HumanMessage(content=user_input)], "ai_messages": [ai_message]}

def retry(state: State):
    return {"retry_num" : state["retry_num"] + 1}

graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("retry", retry)

 

def route(state: State):
    if "반복" in state["human_messages"][-1].content:
        return "retry"
    else:
        return END


graph_builder.add_edge(START, "chatbot")
graph_builder.add_conditional_edges("chatbot", route)
graph_builder.add_edge("retry", "chatbot")
graph = graph_builder.compile()

 

for chunk in graph.stream({"human_messages" : "반복", "retry_num": 0}, stream_mode="updates"):
    print(chunk)
    for node, value in chunk.items():
        if node:
            print(node)
        if "messages" in value:
            print(value['messages'].content)

 

graph.invoke({"human_messages" : "반복", "retry_num": 0})

 

'AI > AI Agent' 카테고리의 다른 글

벡터 데이터베이스  (0) 2025.09.18
LangGraph Reflection  (0) 2025.09.15
랭그래프를 이용한 간단한 챗봇  (0) 2025.09.10
LangGraph  (1) 2025.09.08
AI Agent  (0) 2025.09.08

1. 랭그래프

랭그래프(LangGraph)는 LangChain 생태계에서 에이전트나 RAG 시스템을 단계별로 구성하고 실행할 수 있게 해주는 그래프 기반 오케스트레이션 프레임워크입니다. 기존 RAG가 직선형 파이프라인이었다면, 랭그래프는 노드(작업 단위)와 엣지(흐름)를 그래프 형태로 정의해 분기, 반복, 조건 처리, 에이전트 루프 같은 복잡한 흐름을 명확하게 표현하고 실행할 수 있습니다. 이를 통해 “검색 → 답변 생성 → 자기평가 → 재검색” 같은 Agentic RAG 워크플로우를 안정적으로 설계·관리할 수 있으며, 디버깅과 모니터링도 쉬워집니다.

 

그래프 구조

그래프 구조(Graph Structure)는 객체(노드, vertex)와 그 객체들을 잇는 관계(간선, edge)로 표현되는 데이터 구조입니다. 노드는 개체를, 간선은 개체 간의 연결이나 관계를 나타내며, 방향성이 있는 경우 방향 그래프, 없는 경우 무방향 그래프로 구분됩니다. 이 구조는 복잡한 관계망을 직관적으로 표현하고 탐색할 수 있어, 소셜 네트워크 분석, 추천 시스템, 경로 탐색, 지식 그래프 등 다양한 분야에서 활용됩니다.

 

2. 그래프

 

자료 구조 알고리즘은 왜 공부해야하는 걸까?

프로그램 개발 능력이 향상되기 때문에 확연히 실력차이가 난다. 생각하는 폭이 넓어지는게 가장 큰 장점이다.

 

1.  그래프 종류

  • 무방향 그래프 : 방향이 없는 그래프, 간선을 통해 노드는 양방향으로 갈 수 있음
  • 방향 그래프 : 간선에 방향이 있는 그래프, 보통 노드 A, B가 A->B로 가는 간선으로 연결되어 있는 경우 <A,B>로 표기
  • 가중치 그래프 : 간선에 비용 또는 가중치가 할당된 그래프
  • 연결 그래프 : 무방향 그래프에서 모든 노드에 대해 항상 경로가 존재하는 경우
  • 비연결 그래프 : 무방향 그래프에서 특정 노드에 대해 경로가 존재하지 않는 경우
  • 순환 그래프 : 단순 경로의 시작 노드와 종료 노드가 동일한 경우
  • 비순환 그래프 : 사이클이 없는 그래

2. 그래프와 트리의 차이

 

3.  너비 우선 탐색(BFS , Breadth-First Search)

대표적인 그래프 탐색 알고리즘으로 정점들과 같은 레벨에 있는 노드들(형제 노드들)을 먼저 우선 탐색하는 방식이다.

이는 한 단계씩 내려가면서 해당 노드와 같은 레벨에 있는 노드들을 먼저 순회한다.

 

https://olrlobt.tistory.com/41

 

앞으로 방문해야하는 곳, 방문했던 곳을 메모리에 방을 만든다.

  • 방문해야하는 곳
    • 1 2 ➡️ 3, 4 ... 
  • 방문했던 곳
    • 1 ➡️ 3, 4 
    • 2 ➡️ 5, 6
    • 3 ➡️ 7

어차피 중복된거 삭제할거니까 !

def bfs(graph, start_node):
    visited,need_visit = list(),list()
    need_visit.append(start_node)

    while need_visit:
        node = need_visit.pop(0)
        if node not in visited:
            visited.append(node)
            need_visit.extend(graph[node])
    return visited
bfs(graph, 'A')
# ['A', 'B', 'C', 'D', 'G', 'H', 'I', 'E', 'F', 'J']

 

 

3. 랭그래프의 필수 구성요소

1. 노드(Node)

노드는 LangGraph 워크플로우 안에서 실행되는 개별 작업 단위입니다. 예를 들어 “질문을 임베딩하기”, “벡터DB에서 관련 문서 검색하기”, “LLM으로 답변 생성하기” 같은 단계가 각각 하나의 노드가 됩니다. 즉, 노드는 전체 워크플로우를 이루는 핵심 블록이라고 할 수 있습니다.

 

2. 엣지(Edge)

엣지는 노드와 노드를 연결하는 흐름을 의미합니다. 한 노드의 결과가 다음 노드의 입력으로 이어지도록 만들어 주며, 워크플로우가 순차적으로 진행될 수 있게 합니다. 예를 들어 “검색 결과 → 답변 생성” 같은 연결이 엣지입니다.

 

3. 상태(State)

상태는 워크플로우 실행 중 유지되고 공유되는 데이터 저장소와 같습니다. 사용자 질문, 검색 결과, 현재까지의 답변 초안 등이 상태에 담겨 각 노드가 읽고 쓸 수 있습니다. 이 덕분에 그래프 전체가 일관된 맥락을 유지할 수 있습니다.

 

4. 조건 분기와 루프(Flow Control)

조건 분기는 특정 상황에 따라 워크플로우의 진행 경로를 바꾸는 기능입니다. 예를 들어 “검색 결과가 충분하면 답변 생성, 부족하면 재검색” 같은 분기 처리가 가능합니다. 루프는 특정 단계를 반복 실행하도록 해주며, 답변이 불충분할 경우 재검색-재생성을 반복하는 Agentic RAG 같은 구조를 구현할 수 있게 합니다.

 

👉 이렇게 네 가지 구성요소(노드, 엣지, 상태, 조건 분기·루프)가 맞물려 LangGraph는 단순한 RAG부터 복잡한 Agentic RAG까지 유연하게 표현할 수 있는 강력한 워크플로우 프레임워크가 됩니다.

 

 

4. 상태

State는 에이전트가 현재 어떤 정보를 가지고 있는지를 표현하는 데이터 구조로, 파이썬에서는 TypedDict나 Pydantic BaseModel을 사용해 정의할 수 있습니다. TypedDict는 단순히 키와 값의 타입을 명시하는 수준이라 잘못된 타입이 들어가도 실행은 되지만, 타입 검사기(mypy 등)에서만 잡아줍니다. 반면 Pydantic BaseModel은 실제 실행 시점에 타입을 엄격하게 검사하여 잘못된 데이터가 들어오면 오류를 발생시킵니다. 따라서 State를 정의할 때는 상황에 따라 단순히 구조만 명시할 것인지, 혹은 런타임에서 데이터 유효성까지 보장할 것인지를 고려해 TypedDict와 Pydantic을 선택하여 활용합니다.

from typing import TypedDict

class User(TypedDict):  # 타입을 고정하는건 아님!
    id: int
    name: str
    email: str

user1: User = {
    'id': 1,
    'name': '김사과',
    'email': 'apple@apple.com'
}
print(user1)

# {'id': 1, 'name': '김사과', 'email': 'apple@apple.com'}

 

user1: User = {
    'id': 1,
    'name': 12345678,
    'email': 'apple@apple.com'
}
print(user1)

# {'id': 1, 'name': 12345678, 'email': 'apple@apple.com'}

 

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

user_data = {
    'id': 1,
    'name': '김사과',
    'email': 'apple@apple.com'
}

user1 = User(**user_data)   # 딕셔너리에 접근해서 각각의 키와 값을 변수로 처리
print(user1)

# id=1 name='김사과' email='apple@apple.com'

 

# 타입이 안맞기때문에 에러가 분명히 난다.
user_data = {
    'id': 1,
    'name': 12345678,
    'email': 'apple@apple.com'
}

user = User(**user_data)
print(user)

# ... For further information visit https://errors.pydantic.dev/2.11/v/string_type

 

State는 입력과 출력의 스키마를 정의하고 이를 업데이트하는 리듀서 함수와 함께 사용됩니다. LangGraph에서는 TypedDict 등을 이용해 입력과 출력 상태를 명확히 정의하고, 각 노드가 상태를 받아 새로운 값을 반환하면 그래프가 이를 이어받아 전체 흐름을 완성합니다. 즉, State는 질문과 답변 같은 에이전트의 맥락 정보를 구조적으로 관리하며, 노드 실행 결과를 반영해 에이전트의 상태를 단계적으로 업데이트하는 핵심 역할을 합니다.

!pip install langgraph

 

from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict


# 입력을 위한 스키마 정의
class InputState(TypedDict):
    question: str


# 출력을 위한 스키마 정의
class OutputState(TypedDict):
    answer: str


# 입력과 출력을 합한 종합 스키마 정의
class OverallState(InputState, OutputState):
    pass

 

# 입력을 처리하고 답변을 생성하는 노드 정의
def answer_node(state: InputState):
    return {"answer": "bye", "question": state["question"]} # 상태 업데이트


graph_builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
graph_builder.add_node(answer_node)  # 답변 노드 추가
graph_builder.add_edge(START, "answer_node")  # 시작 엣지 추가
graph_builder.add_edge("answer_node", END)  # 끝 엣지 추가
graph = graph_builder.compile()  # Compile the graph

# 입력 invoke 및 결과 출력
print(graph.invoke({"question": "hi"}))
# {'answer': 'bye'}
graph

 

 

State는 에이전트가 사용하는 데이터 구조로, 단순히 값만 담는 것이 아니라 값이 어떻게 업데이트될지(리듀서 함수)까지 함께 정의할 수 있습니다. 기본 TypedDict를 사용하면 단순히 키와 값의 타입만 지정하지만, Annotated와 리듀서 함수를 함께 쓰면 상태 업데이트 시 단순 덮어쓰기 대신 지정된 동작(예: add로 리스트 이어 붙이기, add_messages로 대화 메시지 누적하기)을 수행합니다. 즉, State는 에이전트의 현재 정보를 표현할 뿐 아니라, 노드 실행 결과가 기존 상태와 어떻게 병합·갱신될지를 규칙으로 내장한 확장된 상태 관리 구조입니다.

from typing_extensions import TypedDict

class State(TypedDict):
    value1: int
    value2: list[str]

 

from typing import Annotated
from typing_extensions import TypedDict
from operator import add

class State(TypedDict):
    value1: int
    value2: Annotated[list[str], add]

 

from typing_extensions import Annotated
from typing_extensions import TypedDict

def add(left, right):   
# 만약 State안에 함수를 콜백으로 등록해놓으면 문자열로 들어왔을 때, 계속 연산해줄 수 있다.
    return left + right

class State(TypedDict):
    value1: int
    value2: Annotated[list[str], add]

 

from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

 

5. 노드

노드(Node)는 에이전트가 실제로 수행해야 할 논리나 작업을 구현한 함수이며, 그래프 안에서 상태(State)를 입력받아 새로운 값이나 업데이트를 반환하는 단위입니다. LangGraph에서는 add_node("노드이름", 함수) 형태로 노드를 등록하며, 각 노드는 입력 상태를 받아 특정 로직을 실행한 뒤 상태에 반영할 딕셔너리를 반환합니다. 이렇게 정의된 노드들은 그래프의 흐름 속에서 순차적으로 연결되어 실행되며, 에이전트가 질문에 답하거나 결과를 가공하는 등의 역할을 수행합니다.

from langgraph.graph import StateGraph

builder = StateGraph(dict)

def my_node(state: dict):
    return {"results": f"Hello, {state['input']}!"}

def my_other_node(state: dict):
    return state

builder.add_node("my_node", my_node)
builder.add_node("other_node", my_other_node)

 

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from operator import add

class State(TypedDict):
    messages: Annotated[list[str], add]

graph = StateGraph(State)

 

def chatbot(state: State):
    answer = "안녕하세요! 무엇을 도와드릴까요?"
    print("Answer : ", answer)

    return {"messages": [answer]}

graph.add_node("chatbot", chatbot)

 

graph.add_edge(START, "chatbot")
graph.add_edge("chatbot", END)
graph = graph.compile()

graph.invoke({"messages": ["안녕!"]})

 

 

6. 엣지

엣지(Edges)는 그래프에서 노드 간의 실행 흐름을 연결하는 경로로, 에이전트가 어떤 순서로 작업을 이어갈지를 정의합니다. 가장 단순한 형태인 기본 엣지(Normal Edge)는 특정 노드가 끝나면 곧바로 다음 노드로 이동하도록 설정하며, graph.add_edge("node_a", "node_b")와 같이 사용합니다. 반면 조건부 엣지(Conditional Edge)는 라우팅 함수를 통해 현재 상태나 결과를 검사한 뒤, 조건에 따라 다른 노드로 분기시켜 실행 흐름을 유연하게 제어합니다. 즉, 엣지는 그래프에서 “다음에 무엇을 할지”를 결정짓는 연결선 역할을 하며, 조건을 두어 에이전트의 실행 경로를 동적으로 설계할 수 있게 합니다.

class State(TypedDict):
    input: str
    output: str
    
router_builder = StateGraph(State)

 

def routing_function(state: State):
    if state["input"] == "isroute":
        return True
    return False
    
router_builder.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})

'AI > AI Agent' 카테고리의 다른 글

벡터 데이터베이스  (0) 2025.09.18
LangGraph Reflection  (0) 2025.09.15
랭그래프를 이용한 간단한 챗봇  (0) 2025.09.10
LangGraph basic  (0) 2025.09.09
AI Agent  (0) 2025.09.08

+ Recent posts