본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 03. 05:17

고성능 AI 에이전트는 분산 시스템이다

요약

고성능 AI 에이전트 구축 시 발생하는 지연 시간 문제를 분산 시스템 관점에서 분석합니다. 단순한 모델 속도보다 입력/출력 토큰의 양과 TTFT, 처리량의 최적화가 중요함을 강조합니다.

핵심 포인트

  • AI 에이전트는 대규모 분산 시스템의 최적화 원리를 따름
  • 단순 토큰 생성 속도보다 작업에 필요한 토큰 양이 핵심
  • TTFT(첫 토큰 생성 시간)와 전체 처리량의 구분 필요
  • 컨텍스트 압축의 한계와 효율적인 데이터 처리 패턴의 중요성

"Codex가 이 겉보기에 간단한 리팩터링(refactor)을 구현하는 데 6시간이 걸렸습니다."

"Perplexity의 리서치(Research) 모드가 멈춘 것 같습니다."

우리 모두는 LLM API가 느리다는 것을 알고 있으며, 모델이 천천히 토큰(token)을 생성하는 동안 스피너(spinner)를 바라보며 기다리는 것에 익숙합니다.

하지만 저지연(low latency)이 필요한 AI 에이전트를 구축할 때는 어떤 일이 벌어질까요?

우리는 버그 리포트, 로그, 코드, 스크린샷, 트레이스(traces), 이슈 댓글을 읽고 버그를 재현한 뒤 최종적으로 검증된 수정 사항을 생성하는 AI 디버깅 에이전트인 FixBugs를 구축하면서 이 문제에 직면했습니다. 이 제품은 모든 코드 변경 사항이 문제를 해결하기 위해 필요한 작업만을 수행하도록 검증된다는 단순한 약속을 가지고 있습니다.

구현은 간단하지 않습니다.

버그 리포트와 그와 관련된 로그/메트릭(metrics)/트레이스는 단 한 번의 모델 호출(model call)에 담기에는 너무 많은 컨텍스트(context)를 포함할 수 있습니다. 저장소(repository)에는 수백 개의 파일이 있을 수 있습니다. 로그는 모델의 유효한 컨텍스트 창(context window)보다 클 수 있습니다. 최종 답변에는 수천 개의 출력 토큰이 필요할 수도 있습니다. 그리고 만약 에이전트가 유용한 답변을 내놓기까지 10분이 걸린다면, 사용자는 에이전트가 고장 났다고 생각할 것입니다.

압축(compaction)이라고도 불리는 요약(Summarization)은 거대한 컨텍스트를 처리하는 일반적인 방법입니다. 하지만 요약은 느리고 종종 필수적인 컨텍스트를 손실합니다.

Claude Code나 Cursor와 같은 현대적인 코딩 에이전트는 로그 파일을 무작정 그레핑(grepping)하거나 특정 오프셋(offsets)에서 읽는 방식에 크게 의존합니다. 코딩 에이전트가 한 번에 처리할 수 있는 유효 컨텍스트 창은 전체 컨텍스트 창보다 작습니다. 예를 들어 GPT 5.5는 400K 토큰의 컨텍스트 창을 가지고 있지만, 실제 '입력 컨텍스트(input context)'는 258K 토큰에 더 가깝습니다.

대화형 에이전트 루프(conversational agent loops)를 넘어서면, 다른 흥미로운 패턴들을 사용할 수 있게 됩니다.

당신은 근본적인 성능 엔지니어링(performance engineering) 문제들이 대규모 분산 시스템(distributed systems)을 최적화할 때 마주치는 문제들과 유사하다는 것을 깨닫게 됩니다.

스캐터-게더(Scatter-gather). 파이프라이닝(Pipelining). 큐(Queues). 백프레셔(Backpressure). 스트리밍(Streaming). 직렬화 가능성(Serializability). 이것들이 우리가 가장 많은 시간을 들여 고민한 문제들입니다.

토큰 수학(token math)부터 시작하기

대부분의 에이전트 성능 논의는 잘못된 지점에서 시작됩니다.

그들은 이렇게 묻습니다:

초당 토큰 수(tokens/sec) 측면에서 어떤 모델이 가장 빠른가?

그것은 나중에 유용한 질문입니다. 첫 번째 질문은 다음과 같아야 합니다:

이 작업에는 얼마나 많은 입력 토큰(input tokens)과 출력 토큰(output tokens)이 필요한가?

LLM 지연 시간(latency)에는 사용자에게 중요한 두 가지 서로 다른 요소가 있습니다.

첫 번째 토큰 생성 시간(Time to first token, TTFT)은 모델이 응답을 시작하기 전까지 사용자가 기다리는 시간입니다. 토큰 처리량(Token throughput)은 전체 답변을 얻는 데 걸리는 시간을 측정하는 척도입니다.

이 둘은 서로 다른 문제입니다.

Diagram showing LLM prefill phase, decode phase, time to first token, and time per output token.

Prefill(프리필)은 첫 번째 토큰 생성 시간에 영향을 미칩니다. Decode(디코딩)는 그 이후의 출력 토큰 스트림에 영향을 미칩니다.

Prefill(프리필) 단계에서 모델은 입력 컨텍스트(input context)를 처리하고 첫 번째 새로운 토큰을 생성하는 데 사용되는 Key/Value 캐시(key/value cache)를 준비합니다. Decode(디코딩) 단계에서 모델은 자기회귀적(autoregressively)으로 출력 토큰을 하나씩 생성합니다.

실제적인 에이전트를 설계할 때는 다음과 같은 대략적인 정신적 모델(mental model)만으로도 충분합니다:

지연 시간 모델 (latency model)
T ≈ TTFT + 출력 토큰 수 × 토큰당 시간/token

입력 토큰은 공짜가 아닙니다. 입력 토큰은 Prefill(프리필) 단계에 영향을 미치며, 따라서 첫 번째 토큰 생성 시간(TTFT)에 영향을 줍니다.

Prefill(프리필) 비용 예시
20,000 입력 토큰 × 0.05ms/token = 1,000ms ≈ 1s TTFT. 이 상수는 모델 및 제공업체마다 다르지만, 비용의 형태(shape)를 파악하는 것이 유용합니다.

하지만 긴 답변은 다른 방식으로 비용이 많이 듭니다. 모든 출력 토큰은 생성되어야 합니다. 만약 에이전트가 모델에게 저장소(repository)의 모든 파일을 설명하도록 요청한다면, 사용자는 그 결정에 대한 대가를 실제 시간(wall-clock time)으로 지불하게 됩니다.

이것이 중요한 이유는 에이전트를 디버깅할 때 대개 출력량이 매우 많기 때문입니다. 에이전트는 단순히

단순한(naive) 버전은 합리적으로 보였습니다:

  1. 버그 컨텍스트(bug context)를 수집합니다.
  2. 저장소 파일들을 수집합니다.
  3. 모든 관련 컨텍스트를 하나의 프롬프트(prompt)에 넣습니다.
  4. 모델에게 파일의 순위를 매기고 그 이유를 설명하도록 요청합니다.

작은 저장소의 경우, 이 방식은 작동합니다.

하지만 파일이 50개라면, 이는 채팅 요청으로 위장한 나쁜 배치 작업(batch job)으로 변질됩니다.

만약 모델이 30,000개의 출력 토큰(output tokens)을 생성하고 엔드포인트(endpoint)가 초당 50개의 출력 토큰을 제공한다면, 약 600초를 기다려야 합니다. 10분입니다. 이는 재시도(retries), 속도 제한(rate limits), 또는 이후의 수정 사항 생성(downstream fix generation)을 고려하기 전의 시간입니다.

더 빠른 성능을 얻기 위해, 우리는 가능한 한 많은 병렬성(parallelism)을 사용해야 한다는 것을 깨달았습니다.

파일 관련성 단계 (file relevance stage)
하나의 거대한 호출: 30,000 tokens ÷ 50 tokens/s = 600s (재시도나 후속 작업 전 약 10분)
16개의 독립적인 호출: 16 × 50 tokens/s ≈ 800 tokens/s (데모 워크로드 기준 약 40초)

각 파일은 청크(chunk)로 분해되었습니다. 각 청크는 고유한 관련성 호출(relevance call)을 가집니다. 이러한 호출들은 동시에(concurrently) 실행되었습니다.

하나의 호출이 초당 50개의 출력 토큰을 제공한다면, 16-vCPU VM에서 실행되는 16개의 독립적인 호출은 워크플로(workflow)에 약 16배 더 유용한 처리량(throughput)을 노출할 수 있습니다.

데모 버전은 파일 관련성 단계를 약 10분에서 약 40초로 단축했습니다.

이 수치는 보편적인 벤치마크(benchmark)는 아닙니다. 이는 제공업체(provider), 모델(model), 프롬프트(prompt), 청크 크기(chunk sizes), 속도 제한(rate limits), 그리고 출력 형식(output format)에 따라 달라집니다.

중요한 부분은 패턴입니다.

이것은 스캐터-게더(scatter-gather) 워크로드였습니다.

독립적인 파일 확인 작업을 분산(scatter)시키고, 증거를 수집(gather)합니다. 로컬 판단들을 하나의 순위가 매겨진 관점으로 병합(merge)합니다.

우리는 이미 이를 수행하는 방법을 알고 있습니다.

"많은 독립적인 레코드에 대해 동일한 분석을 수행한 다음, 결과를 결합하십시오."

이것이 Hadoop이 하는 일이며, 데이터 분석에 매우 유용한 이유입니다.

LLM 에이전트도 동일한 범주의 문제들을 가지고 있습니다.

청킹(chunking)은 공짜가 아니다

청킹(chunking)은 남용하기 쉽습니다.

설명을 위해 맵(map) 단계와 리듀스(reduce) 단계 모두에 대한 출력을 예로 들어보겠습니다.

청크 매핑 (Mapping a chunk)

만약 다음과 같은 정보가 주어진다면:

  • A: 버그 리포트(bug report).
  • B: 저장소 파일 트리(repository file tree).

이제 버그 리포트(bug report)와 관련이 있는 코드 파일이 무엇인지 파악해야 합니다.

하나의 청크(chunk)에 얼마나 많은 파일을 추가해야 할까요?

바이트 수준의 세분성(byte-level granularity)을 사용하여 모델 호출이 처리할 수 있는 만큼 최대한 많은 컨텍스트를 밀어 넣는 것은 좋은 생각이 아닙니다.

대신 파일 수준의 세분성(file level granularity)을 가져야 하며, 가능한 경우 파일 전체를 추가해야 합니다.

청크 축소 (Reducing chunks)

다음과 같은 정보가 주어졌다고 가정해 봅시다:

  • A: 버그 리포트(bug report).
  • B: 로그 파일(log files) 및 트레이스(traces).

이제 입력값에서 버그와 관련된 로그 스니펫(log snippets)을 추출해야 합니다.

로그 파일을 매핑하는 것은 간단합니다. 탐욕적(greedily)으로 청킹할 수 있습니다.

축소(Reducing)는 조금 더 복잡합니다.

병렬적인 LLM 호출을 통해 일련의 로그 스니펫을 얻었지만, 스니펫을 시간 순서대로 배치하거나, 구간(span)별로 그룹화하거나, 서비스 이름별로 분리하지 않는다면 그 스니펫들은 전혀 유용하지 않을 것입니다.

제 경험상, "축소(Reduce)" 단계는 사람들이 기대하는 것보다 종종 더 엉망인 경우가 많습니다.

제가 현재 사용하는 규칙은 다음과 같습니다:

증거를 위해 청킹하라 (Chunk for evidence).
판단을 위해 병합하라 (Merge for judgment).

로컬 호출(local calls)은 사실, 신호, 그리고 후보 설명을 찾아내야 합니다. 최종 호출(final call)은 충돌을 해결하고, 증거의 순위를 매기며, 다음에 무엇을 할지 결정해야 합니다.

스트리밍은 대기 시간을 바꾼다

청킹이 해결하지 못하는 또 다른 지연 시간(latency) 문제가 있습니다.

때때로 사용자는 에이전트가 살아있는지 확인해야 할 필요가 있습니다.

대화형 디버깅(interactive debugging)에서는 전체 완료 시간(total completion time)보다 첫 번째 토큰 생성 시간(time to first token, TTFT)이 더 중요합니다. 엔지니어에게 항상 최종 보고서 전체가 즉시 필요한 것은 아닙니다. 그들에게 필요한 것은 첫 번째 유용한 가설, 첫 번째 파일 이름, 그리고 조사가 진행되고 있다는 첫 번째 신호입니다.

스트리밍(streaming)이 도움이 됩니다.

스트리밍 트레이드오프 (streaming tradeoff)
데모 워크로드(demo workload) | TTFT (스트리밍 미사용): 13s | TTFT (스트리밍 사용): 2.4s | 처리량 (스트리밍 미사용): 486 tok/s | 처리량 (스트리밍 사용): 244 tok/s

데모에서 스트리밍은 첫 번째 토큰 생성 시간(TTFT)을 13초에서 2.4초로 단축했습니다.

이는 엄청난 UX(사용자 경험)의 차이를 만듭니다.

하지만 처리량(throughput)은 나빠졌습니다: 스트리밍을 사용하지 않을 때 초당 486 토큰(tokens/sec)이었던 것이 스트리밍 사용 시 초당 244 토큰(tokens/sec)이 되었습니다.

이것은 오직 "N초 내에 요청 완료"만을 측정할 경우 사라지는 종류의 트레이드오프 (tradeoff)입니다. 스트리밍 (Streaming)은 처리량 (throughput) 최적화가 아닙니다. 그것은 사용자 경험 (user-experience) 최적화입니다.

채팅과 유사한 워크플로우 (workflows)의 경우, 대개 그만한 가치가 있습니다.

에이전트 파이프라인 (agent pipeline) 내부의 배치 (batch) 단계의 경우에는 그렇지 않을 수도 있습니다.

FixBugs는 두 가지 모드를 모두 사용합니다. 사용자 대상 단계는 진행 상황을 스트리밍합니다. 내부 워커 (worker) 단계는 전체 작업 완료, 재시도 (retry) 동작, 그리고 큐 처리량 (queue throughput)을 최적화합니다.

이러한 구분이 시스템을 정직하게 유지합니다.

동시성에는 한계가 있다

LLM 호출을 처음으로 병렬화 (parallelize)하여 5배 또는 10배의 개선을 목격했을 때, 다음 해결책이 "더 많은 워커"라고 결론 내리고 싶은 유혹에 빠지기 쉽습니다.

그것은 작동하다가, 어느 순간 작동하지 않게 됩니다.

[

Graph showing LLM throughput gains flattening as concurrency increases.
]

처리량은 동시성 (concurrency)과 함께 향상되다가, 점차 평탄해지기 시작합니다. 결국 더 많은 워커를 추가해도 큰 이득을 얻지 못하게 됩니다.

낮은 동시성에서는 시스템에 유휴 용량 (idle capacity)이 있습니다. 워커를 추가하면 활용도 (utilization)가 향상됩니다. 처리량이 빠르게 상승합니다.

더 높은 동시성에서는 기울기가 변합니다. 이제 당신은 공유된 병목 현상 (bottlenecks)과 싸워야 합니다: 제공업체 속도 제한 (provider rate limits), GPU 스케줄링 (scheduling), KV 캐시 (KV cache) 메모리, 네트워크 오버헤드 (network overhead), 큐잉 (queueing), 재시도 (retries), 그리고 당신 자신의 후처리 (post-processing) 과정입니다.

불쾌한 부분은 사용자 지연 시간 (latency)은 악화되는 반면 처리량은 정체될 수 있다는 점입니다.

[

Graph showing time to first token getting worse as concurrency rises.
]

동시성은 개별 사용자를 훨씬 더 오래 기다리게 만들면서도, 전체적인 처리량은 건강하게 유지할 수 있습니다.

이것이 바로 "초당 토큰 수 (tokens per second)"만으로는 충분하지 않은 이유입니다.

최소한 네 가지 지표가 필요합니다:

  • 첫 번째 토큰까지의 시간 (time to first token)
  • 초당 출력 토큰 수 (output tokens per second)
  • 총 실제 경과 시간 (total wall-clock time)
  • 부하 상황에서의 실패/재시도율 (failure/retry rate under load)

그리고 이 지표들을 단계별로 기록해야 합니다.

AI 디버깅 에이전트(AI debugging agent)에게 "전체 과정에 90초가 걸렸다"는 말은 유용한 측정치가 아닙니다. 어느 단계에서 90초가 소요되었나요? 파일 관련성(File relevance)? 로그 압축(Log compression)? 근본 원인 분석(Root cause analysis)? 재현(Reproduction)? 수정안 생성(Fix generation)? 검증(Validation)?

만약 이를 알지 못한다면, 최적화할 수 없습니다.

큐(queues)가 요청 체인(request chains)보다 낫다

워크플로(workflow)가 두 개 이상의 단계로 구성되면, 단일 요청 체인(single request chain)은 취약해집니다.

버그를 분석합니다. 그다음 재현합니다. 그다음 근본 원인을 식별합니다. 그다음 수정안을 생성합니다. 마지막으로 수정안을 검증합니다.

만약 이것이 하나의 동기식 체인(synchronous chain)으로 실행된다면, 모든 단계는 다른 모든 단계의 지연 시간(latency)과 실패 모드(failure mode)를 상속받습니다. 느린 재현 시도가 근본 원인 분석 작업을 차단합니다. 제공자(provider)의 재시도가 전체 요청을 차단합니다. 하나의 비용이 많이 드는 버그가 그 뒤에 있는 더 작은 버그들을 굶주리게(starve) 만들 수 있습니다.

더 나은 형태는 파이프라인(pipeline)입니다.

Pipeline diagram showing analyze, reproduce bug, root cause, and fix stages as independent microservices connected by message queues.

독립적인 단계들은 하나의 긴 차단 요청 체인(blocking request chain) 없이도 들어오는 버그들이 시스템을 통과할 수 있게 해줍니다.

FixBugs에서 자연스러운 단계는 다음과 같습니다:

  • analysis: 버그 리포트와 아티팩트(artifacts)를 파싱(parse)하고 압축합니다.
  • reproduction: 버그를 재현하고 실패하는 테스트를 작성합니다.
  • root cause: 가장 가능성 높은 원인을 식별합니다.
  • fix: 패치(patch)를 생성합니다.
  • validation: 패치가 재현된 실패를 해결함을 증명합니다.

이러한 단계들은 모두 동일한 작업량(workload)을 갖지 않습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
1

댓글

0