LangGraph Agent는 정말 JSON으로 응답하고 프롬프트와 연동될까?

안녕하세요! LLM 개발을 시작하신 초보 개발자 여러분을 위해 LangGraph의 핵심 개념을 쉽게 설명해드리겠습니다. 특히 Agent가 JSON으로 응답하는 방식과 프롬프트와의 연동 과정을 자세히 알아보겠습니다.

LangGraph가 뭔가요?

LangGraph는 LangChain에서 만든 AI Agent 프레임워크입니다. 복잡한 작업을 여러 단계로 나누어 처리할 수 있게 해주는 도구라고 생각하시면 됩니다.

간단히 말해서:

  • Graph(그래프): 작업 흐름을 노드(Node)와 엣지(Edge)로 연결한 구조
  • Node(노드): 실제 작업을 수행하는 함수들 (LLM 호출, 도구 사용 등)
  • Edge(엣지): 노드들을 연결하는 선, 다음에 어떤 노드로 갈지 결정
  • State(상태): 모든 노드가 공유하는 데이터 저장소

Agent의 JSON 응답, 정말일까?

네, 맞습니다! LangGraph의 Agent는 실제로 JSON 형태로 응답을 주고받습니다. 하지만 이게 어떻게 작동하는지 단계별로 살펴봅시다.

Agent의 기본 응답 구조

# 일반적인 Agent 응답 예시
{
    "messages": [
        {
            "role": "assistant",
            "content": "날씨를 확인해드리겠습니다.",
            "tool_calls": [
                {
                    "name": "get_weather",
                    "args": {"city": "서울"},
                    "id": "call_12345"
                }
            ]
        }
    ]
}

Tool 호출 후 응답

# Tool 실행 후 받는 응답
{
    "messages": [
        {
            "role": "tool",
            "content": "서울의 현재 기온은 15도입니다.",
            "tool_call_id": "call_12345"
        }
    ]
}

State(상태)와 JSON의 관계

LangGraph에서 가장 중요한 개념이 바로 State입니다. 이 State가 JSON 형태로 관리되며, 모든 노드가 이를 공유합니다.

State 정의 예시

from typing_extensions import TypedDict
from langgraph.graph import MessagesState

class AgentState(MessagesState):
    # 메시지는 기본으로 포함됨
    user_name: str = ""
    weather_data: dict = {}
    task_completed: bool = False

State가 노드를 거치며 업데이트되는 과정

# 초기 State
initial_state = {
    "messages": [{"role": "user", "content": "서울 날씨 알려줘"}],
    "user_name": "김철수",
    "weather_data": {},
    "task_completed": False
}

# Agent 노드 처리 후
after_agent = {
    "messages": [
        {"role": "user", "content": "서울 날씨 알려줘"},
        {
            "role": "assistant", 
            "tool_calls": [{"name": "get_weather", "args": {"city": "서울"}}]
        }
    ],
    "user_name": "김철수",
    "weather_data": {},
    "task_completed": False
}

# Tool 노드 처리 후
after_tool = {
    "messages": [
        {"role": "user", "content": "서울 날씨 알려줘"},
        {"role": "assistant", "tool_calls": [...]},
        {"role": "tool", "content": "서울의 현재 기온은 15도입니다."}
    ],
    "user_name": "김철수",
    "weather_data": {"temperature": 15, "city": "서울"},
    "task_completed": True
}

프롬프트와 JSON의 연동 과정

Agent가 Tool을 호출하고 결과를 받는 과정을 자세히 살펴보겠습니다.

1단계: 사용자 입력과 프롬프트 결합

def agent_node(state: AgentState):
    messages = state["messages"]
    
    # 시스템 프롬프트 + 사용자 메시지 조합
    prompt_messages = [
        {"role": "system", "content": "당신은 날씨 정보를 제공하는 도우미입니다."},
        *messages  # 기존 대화 내역
    ]
    
    # LLM 호출
    response = llm_with_tools.invoke(prompt_messages)
    
    # State 업데이트
    return {"messages": [response]}

2단계: Tool 호출 결정

LLM은 프롬프트를 보고 Tool을 호출할지 직접 답변할지 결정합니다:

# LLM이 Tool 사용을 결정한 경우
{
    "role": "assistant",
    "content": "",
    "tool_calls": [
        {
            "name": "get_weather",
            "args": {"city": "서울"},
            "id": "call_abc123"
        }
    ]
}

# LLM이 직접 답변하는 경우
{
    "role": "assistant",
    "content": "죄송하지만 실시간 날씨 정보를 확인할 수 없습니다."
}

3단계: Tool 실행과 결과 반영

def tool_node(state: AgentState):
    messages = state["messages"]
    last_message = messages[-1]
    
    results = []
    for tool_call in last_message.tool_calls:
        # Tool 실행
        if tool_call.name == "get_weather":
            result = get_weather(tool_call.args["city"])
        
        # Tool 결과를 메시지 형태로 포맷
        tool_message = {
            "role": "tool",
            "content": result,
            "tool_call_id": tool_call.id
        }
        results.append(tool_message)
    
    return {"messages": results}

구조화된 출력(Structured Output) 강제하기

LangGraph에서는 Agent가 항상 일정한 JSON 형태로 응답하도록 강제할 수 있습니다.

방법 1: Response Tool 사용

from pydantic import BaseModel

class WeatherResponse(BaseModel):
    temperature: float
    condition: str
    city: str

# Response Tool을 추가
tools = [get_weather, WeatherResponse]
model_with_tools = model.bind_tools(tools, tool_choice="any")

방법 2: with_structured_output 사용

from pydantic import BaseModel

class FinalResponse(BaseModel):
    answer: str
    confidence: float
    sources: list[str]

# 구조화된 출력 강제
structured_llm = llm.with_structured_output(FinalResponse)

실제 워크플로우 예시

간단한 날씨 Agent의 전체 흐름을 보겠습니다:

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

# 1. State 정의
class WeatherAgentState(MessagesState):
    weather_data: dict = {}

# 2. 노드 함수들
def agent_node(state: WeatherAgentState):
    messages = state["messages"]
    response = llm_with_tools.invoke(messages)
    return {"messages": [response]}

def weather_tool_node(state: WeatherAgentState):
    # Tool 실행 로직
    return ToolNode(tools)(state)

# 3. 조건부 엣지 함수
def should_continue(state: WeatherAgentState):
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return END

# 4. 그래프 구성
workflow = StateGraph(WeatherAgentState)
workflow.add_node("agent", agent_node)
workflow.add_node("tools", weather_tool_node)

workflow.add_edge(START, "agent")
workflow.add_conditional_edges("agent", should_continue)
workflow.add_edge("tools", "agent")

app = workflow.compile()

JSON 응답의 실제 활용

Frontend와의 연동

// Frontend에서 LangGraph Agent 호출
const response = await fetch('/api/agent', {
    method: 'POST',
    body: JSON.stringify({
        messages: [{role: 'user', content: '서울 날씨 알려줘'}]
    })
});

const data = await response.json();
// data.structured_response로 구조화된 데이터 접근
console.log(data.structured_response.temperature);

API 응답 표준화

# API 엔드포인트에서
@app.post("/agent")
async def chat_with_agent(request: ChatRequest):
    result = app.invoke({"messages": request.messages})
    
    return {
        "messages": result["messages"],
        "structured_response": result.get("structured_response"),
        "metadata": {
            "tokens_used": result.get("token_count"),
            "execution_time": result.get("duration")
        }
    }

디버깅과 모니터링

LangGraph의 JSON 기반 구조는 디버깅을 쉽게 만들어줍니다:

# State 변화 추적
for step in app.stream(initial_input):
    print(f"Step: {step}")
    print(f"State: {json.dumps(step, indent=2)}")

주의사항과 Best Practices

State 크기 관리

  • State가 너무 커지지 않도록 주의
  • 필요없는 데이터는 정기적으로 정리

JSON 직렬화 이슈

  • 모든 State 필드가 JSON 직렬화 가능해야 함
  • datetime, custom 객체 등은 변환 필요

에러 처리

def safe_agent_node(state: AgentState):
    try:
        return agent_node(state)
    except Exception as e:
        return {
            "messages": [{
                "role": "assistant", 
                "content": f"오류가 발생했습니다: {str(e)}"
            }],
            "error": True
        }

마무리

LangGraph에서 Agent가 JSON으로 응답하는 것은 맞습니다! 하지만 이는 단순히 JSON을 반환하는 것이 아니라:

  1. State 기반 관리: 모든 데이터가 State라는 JSON 구조로 관리됨
  2. 프롬프트 연동: LLM이 프롬프트를 보고 Tool 호출 여부를 JSON으로 결정
  3. Tool 결과 통합: Tool 실행 결과가 다시 JSON 형태로 State에 반영
  4. 구조화된 출력: 최종 응답도 원하는 JSON 형태로 강제 가능

이런 방식으로 LangGraph는 복잡한 Agent 워크플로우를 예측 가능하고 디버깅하기 쉬운 형태로 만들어줍니다.

처음에는 복잡해 보일 수 있지만, 이 구조를 이해하면 매우 강력하고 유연한 Agent 시스템을 만들 수 있습니다. 천천히 간단한 예제부터 시작해서 점진적으로 복잡한 시스템을 구축해보세요!

#LangGraph #AI개발 #Agent #JSON #LLM #초보개발자 #프롬프트엔지니어링 #상태관리 #워크플로우

Leave a Comment