스레드 트레이스

스레드 트레이스 (Thread Traces)

트레이스를 스레드로 그룹화해서 전체 대화 워크플로를 평가하는 방법을 다루는 페이지예요. Confident AI에서 "스레드"는 공유된 스레드 ID로 연결된 하나 이상의 트레이스 그룹이에요. 채팅봇·멀티턴 에이전트 같은 대화형 AI 앱을 만들 때 전체 대화를 하나의 단위로 보고 평가할 수 있게 해준답니다.

출처: 문서

본문

개요 (Overview)

Confident AI의 "스레드(thread)"는 공유된 스레드 ID로 연결된 하나 이상의 트레이스 그룹이에요. 이것은 대화형 AI 앱 — 채팅봇, 멀티턴 에이전트 등 — 을 만들 때 유용한데, 전체 대화를 하나의 단위로 보고 평가하고 싶기 때문이에요.

앱에 대한 각 호출은 트레이스를 만들고, 같은 스레드 ID를 가진 트레이스들은 대화의 턴(turns)으로 시간 순으로 함께 그룹화돼요.

스레드는 트레이스를 함께 묶는 것이지 스팬을 묶는 게 아니에요. 각 트레이스는 대화의 한 턴을 나타내요.

스레드 만들기 (Create a Thread)

스레드를 만드는 가장 간단한 방법은 앱의 각 턴을 turn()으로 감싸는 것이에요. 그것은 그 턴에 대해 새 트레이스를 시작하고 주어진 스레드 ID로 스탬프를 찍어서, 같은 스레드 ID를 공유하는 트레이스들이 하나의 스레드로 그룹화돼요.

Python

from openai import OpenAI
from confident_trace import init, turn, update_trace, shutdown

init()
client = OpenAI()

def llm_app(query: str, thread_id: str):
    with turn("llm_app", thread_id=thread_id):
        res = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": query}]
        ).choices[0].message.content
        update_trace(input=query, output=res)
        return res

try:
    llm_app("What's the weather in SF?", thread_id="your-thread-id")
    llm_app("What about tomorrow?", thread_id="your-thread-id")
finally:
    shutdown()

TypeScript

import OpenAI from "openai";
import { init, turn, updateTrace } from "confident-trace";

const runtime = init();
const openai = new OpenAI();

const llmApp = async (query: string, threadId: string) => {
  return turn({ threadId }, async () => {
    const res = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: query }],
    });
    const data = res.choices[0].message.content;
    updateTrace({ input: query, output: data });
    return data;
  });
};

try {
  await llmApp("What's the weather in SF?", "your-thread-id");
  await llmApp("What about tomorrow?", "your-thread-id");
} finally {
  await runtime.shutdown();
}

자동 계측된 스팬(위의 OpenAI 호출 같은)이 턴 안에 중첩되도록 Node preload로 진입점을 실행하는 걸 잊지 마세요.

thread_id / threadId는 어떤 문자열이든 될 수 있어요 — 보통 앱의 세션 ID나 대화 ID죠. turn()은 사용자와 고객을 미리 식별하고 싶다면 선택 사항인 user_id / userId와 customer_id / customerId 필드도 받아들이고, 동기·비동기 코드 모두에서 동작해요.

각 대화 턴의 LLM 호출 전에 turn()을 호출하세요. 그것은 새 트레이스를 시작하고, 그 안에서 만들어진 모든 스팬 — 자동 계측된 것 포함 — 이 스레드 ID를 상속해요.

기존 트레이스에 스레드 ID 추가 (Add a Thread ID to an Existing Trace)

기존 트레이스에 스레드 ID를 설정하는 것은 턴을 시작하는 것과 같지 않아요. 현재 트레이스에 라벨만 붙일 뿐, 새 것을 만들지는 않아요. 이 패턴은 애플리케이션이 이미 모든 요청에 대해 별도의 루트 트레이스를 보장할 때만 사용하세요. 각 요청이 진짜로 대화의 새 턴이라면 그 경계를 세우려고 turn()을 사용하세요.

Python

from openai import OpenAI
from confident_trace import span, update_trace

client = OpenAI()

def llm_app(query: str):
    with span("llm_app", type="agent"):
        res = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": query}]
        ).choices[0].message.content
        update_trace(thread_id="your-thread-id", input=query, output=res)
        return res

TypeScript

import OpenAI from "openai";
import { withSpan, updateTrace } from "confident-trace";

const openai = new OpenAI();

const llmApp = async (query: string) => {
  return withSpan({ name: "llm_app", type: "agent" }, async () => {
    const res = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: query }],
    });
    const data = res.choices[0].message.content;
    updateTrace({ threadId: "your-thread-id", input: query, output: data });
    return data;
  });
};

앱이 완전히 자동 계측되고 이미 각 요청에 대해 별도의 트레이스를 시작한다면, 트레이스 컨텍스트가 래퍼 스팬을 추가하지 않고 그 트레이스에 스레드 ID(그리고 선택적으로 사용자와 고객)를 스탬프할 수 있어요:

Python

from confident_trace import trace_context

def llm_app(query: str, thread_id: str):
    with trace_context(thread_id=thread_id):
        return client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": query}]
        ).choices[0].message.content

TypeScript

import { traceContext } from "confident-trace";

const llmApp = async (query: string, threadId: string) => {
  const res = await traceContext({ threadId }, () =>
    openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: query }],
    }),
  );
  return res.choices[0].message.content;
};

트레이스 컨텍스트는 트레이스도 스팬도 만들지 않아요. 그 안에서 시작된 트레이스에 기본값만 공급할 뿐이죠. 턴에 대한 새 트레이스를 보장하거나 스레드 I/O를 명시적으로 설정해야 할 때는 turn()을 사용하세요. 전체 트레이스 컨텍스트 의미론은 Update Trace Properties에서 볼 수 있어요.

트레이스를 스레드로 연결한다고 병합되는 건 아니에요 — 각 턴은 자신만의 트레이스와 스팬 트리를 유지하고, 스레드는 단순히 Confident AI가 표시·평가를 위해 그룹화하는 방식이에요. 이는 또한 재개된 대화(예: 프레임워크 체크포인트를 통한)가 이전 트레이스의 연속이 아니라 같은 스레드의 새 트레이스로 나타난다는 뜻이에요.

스레드 I/O 설정 (Set Thread I/O)

엄격히 강제되지는 않지만, 각 트레이스에 대해 input을 원시 사용자 텍스트로, output을 생성된 LLM 텍스트로 설정해야 해요. 이 값들은 Confident AI 표시와 스레드 평가(thread evaluations)를 위한 대화 턴으로 사용돼요.

Python

from openai import OpenAI
from confident_trace import turn, update_trace

client = OpenAI()

def llm_app(query: str):
    with turn("llm_app", thread_id="your-thread-id"):
        messages = [{"role": "user", "content": query}]
        res = client.chat.completions.create(
            model="gpt-4o",
            messages=messages
        ).choices[0].message.content

        # ✅ Do this — query is the raw user input
        update_trace(input=query, output=res)

        # ❌ Don't do this — messages is not the raw user input
        # update_trace(input=messages, output=res)
        return res

TypeScript

import OpenAI from "openai";
import { turn, updateTrace } from "confident-trace";

const openai = new OpenAI();

const llmApp = async (query: string) => {
  return turn({ threadId: "your-thread-id" }, async () => {
    const messages = [{ role: "user" as const, content: query }];
    const res = await openai.chat.completions.create({
      model: "gpt-4o",
      messages,
    });
    const data = res.choices[0].message.content;

    // ✅ Do this — query is the raw user input
    updateTrace({ input: query, output: data });

    // ❌ Don't do this — messages is not the raw user input
    // updateTrace({ input: messages, output: data });
    return data;
  });
};

모든 트레이스에 input과 output을 둘 다 설정할 필요는 없어요. 턴에 사용자 입력만 있거나 LLM 출력만 있으면 하나만 설정해도 돼요. Confident AI는 그에 따라 UI와 평가에서 턴을 포맷해요.

Python

# ✅ Set only input (e.g. user message with no immediate LLM response)
update_trace(thread_id="your-thread-id", input=query)

# ✅ Set only output (e.g. proactive LLM message with no user input)
update_trace(thread_id="your-thread-id", output=res)

# ✅ Omit both (e.g. background processing step in the conversation)
update_trace(thread_id="your-thread-id")

TypeScript

// ✅ Set only input
updateTrace({ threadId: "your-thread-id", input: query });

// ✅ Set only output
updateTrace({ threadId: "your-thread-id", output: data });

// ✅ Omit both
updateTrace({ threadId: "your-thread-id" });

I/O를 제공하지 않으면 트레이스의 기본 I/O 값으로 기본 설정돼요. 스레드에는 입력이나 출력이 설정된 트레이스가 최소 하나는 있어야 해요.

스레드 필드 설정 (Set Thread Fields)

스레드에 커스텀 메타데이터와 태그를 붙여 프로덕션 대화를 DVA 버전, 클라이언트, 에이전트 ID, 상태 플래그 같은 속성으로 라벨링할 수 있어요. 둘 다 관측소 전반에서 필터링·그룹화 가능해서 프로덕션 트래픽을 슬라이싱하기 쉬워요.

스레드 필드는 개별 트레이스의 태그(tags)와 메타데이터(metadata)와는 별개예요 — 대화 전체를 묘사하죠. 턴을 시작할 때 thread 객체를 전달해서 ID, 태그, 메타데이터를 함께 설정해요. 메타데이터 값은 어떤 JSON 직렬화 가능한 타입이든 될 수 있고, 태그는 문자열 배열이에요.

Python

from confident_trace import turn

with turn(thread={
    "id": "chat-42",
    "tags": ["support"],
    "metadata": {"channel": "web"},
}):
    agent.invoke(...)

TypeScript

import { turn } from "confident-trace";

await turn({
  thread: {
    id: "chat-42",
    tags: ["support"],
    metadata: { channel: "web" },
  },
}, async () => {
  await agent.invoke(...);
});

스레드를 식별하는 방법은 두 가지 중 하나를 골라 쓰면 돼요:

  • ID만 필요할 때는 thread_id / threadId를 넘겨요.
  • 스레드 태그나 메타데이터도 설정하고 싶다면 thread 객체를 넘겨요. 이 객체는 id, tags, metadata를 담을 수 있어요. turn()은 객체에 ID가 여전히 필요해요.

같은 두 옵션은 트레이스 컨텍스트와 트레이스 업데이트 헬퍼에서도 쓸 수 있어요.

스레드 태그와 메타데이터는 현재 트레이스에서 그 필드에 이전에 설정된 것을 대체해요. 생략한 필드는 그대로 유지돼요. 스레드 메타데이터는 나머지 트레이스 데이터와 같은 콘텐츠 컨트롤(content controls)을 받아요.

호출된 툴 설정 (Set Tools Called)

LLM 앱이 툴/함수 호출을 사용한다면, 주어진 턴에 어떤 툴이 호출됐는지 기록할 수 있어요. 이것은 트레이스에, 그것이 생성하는 데 도움을 준 output과 함께 붙고, 각 툴은 최소한 name을 가진 일반 객체예요.

Python

from confident_trace import turn, update_trace

def llm_app(query: str):
    with turn("llm_app", thread_id="your-thread-id"):
        res, tools = call_agent(query)
        update_trace(
            input=query,
            output=res,
            tools_called=[{"name": "WebSearch"}, {"name": "Calculator"}],
        )
        return res

TypeScript

import { turn, updateTrace } from "confident-trace";

const llmApp = async (query: string) => {
  return turn({ threadId: "your-thread-id" }, async () => {
    const { res, tools } = await callAgent(query);
    updateTrace({
      input: query,
      output: res,
      toolsCalled: [{ name: "WebSearch" }, { name: "Calculator" }],
    });
    return res;
  });
};

검색 컨텍스트 설정 (Set Retrieval Context)

RAG 기반 대화형 앱의 경우, 응답을 생성하는 데 사용된 검색 컨텍스트를 기록할 수 있어요. 이렇게 하면 Confident AI가 대화 턴 전반에서 검색 품질을 평가할 수 있어요.

Python

from confident_trace import turn, update_trace

def llm_app(query: str):
    with turn("llm_app", thread_id="your-thread-id"):
        chunks = retrieve(query)
        res = generate(query, chunks)
        update_trace(
            input=query,
            output=res,
            retrieval_context=[chunk.text for chunk in chunks],
        )
        return res

TypeScript

import { turn, updateTrace } from "confident-trace";

const llmApp = async (query: string) => {
  return turn({ threadId: "your-thread-id" }, async () => {
    const chunks = await retrieve(query);
    const res = await generate(query, chunks);
    updateTrace({
      input: query,
      output: res,
      retrievalContext: chunks.map((c) => c.text),
    });
    return res;
  });
};

tools_called와 retrieval_context를 같은 트레이스에 결합할 수 있어요 — 그 턴에서 출력이 어떻게 생성됐는지에 대한 보완적인 맥락을 제공하거든요.

다음 단계 (Next Steps)

스레드를 설정했다면 대화 품질을 평가하거나 트레이스에 더 많은 맥락을 추가해요.

Evaluate Threads

전체 대화 스레드에서 온라인 평가를 실행해서 멀티턴 품질을 모니터링해요.

Customize Traces

트레이스에 태그·메타데이터·사용자 정보·고객 정보를 추가해요.

더 알아보기