본문으로 건너뛰기

v1.7.1 — Converse Stream API + 스트리밍 UX 개선

· 약 4분

멀티 라우트 합성에 Bedrock Converse Stream API를 도입하고, AgentCore Gateway 응답에 타이핑 효과를 적용한 릴리스입니다. 모니터링 쿼리에 시간 범위 필터를 추가하여 pg pool 고갈 문제도 해결했습니다.

핵심 변화

Converse Stream API · 타이핑 효과 (50char/15ms) · 채팅 버블 너비 안정화 · 모니터링 쿼리 시간 필터 3건

Converse Stream API로 실시간 합성

멀티 라우트 응답 합성이 가짜 청킹에서 진짜 토큰 스트리밍으로 전환되었습니다.

Before / After

항목Before (v1.7.0)After (v1.7.1)
합성 APIInvokeModelCommandConverseStreamCommand
합성 함수synthesizeResponses()synthesizeResponsesStreaming()
스트리밍 방식전체 응답 생성 → simulateStreaming()토큰 생성 즉시 chunk 이벤트 전송
첫 토큰 지연응답 전체 완료까지 대기 (5~15초)~200ms (첫 토큰 즉시)
사용자 체감긴 대기 후 빠른 출력즉시 시작, 자연스러운 흐름

코드 변경

BeforesynthesizeResponses() + simulateStreaming():

// src/app/api/ai/route.ts (v1.7.0)
const synthesized = await synthesizeResponses(
lastMsg, successful, modelKey, clientLang
);
// 완성된 텍스트를 50자씩 나눠서 가짜 스트리밍
await simulateStreaming(synthesized, send);

AftersynthesizeResponsesStreaming() + ConverseStreamCommand:

// src/app/api/ai/route.ts (v1.7.1)
const synthesized = await synthesizeResponsesStreaming(
lastMsg, successful, send, modelKey, clientLang
);
// send() 콜백이 contentBlockDelta마다 즉시 chunk 전송

synthesizeResponsesStreaming() 핵심 로직:

const response = await bedrockClient.send(new ConverseStreamCommand({
modelId,
system: [{ text: systemPrompt }],
messages: [{ role: 'user', content: [{ text: synthesisPrompt }] }],
inferenceConfig: { maxTokens: 4096 },
}));

for await (const event of response.stream) {
if (event.contentBlockDelta?.delta?.text) {
const text = event.contentBlockDelta.delta.text;
fullContent += text;
send('chunk', { delta: text }); // 토큰 즉시 전송
}
}

타이핑 효과 (simulateStreaming)

AgentCore Gateway는 완성된 텍스트를 한 번에 반환하기 때문에, 타이핑 효과를 시뮬레이션합니다.

// src/app/api/ai/route.ts
const CHUNK_SIZE = 50; // 청크당 50자
const CHUNK_DELAY_MS = 15; // 청크 간 15ms 딜레이

async function simulateStreaming(
text: string,
send: (event: string, data: any) => void,
): Promise<void> {
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
const chunk = text.slice(i, i + CHUNK_SIZE);
send('chunk', { delta: chunk });
if (i + CHUNK_SIZE < text.length) {
await new Promise(r => setTimeout(r, CHUNK_DELAY_MS));
}
}
}

적용 대상 (3곳):

  1. 단일 AgentCore Gateway 응답 — 완성된 텍스트에 타이핑 효과
  2. 멀티 라우트 합성 (v1.7.0)synthesizeResponses() 결과에 적용 (v1.7.1에서 Converse Stream으로 대체)
  3. 멀티 라우트 단일 성공 — 1개 Gateway만 성공한 경우
왜 시뮬레이션이 필요한가?

Bedrock 직접 호출은 InvokeModelWithResponseStreamCommand로 진짜 스트리밍이 가능하지만, AgentCore Gateway는 내부적으로 도구를 여러 번 호출한 뒤 최종 텍스트만 반환합니다. 사용자에게 일관된 스트리밍 경험을 제공하기 위해 50자/15ms 타이핑 효과를 적용합니다.

채팅 버블 너비 안정화 (PR #8)

SSE 스트리밍 중 채팅 버블 너비가 갑자기 변하는 문제를 수정했습니다.

항목BeforeAfter
CSSmax-w-5xlw-full max-w-5xl
동작콘텐츠 증가 시 너비 점프첫 청크부터 최대 너비 고정
파일src/app/ai/page.tsx동일

Before — 너비 점프:

<div class="max-w-5xl rounded-lg px-4 py-3 ...">
<!-- 콘텐츠가 짧을 때 좁고, 길어지면 갑자기 넓어짐 -->
</div>

After — 안정적 너비:

<div class="w-full max-w-5xl rounded-lg px-4 py-3 ...">
<!-- 처음부터 w-full로 최대 너비 확보, max-w-5xl로 상한 제한 -->
</div>
CSS 패턴

SSE 스트리밍처럼 콘텐츠가 점진적으로 추가되는 컨테이너에는 w-full max-w-{size} 조합을 사용하세요. max-w-{size}만 사용하면 콘텐츠 양에 따라 너비가 변합니다.

모니터링 쿼리 시간 범위 필터

시간 범위 제한이 없는 모니터링 쿼리가 Steampipe API를 통해 전체 이력 데이터를 가져와 pg pool 5개 커넥션을 모두 점유하는 문제를 수정했습니다.

쿼리파일BeforeAfter
ec2CpuHourlyqueries/metrics.ts시간 제한 없음WHERE timestamp >= NOW() - INTERVAL '24 hours'
ebsIopsHourlyqueries/metrics.tsWHERE r.timestamp IS NOT NULLAND r.timestamp >= NOW() - INTERVAL '24 hours'
rdsCpuDailyqueries/metrics.ts시간 제한 없음WHERE timestamp >= NOW() - INTERVAL '30 days'
pg Pool 고갈 증상

46+ EC2 인스턴스 + 다수 EBS 볼륨 환경에서 시간 제한 없는 쿼리가 수 분간 커넥션을 점유합니다. pg pool max: 5가 모두 사용되면 대시보드와 모든 페이지가 statement_timeout: 120s까지 대기 후 실패합니다.

3가지 스트리밍 모드 요약

v1.7.1에서 AWSops AI 응답은 상황에 따라 3가지 스트리밍 모드를 사용합니다.

모드사용 시점구현첫 토큰 지연
Real StreamingBedrock 직접 호출InvokeModelWithResponseStreamCommand~200ms
Converse Stream멀티 라우트 합성ConverseStreamCommand + contentBlockDelta~200ms
Simulated StreamingAgentCore Gateway 응답simulateStreaming() (50자/15ms)응답 완료 후 즉시
통합 클라이언트 경험

3가지 모드 모두 동일한 SSE chunk 이벤트를 사용하기 때문에, 클라이언트(src/app/ai/page.tsx)는 모드를 구분하지 않고 streamingContent 상태에 delta를 누적하여 ReactMarkdown으로 실시간 렌더링합니다.