LabHub
배우기 러닝패스 코스

AI Agents — A Graph, Not a Model

An Agent Is a State Machine, Not a Model

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

에이전트는 모델이 아니라 상태 기계다. 어려운 부분은 모델을 부르는 자리가 아니라, 언제 멈출지·무엇을 들고 다닐지·실패하면 어디로 갈지를 정하는 자리다.

Concept map: 상태 기계 · 멈추지 않는다. · 모르는 것을 지어낸다. · 왜 그랬는지 모른다.

왜 필요한가 — 데모와 제품이 갈리는 곳

에이전트 데모는 대개 잘 돈다. 질문 하나를 넣으면 도구를 부르고 답이 나온다. 그런데 제품이 되는 순간 세 가지가 한꺼번에 터진다.

셋 다 모델의 문제가 아니다. 그래프의 문제다. 그래서 이 코스는 모델을 부르지 않고 그래프만 다룬다. 모델을 붙이는 것은 노드 하나를 바꾸는 일이고, 그 자리는 어렵지 않다.

상태는 덮어쓰는가, 쌓이는가

LangGraph 에서 노드는 바꿀 부분만 담은 딕셔너리를 돌려준다. 그것을 어떻게 합칠지는 상태 타입이 정한다.

class State(TypedDict):
    question: str
    steps: Annotated[list, operator.add]   # 쌓인다
    tries: int                              # 덮어쓴다

Annotated[list, operator.add] 가 없으면 마지막 노드가 돌려준 값만 남는다. 그러면 "지나온 길" 이 사라지고, 나중에 무슨 일이 있었는지 물어볼 수 없다. 무엇을 쌓고 무엇을 덮어쓸지는 설계 결정이다.

그래프로 옮기면 무엇이 시험 가능해지는가

에이전트를 한 덩어리 반복문으로 쓰면, 무엇이 잘못되었는지 물어볼 자리가 없다. 그래프로 나누는 진짜 이유는 각 마디를 따로 시험할 수 있게 되는 것이다.

마디는 순수 함수에 가깝게 만든다. 상태를 받아 상태의 일부를 돌려주는 형태면, 그 마디만 떼어 내 입력을 주고 결과를 볼 수 있다. 모델 호출은 마디 안에 두되 주입할 수 있게 해서, 시험에서는 정해진 답을 돌려주는 가짜로 바꾼다.

def plan(state: State, llm=None) -> dict:
    llm = llm or default_llm
    out = llm.invoke(state["messages"])
    return {"plan": parse_plan(out), "step": state["step"] + 1}

갈림은 마디가 아니라 조건 함수로 둔다. 다음에 어디로 갈지 정하는 함수를 따로 두면, 그 함수만으로 "이런 상태에서는 도구를 부르고, 이런 상태에서는 끝난다" 를 표로 만들어 시험할 수 있다. 모델을 부르지 않고도 흐름 전체를 확인하게 된다.

상태는 무엇이 쌓이고 무엇이 덮이는지 명시한다. 대화 기록은 쌓이고, 현재 계획은 덮인다. 이것을 타입에 적어 두면 마디를 늘릴 때 헷갈리지 않는다. 쌓여야 할 것을 덮으면 맥락이 사라지고, 덮여야 할 것을 쌓으면 프롬프트가 부풀어 오른다.

중간 상태를 저장하면 재개와 사람 개입이 함께 온다. 마디마다 상태를 저장해 두면, 중간에 멈춰 사람에게 물어보고 그 답으로 이어서 갈 수 있다. 승인이 필요한 작업(메일 보내기, 결제, 삭제)은 이 자리에서 끊는다.

관측은 마디 단위로 남긴다. 어느 마디에서 몇 초가 걸렸고 토큰을 얼마나 썼는지가 남아야 느린 곳과 비싼 곳을 안다. 전체 소요 시간 하나만 재면 개선할 자리를 못 찾는다.

현장에서

면접에서 "에이전트를 만들어 봤나" 를 물으면 실제로 듣고 싶어 하는 것은 프레임워크 이름이 아니라 이 세 가지다 — 끝내는 조건을 무엇으로 두었나, 도구가 실패하면 어디로 보내나, 무엇을 남겨 두었나.

그리고 이건 에이전트만의 이야기가 아니다. 재시도 상한, 실패 경로, 관측 기록은 백엔드에서 늘 하던 판단이다. LangGraph 는 그 판단을 그래프로 적게 해 줄 뿐이다.