멀티모달 콘텐츠로 평가 실행하기

멀티모달 콘텐츠로 평가 실행하기

파일 첨부물이 있는 데이터셋 예시를 생성하고, 멀티모달 콘텐츠로 LangSmith 평가를 실행할 때 프롬프트와 평가기에 그 예시를 사용하는 방법을 알아봐요.

LangSmith를 사용하면 이미지, 오디오 파일, 문서 같은 파일 첨부물이 있는 데이터셋 예시를 만들고, 멀티모달 콘텐츠로 평가를 실행할 때 프롬프트와 평가기에 그 예시를 사용할 수 있어요.

예시에 base64 인코딩으로 멀티모달 데이터를 포함할 수는 있지만, 이 방식은 비효율적입니다 — 인코딩된 데이터는 원본 바이너리 파일보다 더 많은 공간을 차지해 LangSmith와의 전송이 느려집니다. 대신 첨부물을 사용하면 두 가지 핵심 이점이 있습니다:

  • 더 효율적인 바이너리 파일 전송 덕분에 더 빠른 업로드 및 다운로드 속도.
  • LangSmith UI에서 다양한 파일 유형의 향상된 시각화.

이 가이드는 첨부물이 있는 예시를 만들고, 그 첨부물을 사용하는 멀티모달 프롬프트와 평가기를 구축하며, 멀티모달 콘텐츠로 평가를 실행하는 방법을 다룹니다. 시작하려면 UI 또는 SDK 탭을 선택하세요.

선호하는 방법을 선택하세요:

출처: 문서

본문

UI

1. 첨부물이 있는 예시 만들기

몇 가지 방법으로 데이터셋에 첨부물이 있는 예시를 추가할 수 있어요.

기존 실행에서 (From existing runs)

실행을 LangSmith 데이터셋에 추가할 때, 소스 실행에서 대상 예시로 첨부물을 선택적으로 전파할 수 있습니다. 자세한 내용은 애플리케이션에서 데이터셋 관리를 참고하세요.

처음부터 (From scratch)

LangSmith UI에서 직접 첨부물이 있는 예시를 만들 수 있어요. 데이터셋 UI의 Examples 탭에서 + Example 버튼을 클릭합니다. 그런 다음 "파일 업로드(Upload Files)" 버튼을 사용해 첨부물을 업로드합니다.

업로드하면 LangSmith UI에서 첨부물이 있는 예시를 볼 수 있습니다. 각 첨부물은 쉬운 검사를 위한 미리보기와 함께 렌더링됩니다.

2. 멀티모달 프롬프트 만들기

LangSmith UI를 사용하면 멀티모달 모델을 평가할 때 프롬프트에 첨부물을 포함할 수 있습니다:

먼저, 멀티모달 콘텐츠를 추가하려는 메시지에서 파일 아이콘을 클릭합니다. 다음으로, 각 예시에 포함하려는 첨부물에 대한 템플릿 변수를 추가합니다.

  • 특정 첨부물을 포함하려면 {{attachment.file_name}} 같은 제안된 변수 이름을 사용할 수 있습니다. 이는 첨부물 목록에서 file_name을 가진 파일을 매핑해 평가기에 전달합니다.
  • 모든 첨부물을 포함하려면 {{attachments}} 변수를 사용합니다.

3. 커스텀 평가기 정의하기

데이터셋 예시의 멀티모달 콘텐츠를 사용하는 평가기를 만들 수 있어요.

평가기는 입력 형식과 구조화된 출력을 모두 지원하는 모델을 사용해야 합니다. 오디오 첨부물의 경우 현재는 Gemini만 지원합니다. 이미지와 PDF 첨부물은 구조화된 출력을 반환하는 모든 비전 지원 모델과 작동합니다.

데이터셋에 이미 첨부물이 있는 예시가 있으므로(1단계에서 추가), 평가기에서 직접 참조할 수 있습니다. 방법은 다음과 같습니다:

  1. 데이터셋 페이지에서 + 평가기(+ Evaluator) 를 선택합니다.

  2. 템플릿 변수(Template variables) 편집기에서 포함할 첨부물에 대한 변수를 추가합니다:

    • 특정 첨부물을 포함하려면 {{attachment.file_name}} 같은 제안된 변수 이름을 사용할 수 있습니다. 이는 첨부물 목록에서 file_name을 가진 파일을 매핑해 평가기에 전달합니다.
    • 모든 첨부물을 포함하려면 {{attachments}} 변수를 사용합니다.

그러면 평가기는 모델의 출력과 함께 이러한 첨부물을 사용해 품질을 판정할 수 있습니다. 예를 들어 다음과 같은 평가기를 만들 수 있습니다:

  • 이미지 설명이 실제 이미지 콘텐츠와 일치하는지 확인.
  • 전사(transcription)가 오디오를 정확히 반영하는지 검증.
  • PDF에서 추출한 텍스트가 올바른지 검증.

첨부물을 사용하지 않고 모델의 텍스트 출력만 평가하는 텍스트 전용 평가기도 만들 수 있습니다:

  • OCR → 텍스트 교정: 비전 모델로 문서에서 텍스트를 추출한 다음, 추출된 출력의 정확성을 평가.
  • 음성-텍스트 → 전사 품질: 음성 모델로 오디오를 텍스트로 전사한 다음, 참조와 비교해 전사를 평가.

트레이스의 입력 또는 출력에 base64 인코딩된 멀티모달 콘텐츠가 있다면(예: 멀티모달 트레이스 로깅 가이드를 따른 경우), 평가에 첨부물이 필요하지 않아요. 평가기 프롬프트에서 {{input}} 또는 {{output}} 같은 표준 변수 매핑을 사용하면 base64 콘텐츠가 시각화와 평가를 위해 LLM 평가기에 올바르게 전달됩니다.

커스텀 평가기 정의에 대한 자세한 내용은 LLM as Judge 가이드를 참고하세요.

4. 첨부물이 있는 예시 업데이트하기

UI에서 첨부물은 20MB 크기로 제한됩니다.

UI에서 예시를 편집할 때 다음을 할 수 있습니다:

  • 새 첨부물 업로드
  • 첨부물 이름 변경 및 삭제
  • 빠른 재설정 버튼을 사용해 첨부물을 이전 상태로 재설정

제출을 클릭하기 전까지 변경 사항은 저장되지 않습니다.

SDK

1. 첨부물이 있는 예시 만들기

SDK로 첨부물이 있는 예시를 업로드하려면 Python 메서드 create_examples / update_examples 또는 TypeScript 메서드 uploadExamplesMultipart / updateExamplesMultipart를 사용하세요.

Python

langsmith>=0.3.13 필요

import requests
import uuid
from pathlib import Path
from langsmith import Client

# 공개적으로 사용 가능한 테스트 파일
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
wav_url = "https://openaiassets.blob.core.windows.net/$web/API/docs/audio/alloy.wav"
img_url = "https://www.w3.org/Graphics/PNG/nurbcup2si.png"

# 파일을 bytes로 가져오기
pdf_bytes = requests.get(pdf_url).content
wav_bytes = requests.get(wav_url).content
img_bytes = requests.get(img_url).content

# 데이터셋 생성
ls_client = Client()
dataset_name = "attachment-test-dataset"
dataset = ls_client.create_dataset(
  dataset_name=dataset_name,
  description="Test dataset for evals with publicly available attachments",
)

inputs = {
  "audio_question": "What is in this audio clip?",
  "image_question": "What is in this image?",
}

outputs = {
  "audio_answer": "The sun rises in the east and sets in the west. This simple fact has been observed by humans for thousands of years.",
  "image_answer": "A mug with a blanket over it.",
}

# 첨부물이 있는 예시 정의
example_id = uuid.uuid4()
example = {
  "id": example_id,
  "inputs": inputs,
  "outputs": outputs,
  "attachments": {
      "my_pdf": {"mime_type": "application/pdf", "data": pdf_bytes},
      "my_wav": {"mime_type": "audio/wav", "data": wav_bytes},
      "my_img": {"mime_type": "image/png", "data": img_bytes},
      # 로컬 파일 경로로 지정된 첨부물의 예시:
      # "my_local_img": {"mime_type": "image/png", "data": Path(__file__).parent / "my_local_img.png"},
  },
}

# 예시 생성
ls_client.create_examples(
  dataset_id=dataset.id,
  examples=[example],
  # 로컬 파일에서 첨부물을 업로드하려면 이 플래그를 주석 해제하세요:
  # dangerously_allow_filesystem=True
)
TypeScript

버전 0.2.13 이상 필요

uploadExamplesMultipart 메서드를 사용해 첨부물이 있는 예시를 업로드할 수 있습니다.

이 메서드는 현재 첨부물을 지원하지 않는 표준 createExamples 메서드와 다릅니다. 각 첨부물은 데이터 유형으로 Uint8Array 또는 ArrayBuffer가 필요합니다.

  • Uint8Array: 바이너리 데이터를 직접 처리하는 데 유용.
  • ArrayBuffer: 고정 길이 바이너리 데이터를 나타내며, 필요에 따라 Uint8Array로 변환할 수 있습니다.

TypeScript SDK에서는 파일 경로를 직접 전달할 수 없습니다. 모든 런타임 환경에서 로컬 파일 접근이 지원되지 않기 때문입니다.

import { Client } from "langsmith";
import { v4 as uuid4 } from "uuid";

// 공개적으로 사용 가능한 테스트 파일
const pdfUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
const wavUrl = "https://openaiassets.blob.core.windows.net/$web/API/docs/audio/alloy.wav";
const pngUrl = "https://www.w3.org/Graphics/PNG/nurbcup2si.png";

// 파일을 ArrayBuffer로 가져오는 헬퍼 함수
async function fetchArrayBuffer(url: string): Promise<ArrayBuffer> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
  }
  return response.arrayBuffer();
}

// 파일을 ArrayBuffer로 가져오기
const pdfArrayBuffer = await fetchArrayBuffer(pdfUrl);
const wavArrayBuffer = await fetchArrayBuffer(wavUrl);
const pngArrayBuffer = await fetchArrayBuffer(pngUrl);

// LangSmith 클라이언트 생성 (환경에 LANGSMITH_API_KEY가 설정되어 있는지 확인)
const langsmithClient = new Client();

// 고유한 데이터셋 이름 생성
const datasetName = "attachment-test-dataset:" + uuid4().substring(0, 8);

// 데이터셋 생성
const dataset = await langsmithClient.createDataset(datasetName, {
  description: "Test dataset for evals with publicly available attachments",
});

// 첨부물이 있는 예시 정의
const exampleId = uuid4();
const example = {
  id: exampleId,
  inputs: {
      audio_question: "What is in this audio clip?",
      image_question: "What is in this image?",
  },
  outputs: {
      audio_answer: "The sun rises in the east and sets in the west. This simple fact has been observed by humans for thousands of years.",
      image_answer: "A mug with a blanket over it.",
  },
  attachments: {
    my_pdf: {
      mimeType: "application/pdf",
      data: pdfArrayBuffer
    },
    my_wav: {
      mimeType: "audio/wav",
      data: wavArrayBuffer
    },
    my_img: {
      mimeType: "image/png",
      data: pngArrayBuffer
    },
  },
};

// 첨부물이 있는 예시를 데이터셋에 업로드
await langsmithClient.uploadExamplesMultipart(dataset.id, [example]);

bytes로 전달되는 것 외에도, 첨부물은 로컬 파일 경로로 지정할 수 있어요. 이를 위해 첨부물 data 값에 경로를 전달하고 dangerously_allow_filesystem=True 인자를 지정합니다:

client.create_examples(..., dangerously_allow_filesystem=True)

2. 평가 실행하기

타깃 함수 정의하기 (Define a target function)

이제 첨부물이 있는 예시를 포함하는 데이터셋이 있으므로, 이 예시들에 대해 실행할 타깃 함수를 정의할 수 있어요. 다음 예시는 단순히 OpenAI의 GPT-4o 모델을 사용해 이미지와 오디오 클립에 대한 질문에 답합니다.

Python

평가하는 타깃 함수는 예시와 연관된 첨부물을 소비하기 위해 두 개의 위치 인자를 가져야 하며, 첫 번째는 inputs, 두 번째는 attachments라고 해야 합니다.

  • inputs 인자는 첨부물을 제외한 예시의 입력 데이터를 포함하는 딕셔너리.
  • attachments 인자는 첨부물 이름을 프리사인드 URL, mime_type, 파일의 bytes 콘텐츠 리더를 포함하는 딕셔너리에 매핑하는 딕셔너리. 파일 콘텐츠를 얻으려면 프리사인드 URL 또는 리더를 사용할 수 있습니다. attachments 딕셔너리의 각 값은 다음 구조의 딕셔너리입니다:
{
    "presigned_url": str,
    "mime_type": str,
    "reader": BinaryIO
}
from langsmith.wrappers import wrap_openai
import base64
from openai import OpenAI

client = wrap_openai(OpenAI())

# 첨부물을 사용하는 타깃 함수 정의
def file_qa(inputs, attachments):
    # 리더에서 오디오 bytes를 읽고 base64로 인코딩
    audio_reader = attachments["my_wav"]["reader"]
    audio_b64 = base64.b64encode(audio_reader.read()).decode('utf-8')

    audio_completion = client.chat.completions.create(
        model="gpt-4o-audio-preview",
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": inputs["audio_question"]
                    },
                    {
                        "type": "input_audio",
                        "input_audio": {
                            "data": audio_b64,
                            "format": "wav"
                        }
                    }
                ]
            }
        ]
    )

    # 대부분의 모델은 base64 인코딩 이미지 외에도 이미지 URL을 직접 받는 것을 지원
    # 이미지 프리사인드 URL을 모델에 직접 파이프할 수 있습니다
    image_url = attachments["my_img"]["presigned_url"]
    image_completion = client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[
          {
            "role": "user",
            "content": [
              {"type": "text", "text": inputs["image_question"]},
              {
                "type": "image_url",
                "image_url": {
                  "url": image_url,
                },
              },
            ],
          }
        ],
    )

    return {
        "audio_answer": audio_completion.choices[0].message.content,
        "image_answer": image_completion.choices[0].message.content,
    }
TypeScript

TypeScript SDK에서는 includeAttachmentstrue로 설정된 경우 config 인자를 사용해 타깃 함수에 첨부물을 전달합니다.

config에는 첨부물 이름을 다음 형식의 객체에 매핑하는 attachments가 포함됩니다:

{
  presigned_url: string,
  mime_type: string,
}
import OpenAI from "openai";
import { wrapOpenAI } from "langsmith/wrappers";

const client: any = wrapOpenAI(new OpenAI());

async function fileQA(inputs: Record<string, any>, config?: Record<string, any>) {
  const presignedUrl = config?.attachments?.["my_wav"]?.presigned_url;
  if (!presignedUrl) {
    throw new Error("No presigned URL provided for audio.");
  }

  const response = await fetch(presignedUrl);
  if (!response.ok) {
    throw new Error(`Failed to fetch audio: ${response.statusText}`);
  }

  const arrayBuffer = await response.arrayBuffer();
  const uint8Array = new Uint8Array(arrayBuffer);
  const audioB64 = Buffer.from(uint8Array).toString("base64");

  const audioCompletion = await client.chat.completions.create({
    model: "gpt-4o-audio-preview",
    messages: [
      {
        role: "user",
        content: [
          { type: "text", text: inputs["audio_question"] },
          {
            type: "input_audio",
            input_audio: {
              data: audioB64,
              format: "wav",
            },
          },
        ],
      },
    ],
  });

  const imageUrl = config?.attachments?.["my_img"]?.presigned_url
  const imageCompletion = await client.chat.completions.create({
    model: "gpt-5.4-mini",
    messages: [
      {
        role: "user",
        content: [
          { type: "text", text: inputs["image_question"] },
          {
            type: "image_url",
            image_url: {
              url: imageUrl,
            },
          },
        ],
      },
    ],
  });

  return {
    audio_answer: audioCompletion.choices[0].message.content,
    image_answer: imageCompletion.choices[0].message.content,
  };
}
커스텀 평가기 정의하기 (Define custom evaluators)

UI에서 이러한 첨부물 입력과 출력을 참조하는 멀티모달 평가기를 정의할 수도 있어요. UI 기반 평가기는 SDK에서 호출된 것을 포함해 모든 실험에서 자동으로 실행됩니다. 지침은 UI 탭을 참고하세요.

평가기가 첨부물을 받을지 여부를 결정하는 규칙은 위와 정확히 동일합니다.

아래 평가기는 LLM을 사용해 추론과 답변이 일관적인지 판정합니다. LLM 기반 평가기 정의 방법에 대한 자세한 내용은 LLM-as-a-judge 평가기 정의 방법을 참고하세요.

# pydantic을 설치했다고 가정
from pydantic import BaseModel

def valid_image_description(outputs: dict, attachments: dict) -> bool:
  """LLM을 사용해 이미지 설명과 이미지가 일관적인지 판정."""
  instructions = """
  Does the description of the following image make sense?
  Please carefully review the image and the description to determine if the description is valid.
  """

  class Response(BaseModel):
      description_is_valid: bool

  image_url = attachments["my_img"]["presigned_url"]
  response = client.beta.chat.completions.parse(
      model="gpt-5.5",
      messages=[
          {
              "role": "system",
              "content": instructions
          },
          {
              "role": "user",
              "content": [
                  {"type": "image_url", "image_url": {"url": image_url}},
                  {"type": "text", "text": outputs["image_answer"]}
              ]
          }
      ],
      response_format=Response
  )
  return response.choices[0].message.parsed.description_is_valid

ls_client.evaluate(
  file_qa,
  data=dataset_name,
  evaluators=[valid_image_description],
)
import { zodResponseFormat } from 'openai/helpers/zod';
import { z } from 'zod';
import { evaluate } from "langsmith/evaluation";

const DescriptionResponse = z.object({
  description_is_valid: z.boolean(),
});

async function validImageDescription({
  outputs,
  attachments,
}: {
  outputs?: any;
  attachments?: any;
}): Promise<{ key: string; score: boolean}> {
  const instructions = `Does the description of the following image make sense?
Please carefully review the image and the description to determine if the description is valid.`;

  const imageUrl = attachments?.["my_img"]?.presigned_url
  const completion = await client.beta.chat.completions.parse({
      model: "gpt-5.5",
      messages: [
          {
              role: "system",
              content: instructions,
          },
          {
              role: "user",
              content: [
                  { type: "image_url", image_url: { url: imageUrl } },
                  { type: "text", text: outputs?.image_answer },
              ],
          },
      ],
      response_format: zodResponseFormat(DescriptionResponse, 'imageResponse'),
  });

  const score: boolean = completion.choices[0]?.message?.parsed?.description_is_valid ?? false;
  return { key: "valid_image_description", score };
}

const resp = await evaluate(fileQA, {
  data: datasetName,
  // 첨부물을 포함하려면 플래그를 전달해야 함
  includeAttachments: true,
  evaluators: [validImageDescription],
  client: langsmithClient
});

3. 첨부물이 있는 예시 업데이트하기

위 코드에서는 첨부물이 있는 예시를 데이터셋에 추가하는 방법을 보여줬습니다. SDK를 사용해 동일한 예시를 업데이트하는 것도 가능합니다.

기존 예시와 마찬가지로, 첨부물로 업데이트할 때 데이터셋이 버전 관리됩니다. 따라서 데이터셋 버전 기록으로 이동해 각 예시에 대한 변경 사항을 볼 수 있습니다. 자세한 내용은 UI에서 데이터셋 생성 및 관리를 참고하세요.

첨부물로 예시를 업데이트할 때, 몇 가지 다른 방법으로 첨부물을 업데이트할 수 있습니다:

  • 새 첨부물 전달
  • 기존 첨부물 이름 변경
  • 기존 첨부물 삭제

다음에 유의하세요:

  • 명시적으로 이름을 변경하거나 유지하지 않은 기존 첨부물은 삭제됩니다.
  • retain 또는 rename에 존재하지 않는 첨부물 이름을 전달하면 오류가 발생합니다.
  • attachmentsattachment_operations 필드에 같은 첨부물 이름이 나타나는 경우 새 첨부물이 기존 첨부물보다 우선합니다.
example_update = {
  "id": example_id,
  "attachments": {
      # 이들은 순수하게 새로운 첨부물
      "my_new_file": ("text/plain", b"foo bar"),
  },
  "inputs": inputs,
  "outputs": outputs,
  # rename/retain에 없는 첨부물은 삭제됩니다.
  # 이 경우, 업로드했다면 "my_img"가 해당됩니다.
  "attachments_operations": {
      # 유지된 첨부물은 정확히 동일하게 유지됨
      "retain": ["my_pdf"],
      # 첨부물 이름 변경은 원본 데이터를 보존함
      "rename": {
          "my_wav": "my_new_wav",
      }
  },
}

ls_client.update_examples(dataset_id=dataset.id, updates=[example_update])
import { ExampleUpdateWithAttachments } from "langsmith/schemas";

const exampleUpdate: ExampleUpdateWithAttachments = {
  id: exampleId,
  attachments: {
    // 이들은 순수하게 새로운 첨부물
    "my_new_file": {
      mimeType: "text/plain",
      data: Buffer.from("foo bar")
    },
  },
  attachments_operations: {
    // 유지된 첨부물은 정확히 동일하게 유지됨
    retain: ["my_img"],
    // 첨부물 이름 변경은 원본 데이터를 보존함
    rename: {
      "my_wav": "my_new_wav",
    },
    // rename/retain에 없는 첨부물은 삭제됩니다
    // 이 경우, "my_pdf"가 해당됩니다
  },
};

await langsmithClient.updateExamplesMultipart(dataset.id, [exampleUpdate]);

더 알아보기 (Learn more)