정해진 순서로 충분한 일에 Agent가 꼭 필요할까요?

학교에 갈 때 정해진 노선버스를 탈 수도 있고 택시를 불러 목적지를 말할 수도 있습니다. 노선버스는 길이 정해져 있어 시간을 예측할 수 있고 택시는 길을 기사가 정해서 유연합니다. 일의 순서를 미리 알면 노선버스가 싸고 확실합니다.

Anthropic은 이 둘을 워크플로(workflow)와 Agent로 나눕니다. 워크플로는 LLM과 도구가 미리 정해진 코드 경로를 따라 움직이는 시스템이고 Agent는 LLM이 스스로 진행 과정과 도구 사용을 정하는 시스템입니다. 같은 문의를 두 방식으로 처리해 보면 차이가 보입니다.

문의

설명을 위한 예시 장면입니다. 같은 문의라도 Agent가 거치는 단계와 횟수는 모델과 상황에 따라 다릅니다.

체이닝은 앞 단계의 결과를 다음 단계로 넘깁니다

프롬프트 체이닝(prompt chaining)은 일을 순서대로 나눠 앞 호출의 출력을 다음 호출의 입력으로 넘기는 방식입니다. Anthropic은 일을 고정된 하위 작업으로 깔끔하게 나눌 수 있을 때 쓰라고 하고 각 호출을 쉬운 과제로 만들어 지연이 늘더라도 정확도를 높이려는 것이라고 설명합니다.

중간에 코드로 하는 검사(gate)를 넣을 수 있습니다. 초안이 너무 짧으면 다듬기 단계를 한 번 더 거치고 충분하면 건너뜁니다. 검사가 코드라서 모델을 부르지 않고 싸게 판단합니다. 그림에서 검사 결과를 바꿔 모델 호출 횟수가 어떻게 달라지는지 보세요.

chain.pypython
# 1 초안을 쓰고 길이 검사로 길을 가른다(코드가 정함)
def too_short(s: State) -> Literal["polish", "__end__"]:
    return "polish" if len(s["draft"]) >= 20 else "__end__"

c = StateGraph(State)
c.add_node("draft", draft); c.add_node("polish", polish)
# 2 조건부 엣지가 검사 문 역할을 한다
c.add_edge(START, "draft")
c.add_conditional_edges("draft", too_short)
c.add_edge("polish", END)
chain = c.compile()
  1. 검사 함수입니다. 모델을 부르지 않고 글자 수만 셉니다. 반환값이 다음 노드의 이름이라 조건부 엣지가 그 길로 보냅니다. 이 예시는 글자 수만 세는 단순한 규칙이고 실제 서비스에서는 개인정보 포함 여부나 금칙어 같은 검사를 넣습니다.
  2. 길을 이어 줍니다. draft에서 검사를 거쳐 polish로 가거나 곧바로 끝으로 갑니다. 어느 길이든 코드가 정해 둔 길입니다. 모델은 각 칸 안에서 글만 씁니다.

라우팅은 입력을 분류해 알맞은 전문 처리로 보냅니다

라우팅(routing)은 입력을 먼저 분류하고 그 결과에 맞는 전문 처리로 보내는 방식입니다. Anthropic은 별도로 처리하는 편이 나은 뚜렷한 범주가 있고 분류를 정확히 할 수 있을 때 쓰라고 합니다. 고객 문의를 일반 질문과 환불과 기술 지원으로 나눠 각각 다른 프롬프트와 도구로 처리하는 것이 예입니다.

분류에는 앞서 배운 구조화 출력(Structured Output)이 잘 맞습니다. 분류 결과를 Literal["계정", "장비", "소프트웨어"]처럼 정해진 값 중 하나로만 받으면 길 고르기 함수가 안전하게 노드를 고릅니다. 스크롤하며 문의가 우체국 분류기를 지나는 모습을 보세요.

문의가 도착

"비밀번호를 잊어버렸어요" 같은 문의가 State의 inquiry 칸에 들어옵니다. 아직 어느 팀 일인지 모릅니다.

분류 노드가 이름표를 붙임

모델이 구조화 출력으로 "계정"이라는 값 하나를 돌려줍니다. 정해진 보기 안의 값이라 오타가 날 일이 없습니다.

길 고르기 함수가 팀 줄로 보냄

이름표를 읽어 계정 팀 노드로 문의를 보냅니다. 장비와 소프트웨어 팀의 줄은 이번에는 쓰이지 않습니다.

팀 노드가 초안을 쓰고 다듬기로

계정 팀 노드가 자기 전문 프롬프트로 초안을 쓰고 모든 팀이 공통으로 쓰는 다듬기 노드에서 만납니다.

router.pypython
# 3 분류 결과는 정해진 보기 중 하나로만 받는다
class Route(BaseModel):
    category: Literal["계정", "장비", "소프트웨어"]
router = model.with_structured_output(Route, method="json_schema")

# 4 이름표를 읽어 갈 노드를 고르는 함수
def by_category(s: State) -> Literal["account", "device", "software"]:
    return {"계정": "account", "장비": "device", "소프트웨어": "software"}[s["category"]]

b.add_edge(START, "classify")
b.add_conditional_edges("classify", by_category)
for n in ["account", "device", "software"]:
    b.add_edge(n, "polish")      # 5 모든 팀이 다듬기에서 만난다
터미널
$ python router.py 비밀번호를 잊어버렸어요 → 계정 노트북 화면이 깜빡여요 → 장비 엑셀을 설치하고 싶어요 → 소프트웨어

출력은 예시입니다. 분류 결과는 모의 서버로 확인했고 실제 모델의 분류는 달라질 수 있습니다. 코드는 langgraph 1.2 기준이고 노드 함수는 생략했습니다.

  1. 분류 모양을 정합니다. 앞 글에서 본 Pydantic 양식입니다. Literal로 보기를 닫아 두면 모델이 다른 값을 내놓을 수 없습니다. method="json_schema"는 모델 회사의 구조화 출력 기능을 쓰라는 뜻입니다.
  2. 길 고르기 함수입니다. State의 category를 읽어 노드 이름으로 바꿔 줍니다. 한글 이름표와 노드 이름을 짝짓는 표를 함수 안에 두었습니다.
  3. 다시 모읍니다. 세 팀 노드 모두 같은 polish 노드로 이어서 공통 처리를 한 곳에 둡니다.
이 그래프가 그려 내는 Mermaid 코드 보기
router.mmd
graph TD;
    __start__ --> classify;
    account --> polish;
    classify -.-> account;
    classify -.-> device;
    classify -.-> software;
    device --> polish;
    software --> polish;
    polish --> __end__;

graph.get_graph().draw_mermaid()의 화살표만 줄여 옮긴 것입니다. 점선은 조건부 엣지입니다.

분류가 애매한 문의를 받을 길도 만들어 둡니다

라우팅은 분류가 맞아야 의미가 있습니다. 모델이 분류를 틀릴 수 있어서 어느 보기에도 딱 맞지 않는 문의를 위한 길을 함께 만들어 두는 편이 안전합니다. 보기에 "기타"를 넣고 그 길 끝에 사람이 확인하는 칸을 두는 식입니다.

이렇게 하면 억지로 가까운 팀에 보내 엉뚱한 답이 나가는 일을 줄일 수 있습니다. 어느 정도로 애매할 때 기타로 보낼지는 분류 결과를 모아 평가하며 정합니다. 평가는 이 시리즈 뒤쪽에서 다룹니다.

길을 미리 알면 워크플로로 충분합니다

Anthropic은 LLM 애플리케이션을 만들 때 가능한 가장 단순한 해법을 찾고 필요할 때만 복잡도를 높이라고 권합니다. 성능이 나아지는 게 확인될 때 복잡도를 더하라는 뜻입니다. 순서가 정해진 일을 Agent로 만들면 호출 횟수가 들쭉날쭉하고 같은 입력의 결과도 달라질 수 있습니다.

그래서 일을 만나면 먼저 순서를 미리 알 수 있는지 묻습니다. 알 수 있으면 체이닝이나 라우팅 같은 워크플로로 짜고 순서를 모르거나 도구를 몇 번 부를지 모델이 판단해야 하면 Agent로 짭니다. 일을 하나 골라 보세요.

다음 글에서는 일을 여러 갈래로 나눠 동시에 맡기고 다시 모으는 병렬 처리, 그리고 Orchestrator-Worker와 Evaluator-Optimizer를 알아보겠습니다.

참고 자료

출처 4개 펼치기