개발

[LLM] LangGraph&LangSmith — 에이전트 워크플로우 구축과 실행 추적

Funital01 2026. 6. 24. 23:10

LLM 애플리케이션을 만들다 보면, 먼저 "이 에이전트가 언제 반복하고, 언제 분기하고, 언제 멈출지"를

어떻게 코드로 명확하게 표현할 것인가 하는 설계의 문제가 있습니다.

 

이어서, "지금 이 질문이 대체 어느 길로 흘러갔고, 토큰을 얼마나 썼으며, 답변 품질은 정말 좋아진 건가"를

어떻게 확인할 것인가 하는 관찰과 검증의 문제가 따라옵니다.

 

그 두 문제를 각각 다룰 수 있는 도구로 LangGraph LangSmith가 있습니다.

LangGraph로 에이전트의 흐름을 그래프로 설계하고,

LangSmith로 그 흐름의 내부를 시각화해서 디버깅하고 데이터 기반으로 평가하는 과정을 살펴보겠습니다.

1부. LangGraph — 흐름을 그래프로 고정하기

LangGraph는 LLM 호출을 상태(State)가 흐르는 그래프로 설계하게 해주는 저수준 오케스트레이션 프레임워크입니다.

단순 LLM 호출이 아니라, 언제 반복하고 분기하고 멈추는지를 코드로 명시하는 제어 도구라고 이해하면 됩니다.

LangGraph는 빠르게 바뀝니다(2025년 10월 v1.0 출시). 명령어와 설정 파일 형식은 공식 문서(docs.langchain.com/oss/python/langgraph/overview)에서 최신본을 한 번 더 확인하는 것이 안전합니다.

LangGraph는 왜 필요한가

LLM을 한 번만 부르면 흐름은 되게 단순합니다.

사용자가 질문하면 LLM이 답하고 끝입니다. 그런데 실제 에이전트는 그렇게 동작하지 않죠.

생각하고, 결정하고, 도구를 호출하고, 결과를 확인하고, 필요하면 다시 또 시도하고,

때로는 사람에게 확인을 받고, 그 사이의 기억을 유지합니다.

 

문제는 LLM이 이 흐름을 스스로 잘 관리하지 못한다는 데 있습니다.

무한 루프에 빠지거나, 중간 상태를 잊어버리거나, 근거 없는 답을 지어내기도 합니다.

 

LangGraph는 이 제어 흐름을 그래프로 고정을 하는데요.

어디서 반복하고 분기하고 끝나는지를 프롬프트 안에 숨기지 않고, 그래프 구조로 겉에 드러냅니다. 

흐름이 코드에 보인다는 점이 핵심입니다.

 

LangGraph는 LangChain 없이도 쓸 수 있지만, 모델과 도구를 연결할 때 LangChain 컴포넌트를 함께 쓰는 경우가 많습니다.

둘의 성격은 다음과 같이 갈립니다.

  LangChain LangGraph
성격 모델·도구 등 컴포넌트 모음 제어 흐름(오케스트레이션) 프레임워크
잘하는 것 단순한 체인, 검색 흐름 반복·분기·다중 에이전트·사람 개입
추상화 수준 높음(편리) 낮음(정밀한 제어)

LangGraph를 쓰는 다섯 가지 이유

첫째, 명시적 제어 흐름.
LangGraph는 분기와 반복을 그래프로 표현해 에이전트의 동작 과정을 쉽게 파악할 수 있습니다. 문서 유형에 따라 적절한 처리 노드로 분기되며, 흐름이 명확하게 보이고 각 노드를 독립적으로 테스트할 수 있습니다.

 

둘째, 반복(Cycle) 구조.
LangGraph는 에이전트가 행동 → 관찰 → 재행동 과정을 반복할 수 있도록 지원합니다. 정보가 부족하면 이전 단계로 돌아가 작업을 계속 수행하고, 충분한 정보가 확보되면 다음 단계로 진행합니다.

 

셋째, 지속성(persistence). 

노드마다 상태를 저장해서, 중간에 죽어도 처음부터 다시 하지 않습니다.

10단계 데이터 파이프라인이 7단계에서 API 타임아웃으로 죽었다면, 1~6단계 결과는 체크포인트에 남아 있어 7단계부터 재개합니다.

토큰과 시간을 통째로 날리지 않는다는 뜻입니다.

 

넷째, 사람 개입(Human-in-the-Loop).
LangGraph는 중요한 작업 전에 실행을 일시 중단하고 사람의 승인을 받을 수 있습니다. 승인 후에는 중단된 지점부터 이어서 실행할 수 있어 안전성과 신뢰성을 높일 수 있습니다.

 

다섯째, 상태 공유(State Sharing).
LangGraph는 메시지, 도구 결과, 중간 산출물을 상태(State)에 저장해 단계 간에 자연스럽게 공유합니다. 이를 통해 여러 에이전트가 동일한 정보를 활용하며 협업할 수 있습니다.

 

여기서 한 가지 주의할 점은, 분기 규칙은 미리 한 번 정해 둔다는 것입니다.

질문이 들어올 때마다 "이번엔 답변 생성으로 갈까, 결과 없음으로 갈까"를 매번 지시하지 않아도 그래프가 알아서 흐릅니다.

def route_found(state):
    return "generate" if state["found"] else "no_result"

 

질문이 조건에 맞으면 답변을 생성하고, 맞지 않으면 결과 없음으로 처리합니다.

이 규칙 한 줄이 모든 질문에 대한 분기를 책임집니다.

언제 쓰고, 언제 안 써도 되나

LangGraph가 항상 정답은 아닙니다.

단순 질의응답이나 한 번의 검색 흐름이라면 LangChain 체인으로 충분하고, 오히려 LangGraph는 과합니다.

지속성이 필요 없는 빠른 데모라면 더 가벼운 도구가 빠릅니다.

 

반면 반복과 분기가 필요한 에이전트, 여러 턴의 맥락을 기억해야 하는 경우(checkpointer),

중간에 사람 승인이 끼어야 하는 경우(interrupt), 여러 전문 에이전트가 협업해야 하는 경우와 같이

 며칠씩 도는 장기 작업에는 필요로 합니다.

한 줄 판단 기준: 반복하거나, 기억하거나, 사람이 끼거나, 여러 모델이 협업해야 하면 → LangGraph.
단발성이면 쓰지 않아도 됩니다.

 

한 가지 오해가 있는데요, LangGraph는 RAG를 대체하는 도구가 아닙니다.

RAG가 필요하면 그래프 안에 "벡터DB 검색 노드"를 LLM 노드 앞에 하나 넣으면 됩니다. 

LangGraph + RAG가 사내 지식 에이전트의 표준 패턴입니다.

핵심 3개념 — State · Node · Edge

LangGraph는 단 세 가지만 이해하면 됩니다.

 

State(상태) 는 그래프 전체에서 공유되는 데이터 공간입니다.

메시지, 도구 결과, 중간 산출물 등 작업 과정에서 필요한 정보를 저장하며, 모든 노드가 이를 읽고 수정할 수 있습니다.

또한 상태별로 덮어쓰기나 누적과 같은 갱신 규칙을 설정할 수 있어 데이터를 효율적으로 관리할 수 있습니다.

 

Node(노드) 는 일을 하는 함수입니다.

State를 받아서 State에 대한 업데이트(부분 딕셔너리)를 돌려줍니다.

여기서 중요한 사실은 노드가 LLM일 필요가 전혀 없다는 것입니다.

숫자 더하기, 텍스트 자르기, API 호출, DB 조회 무엇이든 노드가 됩니다.

def add_one(state: State):
    return {"n": state["n"] + 1, "log": ["add_one 실행"]}

 

Edge(엣지) 는 노드 간의 연결로, 작업이 다음에 어디로 진행될지를 결정합니다. 항상 정해진 노드로 이동하는 일반 엣지와 조건에 따라 다른 노드로 분기하는 조건부 엣지가 있으며, 그래프는 START에서 시작해 END에서 종료됩니다.

 

순수 파이썬으로 짜던 것과 LangGraph의 개념은 다음과 같이 대응됩니다.

순수 파이썬으로 만든 것 LangGraph
state 딕셔너리 State (TypedDict)
state를 받아 dict 반환하는 함수 Node (add_node)
nodes 리스트(순서) Edge (add_edge)
route 함수 조건부 엣지 (add_conditional_edges)
run() 함수 compile()
run(init, nodes) 호출 app.invoke()

 

실습으로 익히기 — 쇼핑몰 문의 답변

 

쇼핑몰 문의와 관련해서 세 단어를 가지고,

쇼핑몰 고객 문의를 자동 처리하는 예시를 통해 확인을 해봅시다.

# ───── 노드 1: 문의 분류 ─────
def classify(state):
    text = state["question"]
    if "환불" in text:
        return {"category": "환불"}
    elif "배송" in text:
        return {"category": "배송"}
    else:
        return {"category": "기타"}

# ───── 노드 2: 답변 생성 ─────
def reply(state):
    답변표 = {
        "환불": "환불은 영업일 기준 3일 내 처리됩니다.",
        "배송": "배송은 2~3일 소요됩니다.",
        "기타": "상담원에게 연결해드리겠습니다.",
    }
    return {"answer": 답변표[state["category"]]}

# state라는 상자에 질문을 담아 단계별로 처리
state = {"question": "환불 언제 되나요???"}

state.update(classify(state))    # 1단계 실행
print("분류 결과:", state)

state.update(reply(state))       # 2단계 실행
print("최종:", state)

 

실행하면 다음과 같이 상자가 단계마다 채워집니다.

분류 결과: {'question': '환불 언제 되나요???', 'category': '환불'}
최종:      {'question': '환불 언제 되나요???', 'category': '환불', 'answer': '환불은 영업일 기준 3일 내 처리됩니다.'}

 

여기서 세 개념을 다시 짚어 보면,

상태는 정보를 담고 단계마다 채워지는 상자입니다.

처음에는 {"question": ...}만 있다가, classify를 거치면 category가 더해지고, reply를 거치면 answer가 더해집니다.

하나의 상자가 계속 채워지고, 모든 단계가 이 상자를 같이 봅니다. 

 

노드는 상자를 받아 일부를 바꾸는 함수입니다. classify는 category만, reply는 answer만 돌려줍니다.

전체 상자가 아니라 자기가 바꾼 것만 돌려준다는 점이 포인트입니다. 

 

엣지는 노드를 잇는 순서입니다. "classify 다음에 reply" — 이 순서가 곧 엣지입니다.

개념 쇼핑몰 예시 한 줄 설명
상태(State) state = {"question": ...} 정보를 담고 다니는 상자
노드(Node) classify, reply 함수 상자를 받아 일부를 바꾸는 함수
엣지(Edge) classify → reply 순서 노드를 잇는 순서

추가사항 — 분기

추가로, 이번에는 환불 문의는 사람 상담원에게, 나머지는 자동 답변으로 보내는 예시입니다.

핵심은 route 함수가 흐름을 결정한다는 것입니다.

def classify(state):
    text = state["question"]
    if "환불" in text:
        return {"category": "환불"}
    elif "배송" in text:
        return {"category": "배송"}
    else:
        return {"category": "기타"}

# 자동 답변 (배송, 기타)
def auto_reply(state):
    답변표 = {"배송": "배송은 2~3일 걸립니다.", "기타": "FAQ를 확인해주세요."}
    return {"answer": 답변표[state["category"]]}

# 사람 연결 (환불은 민감하니까)
def human_reply(state):
    return {"answer": "환불 건은 상담원이 직접 도와드립니다. 연결 중...", "사람연결": True}

# 분기: category를 보고 어디로 갈지 결정
def route(state):
    if state["category"] == "환불":
        return human_reply      # 환불이면 사람에게
    else:
        return auto_reply       # 나머지는 자동

def run(question):
    state = {"question": question}
    state.update(classify(state))        # 분류
    next_step = route(state)             # 분기 결정
    state.update(next_step(state))       # 정해진 길로
    return state

print(run("환불 언제 되나요???"))
print(run("배송 얼마나 걸려요?"))
{'question': '환불 언제 되나요???', 'category': '환불', 'answer': '환불 건은 상담원이 직접 도와드립니다. 연결 중...', '사람연결': True}
{'question': '배송 얼마나 걸려요?', 'category': '배송', 'answer': '배송은 2~3일 걸립니다.'}

 

같은 질문이 들어와도 내용에 따라 다른 길로 갑니다. 이것이 바로 분기입니다!

if문을 코드 여기저기 흩어 놓지 않고 route라는 함수 하나로 모았다는 점이 중요합니다.

LangGraph Studio — 그래프가 실제로 어떻게 흘렀는지 보기

LangGraph Studio는 에이전트 그래프의 실행 과정을 시각적으로 확인하고 디버깅할 수 있는 도구입니다.

복잡한 그래프의 실제 실행 경로를 보여주며, 각 단계의 State와 입력·출력 데이터를 확인할 수 있습니다.

 

또한 특정 시점으로 되돌아가 다시 실행하는 타임 트래블(Time Travel) 기능과

코드 변경 사항을 즉시 반영하는 핫 리로드(Hot Reload) 기능을 제공해 개발과 디버깅을 효율적으로 지원합니다.

보는 방법 무엇을 보여주나 한계
코드 읽기 구조 실제 흐름은 모름
draw_mermaid_png 갈 수 있는 모든 길 이번에 어디로 갔는지는 모름
print / stream 거친 노드 순서 텍스트라 한눈에 안 들어옴
LangGraph Studio 실제 실행 경로 + 단계별 상태 + 되감기 로컬에서 띄워야 함

 


2부. LangSmith — 보이지 않는 내부를 숫자로 보기

LangGraph로 흐름을 만들었다면,

다음으로 "이게 실제로 잘 돌아가고 있나? 토큰은 얼마나 쓰나? 답변 품질이 정말 좋아졌나?"를 확인할 수 있어야 하는데요.

 

LangSmith는 그 답을 해결해주는 도구입니다.

LLM 기반 애플리케이션, 특히 RAG나 AI 에이전트를 개발할 때 디버깅, 테스트, 평가, 모니터링을 지원하는

올인원 LLMOps 플랫폼으로, LangChain을 만든 회사에서 개발했습니다.

 

LangSmith를 익히면 눈에 보이지 않는 AI 서비스의 내부 작동 과정을 시각화해 디버깅하고,

개발한 모델의 성능을 데이터 기반으로 객관적으로 평가·운영하는 LLMOps 역량을 갖추게 됩니다.

네 가지 핵심 기능

핵심기능은 다음과 같습니다.

  • Tracing & Debugging: AI 파이프라인의 실행 과정과 입출력 데이터를 시각화하여 오류와 병목 지점을 쉽게 분석할 수 있습니다.
  • 프롬프트 관리: 프롬프트를 웹에서 수정·테스트하고 버전별로 관리하여 실험과 개선을 빠르게 수행할 수 있습니다.
  • 자동 평가(Evaluation): 테스트 데이터셋을 활용해 답변 품질을 정량적으로 평가하고 성능 변화를 객관적으로 검증할 수 있습니다.
  • LLMOps 모니터링: 요청 수, 에러율, 토큰 사용량, 비용 등을 실시간으로 확인하여 서비스 운영을 효율적으로 관리할 수 있습니다.

Trace 목록 읽기 — 비용이 자동으로 집계

Trace 목록을 열면 각 호출의 Tokens와 Cost 열이 보입니다.

여기서 가장 먼저 읽어야 할 규칙은 단순합니다. 

Tokens / Cost가 찍힌 행은 GPT를 실제로 부른 것이고, 비어 있는 행은 GPT를 부르지 않은 것입니다.

질문 Tokens Cost 의미
내과 환자 처방 133 $0.0000276 검색 성공 → GPT 답변
무릎 아픈 환자 168 $0.0000423 검색 성공 → GPT 답변
ChatOpenAI(맨 아래) 40 $0.0000132 테스트로 부른 GPT

 

이 화면이 알려주는 것은 두 가지입니다. 

  • 비용이 자동으로 집계됩니다. LangSmith가 호출마다 토큰과 비용을 자동으로 달아 주니 어떤 질문이 비싼지 한눈에 보이고, 토큰이 많을수록 비용도 비례해 커집니다.
  •  분기가 비용을 아낍니다. 비용이 0이 나오게 되면, 앞서 LangGraph에서 만든 "못 찾으면 안 부른다" 분기가 실제로 돈도 아끼고 헛소리도 막았다는 증거입니다. 그 효과가 Cost 열에 0으로 그대로 찍혀 있을 수 있습니다.

즉, "이 앱이 한 달에 토큰을 얼마 쓰는가, 어떤 질문이 비싼가, 분기가 비용을 아끼는가"를

숫자로 볼 수 있다는 것이 트레이스 목록의 가치입니다.

트레이스 상세 — 단계 트리로 흐름을 따라가기

 

Trace 패널을 선택을 하면 단계 트리를 볼 수 있습니다.

진료RAG          1.06s · 168토큰 · <$0.0001   ← 부모 (전체)
├─ 검색           0.15s                        ← 자식 1 (FAISS 검색)
└─ 답변생성        0.91s · 168토큰              ← 자식 2 (GPT 답변)
   └─ ChatOpenAI  gpt-4o-mini · 0.90s · 168    ← 실제 GPT 호출

 

여기서 두 가지가 읽힙니다.

먼저 시간이 어디서 걸렸는가입니다. 검색은 0.15초로 빠르고, 답변 생성이 0.91초입니다.

이 말은 거의 모든 시간이 GPT 호출에 쓰였습니다. RAG가 느리다면 검색이 아니라 GPT 탓이라는 게 한눈에 확인됩니다.

 

또한, 답변 생성 안에 ChatOpenAI가 또 들어 있다는 점입니다. 

@traceable 함수 안에서 llm.invoke를 불렀더니 그 GPT 호출까지 자동으로 자식으로 추적된 것입니다.

 

단계 트리 오른쪽은 클릭한 단계의 상세를 보여줍니다.

부모인 진료RAG를 선택하면 Input에는 질문이, Output에는 답변이, Metadata에는 따로 설정한 값들이 그대로 기록되어 있습니다.

 

왼쪽 트리에서 검색(0.15s) 을 클릭하면 검색이 실제로 무엇을 가져왔는지가 보입니다.

오른쪽 Output에는 found: true와 함께 FAISS가 찾아온 진료기록 원문(김서연 기록)이 들어 있습니다.

여기서 읽어야 할 것이 있는데요.

 

하나는 found: true가 다음 길을 정했다는 점입니다. 이 값이 true라서 route_found가 답변 생성으로 보낸 것입니다.

LangGraph에서 만든 분기가 실제로 어떻게 작동했는지를 눈으로 확인하는 화면입니다.

 

다른 하나는 context에 k=2로 2건이 들어왔다는 점입니다. 김서연(무릎, 정답)이 맨 앞이고 최유나(소아과)도 함께 있는데,

retrieve에서 k=2로 설정해 상위 2건을 가져왔기 때문입니다.

트리 깊이도 숫자로 찍혀, 부모 진료RAG는 depth 0, 그 자식인 검색은 depth 1로 나타납니다.

 

답변 생성이나 ChatOpenAI를 클릭해 Input을 열면, 방금 이 검색 결과(김서연 기록)가

GPT 프롬프트 안에 합쳐져 들어간 모습이 보입니다.

 

"검색이 가져온 것을 GPT에게 어떻게 넘겼는가"의 흐름이 그대로 드러나는 것입니다.

평가 — v1&v2 성적표 읽기

 

지금까지의 과정 후에는 평가로 이어지게 됩니다.

평가는 데이터셋의 질문에 대해 모범답안(Reference Output)  실제 답변(Output) 을 비교하여 수행됩니다. 이번 실험에서는 4개의 질문 중 3개가 1.00점, 1개가 0.00점을 받아 평균적으로 높은 성능을 보였습니다.

  • 1번(위염 처방약), 2번(무릎 아픈 환자), 4번(홍길동 진단명) 은 정답을 정확히 포함하여 높은 점수를 획득했습니다.
  • 3번(우주여행 비용) 은 실제로는 올바르게 “관련 정보 없음”을 답했지만, 모범답안과 표현이 달라 평가 기준에 의해 0.00점이 부여되었습니다.
  • 검색 결과가 없는 질문은 GPT를 호출하지 않아 토큰 사용량이 0 이고 응답 시간도 짧게 나타났습니다.

프롬프트를 개선한 v2는 답변 품질이 향상되었지만 평가 점수는 v1과 동일하게 나타났습니다.

  • v2는 “환자 이름을 포함하라”는 지시를 반영하여 더 구체적인 답변을 생성했습니다.
  • 응답 속도, 토큰 사용량, 비용은 두 버전이 거의 비슷했습니다.
  • 그러나 현재 평가 기준이 정답 포함 여부와 의미 일치 만 확인하기 때문에, 환자 이름 포함과 같은 품질 개선은 점수에 반영되지 않았습니다.

두 버전을 비교해보면서 느낄 수 있던 점은

좋은 프롬프트 개선이 항상 평가 점수 향상으로 이어지는 것은 아니라는 것입니다.

 

 평가 지표가 측정하려는 품질 요소를 반영하지 못하면 실제 성능 향상도 점수로 드러나지 않습니다.

따라서 원하는 개선 사항을 정확히 평가할 수 있도록 평가 기준을 설계하는 것이 중요합니다.

 

Insights

기존에는 LLM의 성능이 모델 자체에 의해 결정된다고 생각했지만,

이번 실습을 통해 워크플로우 설계와 평가 체계가 결과 품질에 큰 영향을 준다는 것을 이해하게 되었습니다.

 

특히 LangGraph의 상태 관리와 조건부 분기, LangSmith의 트레이싱 및 평가 기능을 직접 확인하면서

LLMOps의 중요성을 체감할 수 있었고, 향후 AI 기반 서비스를 개발할 때 이러한 도구들을 적극적으로 활용해 보고 싶습니다!

 

 

Reference

https://www.langchain.com/blog/langgraph-studio-the-first-agent-ide

 

LangGraph Studio: The first agent IDE

LangGraph Studio provides a specialized agent IDE for visualizing, interacting with, and debugging complex agentic applications. See how to use it on your desktop today.

www.langchain.com

https://smith.langchain.com

 

LangSmith

View in LangSmith

smith.langchain.com