학교에 갈 때 정해진 노선버스를 탈 수도 있고 택시를 불러 목적지를 말할 수도 있습니다. 노선버스는 길이 정해져 있어 시간을 예측할 수 있고 택시는 길을 기사가 정해서 유연합니다. 일의 순서를 미리 알면 노선버스가 싸고 확실합니다.
Anthropic은 이 둘을 워크플로(workflow)와 Agent로 나눕니다. 워크플로는 LLM과 도구가 미리 정해진 코드 경로를 따라 움직이는 시스템이고 Agent는 LLM이 스스로 진행 과정과 도구 사용을 정하는 시스템입니다. 같은 문의를 두 방식으로 처리해 보면 차이가 보입니다.
설명을 위한 예시 장면입니다. 같은 문의라도 Agent가 거치는 단계와 횟수는 모델과 상황에 따라 다릅니다.
프롬프트 체이닝(prompt chaining)은 일을 순서대로 나눠 앞 호출의 출력을 다음 호출의 입력으로 넘기는 방식입니다. Anthropic은 일을 고정된 하위 작업으로 깔끔하게 나눌 수 있을 때 쓰라고 하고 각 호출을 쉬운 과제로 만들어 지연이 늘더라도 정확도를 높이려는 것이라고 설명합니다.
중간에 코드로 하는 검사(gate)를 넣을 수 있습니다. 초안이 너무 짧으면 다듬기 단계를 한 번 더 거치고 충분하면 건너뜁니다. 검사가 코드라서 모델을 부르지 않고 싸게 판단합니다. 그림에서 검사 결과를 바꿔 모델 호출 횟수가 어떻게 달라지는지 보세요.
# 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()draft에서 검사를 거쳐 polish로 가거나 곧바로 끝으로 갑니다. 어느 길이든 코드가 정해 둔 길입니다. 모델은 각 칸 안에서 글만 씁니다.라우팅(routing)은 입력을 먼저 분류하고 그 결과에 맞는 전문 처리로 보내는 방식입니다. Anthropic은 별도로 처리하는 편이 나은 뚜렷한 범주가 있고 분류를 정확히 할 수 있을 때 쓰라고 합니다. 고객 문의를 일반 질문과 환불과 기술 지원으로 나눠 각각 다른 프롬프트와 도구로 처리하는 것이 예입니다.
분류에는 앞서 배운 구조화 출력(Structured Output)이 잘 맞습니다. 분류 결과를 Literal["계정", "장비", "소프트웨어"]처럼 정해진 값 중 하나로만 받으면 길 고르기 함수가 안전하게 노드를 고릅니다. 스크롤하며 문의가 우체국 분류기를 지나는 모습을 보세요.
"비밀번호를 잊어버렸어요" 같은 문의가 State의 inquiry 칸에 들어옵니다. 아직 어느 팀 일인지 모릅니다.
모델이 구조화 출력으로 "계정"이라는 값 하나를 돌려줍니다. 정해진 보기 안의 값이라 오타가 날 일이 없습니다.
이름표를 읽어 계정 팀 노드로 문의를 보냅니다. 장비와 소프트웨어 팀의 줄은 이번에는 쓰이지 않습니다.
계정 팀 노드가 자기 전문 프롬프트로 초안을 쓰고 모든 팀이 공통으로 쓰는 다듬기 노드에서 만납니다.
# 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 모든 팀이 다듬기에서 만난다출력은 예시입니다. 분류 결과는 모의 서버로 확인했고 실제 모델의 분류는 달라질 수 있습니다. 코드는 langgraph 1.2 기준이고 노드 함수는 생략했습니다.
Literal로 보기를 닫아 두면 모델이 다른 값을 내놓을 수 없습니다. method="json_schema"는 모델 회사의 구조화 출력 기능을 쓰라는 뜻입니다.category를 읽어 노드 이름으로 바꿔 줍니다. 한글 이름표와 노드 이름을 짝짓는 표를 함수 안에 두었습니다.polish 노드로 이어서 공통 처리를 한 곳에 둡니다.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를 알아보겠습니다.