Skip to content
LangSmith 文档版本 跟随上游 源快照 03c3e659 更新于 2026-10-18

配置线程

许多 LLM 应用具有类似聊天机器人的界面,用户与 LLM 应用进行多轮对话。为了追踪这些对话,你可以在 LangSmith 中使用线程。

将追踪分组到线程

要将追踪关联到线程,你需要传入一个特殊的 metadata 键,其值为该线程的唯一标识符。键名应为以下之一:

  • session_id
  • thread_id

值可以是任意字符串,但我们建议使用 UUID v7 线程 ID。

LangSmith SDK 导出了一个 uuid7 辅助函数(Python v0.4.43+,JS v0.3.80+):

  • Python:from langsmith import uuid7
  • JS/TS:import { uuid7 } from 'langsmith'

有关说明,请参阅向追踪添加元数据和标签。

WARNING

重要: 为确保过滤和 token 计数在整个线程中正常工作,你必须在 all runs 上设置线程元数据(session_id 或 thread_id),包括追踪中的子运行。

如果子运行没有 thread_id 元数据,它们将不会在以下情况下被包含:

  • 按线程过滤运行。
  • 计算线程的 token 使用量。
  • 聚合线程的成本。

创建子运行时(例如,使用 @traceable 处理嵌套函数或创建子 span),确保将线程元数据传播到所有子运行。

Example

此示例演示如何使用结构化消息格式记录和检索对话历史,以维护长时间运行的聊天。

示例设置了一个 THREAD_ID,并通过 metadata 将其传递给追踪包装器,将该会话的每次运行链接到 LangSmith 中的同一线程。对话历史在轮次之间本地持久化——在生产环境中,请将基于文件或内存的存储替换为数据库或缓存。get_chat_history 标志控制流水线是继续现有线程还是启动新线程:

python
import os
import json
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

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

# Initialize clients
langsmith_client = Client()
client = wrap_openai(openai.Client())

# Configuration
THREAD_ID = str(uuid7())

# Using a local directory to store thread history. For production use, use a persistent storage solution.
THREADS_DIR = os.path.join(os.path.dirname(__file__), "threads")

# gets a history of all LLM calls in the thread to construct conversation history
def get_thread_history(thread_id: str) -> list:
    path = os.path.join(THREADS_DIR, f"{thread_id}.json")
    if not os.path.exists(path):
        return []
    with open(path, "r") as f:
        return json.load(f)

def save_thread_history(thread_id: str, messages: list):
    os.makedirs(THREADS_DIR, exist_ok=True)
    with open(os.path.join(THREADS_DIR, f"{thread_id}.json"), "w") as f:
        json.dump(messages, f, indent=2, default=str)

@traceable(name="Chat Bot", metadata={"thread_id": THREAD_ID})
def chat_pipeline(messages: list, get_chat_history: bool = False):
    # Whether to continue an existing thread or start a new one
    if get_chat_history:
        history_messages = get_thread_history(THREAD_ID)
        # Get existing conversation history and append new messages
        all_messages = history_messages + messages
    else:
        all_messages = messages

    # Invoke the model
    chat_completion = client.chat.completions.create(
        model="gpt-5.4-mini", messages=all_messages
    )

    response_message = chat_completion.choices[0].message
    print("Response from model:", response_message)

    full_conversation = all_messages + [{"role": response_message.role, "content": response_message.content}]
    save_thread_history(THREAD_ID, full_conversation)

    return {"messages": full_conversation}

# Format message
messages = [
    {
        "content": "Hi, my name is Sally",
        "role": "user"
    }
]

# Call the chat pipeline
result = chat_pipeline(messages, get_chat_history=False)
typescript
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import * as dotenv from "dotenv";
import OpenAI from "openai";
import { traceable } from "langsmith/traceable";
import { wrapOpenAI } from "langsmith/wrappers";
import { uuid7 } from "langsmith";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Load environment variables from .env file
dotenv.config();

// Initialize client
const client = wrapOpenAI(new OpenAI());

// Configuration
const THREAD_ID = uuid7();

// Using a local directory to store thread history. For production use, use a persistent storage solution.
const THREADS_DIR = path.join(__dirname, "threads");

type Message = { role: string; content: string };

// Gets a history of all LLM calls in the thread to construct conversation history
function getThreadHistory(threadId: string): Message[] {
  const filePath = path.join(THREADS_DIR, `${threadId}.json`);
  if (!fs.existsSync(filePath)) return [];
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}

function saveThreadHistory(threadId: string, messages: Message[]): void {
  fs.mkdirSync(THREADS_DIR, { recursive: true });
  fs.writeFileSync(
    path.join(THREADS_DIR, `${threadId}.json`),
    JSON.stringify(messages, null, 2)
  );
}

const chatPipeline = traceable(
  async function chatPipeline({ messages, get_chat_history = false }: { messages: Message[]; get_chat_history?: boolean }) {
    // Whether to continue an existing thread or start a new one
    if (get_chat_history) {
      const historyMessages = getThreadHistory(THREAD_ID);
      // Get existing conversation history and append new messages
      messages = [...historyMessages, ...messages];
    }

    // Invoke the model
    const chatCompletion = await client.chat.completions.create({
      model: "gpt-5.4-mini",
      messages,
    });

    const responseMessage = chatCompletion.choices[0].message;
    console.log("Response from model:", responseMessage);

    const fullConversation: Message[] = [
      ...messages,
      { role: responseMessage.role, content: responseMessage.content ?? "" },
    ];
    saveThreadHistory(THREAD_ID, fullConversation);

    return { messages: fullConversation };
  },
  { name: "Chat Bot", metadata: { thread_id: THREAD_ID } }
);

// Format message
const messages: Message[] = [{ role: "user", content: "Hi! My name is Sally" }];

// Call the chat pipeline
await chatPipeline({ messages, get_chat_history: false });
java
import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import com.langchain.smith.tracing.TraceConfig;
import com.langchain.smith.tracing.Tracing;
import com.langchain.smith.wrappers.openai.OpenAITracing;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.ChatModel;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionMessageParam;
import com.openai.models.chat.completions.ChatCompletionUserMessageParam;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

class ThreadsChatPipeline {
  private static final String THREAD_ID = "01990f3e-7f97-74c5-a9b6-8d3f7e8e2f11";

  private static final class OpenAiResources {
    private static final LangsmithClient langsmith = LangsmithOkHttpClient.fromEnv();
    private static final ExecutorService executor = Executors.newSingleThreadExecutor();
    private static final Map<String, Object> threadMetadata = new HashMap<>();

    static {
      threadMetadata.put("thread_id", THREAD_ID);
    }

    private static final OpenAIClient openai =
        OpenAITracing.wrapOpenAI(
            OpenAIOkHttpClient.fromEnv(),
            TraceConfig.builder()
                .client(langsmith)
                .executor(executor)
                .metadata(threadMetadata)
                .build());

    private static final List<ChatCompletionMessageParam> threadHistory = new ArrayList<>();

    static final Function<ChatRequest, Map<String, List<ChatCompletionMessageParam>>> CHAT_PIPELINE =
        Tracing.traceFunction(
            request -> {
              List<ChatCompletionMessageParam> allMessages = new ArrayList<>();
              if (request.getChatHistory()) {
                allMessages.addAll(threadHistory);
              }
              allMessages.addAll(request.getMessages());

              ChatCompletion chatCompletion =
                  openai
                      .chat()
                      .completions()
                      .create(
                          ChatCompletionCreateParams.builder()
                              .model(ChatModel.GPT_5_5)
                              .messages(allMessages)
                              .build());

              String content = chatCompletion.choices().get(0).message().content().orElse("");
              List<ChatCompletionMessageParam> fullConversation = new ArrayList<>(allMessages);
              fullConversation.add(
                  ChatCompletionMessageParam.ofAssistant(
                      ChatCompletionAssistantMessageParam.builder().content(content).build()));
              threadHistory.clear();
              threadHistory.addAll(fullConversation);

              return Collections.singletonMap("messages", fullConversation);
            },
            TraceConfig.builder()
                .name("Chat Bot")
                .client(langsmith)
                .executor(executor)
                .metadata(threadMetadata)
                .build());

    private OpenAiResources() {}

    static ExecutorService executor() {
      return executor;
    }
  }

  static Function<ChatRequest, Map<String, List<ChatCompletionMessageParam>>> chatPipeline() {
    return OpenAiResources.CHAT_PIPELINE;
  }

  public static void main(String[] args) throws InterruptedException {
    try {
      List<ChatCompletionMessageParam> messages =
          Collections.singletonList(
              ChatCompletionMessageParam.ofUser(
                  ChatCompletionUserMessageParam.builder()
                      .content("Hi, my name is Sally")
                      .build()));
      chatPipeline().apply(new ChatRequest(messages, false));
    } finally {
      OpenAiResources.executor().shutdown();
      if (!OpenAiResources.executor().awaitTermination(10, TimeUnit.SECONDS)) {
        throw new IllegalStateException("Timed out waiting for LangSmith traces to submit");
      }
    }
  }

  static class ChatRequest {
    private final List<ChatCompletionMessageParam> messages;
    private final boolean getChatHistory;

    ChatRequest(List<ChatCompletionMessageParam> messages, boolean getChatHistory) {
      this.messages = messages;
      this.getChatHistory = getChatHistory;
    }

    List<ChatCompletionMessageParam> getMessages() {
      return messages;
    }

    boolean getChatHistory() {
      return getChatHistory;
    }
  }
}
kotlin
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient
import com.langchain.smith.tracing.TraceConfig
import com.langchain.smith.tracing.traceable
import com.langchain.smith.wrappers.openai.wrapOpenAI
import com.openai.client.okhttp.OpenAIOkHttpClient
import com.openai.models.ChatModel
import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam
import com.openai.models.chat.completions.ChatCompletionCreateParams
import com.openai.models.chat.completions.ChatCompletionMessageParam
import com.openai.models.chat.completions.ChatCompletionUserMessageParam
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

val threadId = "01990f3e-7f97-74c5-a9b6-8d3f7e8e2f11"
val langsmith by lazy { LangsmithOkHttpClient.fromEnv() }
val executor by lazy { Executors.newSingleThreadExecutor() }
val threadMetadata by lazy { mapOf("thread_id" to threadId) }
val openai by lazy {
    wrapOpenAI(
        OpenAIOkHttpClient.fromEnv(),
        TraceConfig.builder()
            .client(langsmith)
            .executor(executor)
            .metadata(threadMetadata)
            .build(),
    )
}
val threadHistory = mutableListOf<ChatCompletionMessageParam>()

data class ChatRequest(
    val messages: List<ChatCompletionMessageParam>,
    val getChatHistory: Boolean = false,
)

val chatPipeline by lazy {
    traceable(
        { request: ChatRequest ->
            val allMessages =
                if (request.getChatHistory) {
                    threadHistory + request.messages
                } else {
                    request.messages
                }

            val chatCompletion =
                openai.chat().completions().create(
                    ChatCompletionCreateParams.builder()
                        .model(ChatModel.GPT_5_5)
                        .messages(allMessages)
                        .build(),
                )

            val content = chatCompletion.choices()[0].message().content().orElse("")
            val fullConversation =
                allMessages +
                    ChatCompletionMessageParam.ofAssistant(
                        ChatCompletionAssistantMessageParam.builder().content(content).build(),
                    )
            threadHistory.clear()
            threadHistory.addAll(fullConversation)

            mapOf("messages" to fullConversation)
        },
        TraceConfig.builder()
            .name("Chat Bot")
            .client(langsmith)
            .executor(executor)
            .metadata(threadMetadata)
            .build(),
    )
}

fun main() {
    try {
        val messages =
            listOf(
                ChatCompletionMessageParam.ofUser(
                    ChatCompletionUserMessageParam.builder()
                        .content("Hi, my name is Sally")
                        .build(),
                ),
            )
        chatPipeline(ChatRequest(messages))
    } finally {
        executor.shutdown()
        check(executor.awaitTermination(10, TimeUnit.SECONDS)) {
            "Timed out waiting for LangSmith traces to submit"
        }
    }
}

Java 和 Kotlin 示例使用专用执行器。关闭执行器并等待终止,可确保后台追踪提交在进程退出前完成。

进行以下调用以继续对话。通过传入 get_chat_history=True / get_chat_history: true / getChatHistory = true,你可以从上次中断的地方继续对话。这意味着 LLM 会收到完整的消息历史并对其做出响应,而不是仅响应最新消息:

python
# Format message
messages = [
    {
        "content": "What is my name",
        "role": "user"
    }
]

# Call the chat pipeline
result = chat_pipeline(messages, get_chat_history=True)
typescript
// Continue the conversation.
const messages: Message[] = [{ role: "user", content: "What is my name" }];

await chatPipeline({ messages, get_chat_history: true });
java
List<ChatCompletionMessageParam> messages =
    Collections.singletonList(
        ChatCompletionMessageParam.ofUser(
            ChatCompletionUserMessageParam.builder()
                .content("What is my name")
                .build()));

ThreadsChatPipeline.chatPipeline().apply(new ThreadsChatPipeline.ChatRequest(messages, true));
kotlin
val messages =
    listOf(
        ChatCompletionMessageParam.ofUser(
            ChatCompletionUserMessageParam.builder()
                .content("What is my name")
                .build(),
        ),
    )

chatPipeline(ChatRequest(messages, getChatHistory = true))

保持对话继续。由于包含过去的消息,LLM 会记住对话:

python
# Continue the conversation.
messages = [
    {
        "content": "What was the first message I sent you?",
        "role": "user"
    }
]

chat_pipeline(messages, get_chat_history=True)
typescript
// Continue the conversation.
const messages: Message[] = [{ role: "user", content: "What was the first message I sent you?" }];

await chatPipeline({ messages, get_chat_history: true });
java
List<ChatCompletionMessageParam> messages =
    Collections.singletonList(
        ChatCompletionMessageParam.ofUser(
            ChatCompletionUserMessageParam.builder()
                .content("What was the first message I sent you?")
                .build()));

ThreadsChatPipeline.chatPipeline().apply(new ThreadsChatPipeline.ChatRequest(messages, true));
kotlin
val messages =
    listOf(
        ChatCompletionMessageParam.ofUser(
            ChatCompletionUserMessageParam.builder()
                .content("What was the first message I sent you?")
                .build(),
        ),
    )

chatPipeline(ChatRequest(messages, getChatHistory = true))

查看线程

你可以在 UI 中查看线程:点击任意项目详情页面中的 Threads 选项卡。表格显示每个线程的第一个输入、最后一个输出、开始时间、轮次数、延迟(P50/P99)、token 使用量、成本和反馈分数。

右侧面板显示项目的汇总统计信息,包括线程和追踪数量、总 token 使用量和中位数 token 使用量、错误率以及 P50/P99 延迟。

INFO

使用线程视图中的 Chat 来分析对话线程、了解用户情绪、识别痛点,并跟踪问题是否已解决。

然后你可以点击进入特定线程。你可以通过三种不同方式查看线程:

  • Trajectory 视图:对话层。将每个轮次视为聊天式线程进行浏览,显示用户和助手消息、工具调用以及子 agent 活动。
  • Turns 视图:逐轮摘要。将每个轮次视为一张卡片查看其输入和输出,支持展开/折叠和自定义输入/输出字段。
  • Details 视图:调试层。深入查看特定运行,检查输入、输出、元数据、计时、错误和子运行。周围的线程上下文保持可见,因此你可以看到该运行在更广泛对话中的位置。

使用页面顶部的按钮或键盘快捷键 T(Trajectory)和 D(Details)在视图之间切换。线程侧面板默认为 Details 视图。右侧面板显示线程的统计信息,包括轮次数、首次和最后开始时间、P50/P99 延迟,以及按输入和输出 token 划分的成本明细。有关每个视图的完整说明,请参阅查看追踪。

查看反馈

反馈分数显示在项目 Threads 选项卡的线程表 Feedback 列中。

在线程内,打开 Trajectory 视图,然后点击轮次元数据行中的 LLM call 链接,进入该运行的 Details 视图,你可以在其中查看该运行的反馈。你还可以在那里看到线程级反馈。

保存线程级过滤器

NOTE

线程过滤器会查看所有运行,只要至少有 1 个运行匹配过滤器,就会显示该线程。

在项目的 Threads 选项卡上,你可以保存常用过滤器。

相关


BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。