본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 20:47

전사(Transcript)에서 타입이 지정된 실행 항목(Action Items)까지: TypeScript를 활용한 세 개의 병렬 에이전트

요약

회의 전사 데이터를 요약, 실행 항목 추출, 감정 분석이라는 세 가지 작업으로 나누어 병렬 에이전트로 처리하는 방법을 소개합니다. TypeScript를 사용하여 각 작업에 최적화된 프롬프트와 온도를 설정하고, 구조화된 데이터를 생성하는 멀티 에이전트 아키텍처를 구현합니다.

핵심 포인트

  • 단일 프롬프트 대신 작업별 전문 에이전트를 활용하여 정확도 향상
  • 병렬 에이전트 실행을 통한 처리 속도 및 효율성 최적화
  • TypeScript를 활용한 타입 지정된 구조화된 데이터(Structured Data) 추출
  • 각 에이전트별 최적의 온도(Temperature) 설정 적용

당신의 회의 요약기는 하나의 프롬프트에서 조용히 세 가지 일을 수행하고 있습니다

LLM(Large Language Model)으로 회의를 요약하는 일반적인 방식은 하나의 프롬프트를 사용하는 것입니다: "여기 전사(transcript)가 있습니다 — 요약을 해주고, 실행 항목(action items)을 추출하고, 모두의 기분이 어땠는지 알려주세요." 한 번의 호출, 하나의 모델, 그리고 하나의 텍스트 덩어리가 결과로 돌아옵니다.

데모에서는 잘 작동하지만, 실제 전사 데이터에서는 흐트러집니다. 이것들은 서로 다른 형태를 가진 세 가지의 서로 다른 작업입니다. 요약(summary)은 산문(prose)처럼 흐르기를 원합니다. 실행 항목(action items)은 모든 행에 담당자가 지정된 엄격한 리스트 형태를 원합니다. 감정(sentiment)은 화자당 하나의 판결을 원합니다. 이들을 단일 프롬프트에 몰아넣으면 서로 충돌합니다. 모델이 요약을 실행 항목에 섞어버리거나, 화자 태깅을 잊어버리거나, "실행 항목"이 수동으로 파싱해야 하는 문단 형태로 돌아오기도 합니다. 또한 이 모든 과정을 직렬(serially)로 처리하느라 비용을 지불해야 하며, 당신이 원했던 것의 절반은 구조화된 데이터(structured data)임에도 불구하고 비구조화된 텍스트를 받게 됩니다.

더 깔끔한 형태가 있습니다. 각각 정확히 한 가지 일만 수행하고, 각자 고유한 온도(temperature) 설정을 가진 세 명의 전문가를 실행하는 것입니다. 그중 두 명은 산문 대신 타입이 지정된 객체(typed objects)를 반환하며, 이들은 서로의 출력을 필요로 하지 않으므로 동시에 실행됩니다. 그런 다음 네 번째 에이전트가 세 가지 결과를 하나의 보고서로 병합합니다.

이 포스트는 open-multi-agent의 meeting-summarizer 쿡북 예제를 바탕으로 정확히 그것을 구축합니다. 전체 코드는 약 280줄의 TypeScript로 구성되어 있으며, 병렬성(parallelism)이 핵심입니다.

얻을 수 있는 결과물

최종 결과물은 고정된 형태를 가진 단일 Markdown 보고서입니다 — 산문 형태의 요약, 실행 항목 테이블, 개인별 감정, 그리고 종합된 다음 단계(next steps)가 포함됩니다. 다음은 21줄짜리 엔지니어링 스탠드업(standup) 미팅을 대상으로 실제로 실행했을 때의 실행 항목 섹션입니다 — 모든 행이 스크립트가 파싱해야 하는 산문이 아닌, 타입이 지정된 데이터로 반환되었습니다:

작업 (Task)담당자 (Owner)마감일 (Due)
billing-v2 마이그레이션을 위한 shadow-write harness 배포Raj2026-04-24
...
전체 보고서에는 세 단락의 요약, 화자별 감정 분석 (sentiment read), 그리고 종합된 다음 단계 (Next Steps) 목록이 포함됩니다. 이 모든 것은 네 개의 에이전트에 의해 생성되며, 그중 세 개는 병렬로 실행되었습니다. 그 구조는 다음과 같습니다.

세 명의 전문가, 하나의 전사(Transcript)

각 전문가는 자신만의 시스템 프롬프트 (system prompt)와 온도 (temperature)를 가진 일반적인 Agent입니다. 먼저 요약기 (summarizer)부터 살펴보겠습니다. 이 에이전트는 스키마 (schema) 없이 산문 형태로 출력하며, 자연스럽게 읽히도록 온도를 약간 높게 설정합니다:

const summaryConfig: AgentConfig = {
  name: 'summary',
  model: 'claude-sonnet-4-6',
...

나머지 두 전문가는 단순히 "LLM을 세 번 호출하는 것"을 넘어 신뢰할 수 있는 단계로 넘어가는 지점입니다. 이들은 텍스트가 아닌 타입이 지정된 객체 (typed objects)를 반환합니다. Zod 스키마를 선언하여 에이전트에게 outputSchema로 전달하면, result.structured를 통해 파싱된 결과를 읽을 수 있습니다.

실행 항목 (Action items)은 목록 형태이며, 모든 항목은 반드시 담당자 (owner)를 포함해야 합니다. 마감일 (due date)은 선택 사항인데, 실제 회의에서는 마감일을 지정하는 경우가 가끔 있기 때문입니다:

const ActionItemList = z.object({
  items: z.array(
    z.object({
...

온도 설정을 주목하십시오: 0.1입니다. 추출 (Extraction) 작업은 창의성을 발휘할 영역이 아닙니다. 동일한 전사(transcript)에 대해 항상 동일한 실행 항목이 도출되어야 하기 때문입니다. 또한 outputSchema가 설정되어 있기 때문에, result.structured는 Jira나 Linear에 즉시 입력할 수 있는 타입이 지정된 { items: [...] } 형태로 반환됩니다. 정규 표현식 (regex)이나 "모델이 생성했을 것으로 기대되는 마크다운 표를 파싱하기" 같은 작업은 필요 없습니다.

감정 분석 (Sentiment)도 동일한 개념이지만 제약 조건이 더 엄격합니다. tone은 열거형 (enum)이므로 모델은 네 가지 값 중 하나만 반환할 수 있으며, 모든 판단에는 반드시 근거를 인용해야 합니다:

const SentimentReport = z.object({
  participants: z.array(
    z.object({
...

evidence 필드는 저렴한 환각 방지책(hallucination guard)입니다. 모델이 각 어조(tone)에 인용구(quote)를 반드시 첨부하도록 강제함으로써, 아무도 표현하지 않은 기분을 지어내는 것을 방지합니다. (이 방식을 적용할 때 주의할 점 하나: 외부 키는 복수형인 itemsparticipants이며, 배열은 그 아래에 위치합니다.)

Fan out: 세 개의 에이전트를 동시에 실행하기

세 명의 전문가 중 어느 누구도 다른 전문가에게 의존하지 않습니다. 이들은 모두 동일한 전사(transcript)를 읽고 독립적인 결과물을 작성합니다. 이것이 바로 팬아웃(fan-out)의 교과서적인 조건입니다. open-multi-agent의 AgentPool은 제한된 범위 내에서 에이전트들을 병렬(concurrently)로 실행합니다. 3개의 슬롯을 할당하고, 에이전트들을 추가한 뒤, Promise.all로 모두 시작하면 됩니다:

function buildAgent(config: AgentConfig): Agent {
  const registry = new ToolRegistry()
  registerBuiltInTools(registry)
...

알아둘 만한 미묘한 차이점 하나는, AgentPool은 에이전트당 잠금(lock)을 보유하므로 동일한 에이전트는 동시에 두 번 실행될 수 없다는 점입니다. 하지만 이름이 서로 다른 세 개의 에이전트는 진정으로 병렬(parallel)로 실행됩니다. 풀(pool) 크기를 3으로 설정하면 이들을 수용하기에 정확히 충분합니다.

이제 대부분의 팬아웃 튜토리얼이 생략하는 부분, 즉 실제로 병렬로 실행되었음을 증명하는 방법입니다. 두 가지를 측정하세요. 전체 Promise.all을 둘러싼 실제 경과 시간(wall-clock time)과 각 에이전트 자체 실행 시간의 합입니다. 만약 작업이 실제로 겹쳐서 실행되었다면, 실제 경과 시간은 합계보다 훨씬 작을 것입니다:

const serialSum = timed.reduce((acc, r) => acc + r.durationMs, 0)
console.log(`Parallel wall time: ${Math.round(parallelElapsed)}ms`)
console.log(`Serial sum (per-agent): ${Math.round(serialSum)}ms`)
...

마지막 블록은 의도적인 것이며, 여러분의 버전에도 유지할 가치가 있습니다. 이것은 **병렬성 자체 점검(parallelism self-check)**입니다. 만약 세 번의 호출이 실질적으로 겹치지 않았다면(예를 들어, 제공업체의 속도 제한(rate-limit)으로 인해 요청이 조용히 직렬화된 경우), 실제 경과 시간이 직렬 합계에 가까워지며 스크립트는 0이 아닌 값으로 종료됩니다. 따라서 이 코드를 실행했을 때 ASSERTION FAILED가 나타난다면, 이는 대개 코드의 버그가 아닙니다. 팬아웃이 큐(queue)로 전락했음을 알려줌으로써 점검 기능이 제 역할을 다하고 있는 것입니다.

DeepSeek를 대상으로 실제 실행했을 때, 세 명의 전문가(specialists)는 **2.21배의 속도 향상 (speedup)**을 보이며 중첩되었습니다. 각 에이전트의 작업 시간을 모두 합산한 25.9초에 비해 실제 소요된 시간(wall time)은 11.7초였습니다. 정확한 수치는 모델 지연 시간(latency)과 네트워크 상태에 따라 변동되는데, 이것이 바로 브로슈어에 적힌 수치를 인용하는 대신 실행 시마다 측정해야 하는 이유입니다.

네 번째 에이전트: 애그리게이터 (aggregator)

팬아웃(Fan-out)을 통해 세 개의 결과를 병렬로 얻을 수 있습니다. 하지만 이 결과들을 하나의 보고서로 병합하는 과정이 여전히 필요하며, 이것이 바로 네 번째 에이전트입니다. 이 에이전트는 앞선 세 에이전트의 결과에 의존하기 때문에 다른 에이전트들이 실행된 이후에 동작합니다. 솔직히 말하자면, 이 패턴은 '3개 병렬'이 아니라 '3개 병렬 + 1개'입니다.

애그리게이터는 산문 형태의 요약(prose summary)을 텍스트로, 두 개의 구조화된 결과(structured results)를 JSON으로 전달받으며, 고정된 4개의 헤딩(heading)을 가진 보고서를 생성하도록 지시받습니다.

const aggregatorPrompt = `아래의 세 가지 분석 내용을 하나의 마크다운(Markdown) 보고서로 병합하세요.

--- 요약 (prose) ---
...

애그리게이터의 시스템 프롬프트(system prompt)는 출력 구조(## Summary / ## Action Items / ## Sentiment / ## Next Steps, 액션 아이템은 표 형식)를 고정하며, 한 가지 중요한 규칙을 추가합니다: 다른 데이터에 근거하지 않은 액션 아이템(action items)을 임의로 만들어내지 마십시오. 애그리게이터의 역할은 다음 단계(next steps)를 형식화하고 합성하는 것이지, 새로운 사실을 발견하는 것이 아닙니다. 이 경계가 애그리게이터가 본래의 목적에서 벗어나지 않도록 잡아줍니다.

실제 실행 사례

Terminal output from the run: the three specialists (summary, action-items, sentiment) each report OK with their timing, a 2.21x parallelism speedup, the typed action-items and sentiment JSON, and the closing token-usage summary

예제는 claude-sonnet-4-6을 사용하지만, 이 수치들은 DeepSeek(deepseek-v4-flash)로 교체하여 실행한 결과입니다. 에이전트 설정(configs)은 동일하며 모델 ID만 변경되었습니다. 세 명의 전문가가 팬아웃(fanned out)되었고, action-itemssentiment 출력값은 각각의 Zod 스키마(schemas)를 통해 검증되었으며, 애그리게이터가 위의 보고서를 생성했습니다. 세 명의 전문가와 애그리게이터를 포함한 전체 실행의 토큰 사용량은 입력 3,225 토큰 및 출력 4,083 토큰이었습니다. (이는 토큰 수이며 달러 금액이 아닙니다. 실제 비용은 사용하는 제공업체와 모델에 따라 달라집니다.)

기대치를 설정하기 위해 한 가지 짚고 넘어갈 점이 있습니다: 팬아웃 (fan-out)은 토큰이 아닌 실제 시간 (wall-clock time)을 절약해 줍니다. 여전히 네 번의 모델 호출을 수행하지만, 단지 하나씩 차례대로 기다리는 과정을 멈춘 것뿐입니다. 또한 단일 프롬프트(single prompt)를 사용할 때는 없었을 추가적인 호출(aggregator, 집계기)이 발생합니다. 아주 짧은 전사(transcript)의 경우, 조정 오버헤드 (coordination overhead)가 이득을 상쇄할 수 있습니다. 이 패턴은 각 전문가(specialist)의 작업량이 늘어날수록 효과를 발휘합니다.

이 패턴이 적합한 경우 — 그리고 적합하지 않은 경우

하나의 입력값이 여러 개의 독립적인 분석을 필요로 할 때 팬아웃 (fan-out)을 사용하세요. 회의(Meeting) → {요약(summary), 실행 항목(actions), 감정(sentiment)}은 전형적인 사례입니다. PR(Pull Request) → {보안 리뷰(security review), 스타일 리뷰(style review), 테스트 커버리지 확인(test-coverage check)}, 또는 지원 티켓(support ticket) → {카테고리(category), 긴급도(urgency), 제안된 답변(suggested reply)}도 마찬가지입니다. 즉, 독립적인 작업들이며, 동일한 소스에서 나오고, 후속 단계에서 사용할 타입이 지정된 출력값(typed outputs)이 필요한 경우입니다.

단계들이 서로 의존하는 경우에는 사용하지 마세요. '조사 후 작성(research-then-write)'은 파이프라인 (pipeline)이지 팬아웃 (fan-out)이 아니며, 이를 강제로 병렬화하면 데이터 흐름이 깨집니다. 또한, 단순히 그러기 위해서 단일 작업을 팬아웃 하지 마세요. 하나의 에이전트(agent)를 사용하는 것이 풀(pool)과 집계기(aggregator)를 사용하는 것보다 더 간단합니다.

동일한 프레임워크 내에 더 높은 수준의 옵션도 있습니다. 여기서는 병렬성을 수동으로 연결했습니다. 즉, 무엇을 동시에 실행할지 직접 결정한 것입니다. 만약 목표를 설명하면 코디네이터(coordinator)가 이를 작업 그래프(task graph)로 분해하고 이를 대신 병렬화해 주기를 원한다면, 그것이 바로 runTeam()이 하는 역할입니다. 이에 대해서는 Goal In, DAG Out에서 작성했습니다. 이 포스트와 같은 수동 연결 방식의 팬아웃 (fan-out)은 구조가 고정되어 있고 명시적인 것을 원할 때 올바른 선택이며, 코디네이터 (coordinator)는 목표에 따라 구조가 변할 때 올바른 선택입니다.

실행하기

npm install @open-multi-agent/core

전체 예제는 리포지토리(repo)에 있습니다. 리포지토리 루트에서 실행하세요 (ANTHROPIC_API_KEY가 필요합니다):

npx tsx packages/core/examples/cookbook/meeting-summarizer.ts

읽을 소스: meeting-summarizer 예제와 그 스크립트 더미 데이터(transcript fixture). 필수적인 부분만 간추린 동일한 fan-out/aggregate 형태는 fan-out-aggregate 패턴을 참고하세요.

솔직히 말씀드리자면, 여기의 스크립트는 합성된(synthetic) 스탠드업 회의이며, 프로젝트의 실제 검증은 아직 초기 단계입니다. 만약 이것을 실제 회의에 적용해 보신다면, 타입 지정 추출이 어느 부분에서 잘 작동했고 어느 부분에서 실패했는지 듣고 싶습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0