추론

추론 (Reasoning)

AG-UI가 LLM 추론(reasoning)을 어떻게 지원하는지 설명해 드릴게요. 체인-오브-쏘트(chain-of-thought)의 가시성(visibility)을 제공하면서도, 대화 턴 사이에 프라이버시와 상태 연속성까지 유지하는 방식이에요.

출처: 문서

본문

AG-UI는 LLM 추론에 대한 일급(first-class) 지원을 제공해요. 대화 턴 사이에 프라이버시와 상태 연속성을 유지하면서 체인-오브-쏘트 가시성을 가능하게 하죠.

개요

현대 LLM은 응답 품질을 높이기 위해 점점 더 체인-오브-쏘트 추론을 사용해요. AG-UI의 추론 지원은 세 가지 핵심 과제를 다룹니다.

  • 추론 가시성: 원문 체인-오브-쏘트를 노출하지 않으면서 추론 신호(예: 요약)를 사용자에게 보여줍니다.
  • 상태 연속성: store:false나 제로 데이터 보존(ZDR) 정책 아래에서도 암호화된 추론 항목을 사용해 턴 사이에 추론 컨텍스트를 유지합니다.
  • 프라이버시 컴플라이언스: 추론 능력을 보존하면서 엔터프라이즈 프라이버시 요구사항을 지원합니다.

액티비티 메시지와 달리 추론 메시지는 에이전트의 내부 사고 과정을 나타내며, 프라이버시를 위해 암호화될 수 있어요. 또한 이후 턴에서 추가 처리를 위해 에이전트로 다시 보내지는 것을 전제로 해요.

ReasoningMessage

ReasoningMessage 타입은 메시지 히스토리에서 추론 콘텐츠를 나타내요.

interface ReasoningMessage {
  id: string
  role: "reasoning"
  content: string // Reasoning content (visible to client)
  encryptedValue?: string // Optional encrypted reasoning for state continuity
}
속성 타입 설명
id string 추론 메시지의 고유 식별자
role "reasoning" 메시지 역할 판별자
content string 클라이언트에 보이는 추론 콘텐츠
encryptedValue string? 상태 연속성을 위한 암호화된 체인-오브-쏘트 블롭

주요 특징:

  • 어시스턴트 메시지와 분리: 최종 응답과 구분해 대화 히스토리를 오염시키지 않습니다.
  • 스트리밍 가능: 콘텐츠가 스트리밍 이벤트로 도착합니다.
  • 선택적 암호화: encryptedValue가 있으면 클라이언트가 저장하고 불투명하게(opaquely) 전달하는 암호화된 체인-오브-쏘트를 나타냅니다.

추론 이벤트

추론 이벤트는 추론 메시지의 라이프사이클을 관리해요. 완전한 이벤트 레퍼런스는 Events에서 확인하세요.

이벤트 흐름

전형적인 추론 흐름은 이 패턴을 따릅니다.

sequenceDiagram
    participant Agent
    participant Client

    Note over Agent,Client: Reasoning begins
    Agent->>Client: ReasoningStart

    Note over Agent,Client: Stream visible reasoning
    Agent->>Client: ReasoningMessageStart
    Agent->>Client: ReasoningMessageContent (delta)
    Agent->>Client: ReasoningMessageContent (delta)
    Agent->>Client: ReasoningMessageEnd

    Note over Agent,Client: Attach encrypted chain-of-thought
    Agent->>Client: ReasoningEncryptedValue

    Note over Agent,Client: Reasoning completes
    Agent->>Client: ReasoningEnd

이벤트 타입

이벤트 용도
ReasoningStart 추론 단계의 시작을 표시
ReasoningMessageStart 스트리밍 추론 메시지의 시작
ReasoningMessageContent 추론 콘텐츠 덩어리를 전달
ReasoningMessageEnd 추론 메시지 완료
ReasoningMessageChunk 메시지 라이프사이클을 자동 관리하는 편의 이벤트
ReasoningEnd 추론 완료를 표시
ReasoningEncryptedValue 메시지나 도구 호출에 암호화된 체인-오브-쏘트를 부착

프라이버시와 컴플라이언스

AG-UI 추론은 프라이버시 우선 원칙으로 설계됐어요.

제로 데이터 보존 (ZDR)

제로 데이터 보존이 필요한 배포에서는:

  1. 암호화된 추론 값이 클라이언트에 복호화 가능한 콘텐츠를 저장하지 않고 턴 사이에 상태를 전달할 수 있어요.
  2. 클라이언트는 ReasoningEncryptedValue 이벤트를 통해 encryptedValue 블롭을 받아 불투명하게 전달합니다.
  3. 추론 콘텐츠를 복호화할 수 있는 건 에이전트(또는 인가된 백엔드)뿐이에요.

가시성 제어

에이전트는 사용자에게 어떤 추론을 보여줄지 제어해요.

  • 완전 가시성: ReasoningMessageContent 이벤트로 전체 체인-오브-쏘트를 스트리밍합니다.
  • 요약만: 요약된 내용을 내보내면서 자세한 추론은 암호화된 값으로 부착합니다.
  • 숨김: 보이는 스트리밍 없이 ReasoningEncryptedValue 이벤트만 사용합니다.

컴플라이언스 고려사항

요구사항 해결책
GDPR 삭제권 추론 능력을 잃지 않고 암호화된 콘텐츠를 폐기할 수 있음
SOC 2 데이터 처리 추론 콘텐츠가 클라이언트에 평문으로 저장되지 않음
HIPAA 최소 필요 요약만 노출되고 상세 추론은 암호화 상태 유지
감사 로깅 ReasoningStart/ReasoningEnd 이벤트가 콘텐츠 노출 없이 감사 추적 제공

구현 예시

기본 추론 흐름

보이는 추론을 보여주는 간단한 구현이에요.

// Agent emits reasoning start
yield {
  type: "REASONING_START",
  messageId: "reasoning-001",
}

// Stream visible reasoning content
yield {
  type: "REASONING_MESSAGE_START",
  messageId: "msg-123",
  role: "reasoning",
}

yield {
  type: "REASONING_MESSAGE_CONTENT",
  messageId: "msg-123",
  delta: "Let me ",
}

yield {
  type: "REASONING_MESSAGE_CONTENT",
  messageId: "msg-123",
  delta: "think through ",
}

yield {
  type: "REASONING_MESSAGE_CONTENT",
  messageId: "msg-123",
  delta: "this step ",
}

yield {
  type: "REASONING_MESSAGE_CONTENT",
  messageId: "msg-123",
  delta: "by step...",
}

yield {
  type: "REASONING_MESSAGE_END",
  messageId: "msg-123",
}

// End reasoning
yield {
  type: "REASONING_END",
  messageId: "reasoning-001",
}

상태 연속성을 위한 암호화 콘텐츠

콘텐츠를 노출하지 않고 턴 사이에 추론 상태를 유지할 땐 ReasoningEncryptedValue 이벤트로 메시지나 도구 호출에 암호화된 체인-오브-쏘트를 부착해요.

// Agent emits reasoning start
yield {
  type: "REASONING_START",
  messageId: "reasoning-002",
}

// Stream a visible summary for the user
yield {
  type: "REASONING_MESSAGE_START",
  messageId: "msg-456",
  role: "reasoning",
}

yield {
  type: "REASONING_MESSAGE_CONTENT",
  messageId: "msg-456",
  delta: "Analyzing your request...",
}

yield {
  type: "REASONING_MESSAGE_END",
  messageId: "msg-456",
}

// Attach encrypted chain-of-thought to the reasoning message
yield {
  type: "REASONING_ENCRYPTED_VALUE",
  subtype: "message",
  entityId: "msg-456",
  encryptedValue: "eyJhbG...TSJ9...",
}

yield {
  type: "REASONING_END",
  messageId: "reasoning-002",
}

// On subsequent turns, client sends back the message with encryptedValue
// which the agent can decrypt to restore reasoning context

도구 호출에 암호화된 추론 부착하기

에이전트가 특정 인자를 고른 이유나 결과를 해석한 방식을 담기 위해 도구 호출에 암호화된 추론을 부착할 수도 있어요.

// Tool call with encrypted reasoning
yield {
  type: "TOOL_CALL_START",
  toolCallId: "tool-123",
  toolCallName: "search_database",
  parentMessageId: "msg-789",
}

yield {
  type: "TOOL_CALL_ARGS",
  toolCallId: "tool-123",
  delta: '{"query": "user preferences"}',
}

yield {
  type: "TOOL_CALL_END",
  toolCallId: "tool-123",
}

// Attach encrypted reasoning explaining why this tool was called
yield {
  type: "REASONING_ENCRYPTED_VALUE",
  subtype: "tool-call",
  entityId: "tool-123",
  encryptedValue: "encrypted-reasoning-about-tool-selection...",
}

ZDR 컴플라이언트 구현

제로 데이터 보존 시나리오에서는:

// Server-side: encrypt reasoning before sending
const encryptedReasoning = await encrypt(detailedChainOfThought, secretKey)

yield {
  type: "REASONING_START",
  messageId: "reasoning-003",
}

// Only emit a high-level summary to the client
yield {
  type: "REASONING_MESSAGE_CHUNK",
  messageId: "summary-001",
  delta: "Processing your request securely...",
}

yield {
  type: "REASONING_MESSAGE_CHUNK",
  messageId: "summary-001",
  delta: "", // Empty delta closes the message
}

// Attach the encrypted chain-of-thought
yield {
  type: "REASONING_ENCRYPTED_VALUE",
  subtype: "message",
  entityId: "summary-001",
  encryptedValue: encryptedReasoning,
}

yield {
  type: "REASONING_END",
  messageId: "reasoning-003",
}

// Client stores only:
// - The encrypted blob (cannot decrypt)
// - The summary text (no sensitive details)
// Full reasoning is never persisted in plaintext

편의 청크 이벤트 사용하기

ReasoningMessageChunk 이벤트는 메시지 라이프사이클을 자동 관리해 구현을 단순화해 줘요.

// First chunk with messageId starts the message automatically
yield {
  type: "REASONING_MESSAGE_CHUNK",
  messageId: "msg-789",
  delta: "Analyzing the problem space...",
}

// Subsequent chunks continue the stream
yield {
  type: "REASONING_MESSAGE_CHUNK",
  messageId: "msg-789",
  delta: " Considering multiple approaches...",
}

// Empty delta (or next non-reasoning event) closes automatically
yield {
  type: "REASONING_MESSAGE_CHUNK",
  messageId: "msg-789",
  delta: "",
}

클라이언트 통합

추론 이벤트 처리하기

import { EventType, type BaseEvent } from "@ag-ui/core"

function handleEvent(event: BaseEvent) {
  switch (event.type) {
    case EventType.REASONING_START:
      // Initialize reasoning UI (e.g., "thinking" indicator)
      console.log("Agent is reasoning...")
      break

    case EventType.REASONING_MESSAGE_CONTENT:
      // Append visible reasoning to UI
      appendReasoningText(event.messageId, event.delta)
      break

    case EventType.REASONING_ENCRYPTED_VALUE:
      // Store encrypted value for the referenced entity
      if (event.subtype === "message") {
        storeMessageEncryptedValue(event.entityId, event.encryptedValue)
      } else if (event.subtype === "tool-call") {
        storeToolCallEncryptedValue(event.entityId, event.encryptedValue)
      }
      break

    case EventType.REASONING_END:
      // Finalize reasoning UI
      console.log("Reasoning complete")
      break
  }
}

암호화된 추론 되돌려 보내기

이후 요청을 만들 때는 저장된 암호화 값을 포함해요.

const response = await agent.run({
  threadId: "thread-123",
  messages: [
    ...previousMessages,
    {
      id: "reasoning-002",
      role: "reasoning",
      content: "Analyzing your request...", // Visible summary
      encryptedValue: storedEncryptedBlob, // Opaque to client
    },
    {
      id: "user-msg-001",
      role: "user",
      content: "Follow up question...",
    },
  ],
})

Thinking 이벤트에서 마이그레이션

THINKING_* 이벤트는 deprecated이며 버전 1.0.0에서 제거될 예정이에요. 새 구현은 REASONING_* 이벤트를 사용하세요.

Deprecated 이벤트

다음 이벤트는 deprecated예요.

Deprecated 이벤트 대체 이벤트
THINKING_START REASONING_START
THINKING_END REASONING_END
THINKING_TEXT_MESSAGE_START REASONING_MESSAGE_START
THINKING_TEXT_MESSAGE_CONTENT REASONING_MESSAGE_CONTENT
THINKING_TEXT_MESSAGE_END REASONING_MESSAGE_END

마이그레이션 단계

  1. 이벤트 타입 업데이트: 모든 THINKING_* 이벤트 타입을 해당 REASONING_* 동등 타입으로 바꿉니다.
  2. 메시지 타입 업데이트: thinking 전용 메시지 타입 대신 role: "reasoning"인 ReasoningMessage를 사용합니다.
  3. 암호화 값 지원 추가: 프라이버시 컴플라이언스 개선을 위해 ReasoningEncryptedValue 이벤트 사용을 고려해 보세요.
  4. 충분히 테스트: 새 이벤트 타입으로 기존 기능이 동작하는지 확인합니다.

마이그레이션 예시

이전(deprecated):

// ❌ Deprecated - do not use
yield { type: "THINKING_START", messageId: "think-001" }
yield { type: "THINKING_TEXT_MESSAGE_START", messageId: "msg-001" }
yield { type: "THINKING_TEXT_MESSAGE_CONTENT", messageId: "msg-001", delta: "..." }
yield { type: "THINKING_TEXT_MESSAGE_END", messageId: "msg-001" }
yield { type: "THINKING_END", messageId: "think-001" }

이후(현재):

// ✅ Current implementation
yield { type: "REASONING_START", messageId: "reasoning-001" }
yield { type: "REASONING_MESSAGE_START", messageId: "msg-001", role: "assistant" }
yield { type: "REASONING_MESSAGE_CONTENT", messageId: "msg-001", delta: "..." }
yield { type: "REASONING_MESSAGE_END", messageId: "msg-001" }
yield { type: "REASONING_END", messageId: "reasoning-001" }

모범 사례

  1. 항상 start/end 이벤트를 짝지어라: 모든 ReasoningStart에는 대응하는 ReasoningEnd가 있어야 해요.
  2. 민감한 추론에는 암호화 값을 써라: 체인-오브-쏘트에 민감한 정보가 있으면 ReasoningEncryptedValue로 메시지나 도구 호출에 암호화 콘텐츠를 부착하세요.
  3. 사용자 피드백을 제공하라: 암호화된 추론이라도 사용자가 에이전트가 동작 중임을 알 수 있도록 보이는 요약을 내보내세요.
  4. 누락된 이벤트를 우아하게 처리하라: 클라이언트는 불완전한 이벤트 스트림에 탄력적으로 대응해야 해요.
  5. 대역폭을 고려하라: 아주 긴 추론 체인은 데이터 전송을 줄이기 위해 요약만 내보내는 걸 고려해 보세요.

관련 문서

  • Events - 완전한 이벤트 타입 레퍼런스
  • Messages - 메시지 타입 문서
  • Serialization - 상태 연속성과 계보(lineage)

더 알아보기 (Learn more)

  • 이벤트 (Events) — 추론 이벤트가 스트리밍되는 전체 흐름을 확인해 보세요.
  • 메시지 (Messages) — 추론 메시지 타입이 메시지 구조에 어떻게 들어맞는지 살펴보세요.