如何正确使用 Langfuse ?

当我第一次在 LLM 应用中接入 Langfuse 时,一旦 traces 出现在 UI 中,我就觉得配置完成了。

如何正确使用 Langfuse ?
梯形图转SCL | 博途AI辅助编程文档 | AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | PLC在线仿真器 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo

当我第一次在 LLM 应用中接入 Langfuse 时,一旦 traces 出现在 UI 中,我就觉得配置完成了。每次模型调用都有一个 generation observation。每个 observation 都包含了系统提示、用户提示、模型响应、延迟、token 用量和所有错误信息。

这足以回答一个重要的问题:"这次请求发生了什么?"

但这不足以回答后来出现的问题:

  1. 是哪个提示版本产生了这个结果?
  2. 能否从 observation 中重放这个请求?
  3. 能否将生产环境中的成功案例转化为数据集条目?
  4. 能否用相同的输入测试新的提示版本?
  5. 能否在不修改编译后提示的情况下更改业务输入?
  6. 能否按提示版本而非仅按操作名称来比较结果?

有几种方法乍看之下很合理,但当我们尝试构建数据集和运行提示实验时才暴露出局限性。本文将介绍最终的配置方案、之前方案为何存在问题,以及如果重新实现 Langfuse 我会做出的决策。

一条日志可以包含检查请求所需的所有内容,但仍然不适合用于重放或实验。

1、为什么基础的 tracing 配置最终会变得不够用

Tracing 集成通常围绕请求来构建。应用程序启动一个 trace,记录模型调用,存储响应,并附加运行时细节。一旦数据可见,这种集成就显得很有用,因为工程师可以检查故障并理解模型收到了什么。

提示开发引入了另一个工作单元:测试用例。测试用例需要结构化的输入,以便提供给不同的提示版本。它可能还需要预期输出、来源 observation,以及足够的上下文来判断两个实验结果是否可比较。

编译后的提示是为模型优化的。数据集条目是为复用优化的。

假设一个用户提示由以下部分组成:

  • 源文档
  • 用户上下文
  • 一组约束条件

如果这三个输入都被编译成一个提示字符串,人类仍然可以阅读它们,但实验运行器就无法独立寻址它们了。

当团队需要以下操作时,这种差异就变得很重要:

  • 替换提示但不替换数据集
  • 在实验中编辑某个输入
  • 用相同的业务数据测试两个提示版本
  • 按输入特征筛选示例
  • 从生产 observation 中构建数据集
  • 无需查询其他系统即可重现模型调用
  • 将评估结果归因到精确的提示版本

可观测性数据和实验就绪数据有交集,但一个不会自动产生另一个。日志结构必须同时支持两者。

2、两种导致问题的方法

2.1 记录完整的系统和用户提示

第一种方法是在每个 generation observation 中存储精确编译后的系统和用户提示。这使每个 observation 都是自包含的。打开一个 generation 就能看到模型收到的完整内容。

静态指令和动态业务值已被组合成大字符串。源文档、用户上下文、约束和偏好可能都出现在用户提示中,但它们不再是独立的字段。

从这些 observation 创建数据集需要以下三种方式之一:

  • 解析编译后的提示
  • 从应用数据重建原始输入
  • 使用整个编译后的提示作为数据集输入

每种方式都会产生不同的问题。

解析将提取逻辑与提示的历史格式绑定。重建需要另一个数据源,如果某个值仅在请求期间存在则可能失败。将编译后的提示存储在数据集中会将旧指令带入每个测试用例。如果数据集行已包含旧提示的指令,用新提示测试它就不是一次干净的比较。实验改变了外部提示,但保留了输入中之前提示的部分内容。

2.2 将提示排除在 Langfuse 之外

可以仅将 Langfuse 用于 tracing。应用程序可以将提示保存在代码中,将渲染后的消息发送给模型提供者,并记录 observation 而不在 Langfuse 中创建相应的提示版本。这在提示改进成为常规工作流之前都能正常运行。

没有 Langfuse 中的提示版本:

  • Observation 与可检查的提示历史断开连接。
  • Playground 实验需要手动重新创建提示。
  • 数据集字段无法自然映射到提示变量。
  • 结果难以按精确的提示修订版本分组。
  • 实验中测试的提示可能与生产中使用的提示产生偏差。

提示仍然存在于源代码管理中。但源代码 diff 与连接到其产生 observation 的可执行提示版本不是一回事。

3、使系统正常工作的分离方式

最终的结构基于为每个表示分配明确的所有权。

3.1 应用程序拥有生产执行权

生产提示保留在代码库中。应用程序在本地渲染它并将结果发送给模型提供者。它不需要在每次模型调用之前从 Langfuse 获取提示。

这个选择保留了我们想要的部署模型:

  • 提示更改通过正常的代码审查。
  • 应用程序代码和提示更改一起部署。
  • 生产行为不会因为外部标签的移动而改变。
  • 提供者调用不依赖于提示管理请求。
  • 回滚应用程序也会回滚运行时使用的提示。

从 Langfuse 获取生产提示可以是有效的架构。这不是我在此设置中想要的控制模型。重要的是,将提示保存在代码中并不意味着要将它们排除在 Langfuse 之外。

3.2 Langfuse 存储每个部署的提示版本

应用程序使用的每个提示版本也会发布到 Langfuse。存储的提示包含模型调用的静态部分:

  • 系统指令
  • 用户消息模板

动态请求值不会存储在提示版本内部。相反,提示使用变量,例如:

  • source_document
  • user_context
  • any_other_inputs
  • preferences

变量名称与 generation input 中的字段匹配,之后也与 dataset input 匹配。Langfuse 支持其管理提示中的变量,这使得在运行实验时可以替换数据集值。官方提示变量文档涵盖了其机制。

3.3 Generation 链接到精确的提示版本

每个 generation observation 都链接到与该模型调用关联的提示版本。此链接标识静态模板。Observation input 标识动态值。

在此设置中,链接并不意味着应用程序在运行时从 Langfuse 获取了提示。它的意思是 Langfuse 拥有应用程序所使用提示的版本化记录。

Langfuse 建议将提示与使用它们的 generations 关联起来,因为指标和评估可以归因到相关的提示版本。参见将提示链接到 traces 和 generations

3.4 Observation input 存储业务输入

Generation input 包含用于渲染提示的最终动态值。对于通用的面向文档的操作,这些值可能包括:

  • source_document
  • user_context
  • any_other_inputs
  • preferences

3.5 Observation metadata 存储运行时上下文

并非与模型调用关联的每个字段都是提示输入。运行时信息属于 observation metadata,包括:模型和提供者、环境、尝试次数、延迟、token 用量、状态、路由信息、输入 schema 版本等。

一个有用的测试是询问更改该值是否应更改实验中渲染的提示。如果是,它可能是输入。如果否,且该值描述了调用的运行方式或位置,它可能是元数据。

3.6 Observation output 存储完整的接受结果

对于成功的调用,observation output 包含应用程序接受的完整结果。

4、各部分如何连接

None

该图包含两个相关路径。

生产路径很直接:

  1. 从代码加载提示。
  2. 与业务输入对象组合。
  3. 调用模型。
  4. 验证输出。

Langfuse 路径记录相同的操作:

  1. 将 generation 链接到匹配的提示版本。
  2. 将业务输入对象存储为 observation input。
  3. 将验证后的结果存储为 observation output。
  4. 将合适的成功 observation 整理到数据集中。
  5. 对该数据集运行其他兼容的提示版本。

Langfuse 参与记录、归因和实验。它不必参与生产提示渲染路径。

5、跨 DEV 和 PROD 的提示更改工作流

只有当发布提示版本是运维工作流的一部分时,数据模型才能正常工作。提示编辑在代码更改时并未完成。匹配的 Langfuse 版本和环境引用也需要更新。你可以在 agent.md 或技能文件中添加此内容。

以下是我使用的工作流。

5.1 更改代码拥有的提示

应用程序模板仍然是生产来源。如果提示引入、删除或重命名变量,generation input 契约会同时更改。

5.2 在 DEV 中发布新的提示版本

之前的版本仍可用于比较和历史记录。

5.3 在 PROD 中发布相同的提示内容

匹配的提示内容也会发布到生产 Langfuse 项目。DEV 和 PROD 可能分配不同的数字版本标识符。工作流不应假设一个项目中的版本 12 代表另一个项目中的版本 12。

记录每个环境中使用的精确提示引用。

5.4 根据代码拥有的模板验证存储的提示

部署前,请验证:

  • DEV 提示与代码拥有的模板匹配。
  • PROD 提示与代码拥有的模板匹配。
  • 系统和用户角色正确。
  • 提示变量名称与 observation input 键匹配。
  • 每个环境指向其预期的精确版本。

当预期的提示引用缺失或过时时,部署不应进行。即使应用程序不在运行时从 Langfuse 获取提示,这也可以被视为运维发布要求。

6、当结构添加太晚时会出什么问题

弱结构的成本很少在第一次 tracing 实现时出现。它出现在团队尝试使用数月积累的 observation 时。

6.1 数据集创建变成提取项目

如果业务值仅存在于编译后的提示内部,创建数据集需要自定义解析。该解析器必须理解分隔符、可选部分、历史提示格式以及可能随时间变化的转换。一个应该涉及选择和审查 observation 的任务变成了数据迁移。

6.2 缺失的输入无法总是恢复

如果有效的提示输入未被记录,原始调用无法被忠实地重放。该值可能仍存在于另一个数据库中,但它可能已更改。它可能是临时计算的。它可能被专门截断用于该请求。后来的重建与记录实际使用的值不是一回事。

6.3 实验因未解析的变量而失败

存储的提示可能期望 user_context,而数据集仅包含编译后的 user_prompt。上下文可能在该字符串内可见,但实验运行器无法将其映射到所需的变量。

6.4 提示比较变得不可靠

包含旧编译提示指令的数据集条目将之前处理的一部分带入每个实验。用新提示测试该行不再隔离提示更改。

6.5 提示历史与结果断开连接

没有精确的提示版本链接,性能无法可靠地归因到产生它的模板。自定义哈希或自由形式的版本字段可以提供帮助,但它不如直接连接到可检查的提示版本有用,后者也可用于实验。

7、实用检查清单

提示设计

  • 将完整的静态系统和用户模板存储在 Langfuse 中。
  • 为每个动态业务输入使用显式变量。
  • 匹配提示变量名称和数据集输入字段名称。
  • 准确保留聊天角色。
  • 将真实请求值排除在提示版本之外。
  • 当任务语义或输入契约实质不同时,创建单独的提示变体。
  • 如果这是你想要的部署模型,保持生产渲染由代码拥有。

Generation observations

  • 将每个 generation 链接到其精确的提示名称和版本。
  • 记录重放所需的所有有效业务输入。
  • 记录提示渲染器使用的最终值,而非早期的请求表示。
  • 每个业务值只存储一次。
  • 不要在规范的数据集就绪输入中放置编译后的系统和用户提示。
  • 将模型、提供者、环境、延迟、用量、尝试次数和状态保存在 metadata 中。
  • 存储成功尝试的完整验证输出。
  • 保留失败尝试用于调试。
  • 将失败尝试排除在数据集之外。

数据集

  • 使数据集输入匹配提示变量契约。
  • 使用成功的验证输出作为候选预期输出。
  • 在将其视为真实数据之前审查或修正输出。
  • 保留对源 observation 的引用。
  • 为不兼容的提示变体保持单独的数据集。
  • 在共享输入契约的提示版本之间复用一个数据集。
  • 当操作重试时,选择最终的成功尝试。
  • 避免将编译后的提示指令带入数据集输入。

提示发布

  • 将每次提示编辑视为新版本。
  • 将相同的提示内容发布到 DEV 和 PROD。
  • 记录每个环境中使用的精确版本。
  • 不要假设数字版本跨项目匹配。
  • 根据代码拥有的模板验证两个存储的版本。
  • 验证提示变量与 observation input 键匹配。
  • 当所需的提示引用缺失或过时时,阻止部署。
  • 保留以前的版本用于历史和比较。
  • 在完成更改之前,至少运行一个具有代表性的数据集实验。

原文链接: How I Structure Langfuse Prompts, Observations, and Datasets

汇智网翻译整理,转载请标明出处