LLM 애플리케이션 트레이싱 튜토리얼

LLM 애플리케이션 트레이싱 튜토리얼

LLM 애플리케이션에 프로토타이핑, 베타 테스트, 프로덕션 전 단계에 걸쳐 LangSmith 관측성(observability)을 추가해요.

이 튜토리얼에서는 검색 증강 생성(RAG)을 사용하는 고객 지원 챗봇을 만들고, 초기 프로토타이핑부터 프로덕션까지 각 개발 단계에 LangSmith 관측성을 추가할 거예요.

튜토리얼을 마치면 다음을 할 수 있게 돼요:

  • 개별 LLM 호출과 전체 애플리케이션 파이프라인을 트레이싱합니다.
  • 사용자 피드백을 수집하고 조회합니다.
  • 메타데이터를 기록하고 필터링 및 A/B 테스트에 사용합니다.
  • 모니터링 대시보드로 프로덕션 성능을 추적합니다.

애플리케이션은 관련 문서 스니펫을 검색해서 사용자 질문에 답변하는 구조예요. 이 튜토리얼에서는 리트리버를 목(mock)으로 대체했으며, 실제 애플리케이션에서는 벡터 검색이나 그와 유사한 것으로 교체하면 돼요.

출처: 문서

본문

사전 준비 사항

시작하기 전에 다음을 준비하세요:

  • LangSmith 계정: smith.langchain.com에서 가입하거나 로그인하세요.
  • LangSmith API 키: API 키 만들기 가이드를 따르세요.
  • OpenAI API 키: OpenAI 대시보드에서 생성하세요.
  • LangSmith CLI (선택): 터미널에서 트레이스를 조사하려면 설치하세요. 설치 방법은 LangSmith CLI를 참조하세요.

필요한 패키지를 설치하세요:

pip install langsmith openai
npm install langsmith openai
npm install -D typescript tsx

프로토타이핑

처음부터 관측성을 갖춰 두면 더 빠르게 반복할 수 있어요. print 문을 추가하거나 디버거를 실행하지 않아도 모델에 무엇이 전송되는지, 무엇이 돌아오는지, 시간이 어디에 쓰이는지 정확히 볼 수 있어요.

환경 설정

셸에서 다음 환경 변수를 설정하세요:

export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="<your-api-key>"
export OPENAI_API_KEY="<your-openai-api-key>"

트레이스를 특정 프로젝트로 보내려면 LANGSMITH_PROJECT 환경 변수를 사용하세요. 설정하지 않으면 LangSmith가 트레이스 수집 시 기본 트레이싱 프로젝트를 자동으로 생성해요.

참고: 다른 문서에서는 이 변수들을 LANGCHAIN_*로 부르기도 해요. 둘 다 동작하지만 LANGSMITH_TRACINGLANGSMITH_API_KEY가 권장 이름이에요.

LLM 호출 트레이싱

우선 모델이 실제로 호출되는 OpenAI 호출부터 트레이싱해 보세요. 이렇게 하면 앱이 보내는 프롬프트와 모델이 반환하는 응답을 즉시 확인할 수 있어요.

OpenAI 클라이언트를 wrap_openai(Python) 또는 wrapOpenAI(TypeScript)로 감싸세요. 다음 코드로 app.py(또는 app.ts) 파일을 만드세요:

from openai import OpenAI
from langsmith.wrappers import wrap_openai

client = wrap_openai(OpenAI())

docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
]

def retriever(query: str) -> list[str]:
    return docs

def support_bot(question: str) -> str:
    context = retriever(question)
    system_message = (
        "You are a helpful customer support agent. "
        "Answer using only the information provided below:\n\n"
        + "\n".join(context)
    )
    response = client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

if __name__ == "__main__":
    print(support_bot("How many users can I have on the Starter plan?"))
import OpenAI from "openai";
import { wrapOpenAI } from "langsmith/wrappers";

const client = wrapOpenAI(new OpenAI());

const docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
];

function retriever(query: string): string[] {
    return docs;
}

async function supportBot(question: string): Promise<string> {
    const context = retriever(question);
    const systemMessage =
        "You are a helpful customer support agent. " +
        "Answer using only the information provided below:\n\n" +
        context.join("\n");
    const response = await client.chat.completions.create({
        model: "gpt-5.4-mini",
        messages: [
            { role: "system", content: systemMessage },
            { role: "user", content: question },
        ],
    });
    return response.choices[0].message?.content ?? "";
}

(async () => {
    console.log(await supportBot("How many users can I have on the Starter plan?"));
})();

support_bot("How many users can I have on the Starter plan?")를 호출하면 OpenAI 호출에 대한 트레이스가 생성돼요.

전체 파이프라인 트레이싱

LLM 호출만 트레이싱해도 유용하지만, 전체 파이프라인(검색 포함)을 트레이싱하면 애플리케이션 동작을 완전히 파악할 수 있어요. 메인 함수에 @traceable(Python) 또는 traceable(TypeScript)을 추가하세요:

from openai import OpenAI
from langsmith import traceable
from langsmith.wrappers import wrap_openai

client = wrap_openai(OpenAI())

docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
]

def retriever(query: str) -> list[str]:
    return docs

@traceable
def support_bot(question: str) -> str:
    context = retriever(question)
    system_message = (
        "You are a helpful customer support agent. "
        "Answer using only the information provided below:\n\n"
        + "\n".join(context)
    )
    response = client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

if __name__ == "__main__":
    print(support_bot("How many users can I have on the Starter plan?"))
import OpenAI from "openai";
import { wrapOpenAI } from "langsmith/wrappers";
import { traceable } from "langsmith/traceable";

const client = wrapOpenAI(new OpenAI());

const docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
];

function retriever(query: string): string[] {
    return docs;
}

const supportBot = traceable(async function supportBot(question: string): Promise<string> {
    const context = retriever(question);
    const systemMessage =
        "You are a helpful customer support agent. " +
        "Answer using only the information provided below:\n\n" +
        context.join("\n");
    const response = await client.chat.completions.create({
        model: "gpt-5.4-mini",
        messages: [
            { role: "system", content: systemMessage },
            { role: "user", content: question },
        ],
    });
    return response.choices[0].message?.content ?? "";
});

(async () => {
    console.log(await supportBot("How many users can I have on the Starter plan?"));
})();

이미지: 바깥쪽 애플리케이션 스팬과 중첩된 LLM 호출 스팬을 보여주는 LangSmith UI의 트레이스 화면

이제 support_bot("How many users can I have on the Starter plan?")을 호출하면 전체 RAG 파이프라인의 트레이스가 생성돼요.

터미널에서 트레이스 확인하기

LangSmith CLI를 설치했다면 UI를 열지 않고도 프로젝트의 최근 트레이스 목록을 확인할 수 있어요:

langsmith trace list --project <your-project> --limit 5

특정 트레이스의 전체 런 계층 구조와 입력/출력을 보려면:

langsmith trace get <trace-id> --full

베타 테스트

프로토타이핑이 잘 동작하면 앱을 소수의 실제 사용자에게 공개해요. 이 단계에서는 사용자가 앱과 어떻게 상호작용할지 정확히 알 수 없으므로 더 풍부한 관측성이 필요해요. 앱이 무엇을 했는지뿐 아니라 사용자가 어떻게 반응했는지도 이해해야 해요.

피드백 수집

사용자 피드백을 특정 트레이스에 연결하면 어떤 응답이 도움이 되었고 어떤 응답이 도움이 되지 않았는지 파악할 수 있어요. 이전 단계의 app.py(또는 app.ts)를 수정해서 각 호출에 런 ID를 추가하고 이후에 점수를 첨부하세요:

import os

from openai import OpenAI
from langsmith import traceable, Client, uuid7
from langsmith.wrappers import wrap_openai

client = wrap_openai(OpenAI())

docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
]

def retriever(query: str) -> list[str]:
    return docs

@traceable
def support_bot(question: str) -> str:
    context = retriever(question)
    system_message = (
        "You are a helpful customer support agent. "
        "Answer using only the information provided below:\n\n"
        + "\n".join(context)
    )
    response = client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

if __name__ == "__main__":
    run_id = str(uuid7())
    support_bot(
        "How many users can I have on the Starter plan?",
        langsmith_extra={"run_id": run_id},
    )
    ls_client = Client()
    # Feedback requires the UUID of the tracing project that owns the run
    project_name = os.environ.get("LANGSMITH_PROJECT", "default")
    session_id = ls_client.create_project(project_name=project_name, upsert=True).id
    ls_client.create_feedback(
        run_id, key="user-score", score=1.0, session_id=session_id
    )
import OpenAI from "openai";
import { wrapOpenAI } from "langsmith/wrappers";
import { traceable, getCurrentRunTree } from "langsmith/traceable";
import { Client } from "langsmith";

const client = wrapOpenAI(new OpenAI());

const docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
];

function retriever(query: string): string[] {
    return docs;
}

let capturedRunId: string;
let capturedProjectName: string;

const supportBot = traceable(async function supportBot(question: string): Promise<string> {
    const runTree = getCurrentRunTree();
    capturedRunId = runTree.id;
    capturedProjectName = runTree.project_name;
    const context = retriever(question);
    const systemMessage =
        "You are a helpful customer support agent. " +
        "Answer using only the information provided below:\n\n" +
        context.join("\n");
    const response = await client.chat.completions.create({
        model: "gpt-5.4-mini",
        messages: [
            { role: "system", content: systemMessage },
            { role: "user", content: question },
        ],
    });
    return response.choices[0].message?.content ?? "";
});

(async () => {
    await supportBot("How many users can I have on the Starter plan?");
    const lsClient = new Client();
    // Feedback requires the UUID of the tracing project that owns the run
    const { id: sessionId } = await lsClient.createProject({
        projectName: capturedProjectName,
        upsert: true,
    });
    await lsClient.createFeedback({
        runId: capturedRunId,
        sessionId,
        key: "user-score",
        score: 1.0,
    });
    await lsClient.flush();
})();

참고: 프로덕션에서는 이 두 부분이 별도 위치에 있어요. run_id가 포함된 support_bot 호출은 앱에 남고, create_feedback은 사용자 피드백을 받는 엔드포인트(예: /feedback API 라우트)로 이동해요. run_id가 둘 사이에 전달되어 피드백이 올바른 트레이스에 연결돼요. 피드백에는 프로젝트 UUID도 필요하므로 run_id와 함께 session_id를 전달하세요.

UI에서 런을 검사하면 Feedback(피드백) 탭에 피드백이 나타나요. 이후 Runs 테이블의 필터링 컨트롤로 피드백 점수별로 런을 필터링할 수 있어요.

메타데이터 기록

메타데이터를 사용하면 필터링과 비교에 유용한 속성으로 런에 태그를 달 수 있어요. 예를 들어 어떤 모델 버전이 사용되었는지, 어떤 사용자가 요청했는지 등을 기록할 수 있어요.

다음 예시는 리트리버(run_type="retriever")와 메인 함수(모델 이름을 담은 metadata 속성)를 모두 트레이싱해요:

from openai import OpenAI
from langsmith import traceable
from langsmith.wrappers import wrap_openai

client = wrap_openai(OpenAI())

docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
]

@traceable(run_type="retriever")
def retriever(query: str) -> list[str]:
    return docs

@traceable(metadata={"llm": "gpt-5.4-mini"})
def support_bot(question: str) -> str:
    context = retriever(question)
    system_message = (
        "You are a helpful customer support agent. "
        "Answer using only the information provided below:\n\n"
        + "\n".join(context)
    )
    response = client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

if __name__ == "__main__":
    support_bot("How many users can I have on the Starter plan?")
import OpenAI from "openai";
import { wrapOpenAI } from "langsmith/wrappers";
import { traceable } from "langsmith/traceable";

const client = wrapOpenAI(new OpenAI());

const docs = [
    "Acme Cloud supports unlimited users on Enterprise plans. Starter plans are limited to 5 users.",
    "To reset your password, click 'Forgot password' on the login page and follow the instructions sent to your email.",
    "API rate limits are 1,000 requests per hour on the Starter plan and 10,000 requests per hour on Enterprise.",
];

const retriever = traceable(
    function retriever(query: string): string[] {
        return docs;
    },
    { run_type: "retriever" }
);

const supportBot = traceable(
    async function supportBot(question: string): Promise<string> {
        const context = await retriever(question);
        const systemMessage =
            "You are a helpful customer support agent. " +
            "Answer using only the information provided below:\n\n" +
            context.join("\n");
        const response = await client.chat.completions.create({
            model: "gpt-5.4-mini",
            messages: [
                { role: "system", content: systemMessage },
                { role: "user", content: question },
            ],
        });
        return response.choices[0].message?.content ?? "";
    },
    { metadata: { llm: "gpt-5.4-mini" } }
);

(async () => {
    await supportBot("How many users can I have on the Starter plan?");
})();

두 메타데이터 값이 모두 트레이스에 나타나요. Runs 테이블의 필터링 컨트롤로 메타데이터별로 런을 필터링할 수 있어요.

프로덕션

튼튼한 관측성을 갖추면 자신 있게 프로덕션에 배포할 수 있어요. 프로덕션에서는 트래픽이 훨씬 많아서 각 트레이스를 개별적으로 검사할 수 없어요. LangSmith는 전체 동작을 이해하고 문제가 보일 때 세부적으로 조사할 수 있는 모니터링 도구를 제공해요.

모니터링

UI 사이드바에서 Monitoring(모니터링)을 선택하고, 왼쪽 상단 드롭다운에서 트레이싱 프로젝트를 선택하세요. 차트는 프로젝트의 핵심 지표(트레이스 수, 지연 시간, 오류율, 피드백 점수, 비용)를 시간에 따라 표시해요. 제공되는 지표와 차트 구성에 대한 자세한 내용은 대시보드를 참조하세요.

이미지: 트레이스 수 차트와 사용 가능한 탭을 보여주는 LangSmith UI의 모니터링 페이지

A/B 테스트

참고: 그룹화(Group-by) 기능에는 특정 메타데이터 키에 대해 서로 다른 값이 최소 2개 이상 있어야 해요.

llm 메타데이터 속성을 기록해 왔으므로, 모니터링 차트를 해당 속성으로 그룹화해서 모델 성능을 시간에 따라 비교할 수 있어요. UI 사이드바의 Monitoring에서 왼쪽 상단의 Group by를 클릭하고, 드롭다운에서 Metadata를 선택한 다음 llm을 선택하세요. 차트가 해당 속성별로 그룹화된 결과를 표시하도록 업데이트돼요. 그룹화와 커스텀 차트에 대한 자세한 내용은 대시보드를 참조하세요.

드릴다운(Down) 조사

모니터링 차트에 예상치 못한 결과가 보이면 데이터 포인트를 클릭해서 툴팁을 고정한 다음, 지표 이름(예: Input)을 클릭해서 해당 시간 구간의 필터링된 런 테이블로 이동하세요. 런 검색과 필터링에 대한 자세한 내용은 트레이스 필터링을 참조하세요.

이미지: Input Tokens 차트의 특정 지점이 강조 표시된 LangSmith UI의 모니터링 페이지

결론

이 튜토리얼에서는 애플리케이션의 전체 개발 수명주기에 걸쳐 LangSmith 관측성을 추가했어요. 프로토타이핑 중 빠른 반복을 돕던 것과 동일한 트레이싱 설정이 프로덕션에서도 계속 가치를 제공해요. 개별 트레이스와 전체 성능 추세를 모두 파악할 수 있게 돼요.

더 자세한 내용은 다음을 참조하세요:

더 알아보기 (Learn more)