LLM 호출 기록하기

LLM 호출 기록하기 (Log LLM calls)

LangChain 또는 LangSmith 지원 통합 밖에서 LLM을 직접 호출할 때는 LangSmith가 토큰 수를 표시하고, 비용을 계산하며, 올바른 프로바이더와 모델로 Playground에서 실행(run)을 열 수 있도록 특정 메타데이터를 제공해야 해요.

완전한 기능을 갖춘 LLM 추적에는 네 가지 요구사항이 있어요:

요구사항 해야 할 일 제공 기능
1. run_type="llm" 설정 @traceablerun_type="llm" 전달 LLM 전용 렌더링, 토큰/비용 표시
2. 입력/출력 형식화 OpenAI, Anthropic 또는 LangChain 메시지 형식 사용 구조화된 메시지 렌더링, Playground 지원
3. ls_providerls_model_name 설정 둘 다 metadata로 전달 비용 추적, Playground 모델 선택
4. 토큰 수 제공 실행에 usage_metadata 설정 토큰 수 및 비용 계산

참고: LangChain OSS, OpenAI 래퍼 또는 Anthropic 래퍼를 사용 중이라면 이러한 세부 사항이 자동으로 처리돼요.

이 페이지의 예시는 traceable 데코레이터/래퍼(권장 방식, Python 및 JS/TS)를 사용해요. RunTree 또는 API를 직접 사용한다면 동일한 요구사항이 적용돼요.

메시지 형식 (Messages format)

커스텀 모델 또는 커스텀 입출력 형식을 추적할 때는 LangChain 형식, OpenAI completions 형식 또는 Anthropic messages 형식 중 하나를 따라야 해요. 자세한 내용은 OpenAI Chat Completions 또는 Anthropic Messages 문서를 참고하세요. LangChain 형식은 다음과 같아요:

  • messages (array, 필수): 대화 내용을 담은 메시지 목록.
    • role (string, 필수): 메시지 유형. 다음 중 하나: system | reasoning | user | assistant | tool
    • content (array, 필수): 메시지 내용. 타입이 지정된 딕셔너리 목록.
      • type (string, 필수): 다음 중 하나: text | image | file | audio | video | tool_call | server_tool_call | server_tool_result.
      • text: type=text(필수), text(string, 필수): 텍스트 내용. annotations(object[]): 텍스트 주석 목록. extras(object): 추가 프로바이더별 데이터.
      • reasoning: type=reasoning(필수), text(string, 필수): 텍스트 내용. extras(object): 추가 프로바이더별 데이터.
      • image: type=image(필수), url(string): 이미지 위치 URL. base64(string, 필수): Base64 인코딩된 이미지 데이터. id(string): 외부 저장 이미지(예: 프로바이더 파일 시스템 또는 버킷)의 참조 ID. mime_type(string): 이미지 MIME 타입 (예: image/jpeg, image/png).
      • file (예: PDF): type=file(필수), url(string): 파일 위치 URL. base64(string, 필수): Base64 인코딩 파일 데이터. id(string): 외부 저장 파일 참조 ID. mime_type(string): 파일 MIME 타입 (예: application/pdf).
      • audio: type=audio(필수), url(string): 오디오 파일 URL. base64(string, 필수): Base64 인코딩 오디오 데이터. id(string): 외부 저장 오디오 파일 참조 ID. mime_type(string): 오디오 MIME 타입 (예: audio/mpeg, audio/wav).
      • video: type=video(필수), url(string): 비디오 파일 URL. base64(string, 필수): Base64 인코딩 비디오 데이터. id(string): 외부 저장 비디오 파일 참조 ID. mime_type(string): 비디오 MIME 타입 (예: video/mp4, video/webm).
      • tool_call: type=tool_call(필수), name(string), args(object, 필수): 도구에 전달할 인자. id(string): 이 도구 호출의 고유 식별자.
      • server_tool_call: type=server_tool_call(필수), id(string, 필수): 이 도구 호출의 고유 식별자. name(string, 필수): 호출할 도구 이름. args(object, 필수): 도구에 전달할 인자.
      • server_tool_result: type=server_tool_result(필수), tool_call_id(string, 필수): 해당 서버 도구 호출의 식별자. id(string): 이 도구 호출의 고유 식별자. status(string, 필수): 서버측 도구의 실행 상태. 다음 중 하나: success | error. output: 실행된 도구의 출력.
    • tool_call_id (string): 이전 assistant 메시지의 tool_calls[i] 항목 id와 일치해야 해요. roletool일 때만 유효해요.
    • usage_metadata (object): 이 필드를 사용해 모델 출력과 함께 토큰 수 및/또는 비용을 보내요. 자세한 내용은 토큰 및 비용 정보 제공하기를 참고하세요.

텍스트 및 추론 (Text and reasoning):

 inputs = {
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Hi, can you tell me the capital of France?"
        }
      ]
    }
  ]
}

outputs = {
  "messages": [
    {
      "role": "assistant",
      "content": [
        {
          "type": "text",
          "text": "The capital of France is Paris."
        },
        {
          "type": "reasoning",
          "text": "The user is asking about..."
        }
      ]
    }
  ]
}

도구 호출 (Tool calls):

input = {
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "What's the weather in San Francisco?"
        }
      ]
    }
  ]
}

outputs = {
  "messages": [
    {
      "role": "assistant",
      "content": [{"type": "tool_call", "name": "get_weather", "args": {"city": "San Francisco"}, "id": "call_1"}],
    },
    {
      "role": "tool",
      "tool_call_id": "call_1",
      "content": [
        {
          "type": "text",
          "text": "{\"temperature\": \"18°C\", \"condition\": \"Sunny\"}"
        }
      ]
    },
    {
      "role": "assistant",
      "content": [
        {
          "type": "text",
          "text": "The weather in San Francisco is 18°C and sunny."
        }
      ]
    }
  ]
}

멀티모달 (Multimodal):

inputs = {
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "What breed is this dog?"
        },
        {
          "type": "image",
          "url": "https://fastly.picsum.photos/id/237/200/300.jpg?hmac=TmmQSbShHz9CdQm0NkEjx1Dyh_Y984R9LpNrpvH2D_U",
          # alternative to a url, you can provide a base64 encoded image
          # "base64": "<base64 encoded image>",
          "mime_type": "image/jpeg",
        }
      ]
    }
  ]
}

outputs = {
  "messages": [
    {
      "role": "assistant",
      "content": [
        {
          "type": "text",
          "text": "This looks like a Black Labrador."
        }
      ]
    }
  ]
}

서버측 도구 호출 (Server-side tool calls):

input = {
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "What is the price of AAPL?"
        }
      ]
    }
  ]
}

output = {
  "messages": [
    {
      "role": "assistant",
      "content": [
        {
          "type": "server_tool_call",
          "name": "web_search",
          "args": {
            "query": "price of AAPL",
            "type": "search"
          },
          "id": "call_1"
        },
        {
          "type": "server_tool_result",
          "tool_call_id": "call_1",
          "status": "success"
        },
        {
          "type": "text",
          "text": "The price of AAPL is $150.00"
        }
      ]
    }
  ]
}

커스텀 I/O 형식을 LangSmith 호환 형식으로 변환하기 (Convert custom I/O formats into LangSmith compatible formats)

커스텀 입력 또는 출력 형식을 사용한다면 @traceable 데코레이터(Python) 또는 traceable 함수(TS)의 process_inputs/processInputsprocess_outputs/processOutputs 함수를 사용해 LangSmith 호환 형식으로 변환할 수 있어요.

process_inputs/processInputsprocess_outputs/processOutputs는 특정 추적의 입력과 출력을 LangSmith에 기록되기 전에 변환할 수 있는 함수를 받아들여요. 추적의 입력과 출력에 접근할 수 있고 처리된 데이터를 담은 새 딕셔너리를 반환할 수 있어요.

커스텀 I/O 형식을 LangSmith 호환 형식으로 변환하기 위해 process_inputsprocess_outputs를 사용하는 템플릿 예시는 다음과 같아요:

class OriginalInputs(BaseModel):
    """Your app's custom request shape"""

class OriginalOutputs(BaseModel):
    """Your app's custom response shape."""

class LangSmithInputs(BaseModel):
    """The input format LangSmith expects."""

class LangSmithOutputs(BaseModel):
    """The output format LangSmith expects."""

def process_inputs(inputs: dict) -> dict:
    """Dict -> OriginalInputs -> LangSmithInputs -> dict"""

def process_outputs(output: Any) -> dict:
    """OriginalOutputs -> LangSmithOutputs -> dict"""


@traceable(run_type="llm", process_inputs=process_inputs, process_outputs=process_outputs)
def chat_model(inputs: dict) -> dict:
    """
    Your app's model call. Keeps your custom I/O shape.
    The decorators call process_* to log LangSmith-compatible format.
    """

추적에서 커스텀 모델 식별하기 (Identify a custom model in traces)

커스텀 모델을 사용할 때는 추적을 보고추적을 필터링할 때 모델을 식별할 수 있도록 다음 metadata 필드도 제공하는 것이 권장돼요.

  • ls_provider: 모델의 프로바이더, 예: "openai", "anthropic".
  • ls_model_name: 모델의 이름, 예: "gpt-5.4-mini", "claude-opus-4-8".
from langsmith import traceable

inputs = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "I'd like to book a table for two."},
]
output = {
    "choices": [
        {
            "message": {
                "role": "assistant",
                "content": "Sure, what time would you like to book the table for?"
            }
        }
    ]
}

@traceable(
    run_type="llm",
    metadata={"ls_provider": "my_provider", "ls_model_name": "my_model"}
)
def chat_model(messages: list):
    return output

chat_model(inputs)
import { traceable } from "langsmith/traceable";

const messages = [
    { role: "system", content: "You are a helpful assistant." },
    { role: "user", content: "I'd like to book a table for two." }
];
const output = {
    choices: [
        {
            message: {
                role: "assistant",
                content: "Sure, what time would you like to book the table for?",
            },
        },
    ],
    usage_metadata: {
        input_tokens: 27,
        output_tokens: 13,
        total_tokens: 40,
    },
};

// Can also use one of:
// const output = {
//     message: {
//         role: "assistant",
//         content: "Sure, what time would you like to book the table for?"
//     }
// };
//
// const output = {
//     role: "assistant",
//     content: "Sure, what time would you like to book the table for?"
// };
//
// const output = ["assistant", "Sure, what time would you like to book the table for?"];

const chatModel = traceable(
    async ({ messages }: { messages: { role: string; content: string }[] }) => {
        return output;
    },
    {
        run_type: "llm",
        name: "chat_model",
        metadata: {
            ls_provider: "my_provider",
            ls_model_name: "my_model"
        }
    }
);

await chatModel({ messages });

커스텀 스트리밍 chat_model을 구현한다면 출력을 비스트리밍 버전과 동일한 형식으로 "reduce"할 수 있어요. 이는 Python에서만 지원돼요:

def _reduce_chunks(chunks: list):
    all_text = "".join([chunk["choices"][0]["message"]["content"] for chunk in chunks])
    return {"choices": [{"message": {"content": all_text, "role": "assistant"}}]}

@traceable(
    run_type="llm",
    reduce_fn=_reduce_chunks,
    metadata={"ls_provider": "my_provider", "ls_model_name": "my_model"}
)
def my_streaming_chat_model(messages: list):
    for chunk in ["Hello, " + messages[1]["content"]]:
        yield {
            "choices": [
                {
                    "message": {
                        "content": chunk,
                        "role": "assistant",
                    }
                }
            ]
        }

list(
    my_streaming_chat_model(
        [
            {"role": "system", "content": "You are a helpful assistant. Please greet the user."},
            {"role": "user", "content": "assistant"},
        ],
    )
)

확인: 커스텀 LLM 추적에서 LangSmith가 모델을 식별하고 비용을 계산하려면 metadatals_model_name을 설정하는 것이 필요해요. 없으면 토큰 수는 기록될 수 있어도 비용은 추정되지 않아요.

metadata 필드 사용 방법에 대한 자세한 내용은 메타데이터 및 태그 추가 가이드를 참고하세요. 커스텀 에이전트 실행이 Trajectory 보기에 나타나는 방식을 사용자 지정하려면 Trajectory 보기 사용자 지정을 참고하세요.

토큰 및 비용 정보 제공하기 (Provide token and cost information)

토큰 수는 LangSmith가 Tracing Projects UI에 표시하는 비용 계산을 가능하게 해요. 두 가지 방법으로 제공할 수 있어요:

  • 실행 트리에 usage_metadata 설정하기: @traceable 함수 안에서 get_current_run_tree() / getCurrentRunTree()를 호출하고 usage_metadata 필드를 설정하세요. 이는 함수의 반환 값을 바꾸지 않아요.
  • 출력에 usage_metadata 반환하기: 함수가 반환하는 딕셔너리의 최상위 키로 usage_metadata를 포함하세요.

지원되는 usage_metadata 필드

필드 타입 설명
input_tokens int 총 입력/프롬프트 토큰
output_tokens int 총 출력/완성 토큰
total_tokens int 입력 + 출력의 합 (선택, 추론 가능)
input_token_details object 세부 구분: cache_read, cache_creation, cache_read_over_200k, ephemeral_5m_input_tokens, ephemeral_1h_input_tokens, audio, text, image
output_token_details object 세부 구분: reasoning, audio, text, image

비용을 직접 보내려면(비선형 가격) input_cost, output_cost, total_cost 필드도 포함할 수 있어요. 모델 가격 구성 및 UI에서 비용 보기 상세는 비용 추적( Cost tracking) 페이지를 참고하세요.

첫 토큰까지 걸리는 시간 (Time-to-first-token)

traceable 또는 SDK 래퍼 중 하나를 사용한다면 LangSmith가 스트리밍 LLM 실행의 first-token 시간을 자동으로 채워요. 그러나 RunTree API를 직접 사용한다면 first-token 시간을 올바르게 채우기 위해 실행 트리에 new_token 이벤트를 추가해야 해요.

예시는 다음과 같아요:

from langsmith.run_trees import RunTree
run_tree = RunTree(
    name="CustomChatModel",
    run_type="llm",
    inputs={ ... }
)
run_tree.post()
llm_stream = ...
first_token = None
for token in llm_stream:
    if first_token is None:
      first_token = token
      run_tree.add_event({
        "name": "new_token"
      })
run_tree.end(outputs={ ... })
run_tree.patch()
import { RunTree } from "langsmith";
const runTree = new RunTree({
    name: "CustomChatModel",
    run_type: "llm",
    inputs: { ... },
});
await runTree.postRun();
const llmStream = ...;
let firstToken;
for (const token of llmStream) {
    if (firstToken == null) {
        firstToken = token;
        runTree.addEvent({ name: "new_token" });
    }
}
await runTree.end({
    outputs: { ... },
});
await runTree.patchRun();

출처: 문서

더 알아보기 (Learn more)