고객 지원 증거 자료: LLM API를 이용해 대용량 문서를 배치 및 스트리밍으로 요약하기
요약
대용량 문서 요약 및 점수화 워크플로우에서 스트리밍보다 배치 처리가 더 안정적입니다. 큐를 사용하여 작업을 분할하고 구조화된 중간 레코드를 생성하는 것이 중요하며, 이는 연결 끊김이나 재시도 오류에 강합니다. 실시간 출력은 최종 검토 가능한 설명으로 제한하여 사용자 경험을 개선해야 합니다.
핵심 포인트
- 대용량 문서 처리는 스트리밍보다 큐잉(배치) 방식이 더 안정적입니다.
- 애플리케이션은 큐 작업, 모델 어댑터, 이벤트 스트림의 세 경계로 구성되어야 합니다.
- 재시도에 대비하여 `candidateId`, `documentHash` 등 고유 식별자를 정의해야 합니다.
- 실시간 출력은 점수화가 완료된 후 조립되는 짧은 설명으로 제한하는 것이 좋습니다.
요약하자면(TL;DR): 대용량 고객 지원 문서의 경우, 먼저 큐에 넣어 배치 방식으로 증거 자료를 분할하고 점수화한 다음, 검토자가 필요한 간결한 결과만 스트리밍합니다. 전체 문서를 한 요청으로 스트리밍하는 것은 반응성이 좋아 보이지만, 추출(extraction), 재시도 동작(retry behavior), UI 상태가 결합됩니다. 유용한 결정은 추상적으로 배치 대 스트리밍이 아닙니다. 어느 단계에 사람이 기다리고 있느냐가 중요합니다.
저의 구체적인 업무는 지원 에이전트 후보자들을 루브릭(rubric)에 따라 점수화하는 것입니다. 이력서, 작업 샘플, 인터뷰 녹취록은 최종 점수표보다 훨씬 클 수 있습니다. 그 점수는 증거 자료를 가리켜야 하고, 재시도를 견뎌야 하며, 모델 제공업체 전반에서 포터블(portable)해야 합니다. 마지막 제약 조건이 설계를 변경합니다: 애플리케이션이 작업 형식과 이벤트 형식을 소유하고, 제공업체 어댑터가 요청 세부 정보를 소유합니다.
LLM API를 사용해 대용량 문서를 배치로 요약할 수 있을까요?
저는 문서 준비, 청크 추출(chunk extraction), 루브릭 점수화를 위해 큐를 사용합니다. 이 단계들은 반복 가능한 작업입니다. 이들은 비멱등성 키(idempotency key)로 재시도될 수 있으며, 검토자는 모든 토큰을 지켜볼 필요가 없습니다. 사람이 결과를 보고 있을 때, 저는 완성된 점수표 또는 작은 일련의 진행 이벤트만 스트리밍합니다.
이러한 분리는 일반적인 실패 모드를 피하게 해줍니다. 긴 스트리밍 요청은 모델이 답변의 대부분을 생성한 후에 연결이 끊어질 수 있습니다. 만약 서버가 부분 텍스트를 진실의 기록으로 취급한다면, 재연결 시 두 번째 점수가 생성되거나 첫 번째 점수가 조용히 손실될 수 있습니다. 큐잉된 단계는 대신 구조화된 중간 레코드(structured intermediate records)를 작성합니다: 문서 해시(document hash), 청크 번호(chunk number), 루브릭 버전(rubric version), 제공업체 요청 ID(provider request id), 그리고 상태입니다. UI는 재연결하여 동일한 상태를 읽을 수 있습니다.
두 번째 함정은 출력 속도(output speed)가 유용한 지연 시간(useful latency)과 같지 않다는 것입니다. 모든 증거를 고려하기 전에 후보에 대한 부분적인 문장을 보여주는 것은 평가자에게 불완전한 점수를 기준으로 삼게 할 수 있습니다. 평가 워크플로우의 경우, 초기의 서술적 출력은 명확한 “처리 중(processing)” 상태보다 종종 더 나쁩니다. 저는 실시간 출력을 제한된 작업에만 사용합니다. 즉, 점수가 스키마 검증을 통과한 후에 조립되는 짧은 설명입니다. 처음에는 스트리밍 LLM API가 모든 단계를 더 빠르게 느끼게 해줄 것이라고 가정했지만, 유용한 접근 방식은 최종적으로 검토 가능한 설명만을 스트리밍하는 것임이 밝혀졌습니다.
이는 시간당 수익(revenue-per-hour)의 문제입니다. 1인 SaaS는 고객이 방어 가능한 점수표를 필요로 할 때, 토큰 단위의 기록을 영구적으로 만드는 데 오후 시간을 쓸 수 없습니다. 저는 핵심 워크플로우만 매주 배포하고, 차별화되지 않은 전송 세부 사항은 소규모 계약을 맺은 제공업체 어댑터(provider adapter)에 아웃소싱합니다.
어떤 토큰 스트림도 신성하지 않습니다.
제가 구현할 가장 작은 범위
이 애플리케이션은 세 가지 경계로 구성됩니다: 큐 작업(queue job), 모델 어댑터(model adapter), 그리고 애플리케이션 이벤트 스트림입니다. 이들 중 어느 것도 특정 공급업체 경로를 알지 못합니다. 어댑터는 텍스트와 사용량 메타데이터를 반환하고, 파서가 해당 텍스트가 수용 가능한지 결정합니다.
먼저, 재시도(retry) 과정에서도 살아남아야 하는 데이터를 정의해야 합니다. 안정적인 candidateId, documentHash, 그리고 rubricVersion은 중복 작업을 가시화합니다. 청크 인덱스(chunk index)는 나중에 로깅을 위해 추가되는 필드가 아니라 키의 일부여야 합니다.
type EvidenceChunk = {
candidateId: string;
documentHash: string;
...
}
그렇다면 큐 워커(queue worker)는 지루할 수 있습니다. 여기서는 지루한 것이 좋습니다. 이는 하나의 청크를 가져오고, JSON을 요청하며, 결과를 검증하고, 결정론적 키(deterministic key) 아래에 기록을 저장합니다. 재시도는 어댑터를 다시 호출하도록 허용되지만, 두 번째 논리적 점수를 생성할 수는 없습니다.
async function scoreChunk(
chunk: EvidenceChunk,
adapter: ModelAdapter,
...
)
실제 구현에는 트랜잭션 클레임(transactional claim)과 아웃박스(outbox) 또는 동등한 영속적 이벤트 기록이 필요합니다. 예제는 의도적으로 save 함수 뒤에 이 부분들을 남겨두었습니다. 이는 저장소 관련 문제이며, 인메모리(in-memory) 프라미스가 큐인 것처럼 가정하는 것은 잘못된 교훈을 줄 것입니다.
모든 청크가 저장된 후, 리듀서(reducer)가 기준별로 점수를 결합합니다. 이 과정에서 원본 청크 키와 증거 발췌문(evidence excerpts)을 유지해야 합니다. 최종 호출이 전체 문서를 재발견하도록 요구하지 않습니다. 이는 프롬프트 크기를 줄이고, 최종 응답의 감사 가능성(auditable)을 높이며, 새로운 제공업체를 평가하는 데 유용한 경계를 제공합니다.
스트림은 별도의 관심사입니다:
type ScoreEvent =
| { type: "status"; value: "queued" | "scoring" | "ready" }
| { type: "text"; value: string }
...
프로덕션 환경에서 HTTP 계층은 이러한 이벤트를 표준 이벤트 스트림(event-stream) 응답으로 매핑할 수 있지만, 제공업체의 청크를 애플리케이션의 진실(application truth)로 노출해서는 안 됩니다. 클라이언트는 재연결하여 상태를 표시하고 최종 검증된 점수판(scorecard)을 렌더링할 수 있습니다. 스트림은 전달 메커니즘일 뿐, 영속성 계층(persistence layer)이 아닙니다.
이 디자인이 가장 먼저 깨지는 지점
청킹(Chunking)은 제가 가장 공격적으로 테스트할 첫 번째 지점입니다. 고정된 문자 수로 분할하면 평가 점수와 그 근거가 되는 단락이 분리될 수 있습니다. 가능한 경우 제목(headings), 단락 경계, 문서 오프셋을 유지합니다. 또한 모든 프롬프트에 루브릭 버전(rubric version)을 포함합니다. 그렇지 않으면 루브릭 수정 사항이 오래된 청크 점수와 새로운 최종 집계값을 혼합할 수 있기 때문입니다. 이 접근 방식은 이미 완전한 답변이 짧은 아주 작은 문서에는 적합하지 않습니다. 큐를 사용하면 상태(state)와 지연 시간이 추가되므로, 그 경우에는 하나의 경계가 지정된 호출(bounded call)을 사용할 것입니다.
하나의 테스트 케이스는 의도적으로 까다롭습니다. 후보자가 인터뷰 기록에서 에스컬레이션(escalation) 작업에 대한 소유권을 주장하지만, 작업 샘플은 다른 사람에게 인계된 내용을 설명하는 경우입니다. 추출기(extractor)는 두 발췌문과 그 오프셋을 모두 보존해야 합니다. 채점기(scorer)는 신뢰할 수 있는 숫자로 두 설득력 있는 문장을 평균화하기보다는, 충돌이 발생했음을 표시하고 검토를 요청해야 합니다. 또 다른 테스트 케이스는 기준에 대한 증거가 아예 없는 경우입니다. 이 결과는 기본적으로 0이 아니라 “증거 불충분(insufficient evidence)”이어야 하는데, 이는 누락된 단락이 낮은 성능의 증거라기보다는 데이터 조건이기 때문입니다. 이러한 사례들은 이상적인 경로(happy-path) 프롬프트를 설계하는 것보다 시간이 더 걸리지만, 문서가 깔끔해 보이지 않을 때도 채점표를 보호합니다.
두 번째 문제는 부분 실패(partial failure)입니다. 한 번 실패한 청크는 신뢰할 수 있는 전체 점수 대신 눈에 보이는 불완전 상태를 생성해야 합니다. 리듀서(reducer)는 예상되는 모든 기준을 요구하거나 결과를 불완전하다고 표시해야 합니다. 재시도는 일시적인 어댑터 오류(transient adapter failures)에만 해야 하며, 잘못된 JSON은 유효성 검사 경로가 필요합니다. 왜냐하면 입력을 변경하지 않고 동일한 요청을 반복하는 것은 거의 문제를 해결해주지 못하기 때문입니다.
세 번째 문제는 제공업체 드리프트(provider drift)입니다. 다양한 API는 다른 배치 제한(batching limits), 스트리밍 이벤트 형태, 컨텍스트 창(context windows), 사용량 필드를 노출합니다. 저는 워크플로우가 필요한 것만 정규화합니다: 완성(completion), 구조화된 텍스트, 사용량(사용 가능한 경우), 취소, 그리고 오류 카테고리입니다. 원본 응답은 보존 기간 제한이 있는 제한적 진단 저장소에 보관됩니다. 핵심 테이블은 제공업체별 필드에 의존하지 않습니다.
평가를 위해, 저는 의도적으로 까다로운 케이스가 포함된 작은 테스트 케이스 세트를 유지합니다: 두 개의 떨어진 섹션에서 기준을 충족하는 후보자, 증거가 없는 문서, 그리고 모순되는 답변이 담긴 기록입니다. 각 어댑터는 동일한 테스트 케이스와 동일한 루브릭(rubric) 버전으로 실행됩니다. 이 테스트는 두 제공업체가 동일한 산문을 생성했는지 여부가 아니라, 점수와 인용이 수용 가능한지를 묻습니다.
대규모로 변경할 사항
처리량이 높아지면, 독립적인 청크(chunk) 작업을 그룹화하는 스케줄러(scheduler), 반복되는 검증 실패를 위한 데드레터 큐(dead-letter queue), 그리고 명시적인 취소 상태(cancellation state)를 추가할 것입니다. 또한 추출(extraction)과 점수 산정(scoring)을 분리하여, 루브릭 변경 시 소스 문서를 재처리하지 않고도 정규화된 증거 자료를 재사용할 수 있도록 할 것입니다. 이 기능들은 단순히 다이어그램이 더 완전해 보인다고 해서 필요한 것이 아니라, 큐 깊이(queue depth)와 검토자 지연 시간(reviewer latency)이 실제 제약 조건일 때 가치가 있습니다.
저는 사용자 대면 계약(user-facing contract)은 안정적으로 유지할 것입니다. 즉, 작업을 제출하고, 영속적인 상태(durable status)를 관찰하며, 증거 자료 링크가 포함된 점수표(scorecard)를 가져오고, 선택적으로 제한된 라이브 설명(bounded live explanation)을 여는 방식입니다. 이 계약 덕분에 내부 어댑터(adapter)가 변경되어도 대시보드나 감사 추적 기록(audit trail)을 다시 작성할 필요가 없습니다. 또한 배치(batch)와 스트리밍(streaming) 결정이 가역적(reversible)이 되게 하는데, 이는 초기 통합에서 약간의 참신함을 짜내는 것보다 솔로 창업자에게 훨씬 중요한 문제입니다.
결정 규칙은 간단합니다. 완료되어야 하고, 재시도 가능하며, 감사 추적이 필요한 작업은 큐에 넣습니다. 이미 경계가 정해져 있고 기다리는 독자가 있는 작업은 스트리밍합니다. 후보 점수 산정의 경우, 긴 문서는 첫 번째 범주(배치)에 속하고, 최종 설명은 두 번째 범주(스트리밍)에 속할 수 있습니다. 이것이 트레이드오프입니다: 배치는 완전성을 보호하는 반면, 스트리밍은 인지된 응답성을 향상시키지만 더 많은 연결 및 부분 상태 실패 모드를 노출합니다.
추가 자료 (Further reading)
- MDN, WebRTC API: [https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API]
- MDN, Server-sent events: [https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events]
- RFC 9110, HTTP Semantics: [https://www.rfc-editor.org/rfc/rfc9110]
- RFC 9457, Problem Details for HTTP APIs: [https://www.rfc-editor.org/rfc/rfc9457]
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기