> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreweave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 퀵스타트: 맞춤형 에이전트 관측성 설정

> Agent Lens로 맞춤형 에이전트를 계측하여 기존 OTel span을 캡처합니다.

CoreWeave Forge SDK를 사용하면 널리 쓰이는 SDK나 맞춤형 하니스로 구축한 에이전트를 트레이스할 수 있습니다. 이 퀵스타트에서는 직접 구축한 멀티턴 에이전트에 CoreWeave Agent Lens를 수동으로 통합하여 OpenTelemetry span을 내보내고 캡처하는 방법을 알아봅니다. 에이전트용 Agent Lens의 개념은 [에이전트 트레이스](/ko/products/agent-lens/tracing/instrument)을 참조하세요.

Claude Agent SDK나 Codex 같은 SDK 또는 하니스와 Agent Lens를 통합하려면 [에이전트 인테그레이션 선택](/ko/products/agent-lens/get-started/integrations)을 참조하세요. Agent Lens는 여러 에이전트 구축용 SDK와 에이전트 하니스를 자동 패치하므로 빠르게 인테그레이션할 수 있습니다.

<h2 id="what-youll-learn">
  학습 내용
</h2>

이 퀵스타트를 마치면 Agent Lens와 호환되는 OTel span을 내보내는 멀티턴 에이전트를 직접 실행할 수 있습니다. 또한 Agent Lens가 대화, 턴, LLM Call, 도구 Call을 에이전트 코드에 어떻게 매핑하는지 이해하게 되므로, 직접 만든 맞춤형 에이전트에도 같은 패턴을 적용할 수 있습니다.

이 가이드의 코드는 Wikipedia에서 정보를 찾아볼 수 있는 간단한 Python 또는 TypeScript 리서치 에이전트를 구성합니다. 이 에이전트는 세 가지 질문(턴 3개)을 던지고, 답을 찾기 위해 언제 Wikipedia를 검색할지는 LLM이 판단합니다. Agent Lens는 대화, 각 질문, 각 AI 응답, 각 Wikipedia 조회 등 모든 단계를 기록하므로 Agent Lens의 Conversations 탭에서 어떤 일이 일어났는지 확인할 수 있습니다.

이 가이드에서는 다음 내용을 다룹니다.

* `tracing.init()`으로 에이전트 트레이싱을 위해 Agent Lens를 초기화합니다.
* `start_conversation` / `startConversation` 및 `start_turn` / `startTurn`으로 대화와 턴을 시작합니다.
* `start_llm` / `startLLM`으로 LLM Call을 래핑하고 사용량을 기록합니다.
* `start_tool` / `startTool`로 도구 실행을 래핑하고 결과를 기록합니다.
* 토큰 수와 비용이 표시되도록 전체 토큰 사용량과 가격을 산정할 수 있는 모델을 기록합니다.
* Conversations 탭에서 생성된 대화, 턴, 도구 Call을 확인합니다.

<h2 id="how-the-agent-lens-sdk-works-with-agents">
  Agent Lens SDK가 에이전트와 함께 작동하는 방식
</h2>

Agent Lens SDK에는 에이전트용 범용 OTel 수집 시스템이 포함되어 있습니다. 즉, Agent Lens는 에이전트 코드의 모든 OTel span에서 정보를 캡처할 수 있습니다. 다만 Agent Lens UI의 Conversations 탭에 에이전트의 트레이스를 렌더링하려면 다음 span을 별도로 처리해야 합니다.

| 개념 | Python | TypeScript | OTel span |
| - | - | - | - |
| 하나의 대화 | `tracing.start_conversation(...)` | `tracing.startConversation(...)` | (span 없음, 턴을 그룹화) |
| 사용자 또는 에이전트 간 한 차례의 주고받기 | `tracing.start_turn(...)` | `tracing.startTurn(...)` | `invoke_agent` |
| 한 번의 LLM API 호출 | `tracing.start_llm(...)` | `tracing.startLLM(...)` | `chat` |
| 한 번의 도구 실행 | `tracing.start_tool(...)` | `tracing.startTool(...)` | `execute_tool` |

Python에서는 네 가지 함수 모두 컨텍스트 관리자(`with tracing.start_*(...) as obj:`)로 사용할 수 있습니다. 블록을 벗어나면 예외가 발생한 경우에도 span을 종료하고 속성을 플러시합니다. TypeScript에서는 반환된 각 객체에서 `.end()`를 호출하세요. 예외가 발생해도 정리 작업이 확실히 수행되도록 `try { ... } finally { obj.end(); }`를 사용하고, 동시에 실행되는 run이 대화를 공유하지 않도록 각 에이전트 run을 `tracing.runIsolated()`로 래핑하세요.

`gen_ai.usage.*`, `gen_ai.agent.name` 등 그 밖의 [GenAI 시맨틱 규칙 속성](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/)을 사용하면 추가 정보를 렌더링할 수 있지만, 필수는 아닙니다.

<h2 id="prerequisites">
  사전 요구 사항
</h2>

* CoreWeave Forge 계정 및 [API 키](https://forge.coreweave.com/settings#apikeys)
* OpenAI API 키
* Python 3.9 이상(Python 예시 실행 시)
* Node.js 18 이상 및 `tsx` 등의 TypeScript 러너(TypeScript 예시는 내장 `fetch`가 필요하며, 일반 JavaScript로는 실행할 수 없습니다)

<h2 id="install-packages">
  패키지 설치
</h2>

개발 환경에 다음 패키지를 설치하세요.

<CodeGroup>
  ```bash Python theme={"system"}
  pip install coreweave openai requests
  ```

  ```bash TypeScript theme={"system"}
  npm install @coreweave/forge-sdk openai
  npm install --save-dev tsx
  ```
</CodeGroup>

TypeScript 예시는 `.mts` 파일로 저장한 뒤 `npx tsx [FILENAME].mts` 명령으로 실행하세요.

<h2 id="initialize-agent-lens">
  Agent Lens 초기화
</h2>

`tracing.init()`은 API 키로 인증하고, 에이전트 span을 Agent Lens로 전송하는 OTel 익스포터를 설정합니다. 프로젝트 이름에는 팀 이름이 포함되어야 합니다. SDK는 `WANDB_API_KEY` 환경 변수에서 API 키를 읽으며, `api_key` / `apiKey` 인수로 직접 전달할 수도 있습니다.

<CodeGroup>
  ```python lines Python theme={"system"}
  import getpass
  import os

  os.environ["WANDB_API_KEY"] = getpass.getpass("Enter your API key: ")
  os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")

  TEAM = input("Enter your team name: ")
  PROJECT = input("Enter your project name: ")

  from coreweave.forge.agentlens import tracing
  tracing.init(f"{TEAM}/{PROJECT}", autopatch_integrations=False)
  ```

  ```typescript lines highlight="" TypeScript theme={"system"}
  // 이 프로젝트를 실행하기 전에 환경에 WANDB_API_KEY와 OPENAI_API_KEY를 설정하세요
  import { tracing } from '@coreweave/forge-sdk/agentlens';

  await tracing.init('[YOUR-TEAM]/[YOUR-PROJECT]');
  ```
</CodeGroup>

<h2 id="define-a-tool">
  도구 정의하기
</h2>

다음 코드는 에이전트의 Wikipedia 검색 도구와, 이 도구를 언제 어떻게 사용할지 지정하는 OpenAI 도구 스키마를 정의합니다.

<CodeGroup>
  ```python lines Python theme={"system"}
  import json
  import requests

  def wikipedia_search(query: str) -> str:
      r = requests.get(
          "https://en.wikipedia.org/w/api.php",
          params={
              "action": "query", "generator": "search", "gsrsearch": query, "gsrlimit": 1,
              "prop": "extracts", "exintro": True, "explaintext": True, "format": "json",
          },
          headers={"User-Agent": "agent-lens-demo"},
      ).json()
      return next(iter(r["query"]["pages"].values()))["extract"]

  wikipedia_tool_schema = {
      "type": "function",
      "function": {
          "name": "wikipedia_search",
          "description": "Search Wikipedia for a topic and return its intro paragraph.",
          "parameters": {
              "type": "object",
              "properties": {"query": {"type": "string"}},
              "required": ["query"],
          },
      },
  }
  ```

  ```typescript lines TypeScript theme={"system"}
  async function wikipediaSearch(query: string): Promise<string> {
    const url = new URL('https://en.wikipedia.org/w/api.php');
    url.search = new URLSearchParams({
      action: 'query',
      generator: 'search',
      gsrsearch: query,
      gsrlimit: '1',
      prop: 'extracts',
      exintro: 'true',
      explaintext: 'true',
      format: 'json',
    }).toString();
    const res = await fetch(url, { headers: { 'User-Agent': 'agent-lens-demo' } });
    const data = (await res.json()) as {
      query: { pages: Record<string, { extract: string }> };
    };
    return Object.values(data.query.pages)[0].extract;
  }

  const wikipediaToolSchema = {
    type: 'function' as const,
    function: {
      name: 'wikipedia_search',
      description: 'Search Wikipedia for a topic and return its intro paragraph.',
      parameters: {
        type: 'object',
        properties: { query: { type: 'string' } },
        required: ['query'],
      },
    },
  };
  ```
</CodeGroup>

<h2 id="run-a-traced-multi-turn-agent">
  트레이스되는 멀티턴 에이전트 실행하기
</h2>

도구와 Agent Lens 초기화가 준비되었으면, 다음 단계에서는 이 둘을 결합해 완전한 에이전트 루프를 구성합니다. 이 루프를 통해 대화, 턴, LLM Call, 도구 Call이 어떻게 중첩되는지 확인할 수 있습니다.

다음 예시는 하나의 대화에서 세 개의 턴을 실행합니다. 각 턴은 다음과 같이 동작합니다.

1. `chat` span을 열고 LLM이 도구 호출 여부를 결정하도록 합니다.
2. LLM이 도구를 요청하면 해당 호출을 감싸는 `execute_tool` span을 열고 결과를 LLM에 다시 전달합니다.
3. 두 번째 `chat` span을 열어 최종 답변을 생성합니다.

<Note>
  Agent Lens SDK는 OpenAI, Anthropic, Google Gen AI 클라이언트 라이브러리로 수행한 Call을 자동으로 트레이스합니다. 이 퀵스타트에서는 span이 어떻게 맞물리는지 보여주기 위해 `start_llm()`으로 각 LLM Call을 직접 기록하므로, `init()`에 `autopatch_integrations=False`를 전달합니다. 이 인수를 전달하지 않으면 각 Call이 `start_llm()` span과 자동 인테그레이션에서 한 번씩, 총 두 번 기록됩니다. 이 인수는 반드시 첫 번째 `init()` 호출에 전달하세요. 이전 호출에서 적용된 패치는 나중에 `autopatch_integrations=False`로 다시 호출해도 제거되지 않습니다. 실제 코드에서는 자동 패치가 LLM Call을 기록하도록 두거나, 자동 패치를 끄고 직접 기록하세요.
</Note>

<CodeGroup>
  ```python lines Python theme={"system"}
  from coreweave.forge.agentlens import tracing
  from openai import OpenAI

  openai_client = OpenAI()
  MODEL = "gpt-4o-mini"

  def run_turn(history, user_message):
      history.append({"role": "user", "content": user_message})

      with tracing.start_turn(user_message=user_message, model=MODEL):
          # LLM Call 1: 모델이 도구를 사용하기로 결정할 수 있습니다.
          with tracing.start_llm(model=MODEL, provider_name="openai") as llm:
              resp = openai_client.chat.completions.create(
                  model=MODEL, messages=history, tools=[wikipedia_tool_schema],
              )
              msg = resp.choices[0].message
              llm.output(msg.content or "")
              # record()는 사용량, 과금 기준 모델, 응답 ID를 한 번의 호출로 설정합니다.
              llm.record(
                  usage=tracing.Usage(
                      input_tokens=resp.usage.prompt_tokens,
                      output_tokens=resp.usage.completion_tokens,
                      cache_read_input_tokens=getattr(
                          resp.usage.prompt_tokens_details, "cached_tokens", 0
                      ),
                  ),
                  response_id=resp.id,
                  response_model=resp.model,
              )
              history.append(msg.model_dump(exclude_none=True))

          # 도구 요청이 없으면 첫 번째 LLM 응답이 최종 답변이 됩니다.
          if not msg.tool_calls:
              return msg.content

          # 요청된 도구 Call을 각각 실행합니다.
          for tc in msg.tool_calls:
              with tracing.start_tool(
                  name=tc.function.name,
                  arguments=tc.function.arguments,
                  tool_call_id=tc.id,
              ) as tool:
                  tool.result = wikipedia_search(**json.loads(tc.function.arguments))
                  history.append({
                      "role": "tool",
                      "tool_call_id": tc.id,
                      "content": tool.result,
                  })

          # LLM Call 2: 최종 답변을 생성합니다.
          with tracing.start_llm(model=MODEL, provider_name="openai") as llm:
              resp = openai_client.chat.completions.create(model=MODEL, messages=history)
              msg = resp.choices[0].message
              llm.output(msg.content)
              llm.record(
                  usage=tracing.Usage(
                      input_tokens=resp.usage.prompt_tokens,
                      output_tokens=resp.usage.completion_tokens,
                      cache_read_input_tokens=getattr(
                          resp.usage.prompt_tokens_details, "cached_tokens", 0
                      ),
                  ),
                  response_id=resp.id,
                  response_model=resp.model,
              )
              history.append({"role": "assistant", "content": msg.content})
              return msg.content

  tracing.init(f"{TEAM}/{PROJECT}", autopatch_integrations=False)

  with tracing.start_conversation(agent_name="research-bot") as conversation:
      history = []
      for question in [
          "Who founded Anthropic?",
          "What is Claude (the AI assistant)?",
          "Summarize what we discussed in one sentence.",
      ]:
          print(f"USER: {question}")
          print(f"AGENT: {run_turn(history, question)}\n")

  tracing.shutdown()
  ```

  ```typescript lines TypeScript theme={"system"}
  import { tracing } from '@coreweave/forge-sdk/agentlens';
  import OpenAI from 'openai';

  const openaiClient = new OpenAI();
  const MODEL = 'gpt-4o-mini';

  // history는 OpenAI 채팅 메시지 목록입니다. 간결하게 작성하기 위해 유형을 느슨하게 지정했습니다.
  async function runTurn(history: any[], userMessage: string): Promise<string | null> {
    history.push({ role: 'user', content: userMessage });

    const turn = tracing.startTurn({ userMessage, model: MODEL });
    try {
      // LLM Call 1: 모델이 도구를 사용하기로 결정할 수 있습니다.
      const llm1 = tracing.startLLM({ model: MODEL, providerName: 'openai' });
      let msg;
      try {
        const resp = await openaiClient.chat.completions.create({
          model: MODEL,
          messages: history,
          tools: [wikipediaToolSchema],
        });
        msg = resp.choices[0].message;
        llm1.output(msg.content ?? '');
        // record()는 사용량, 과금 기준 모델, 응답 ID를 한 번의 호출로 설정합니다.
        llm1.record({
          usage: {
            inputTokens: resp.usage?.prompt_tokens,
            outputTokens: resp.usage?.completion_tokens,
            cacheReadInputTokens: resp.usage?.prompt_tokens_details?.cached_tokens,
          },
          responseId: resp.id,
          responseModel: resp.model,
        });
        history.push(msg);
      } finally {
        llm1.end();
      }

      // 도구를 요청하지 않았다면 첫 번째 LLM 응답이 곧 답변입니다.
      if (!msg.tool_calls?.length) {
        return msg.content ?? null;
      }

      // 요청된 각 도구 Call을 실행합니다.
      for (const tc of msg.tool_calls) {
        if (tc.type !== 'function') continue;
        const tool = tracing.startTool({
          name: tc.function.name,
          args: tc.function.arguments,
          toolCallId: tc.id,
        });
        try {
          const { query } = JSON.parse(tc.function.arguments);
          tool.result = await wikipediaSearch(query);
          history.push({ role: 'tool', tool_call_id: tc.id, content: tool.result });
        } finally {
          tool.end();
        }
      }

      // LLM Call 2: 최종 답변을 생성합니다.
      const llm2 = tracing.startLLM({ model: MODEL, providerName: 'openai' });
      try {
        const resp = await openaiClient.chat.completions.create({
          model: MODEL,
          messages: history,
        });
        const msg2 = resp.choices[0].message;
        llm2.output(msg2.content ?? '');
        llm2.record({
          usage: {
            inputTokens: resp.usage?.prompt_tokens,
            outputTokens: resp.usage?.completion_tokens,
            cacheReadInputTokens: resp.usage?.prompt_tokens_details?.cached_tokens,
          },
          responseId: resp.id,
          responseModel: resp.model,
        });
        history.push({ role: 'assistant', content: msg2.content });
        return msg2.content ?? null;
      } finally {
        llm2.end();
      }
    } finally {
      turn.end();
    }
  }

  await tracing.init('[YOUR-TEAM]/[YOUR-PROJECT]');

  await tracing.runIsolated(async () => {
    const conversation = tracing.startConversation({ agentName: 'research-bot' });
    try {
      const history: any[] = [];
      for (const question of [
        'Who founded Anthropic?',
        'What is Claude (the AI assistant)?',
        'Summarize what we discussed in one sentence.',
      ]) {
        console.log(`USER: ${question}`);
        console.log(`AGENT: ${await runTurn(history, question)}\n`);
      }
    } finally {
      conversation.end();
    }
  });

  await tracing.shutdown();
  ```
</CodeGroup>

<h2 id="record-token-usage-and-cost">
  토큰 사용량 및 비용 기록
</h2>

각 `chat` span에는 토큰 사용량과 모델 ID가 포함됩니다. Agent Lens는 사용량을 바탕으로 토큰 수를 표시하고, 사용량과 모델 ID를 함께 사용해 비용을 산출합니다. 따라서 값이 불완전하거나 가격을 책정할 수 없는 값이면 트레이스의 나머지 부분이 정상으로 보이더라도 토큰이 `0 in / 0 out`으로, 비용이 `Cost -`로 표시됩니다. `record(...)`를 사용하면 이러한 필드(`output_messages`, `response_id`, `reasoning` 등 포함)를 한 번의 호출로 설정할 수 있습니다. 전달한 필드만 적용됩니다.

비용이 표시되려면 다음 두 가지가 올바르게 설정되어야 합니다.

* **완전한 사용량.** `input_tokens`는 캐시된 토큰을 포함한 *전체* 입력입니다. Agent Lens는 캐시 읽기와 캐시 쓰기에 각각 별도의 요율을 적용하고 이를 입력 합계에서 차감합니다. 따라서 `cache_read_input_tokens`와 `cache_creation_input_tokens`는 이들을 포함한 전체 `input_tokens`와 *함께* 보고해야 합니다. 프롬프트 캐싱을 지원하는 공급자(예: Anthropic)에서는 캐시된 토큰이 입력의 대부분을 차지하는 경우가 흔하므로, 이를 누락하면 사용량과 비용이 거의 0으로 표시됩니다.
* **가격 책정이 가능한 모델 ID.** 응답에서 반환된 구체적인 ID(`resp.model`)를 `response_model`로 전달하세요. 비용은 모델을 기준으로 조회됩니다. Agent Lens는 `response_model`(공급자가 실제로 서빙한 정확한 모델)을 우선 사용하고, 없으면 `start_llm`에 전달한 `model`을 사용합니다. `opus`나 `sonnet` 같은 별칭으로는 가격을 책정할 수 없으므로 `Cost -`로 표시됩니다.

OpenAI는 캐시된 토큰을 `prompt_tokens`에 포함해 집계하므로 위 예시를 그대로 적용할 수 있습니다. 반면 Anthropic은 캐시된 토큰을 `input_tokens`와 *별도로* 보고하므로, Agent Lens가 가격 산정에 사용하는 합계에 이를 다시 더해야 합니다.

<CodeGroup>
  ```python lines Python theme={"system"}
  with tracing.start_llm(model=MODEL, provider_name="anthropic") as llm:
      resp = anthropic_client.messages.create(
          model=MODEL, max_tokens=1024, messages=history,
      )
      u = resp.usage
      llm.output(resp.content[0].text)
      llm.record(
          usage=tracing.Usage(
              input_tokens=u.input_tokens
              + u.cache_read_input_tokens
              + u.cache_creation_input_tokens,
              output_tokens=u.output_tokens,
              cache_read_input_tokens=u.cache_read_input_tokens,
              cache_creation_input_tokens=u.cache_creation_input_tokens,
          ),
          response_id=resp.id,
          response_model=resp.model,
      )
  ```

  ```typescript lines TypeScript theme={"system"}
  const u = resp.usage;
  llm.record({
    usage: {
      inputTokens:
        u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens,
      outputTokens: u.output_tokens,
      cacheReadInputTokens: u.cache_read_input_tokens,
      cacheCreationInputTokens: u.cache_creation_input_tokens,
    },
    responseId: resp.id,
    responseModel: resp.model,
  });
  ```
</CodeGroup>

<h2 id="see-your-agent-traces-in-the-conversations-tab">
  Conversations 탭에서 에이전트 트레이스 확인하기
</h2>

Agent Lens에서 프로젝트를 열고 **Conversations**를 선택하세요. 다음 내용을 확인할 수 있습니다.

* 턴 세 개로 구성된 `research-bot`의 대화 하나
* 각 턴(`invoke_agent`)에 포함된 `chat` span 두 개와 그 안에 중첩된 `execute_tool` span 하나
* 각 `chat`의 토큰 수, 지연 시간, 모델, 전체 메시지 교환 내역

대화를 선택하면 **Thread** 및 **Spans** 탭에서 입력, 출력, 도구 인수, 도구 결과를 자세히 살펴볼 수 있습니다.

<h2 id="link-to-a-conversation-from-your-app">
  앱에서 대화로 연결하기
</h2>

자체 UI에서 Agent Lens의 대화로 바로 이동하는 딥 링크를 만들려면 대화 ID가 필요합니다. `start_conversation` / `startConversation`은 이 ID를 `conversation_id` / `conversationId`로 제공합니다. 이 ID를 로깅하거나 자체 요청 ID와 함께 저장해 두면 나중에 **Conversations** 탭에서 해당 대화를 열 수 있습니다.

<CodeGroup>
  ```python lines Python theme={"system"}
  with tracing.start_conversation(agent_name="research-bot") as conversation:
      # ... 턴 실행 ...
      print(f"Agent Lens conversation ID: {conversation.conversation_id}")
  ```

  ```typescript lines TypeScript theme={"system"}
  await tracing.runIsolated(async () => {
    const conversation = tracing.startConversation({ agentName: 'research-bot' });
    try {
      // ... 턴 실행 ...
      console.log(`Agent Lens conversation ID: ${conversation.conversationId}`);
    } finally {
      conversation.end();
    }
  });
  ```
</CodeGroup>

<h2 id="next-steps">
  다음 단계
</h2>

* [Agent Lens로 에이전트를 트레이스하는 방법](/ko/products/agent-lens/tracing/instrument)과 Agent Lens SDK에서 사용할 수 있는 기능 및 옵션을 알아보세요.
* Agent Lens를 에이전트와 통합하는 다른 방법은 [에이전트 인테그레이션 선택하기](/ko/products/agent-lens/get-started/integrations)를 참조하세요.
