Langfuse WorkshopClickHouse Workshops

02 追踪

这是跟踪步骤的白板 — 与 checkpoint/01-base-app 相同的代码,还没有 Langfuse 接线。Langfuse 包已在 package.json 中 — 运行 npm inst...

研讨会材料在公共 langfuse/langfuse-workshop 存储库中维护。使用存储库获取可运行的应用、检查点分支和本地设置。

查看此 Markdown 文件

起点

git checkout checkpoint/02-tracing

这是跟踪步骤的白板 — 与 checkpoint/01-base-app 相同的代码,还没有 Langfuse 接线。Langfuse 包已在 package.json 中 — 如果你还没有,请运行 npm install。确保 .env 有你的 OPENAI_API_KEY 和 Langfuse 密钥。

为什么我们跟踪

跟踪记录你的代理采取的每一步 — 每个模型调用、每个工具调用、输入的输入和返回的输出 — 按它们发生的顺序。它将代理从黑盒变成可以打开并在事后检查的东西,所以当答案错误时,你可以指向错误发生的确切步骤,而不是猜测。

如果你想了解更大的动机,请参见 Langfuse Academy 关于跟踪的课程。如果你想了解技术细节(SDK 选项、OpenTelemetry 内部、span 属性),跟踪文档 涵盖了这一点。

目标

当 Dad 问"我如何打开蓝牙?"时,代理不只是调用一次 OpenAI。幕后它询问 OpenAI 做什么,调用 get_support_context 来获取 Dad 的 iPhone 设置,再次询问 OpenAI,为蓝牙步骤调用 search_help_library,然后再询问一次 OpenAI 以生成编号答案。今天这些都不可见。

本章的目标是使 Langfuse 中的每一步都可见 — 一个聊天转折变成一个嵌套跟踪,其中代理运行、OpenAI 生成和两个工具调用都按顺序记录。

Spec 的逐步过程

我们将分三个步骤构建跟踪,这些步骤反映代理的结构:

  1. 首次跟踪 — 记录 OpenAI 生成本身。
  2. 嵌套跟踪 — 将生成分组到每个转折的一个代理运行中。
  3. 记录工具调用 — 使每个工具调用成为自己的观测。

步骤 1 — 首次跟踪

我们想要 OpenAI 调用的可观测性,以查看输入和输出是什么,以及花费、令牌和时间。两个更改已足够。

src/server/index.ts

在文件顶部附近启动 Langfuse span 处理器:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

new NodeSDK({ spanProcessors: [new LangfuseSpanProcessor()] }).start();

处理器从 Node 进程环境读取 LANGFUSE_PUBLIC_KEY、LANGFUSE_SECRET_KEY 和 LANGFUSE_BASE_URL。在本研讨会中,服务器在 Langfuse SDK 启动前加载存储库 .env,所以请编辑 .env 而不是依赖导出的 shell 值。

旁注:如果最后一个跟踪有时在你停止或重启 npm run dev 时出现晚,请回到 index.ts 并将上面的单行变成命名的 langfuseSpanProcessor 和 sdk 变量,以便 shutdown() 可以刷新它们:

const langfuseSpanProcessor = new LangfuseSpanProcessor();
const sdk = new NodeSDK({ spanProcessors: [langfuseSpanProcessor] });
sdk.start();

async function shutdown() {
  server.close();
  await langfuseSpanProcessor.forceFlush();
  await sdk.shutdown();
}

你不需要这个来理解跟踪模型,但它避免了在本地开发中令人困惑的"我的最后一个跟踪去哪里了?"时刻。

src/server/support-agent.ts

添加导入:

import { observeOpenAI } from "@langfuse/openai";

然后 在创建 OpenAI 客户端的地方包装它。在 runSupportConversation 中找到此行:

const openai = new OpenAI({ apiKey: env.openaiApiKey });

将其更改为:

const openai = observeOpenAI(new OpenAI({ apiKey: env.openaiApiKey }));

这就是整个差异。没有工厂函数,没有单独的原始客户端 — observeOpenAI 在调用站点处内联包装 OpenAI 客户端,下面的 openai.chat.completions.create(...) 现在为每个调用生成一个跟踪。

验证: npm run dev,提一个问题,刷新 Langfuse — 你应该看到每个 OpenAI 调用一个生成,包含提示词、响应、令牌和延迟。每个生成仍然是其自己的顶级跟踪;我们接下来修复这个。

步骤 1 后的 Langfuse Traces 视图 — 每个聊天转折显示为独立的 openai-chat-completion 生成。

步骤 2 — 嵌套跟踪

要将生成放入上下文中,我们在每个转折的一个代理运行中对它们进行分组。在 src/server/support-agent.ts 中进行三个编辑 — 无需更改函数体。

1. 添加导入:

import { observe } from "@langfuse/tracing";

2. 降低现有函数。 找到:

export async function runSupportConversation(request: ChatRequest): Promise<ChatResponse> {

删除 export 并重新命名:

async function runSupportConversationInner(request: ChatRequest): Promise<ChatResponse> {

函数体保持完全一致。

3. 在文件底部添加包装的导出:

export const runSupportConversation = observe(runSupportConversationInner, {
  name: "dad-it-support-chat-turn",
  asType: "agent"
});

index.ts 仍然以相同方式导入 runSupportConversation。observe(...) 自动捕获函数参数作为跟踪输入,返回值作为跟踪输出。

验证: 一个聊天转折现在应该显示为单个 dad-it-support-chat-turn 观测,其中 OpenAI 生成嵌套在其下。

步骤 2 后的跟踪树 — 一个 dad-it-support-chat-turn 代理根,OpenAI 生成作为子项。

步骤 3 — 记录工具调用

OpenAI 生成已经在其 tool_calls 输出中提及了工具调用,但我们没有实际工具执行的观测 — 无法看到什么输入进去,什么出来。同样的 observe(...) 模式可以应用于每个工具。

src/server/tools.ts

添加导入和两个观测的帮助程序在 executeTool 上方,然后 用下面的版本替换现有的 executeTool,使 switch 调用包装的帮助程序而不是内联做工作。TOOL_DEFINITIONS 保持不变。

import { observe } from "@langfuse/tracing";

const getSupportContextTool = observe(
  async () => {
    const context = getSupportContext();

    return {
      ok: true,
      context: {
        id: context.id,
        label: context.label,
        devices: context.devices,
        deviceSummary: context.deviceSummary,
        responseStyle: context.responseStyle,
        scopeHighlights: context.scopeHighlights,
        notableApps: context.notableApps
      }
    };
  },
  { name: "get_support_context", asType: "tool" }
);

const searchHelpLibraryTool = observe(
  async (input: { question: string }) => {
    const guides = searchGuides(input.question);

    return {
      ok: true,
      results: guides.map((guide) => ({
        id: guide.id,
        title: guide.title,
        summary: guide.summary,
        steps: guide.steps,
        caution: guide.caution ?? null
      }))
    };
  },
  { name: "search_help_library", asType: "tool" }
);

export async function executeTool(name: string, input: Record<string, unknown>): Promise<ToolResult> {
  switch (name) {
    case "get_support_context":
      return getSupportContextTool();

    case "search_help_library":
      return searchHelpLibraryTool({ question: String(input.question ?? "") });

    default:
      return { ok: false, error: `Unsupported tool: ${name}` };
  }
}

如果 npm run dev 以 Multiple exports with the same name "executeTool" 停止,原始 executeTool 仍然在文件更下方。删除它,仅保留上面的版本。

步骤 3 后的完整跟踪 — dad-it-support-chat-turn(代理)与 OpenAI 生成和 get_support_context + search_help_library 工具观测作为下方的兄弟项。

如何验证你已完成

  • 单个用户转折在 Langfuse 中创建一个跟踪。
  • 根观测:dad-it-support-chat-turn(类型 agent)。
  • 来自 observeOpenAI(...) 的子生成,包含提示词、响应、令牌、延迟。
  • 子工具观测:get_support_context、search_help_library。
  • 根输入是聊天请求;根输出是聊天响应。

总结

相同的模式,不同的观测类型,相同的概念:observe(fn, { asType }) 包装一个函数并发出一个 span,带有你给它的名称和类型。observeOpenAI(client) 是该包装针对 OpenAI SDK 的专用版本。

根据 Langfuse 最佳实践添加丰富跟踪的更直接方式是 Langfuse 技能(/langfuse)。它将推荐的模式应用到你的代码库,无需你手工滚动每个包装。本演练的存在是为了让你理解技能在幕后做什么。

observeOpenAI 本身包装官方 OpenAI SDK — 在幕后它与 Langfuse OpenAI JS 自动检测 相同。如果你使用不同的 SDK(Anthropic、Vercel AI SDK、你自己的 HTTP 客户端),Langfuse 集成目录 具有等效的包装或自动检测指南。

附录/奖励部分 — 用户和会话 ID

上面的演练让你得到了干净的 parent → generation → tool 形状的跟踪。接下来大多数团队想要的是 按用户和会话切片跟踪 — 这样你可以提取"这个用户与代理进行的每一次转折"或"昨天早上的完整多转折会话"。 为了简单起见,我们在实时跟踪演练中跳过这一步,但跟踪后的检查点包括它,以便后续章节可以使用会话/用户视图而无需另一个代码步骤。参见 Langfuse 文档中关于 Sessions 和 Users 的信息。

简而言之:

import { propagateAttributes } from "@langfuse/tracing";

return propagateAttributes(
  {
    userId: request.userId ?? `workshop-${context.id}`,
    sessionId: request.sessionId,
    tags: ["langfuse-workshop", "dad-it-support"]
  },
  async () => {
    // ...the same tool-calling loop...
  }
);

propagateAttributes(...) 块内的任何东西 — 包括 observeOpenAI 发出的所有子 span — 自动获得 userId、sessionId 和标签附加。Langfuse Users 视图、Sessions 视图和标签过滤器在属性存在的时刻就亮起来了。

最终状态

这个完成的跟踪应用是 03-prompt-management 和 04-monitoring 的起点。

本页内容

Track your progress?

Optional. We email a link to confirm your address; progress records once you open it.

Please use your work email address, not a personal one.

Progress tracking also requires accepting the current Terms of Service in Privacy settings.

ZH