LLM 호출 기록하기
LLM 호출 기록하기 (Log LLM calls)
LangChain 또는 LangSmith 지원 통합 밖에서 LLM을 직접 호출할 때는 LangSmith가 토큰 수를 표시하고, 비용을 계산하며, 올바른 프로바이더와 모델로 Playground에서 실행(run)을 열 수 있도록 특정 메타데이터를 제공해야 해요.
완전한 기능을 갖춘 LLM 추적에는 네 가지 요구사항이 있어요:
| 요구사항 | 해야 할 일 | 제공 기능 |
|---|---|---|
1. run_type="llm" 설정 |
@traceable에 run_type="llm" 전달 |
LLM 전용 렌더링, 토큰/비용 표시 |
| 2. 입력/출력 형식화 | OpenAI, Anthropic 또는 LangChain 메시지 형식 사용 | 구조화된 메시지 렌더링, Playground 지원 |
3. ls_provider 및 ls_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|toolcontent(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와 일치해야 해요.role이tool일 때만 유효해요.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/processInputs와 process_outputs/processOutputs 함수를 사용해 LangSmith 호환 형식으로 변환할 수 있어요.
process_inputs/processInputs와 process_outputs/processOutputs는 특정 추적의 입력과 출력을 LangSmith에 기록되기 전에 변환할 수 있는 함수를 받아들여요. 추적의 입력과 출력에 접근할 수 있고 처리된 데이터를 담은 새 딕셔너리를 반환할 수 있어요.
커스텀 I/O 형식을 LangSmith 호환 형식으로 변환하기 위해 process_inputs와 process_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가 모델을 식별하고 비용을 계산하려면
metadata에ls_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();
관련 문서 (Related)
- 커스텀 계측: 핵심
@traceable및RunTree패턴. - 추적된 함수 내에서 현재 실행(스팬) 접근하기:
get_current_run_tree()로 런타임에usage_metadata및 기타 필드 설정. - OpenAI 애플리케이션 추적: OpenAI 래퍼 사용 시 자동 토큰·비용 추적.
- Anthropic 애플리케이션 추적: Anthropic 래퍼 사용 시 자동 토큰·비용 추적.
- 통합 개요: 내장 LangSmith 지원을 제공하는 프로바이더·프레임워크 전체 목록.
출처: 문서