구조화된 출력

구조화된 출력 (Structured Output)

에이전트가 단순히 텍스트만 돌려주는 것으로는 부족할 때가 있어요. 카드, 표, 차트, 단계별 분해 같은 UI에 바로 매핑할 수 있는 타입 있는 데이터가 필요하죠. 구조화된 출력을 쓰면 에이전트가 평문 대신 타입이 있는, 기계가 읽을 수 있는 데이터를 반환합니다. 이 페이지에서 그 방법을 설명해 드릴게요.

출처: LangChain 공식 문서 — frontend-structured-output

구조화된 출력이란?

자유 형식 텍스트 응답 대신, 에이전트는 도구 호출(tool call)을 사용해 미리 정의된 스키마를 따르는 구조화된 객체를 반환합니다. 이렇게 하면 다음과 같은 장점이 있어요.

  • 타입 안전한 데이터 (Type-safe data) — 응답을 알려진 TypeScript 타입으로 파싱합니다.
  • 정밀한 렌더링 제어 (Precise rendering control) — 각 필드를 저마다의 UI 처리 방식으로 렌더링합니다.
  • 일관된 형식 (Consistent formatting) — 모델이 무엇이든, 모든 응답이 같은 구조를 따릅니다.

에이전트는 응답 데이터를 인자로 담은 "구조화된 출력" 도구를 호출해서 이걸 달성합니다. 그 도구 자체는 어떤 로직도 실행하지 않아요. 순수하게 타입 있는 데이터를 돌려주기 위한 운반체(vehicle)일 뿐이죠.

사용 사례

  • 제품 비교 — 기능 표, 장단점 목록, 평점
  • 데이터 분석 — 지표, 분해, 하이라이트가 담긴 요약
  • 단계별 가이드 — 설명과 코드 스니펫이 있는 순서형 지침
  • 요리 레시피 — 재료, 단계, 시간, 영양 정보
  • 수학·과학 — LaTeX로 렌더링된 공식, 단계별 유도 과정
  • 여행 계획 — 날짜, 위치, 비용 추정치가 있는 일정

스키마 정의하기

에이전트가 반환하는 구조화된 데이터를 위한 TypeScript 타입을 정의하세요. 이 스키마의 형태가 UI를 어떻게 렌더링할지 결정하고, 어떤 형태든 패턴은 똑같이 동작합니다.

interface MathSolution {
  problem: string; // The original math problem
  steps: {
    explanation: string;
    latex: string; // Optional display math for this step
  }[]; // Step-by-step derivation
  finalAnswer: string; // Plain-text final answer
  finalAnswerLatex: string; // LaTeX representation of the final answer
}

메시지에서 구조화된 출력 추출하기

구조화된 출력은 마지막 AIMessagetool_calls 배열에 담겨 있어요. AI 메시지를 찾아 첫 번째 도구 호출의 인자에 접근하면 됩니다.

import { AIMessage } from "langchain";

function extractStructuredOutput<T>(messages: any[]): T | null {
  const aiMessage = messages.find(AIMessage.isInstance);
  const toolCall = aiMessage?.tool_calls?.[0];
  if (!toolCall) return null;

  return toolCall.args as T;
}

구조화된 출력 도구 호출은 에이전트가 스트리밍을 끝내기 전까지 args가 채워지지 않을 수 있어요. 스트리밍 중에는 args가 부분적으로 채워지거나 undefined일 수 있으니, 렌더링 전에 항상 완전성을 확인하세요.

useStream 설정하기

useStream을 구조화된 출력 에이전트에 연결하고, stream.messages를 읽어 최신 AIMessage 도구 호출에서 타입 있는 페이로드를 추출합니다. args가 완성되면 커스텀 UI를 렌더링하고, stream.isLoading이 true인 동안(도구 인자가 점진적으로 스트리밍될 수 있으니) 로딩 상태를 보여준 뒤, stream.submit()으로 다음 프롬프트를 보내면 돼요.

아래 코드는 타입 안전한 스트림 상태를 위해 useStream<typeof myAgent>를 사용합니다. Python 또는 JavaScript 백엔드의 타입 추론에 대해서는 프론트엔드 개요의 해당 섹션을 참고하세요.

import { useStream } from "@langchain/react";
import { AIMessage } from "langchain";

function MathSolutionChat() {
  const stream = useStream<typeof myAgent>({
    apiUrl: "http://localhost:2024",
    assistantId: "structured_output_latex",
  });

  const solution = extractStructuredOutput<MathSolution>(stream.messages);

  return (
    <div>
      {!solution && !stream.isLoading && (
        <PromptInput onSubmit={(text) =>
          stream.submit({ messages: [{ type: "human", content: text }] })
        } />
      )}
      {stream.isLoading && <LoadingIndicator />}
      {solution && <SolutionCard solution={solution} />}
    </div>
  );
}

Vue, Svelte, Angular에서도 각각의 관용적 반응형 형태(computed, $derived, computed())로 같은 구조를 구현합니다.

구조화된 데이터 렌더링하기

타입 있는 객체를 얻으면, 각 필드를 적절한 UI 요소에 매핑하는 컴포넌트를 만드세요. 이것이 이 패턴의 핵심입니다 — 구조화된 데이터를 목적에 맞는 인터페이스로 바꾸는 거죠.

function SolutionCard({ solution }: { solution: MathSolution }) {
  return (
    <div className="solution-card">
      <h3>{solution.problem}</h3>
      <ol>
        {solution.steps.map((step, i) => (
          <li key={i}>
            <span>{step.explanation}</span>
            {step.latex && <LatexBlock latex={step.latex} />}
          </li>
        ))}
      </ol>
      <strong>{solution.finalAnswer}</strong>
      {solution.finalAnswerLatex && <LatexBlock latex={solution.finalAnswerLatex} />}
    </div>
  );
}

LaTeX 공식은 KaTeX나 MathJax 같은 라이브러리로 렌더링할 수 있어요.

부분 스트리밍 데이터 다루기

스트리밍 중에는 도구 호출 인자가 불완전한 JSON일 수 있어요. 추출 로직에서 이런 경우를 막아야 합니다. requiredFields 파라미터를 쓰면 핵심 필드가 채워질 때까지 기다렸다가 렌더링할 수 있어요.

const solution = extractStructuredOutput<MathSolution>(stream.messages, [
  "problem",
  "steps",
  "finalAnswer",
]);

스트리밍 중 점진적으로 렌더링하기

완전한 구조화된 출력을 기다리는 대신, 필드가 도착할 때마다 렌더링할 수도 있어요. 스키마가 문제 → 유도 단계 → 최종 답변처럼 자연스러운 위에서 아래 순서를 갖고, 에이전트가 보통 스키마 순서대로 필드를 생성한다면, UI가 자연스럽게 채워집니다. 이것은 사용자에게 즉각적인 피드백을 줘요.

모범 사례

  • 렌더링 전 검증 — 스트리밍이 부분 데이터를 전달할 수 있으니 필수 필드가 있는지 항상 확인하세요.
  • 일반화된 추출 함수 사용 — 타입과 필수 필드로 추출 로직을 파라미터화해 다양한 스키마에서 재사용하세요.
  • 점진적 렌더링 — 완전한 객체를 기다리지 말고 필드가 도착할 때마다 보여줘 즉각적인 피드백을 주세요.
  • 폴백 표현 제공 — 필드가 풍부한 렌더링(LaTeX, Markdown, 차트)을 지원한다면 스키마에 평문 버전도 함께 넣어 폴백으로 쓰세요.
  • 스키마는 가능하면 평평하게 — 깊게 중첩된 스키마는 점진적 렌더링이 어렵고 부분 스트리밍 중 깨질 가능성이 높아요.
  • UI를 데이터에 맞추기 — 배열은 표, 중첩 객체는 카드, 상태 필드는 배지처럼 각 필드 타입에 가장 잘 맞는 렌더링 전략을 고르세요.

더 알아보기 (Learn more)