Multi-Agent 20

보고서 생성 파이프라인의 Supervisor 역할 파헤치기

Supervisor가 LLM 한 번 더 호출하는 역할이라고 생각하기 쉽다.

근거 · 교안 p78-p85

보고서 생성 파이프라인의 Supervisor 역할 파헤치기 대표 이미지

1장: Agent들의 ‘내용 생성’ 역할, Supervisor와는 무엇이 다른가요?

솔라는 모니터에 떠 있는 코드 조각과 다이어그램을 번갈아 보았다. ‘보고서 생성 파이프라인’. 여러 개의 상자가 화살표로 연결된 그림이었다. Draft Agent, Review Agent, Revise Agent… 눈에 익은 이름들 사이에서 유독 ‘Supervisor’라는 이름이 눈길을 끌었다. 감독관, 관리자. 다른 Agent들을 전부 거느리는 듯한 이름이었다.

“언니, 이 파이프라인 구조 좀 봐. 보고서 초안을 쓰는 Agent, 그걸 검토하는 Agent, 다시 수정하는 Agent가 쭉 있는데, 마지막에 Supervisor가 있네. 다른 Agent들이 만든 걸 총괄해서 최종 결론이나 요약 부분을 멋지게 완성하는, 제일 중요한 역할을 하는 Agent인가 봐.”

솔라는 자신이 내린 결론이 꽤 그럴듯하다고 생각하며 옆에 앉은 루나에게 말을 건넸다. 가장 높은 직책의 이름이니, 가장 중요한 결과물을 만들어내는 게 당연하게 느껴졌다.

루나는 솔라의 화면을 잠시 들여다보더니, 대답 대신 메모장을 열었다. 그리고는 방금 솔라가 언급했던 Agent들의 이름을 하나씩 적어 내려갔다.

- draft_agent
- review_agent
- revise_agent
- final_agent

루나는 Supervisor를 뺀 네 개의 이름을 목록으로 만든 뒤, 솔라 쪽으로 모니터를 돌렸다.

“솔라, 이 네 Agent가 각각 어떤 ‘내용물’을 만들어내는지에 집중해서 한 줄로 설명해 볼래?”

“응? Supervisor 빼고?”

“일단 이것들부터.”

솔라는 잠시 고개를 갸웃했지만, 곧 화면 속 코드의 주석과 설명을 다시 읽으며 입을 열었다.

“음… draft_agent는 주제를 받아서 보고서 ‘초안’을 작성해. 이건 확실히 글 내용을 직접 쓰는 역할이네.”

솔라는 첫 번째 항목을 손가락으로 가리키며 말했다.

“그리고 review_agent는 그 초안을 읽고 ‘검토 의견’을 만들어. 보고서를 직접 고치진 않지만, ‘이 부분은 논리가 부족하다’ 같은 피드백 텍스트를 생성하니까… 이것도 어떤 종류의 내용을 만드는 건 맞지.”

“계속해 봐.”

revise_agentreview_agent가 만든 피드백을 보고 초안을 ‘수정’하는 역할을 하고. 이것도 당연히 보고서 내용에 직접 관여하는 거고. 마지막으로 final_agent는 수정된 원고를 최종 보고서 형식으로 ‘완성’하는 역할을 맡아.”

목록을 하나씩 짚어가던 솔라는 손가락을 멈추고는 한 가지 공통점을 발견한 듯 말했다.

“알겠다. 여기 있는 네 Agent는 전부 보고서의 텍스트, 즉 ‘본문’을 처음부터 쓰거나, 고치거나, 아니면 고칠 점을 지적하는 글을 쓰는 역할이야. 전부 결과물의 내용물 자체에 관여하고 있어.”

솔라는 스스로 정리한 내용을 바탕으로 처음의 추리로 돌아갔다.

“그럼 Supervisor는 역시… 이 Agent들이 만들어낸 결과물을 최종적으로 다듬어서 더 세련된 서론이나 통찰력 있는 결론을 덧붙이는 역할이 맞는 거 아닐까? ‘감독관’이라는 이름에 어울리게 말이야.”

솔라의 확신에 찬 목소리에 루나는 고개를 젓는 대신, 다른 비유를 꺼냈다.

“이 파이프라인을 ‘보고서 공장’이라고 생각해 보자. 방금 솔라 네가 한 일은 공장의 직원 명부를 확인한 거야. 초안을 쓰는 작업자(draft_agent), 품질을 검수하는 검수원(review_agent), 제품을 수정하는 기술자(revise_agent), 그리고 최종 포장을 담당하는 직원(final_agent)까지. 모두 생산 라인에 있는 사람들이지.”

루나는 솔라가 짚었던 네 개의 Agent 목록을 손으로 한데 묶는 시늉을 했다. ‘내용 생성’이라는 보이지 않는 그룹으로 묶인 듯했다.

“자, 그럼 이 공장을 운영하려면 누가 더 필요할까? 검수가 끝난 제품을 다시 수정 라인으로 보내거나, 수정이 끝난 제품을 포장 라인으로 보낼지, 아니면 불량품이라 폐기할지… 이런 ‘흐름’을 결정하는 사람이 필요하지 않을까?”

‘흐름을 결정하는 사람.’

그 말에 솔라의 머릿속에서 무언가 번쩍했다. 생산 라인의 작업자가 아니라, 그들의 작업을 지휘하는 역할.

“아… 공장장?”

솔라는 나지막이 중얼거렸다. 자신이 ‘내용물’에만 집착하고 있었다는 사실을 깨달았다. 글을 쓰는 Agent, 글을 평가하는 Agent, 글을 고치는 Agent. 모두 중요한 역할을 하지만, 이들의 작업 순서와 흐름을 누군가 조율해주지 않으면 공장은 멈춰버린다.

“그럼 Supervisor는… 글을 쓰는 게 아니라, 누가 일할 차례인지, 다음 작업이 무엇인지를 정해주는 역할이라는 거야?”

솔라는 다이어그램의 ‘Supervisor’ 상자를 다시 보았다. 아까는 가장 똑똑한 작가처럼 보였던 상자가, 이제는 분주한 생산 라인 위층의 관제실처럼 보이기 시작했다. 각 Agent들이 자신의 역할에만 집중할 수 있도록, 전체 작업 흐름을 지휘하는 제어자.

“맞아. Supervisor는 보고서의 내용을 한 글자도 쓰지 않아.”

루나의 짧은 확답에 솔라는 고개를 끄덕였다. Supervisor가 LLM을 호출해 무언가를 더 ‘쓰는’ 역할일 거라는 첫 추측은 완전히 빗나갔다. 역할을 분리해서 보니 명확해졌다. 내용을 만드는 Agent들과 흐름을 제어하는 Supervisor는 애초에 종류가 다른 존재였다.

한 가지 큰 오해는 풀렸지만, 곧바로 새로운 궁금증이 고개를 들었다.

“공장장 역할이라는 건 이제 알겠어. 그런데… 그럼 대체 무슨 기준으로 다음에 뭘 할지 결정하는 거지? 그냥 초안, 검토, 수정 순서대로 시키기만 한다면 굳이 ‘Supervisor’라는 거창한 이름까지 붙일 필요는 없잖아?”

2장: Supervisor: 내용을 쓰지 않고, 상태로 흐름을 제어하는 방법

솔라의 손가락이 키보드 위를 가볍게 스쳤다. 화면에는 여러 Agent들의 목록 대신, supervisor라는 이름의 함수 코드가 나타났다. 지난번 확인한 네 개의 Agent가 ‘내용’을 만드는 생산 라인의 작업자들이라면, 이 함수는 그들을 지휘하는 공장장의 ‘업무 지침서’인 셈이었다.

솔라는 지침서의 내용을 꼼꼼히 훑어보았다. if, elif, else로 이어지는 조건문들이 눈에 들어왔다. 생각보다 복잡하지 않은 구조였다.

“언니, 이 Supervisor 함수를 보니까 그냥 순서대로 일을 시키는 것 이상이긴 하네. 검토 결과(VERDICT)가 ‘수정 필요’이면 revise_agent를 호출하고, ‘통과’면 final_agent를 호출하고… 뭐, 이 정도면 똑똑한 갈림길 신호등 정도 역할 아닐까?”

솔라는 어깨를 으쓱하며 말했다. ‘공장장’이라는 거창한 비유에 비해, 눈앞의 코드는 단순한 분기문처럼 보였다. 특정 조건에 따라 다음 행선지를 알려주는 역할. 그 이상도 이하도 아닌 것 같았다.

루나는 솔라의 화면 속 supervisor 함수를 가리키는 대신, 그 함수 정의 바로 윗부분을 손가락으로 톡 쳤다. def supervisor(state: ReportState): 라고 적힌 부분이었다.

“이 함수의 유일한 입력값이 ‘state’라는 점에 주목해봐. 이 ‘상태(state)’ 안에 뭐가 들어있는지 확인하면, 이 신호등이 뭘 보고 신호를 바꾸는지 알 수 있을 거야.”

루나의 말에 솔라는 스크롤을 올려 ReportState의 정의를 찾아봤다. 그 안에는 draft, review, revision_count 같은 여러 필드들이 정의되어 있었다. 파이프라인의 각 단계를 거치면서 생성되거나 변경되는 정보들이 차곡차곡 쌓이는, 일종의 작업 현황판이었다.

“아… 이 ReportState가 보고서 작업의 모든 기록을 담고 있는 거구나. 초안 내용, 검토 의견, 그리고 이건… revision_count? 수정 횟수까지 세고 있네.”

솔라는 다시 supervisor 함수로 돌아와, 루나가 했던 말을 되짚으며 코드를 다시 읽기 시작했다. 이번에는 단순히 if 조건문만 보는 게 아니라, 그 조건문이 state의 어떤 값을 확인하는지에 집중했다.

def supervisor(state: ReportState):
    # ... (초안, 검토가 없는 경우 등 초기 단계 처리)

    # 검토 결과(VERDICT) 확인
    if state['review']['VERDICT'] == 'revise':
        # 수정 횟수 제한 확인
        if state['revision_count'] > MAX_REVISIONS:
            # 수정 횟수 초과 시, 최종 보고서로 강제 이동
            return "final_agent"
        else:
            # 수정 횟수 내라면, 수정 Agent로 이동
            return "revise_agent"
    else:
        # 검토 결과 '통과' 시, 최종 보고서로 이동
        return "final_agent"

“잠깐만. 이거 그냥 신호등이 아니었네.”

솔라의 눈빛이 달라졌다. 처음에는 ‘수정 필요’이면 revise_agent, ‘통과’면 final_agent로 보내는 단순한 이분법으로 보였다. 하지만 다시 보니 훨씬 더 정교한 규칙이 숨어 있었다.

“단순히 검토 의견만 보는 게 아니야. ‘수정 필요’라는 판단이 나와도, revision_count라는 상태 값을 또 확인해. 만약 최대 수정 횟수를 넘었으면, 더 이상 수정 기회를 주지 않고 그냥 최종 단계로 넘겨버리네.”

솔라는 마우스를 움직여 state['revision_count'] 부분을 강조했다. Supervisor는 단순히 눈앞의 갈림길만 보는 게 아니었다. 지금까지의 작업 ‘이력’까지 참고해서 의사결정을 내리고 있었다. 검수원의 판정(VERDICT)과 수정 작업의 횟수(revision_count)라는 두 가지 상태 정보를 조합해 다음 행동을 결정하는 것이다.

“이제 알겠어. Supervisor는 그냥 ‘다음은 너!’ 하고 순서만 부르는 역할이 아니었어. ReportState라는 작업 현황판을 계속 읽으면서, 정해진 규칙에 따라 전체 작업 흐름을 동적으로 바꾸는 거였구나.”

솔라는 자신이 처음 생각했던 ‘단순한 신호등’이라는 비유가 틀렸음을 인정했다. Supervisor의 본질은 ‘조건’이 아니라, 그 조건을 판단하는 근거인 ‘상태’에 있었다. 각 Agent들이 만들어내는 결과물과 작업 이력 자체가 다음 흐름을 결정하는 중요한 정보가 되는 것이다. 이것이야말로 진정한 의미의 ‘관리’였다.

“그래. Supervisor는 내용을 만들지 않는 대신, 파이프라인의 현재 ‘상태’를 읽고 다음 단계를 어디로 보낼지 ‘라우팅(routing)’하는 역할이야. 그래서 이걸 ‘상태 기반 라우팅’이라고 부를 수 있지.”

루나의 설명에 솔라는 고개를 끄덕였다. 머릿속에 흩어져 있던 조각들이 맞춰지는 기분이었다.

하지만 새로운 그림이 완성되자, 그 그림의 한구석에 또 다른 빈틈이 보였다.

“상태를 보고 다음 목적지를 알려주는 역할이라는 건 완벽히 이해했어. 그런데… 이 supervisor 함수가 ‘revise_agent’라는 문자열을 반환한다고 해서, 어떻게 revise_agent가 진짜로 실행되는 거지? 이 함수는 그냥 어떤 Agent를 호출해야 할지 ‘알려줄’ 뿐이잖아. 이 결정을 받아서 실제로 파이프라인의 다음 단계를 작동시키는 더 큰 구조가 필요할 것 같은데?”

3장: StateGraph와 조건부 에지로 구현하는 Supervisor의 지휘

솔라의 손가락이 supervisor 함수 코드 위에서 멈췄다. 함수는 분명 “revise_agent”“final_agent” 같은 문자열을 반환하고 있었다. 하지만 거기까지였다. 솔라는 코드 전체에서 이 supervisor 함수를 호출하고, 그 반환값을 받아 다음 동작을 결정하는 if문이나 switch문을 찾으려고 애썼지만, 아무것도 발견할 수 없었다. 마치 목적지만 알려주고 사라지는 유령 내비게이션 같았다.

루나는 골똘히 생각에 잠긴 솔라의 옆모습을 보다, 조용히 마우스를 움직여 화면을 스크롤했다. supervisor 함수가 있던 곳에서 한참 떨어진, workflow = StateGraph(ReportState)라는 생소한 코드가 나타났다. 솔라는 복잡해 보이는 코드 뭉치에 미간을 찌푸렸다.

“언니, 이건 또 뭐야? 그래프? 갑자기 웬 수학 시간이지?”

“네가 찾고 있던 ‘더 큰 구조’가 바로 이거야.”

루나는 supervisor 함수가 아니라, 완전히 새로운 구조를 가리켰다. 그녀는 책상 위 메모 패드에 네모난 상자들을 그리기 시작했다. 각 상자 안에는 draft_agent, review_agent처럼 파이프라인의 각 단계를 적어 넣었다.

“이 상자 하나하나가 ‘정거장’이라고 생각해봐. 코드에서는 workflow.add_node()라는 명령으로 이 정거장들을 만들지. 파이프라인이 머무를 수 있는 상태(state) 지점들이야.”

솔라는 루나의 그림을 보며 고개를 끄덕였다. Draft 정거장, Review 정거장, Revise 정거장, Final 정거장. 파이프라인의 흐름이 눈에 보이는 듯했다.

“그럼 이 정거장들을 잇는 선로도 있어야 하잖아. Draft 다음엔 Review로 가고… 그런 식으로.”

“맞아. workflow.add_edge()는 그런 고정된 선로를 놓는 거야. A 정거장에서 출발하면 무조건 B 정거장으로 가는 거지.”

루나는 Draft 상자에서 Review 상자로 단단한 실선을 그었다. 하지만 다음 선을 그으려다 펜을 멈췄다.

“그런데 Review 정거장 다음은 좀 복잡해. 검토 결과에 따라 ‘수정’하러 가야 할 수도 있고, ‘최종본’으로 바로 가야 할 수도 있잖아. 이럴 땐 고정된 선로 하나로는 부족해.”

루나는 솔라의 화면 속 add_conditional_edges 라는 함수 호출 부분을 가리켰다.

workflow.add_conditional_edges(
    "review_agent", # 출발 정거장
    supervisor,      # 어떤 선로로 갈지 결정할 '분기기'
    {
        "revise_agent": "revise_agent", # 결정이 "revise_agent"이면 revise_agent 정거장으로
        "final_agent": "final_agent",   # 결정이 "final_agent"이면 final_agent 정거장으로
    },
)

“바로 여기서 supervisor 함수가 등장해. add_conditional_edges는 특정 정거장에 ‘조건부 분기기’를 설치하는 것과 같아. 이 코드는 ‘review_agent 정거장에 도착하면, supervisor에게 물어서 다음 행선지를 정하라’는 뜻이야.”

그 순간, 솔라의 머릿속에 흩어져 있던 퍼즐 조각들이 ‘철컥’하고 맞춰졌다. 자신이 왜 supervisor 함수를 호출하는 코드를 찾을 수 없었는지 깨달았다. supervisor는 다른 함수가 직접 호출하는 방식이 아니었다. 거대한 StateGraph라는 시스템의 일부로, 특정 지점에서 의사결정을 내리는 ‘부품’처럼 등록되어 있었던 것이다.

“아! 그러니까 supervisor 함수는 우리가 직접 부르는 게 아니라, StateGraphreview_agent 작업이 끝났을 때 알아서 실행시켜주는 거구나! 그리고 supervisor‘revise_agent’라는 문자열을 뱉어내면, 이 마지막 사전(dictionary) 부분이 그 문자열과 똑같은 이름의 다음 정거장으로 가는 선로를 연결해주는 거고!”

솔라는 메모 패드에 직접 펜을 들었다. Review 상자에서 두 갈래의 점선을 그렸다. 한쪽은 Revise 상자로, 다른 한쪽은 Final 상자로 향했다. 그리고 두 갈래 길이 나뉘는 지점에 작은 다이아몬드 표시를 하고 ‘Supervisor’라고 적었다. Supervisor는 공장장이나 신호등이라기보다, 정교한 철도 시스템의 ‘자동 분기기’에 가까웠다. 입력된 상태(ReportState)에 따라 정해진 규칙대로 선로를 바꾸는 장치.

“이제 Supervisor가 어떻게 전체 흐름을 지휘, 즉 ‘오케스트레이션’하는지 알겠어. 단순히 다음 할 일을 알려주는 게 아니라, StateGraph라는 전체 구조 안에서 조건부 에지(conditional edge)를 통해 동적으로 경로를 만들어내는 역할이었네.”

처음에는 Supervisor가 가장 똑똑한 ‘작가’일 거라고 생각했다. 그다음엔 의사결정을 내리는 ‘공장장’으로 보였다. 이제 솔라의 눈에는 StateGraph라는 설계도 위에서 각 노드들을 어떻게 연결할지 정의하는 ‘시스템 설계자’의 역할로 보였다. Supervisor의 역할 자체는 변하지 않았지만, 그것이 작동하는 더 큰 구조를 이해하자 그 정체성이 완전히 다르게 느껴졌다.

루나는 만족스럽게 고개를 끄덕이는 솔라를 보며 새로운 질문을 던졌다.

“그럼 만약 우리가 보고서 초안에 표나 그래프가 포함된 경우에만, 별도의 ‘데이터 검증’ 단계를 추가하고 싶다면 이 설계도를 어떻게 바꿔야 할까?”

솔라는 잠시 고민하더니, 자신감 있는 목소리로 메모 패드에 새로운 설계를 그려나가기 시작했다.

“일단 data_check_agent라는 새로운 정거장(node)을 만들어야지. 그리고 draft_agentreview_agent 사이에 새로운 분기기를 설치해야 해. 이 분기기 역할을 할 새로운 supervisor 함수는 보고서 초안 상태를 보고, 표나 그래프가 있으면 ‘data_check_agent’를, 없으면 ‘review_agent’를 반환하도록 짜면 돼.”

솔라는 draft_agent 노드에서 출발하는 새로운 조건부 에지를 그리고, 그것이 data_check_agentreview_agent로 갈라지는 모습을 능숙하게 표현했다. Supervisor가 단순한 내용 생성자가 아니라, 상태와 정책에 따라 흐름을 제어하는 강력한 도구임을 완벽히 체화한 움직임이었다. 더 이상 코드의 한 줄 한 줄을 쫓는 것이 아니라, 전체 파이프라인의 흐름을 지휘하는 관점에서 시스템을 바라보고 있었다.