将跟踪归组到线程中
要将跟踪关联到一个线程中,你需要传入一个特殊的metadata 键,其值是该线程的唯一标识符。键名应为以下之一:
session_idthread_id
uuid7 辅助函数(Python v0.4.43+,JS v0.3.80+):
- Python:
from langsmith import uuid7 - JS/TS:
import { uuid7 } from 'langsmith'
重要提示: 为确保在整个线程范围内正确进行过滤和 Token 计数,你必须在所有运行(包括跟踪内的子运行)上设置线程元数据(
session_id 或 thread_id)。如果子运行没有 thread_id 元数据,在以下情况中它们将不会被包含:- 按线程过滤运行时。
- 计算某个线程的 Token 用量时。
- 跨线程聚合成本时。
@traceable 处理嵌套函数或创建子跨度),请确保将线程元数据传播到所有子运行。示例
此示例演示了如何使用结构化的消息格式记录和检索对话历史,以维持长期运行的聊天。 该示例设置了一个THREAD_ID,并通过 metadata 将其传递给跟踪包装器,将该会话的每次运行都关联到 LangSmith 中的同一个线程。对话历史在各轮次之间被持久化存储在本地——在生产环境中,应使用数据库或缓存替代基于文件或内存存储的方式。get_chat_history 标志控制管道是继续一个现有线程还是开始一个新线程:
import os
import json
from dotenv import load_dotenv
# 从 .env 文件加载环境变量
load_dotenv()
import openai
from langsmith import traceable, Client, uuid7
from langsmith.wrappers import wrap_openai
# 初始化客户端
langsmith_client = Client()
client = wrap_openai(openai.Client())
# 配置
THREAD_ID = str(uuid7())
# 使用本地目录存储线程历史。生产环境中请使用持久化存储方案。
THREADS_DIR = os.path.join(os.path.dirname(__file__), "threads")
# 获取线程中所有 LLM 调用的历史记录以构建对话历史
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):
# 判断是继续一个现有线程还是开始一个新线程
if get_chat_history:
history_messages = get_thread_history(THREAD_ID)
# 获取现有对话历史并附加新消息
all_messages = history_messages + messages
else:
all_messages = messages
# 调用模型
chat_completion = client.chat.completions.create(
model="gpt-5.4-mini", messages=all_messages
)
response_message = chat_completion.choices[0].message
print("来自模型的响应:", 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}
# 格式化消息
messages = [
{
"content": "嗨,我叫萨莉",
"role": "user"
}
]
# 调用聊天管道
result = chat_pipeline(messages, get_chat_history=False)
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));
// 从 .env 文件加载环境变量
dotenv.config();
// 初始化客户端
const client = wrapOpenAI(new OpenAI());
// 配置
const THREAD_ID = uuid7();
// 使用本地目录存储线程历史。生产环境中请使用持久化存储方案。
const THREADS_DIR = path.join(__dirname, "threads");
type Message = { role: string; content: string };
// 获取线程中所有 LLM 调用的历史记录以构建对话历史
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 }) {
// 判断是继续一个现有线程还是开始一个新线程
if (get_chat_history) {
const historyMessages = getThreadHistory(THREAD_ID);
// 获取现有对话历史并附加新消息
messages = [...historyMessages, ...messages];
}
// 调用模型
const chatCompletion = await client.chat.completions.create({
model: "gpt-5.4-mini",
messages,
});
const responseMessage = chatCompletion.choices[0].message;
console.log("来自模型的响应:", 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 } }
);
// 格式化消息
const messages: Message[] = [{ role: "user", content: "嗨!我叫萨莉" }];
// 调用聊天管道
await chatPipeline({ messages, get_chat_history: false });
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_CHAT_LATEST)
.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;
}
}
}
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_CHAT_LATEST)
.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"
}
}
}
get_chat_history=True / get_chat_history: true / getChatHistory = true,你可以从上次中断的地方继续对话。这意味着大语言模型会接收整个消息历史并对其做出响应,而不仅仅是回应最新的一条消息:
# 格式化消息
messages = [
{
"content": "我叫什么名字",
"role": "user"
}
]
# 调用聊天管道
result = chat_pipeline(messages, get_chat_history=True)
// 继续对话。
const messages: Message[] = [{ role: "user", content: "我叫什么名字" }];
await chatPipeline({ messages, get_chat_history: true });
List<ChatCompletionMessageParam> messages =
Collections.singletonList(
ChatCompletionMessageParam.ofUser(
ChatCompletionUserMessageParam.builder()
.content("What is my name")
.build()));
ThreadsChatPipeline.chatPipeline().apply(new ThreadsChatPipeline.ChatRequest(messages, true));
val messages =
listOf(
ChatCompletionMessageParam.ofUser(
ChatCompletionUserMessageParam.builder()
.content("What is my name")
.build(),
),
)
chatPipeline(ChatRequest(messages, getChatHistory = true))
# 继续对话。
messages = [
{
"content": "我发给你的第一条消息是什么?",
"role": "user"
}
]
chat_pipeline(messages, get_chat_history=True)
// 继续对话。
const messages: Message[] = [{ role: "user", content: "我发给你的第一条消息是什么?" }];
await chatPipeline({ messages, get_chat_history: true });
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));
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 延迟。使用线程视图中的 Chat 来分析对话线程、了解用户情绪、识别痛点并跟踪问题是否已解决。
- Messages(消息)视图(测试版):对话层面。以聊天式线程形式扫描每一轮,显示用户和助手的消息、工具调用和子代理活动。
- Turns(轮次)视图:按轮次摘要。将每一轮显示为一个卡片,展示其输入和输出,并支持展开/折叠以及自定义输入/输出字段。
- Details(详情)视图:调试层面。深入查看特定运行,检查输入、输出、元数据、计时、错误和子运行。周围的线程上下文保持可见,让你能看到该运行在更广泛对话中的位置。
M(消息)、T(轮次)和 D(详情)在视图之间切换。在 Messages 视图处于测试版期间,线程侧面板默认显示 Details 视图。右侧面板显示线程的统计信息,包括轮次计数、首次和末次开始时间、P50/P99 延迟以及按输入和输出 Token 的成本细分。有关每个视图的完整描述,请参阅查看跟踪。
查看反馈
反馈分数可以在项目 Threads(线程)选项卡的线程表格的 Feedback(反馈)列中看到。 在线程内部,打开 Messages 视图,点击某一轮元数据行中的 LLM call(LLM 调用)链接,即可跳转到该运行的 Details 视图,在那里你可以查看该运行的反馈。你还可以在那里看到线程级反馈。保存线程级过滤器
线程过滤器会检查所有运行,如果至少有 1 个运行匹配过滤器,则会显示该线程。
相关内容
- 可观测性概念:关于线程的背景知识及其与运行和跟踪的关系。
- 向跟踪添加元数据和标签:如何传递
thread_id和其他元数据键。 - 过滤跟踪:在跟踪 UI 中按线程元数据进行过滤。
- 设置多轮在线评估器:评估线程而非单个运行。
- 使用 SDK 记录用户反馈:将反馈附加到线程内的运行。
Connect these docs to Claude, VSCode, and more via MCP for real-time answers.

