主题
配置线程
许多 LLM 应用具有类似聊天机器人的界面,用户与 LLM 应用进行多轮对话。为了追踪这些对话,你可以在 LangSmith 中使用线程。
将追踪分组到线程
要将追踪关联到线程,你需要传入一个特殊的 metadata 键,其值为该线程的唯一标识符。键名应为以下之一:
session_idthread_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 选项卡上,你可以保存常用过滤器。
相关
- 可观测性概念:线程的背景知识以及它们与运行和追踪的关系。
- 向追踪添加元数据和标签:如何传递
thread_id和其他元数据键。 - 过滤追踪:在追踪 UI 中按线程元数据过滤。
- 设置多轮在线评估器:评估线程而不是单个运行。
- 使用 SDK 记录用户反馈:将反馈附加到线程内的运行。
- 在 UI 中创建和管理数据集:将线程添加到数据集。