跳转到内容

在 Potato 中审阅 Langfuse 轨迹

通过轮询 Langfuse 项目或转换导出文件,把 Langfuse 轨迹拉进 Potato,让多名评审者审阅每条轨迹,并把答案作为分数写回。

Potato 以两种方式读取 Langfuse 轨迹:轮询 Langfuse 项目中已完成的轨迹,或用 python -m potato.trace_converter --input-format langfuse 转换轨迹文件。被轮询到的轨迹,其评审者合并后的答案会作为分数写回 Langfuse,挂在该轨迹和每个被评判的观测(observation)上;转换得到的轨迹,其结果保留在 Potato 的导出文件中。

Langfuse 是一个开源、可自托管的 AI 工程平台,用于调试和评估 LLM 与智能体应用,Langfuse 仓库在 ee 文件夹之外采用 MIT 许可。在 Langfuse 数据模型中,一条轨迹(trace)代表一次请求或操作。观测是轨迹内的一个步骤,例如一次 LLM 调用、一次工具调用或一个检索步骤,观测之间相互嵌套,以反映应用的结构。会话(session)把同一次用户交互产生的多条轨迹归在一起,分数则是附加在轨迹、观测或会话上的一个值。

在 Langfuse 自带的标注队列中,评审者依据分数配置为轨迹、观测和会话打分。Potato 在此基础上增加了每条轨迹由多名盲评审者审阅、评审者之间的一致性计算,以及对分歧的裁决,然后把结果写回 Langfuse 中的同一条轨迹。LLM 与智能体轨迹的人工审核队列对比了 Langfuse、LangSmith、Braintrust、Arize 和 Potato 的审阅功能。

轮询与导出文件的比较

下表比较了两条路径。

轮询(trace_ingestion)导出文件(trace_converter)
轨迹项目中在回溯窗口内开始的每条轨迹,之后每条新轨迹在结束时各取一次文件中的轨迹
Langfuse APIObservations API v2,旧版 traces API 作为后备无,因为文件由你自己生成
显示读取的条目字段steps,每个观测一项conversation,每个思考、动作或结果一轮
保留的观测类型所有类型GENERATION、SPAN 和 EVENT
会话 ID保存在 metadata.session_id 下不保留
向 Langfuse 发送分数是否

轮询适合希望在 Langfuse 中把评审后的分数放在每条轨迹旁边的团队。导出文件适合结果可以留在 Potato 中的固定样本,为论文或保留评估集挑选的轨迹就是典型的固定样本。

轮询 Langfuse 项目

下面的配置每分钟轮询一次 Langfuse Cloud,并把每条轨迹交给两名评审者。第一次轮询会取回过去七天内开始的所有轨迹。两名评审者都作答后,Potato 把每个合并后的答案作为名为 human.resolved、human.helpfulness 或 human.step_check 的分数发回该轨迹。该配置是完整的,可以通过 python -m potato.validate_cli --strict。

yaml
annotation_task_name: Langfuse trace review
task_dir: .
output_annotation_dir: annotation_output/
data_files:
  - data/traces.jsonl
item_properties:
  id_key: id
  text_key: task_description
user_config:
  allow_all_users: true
  users: []
 
# Pull every trace that started in the last 7 days, then poll once a minute.
# Keys for write-back come from LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY.
trace_ingestion:
  enabled: true
  api_key: change-me
  sources:
    - type: langfuse
      api_url: https://cloud.langfuse.com
      public_key: pk-lf-...
      secret_key: sk-lf-...
      poll_interval: 60
      lookback_hours: 168
  writeback:
    trigger: fully_annotated
    min_annotators: 2
    score_prefix: human
    langfuse:
      score_configs: true
 
num_annotators_per_item: 2
 
instance_display:
  fields:
    - key: steps
      type: agent_trace
      label: Langfuse trace
      display_options:
        collapse_observations: true
 
annotation_schemes:
  - annotation_type: radio
    name: resolved
    description: Did the agent answer the user's question correctly?
    labels: [correct, partly_correct, wrong]
  - annotation_type: likert
    name: helpfulness
    description: How helpful was the final answer?
    size: 5
    min_label: Not helpful
    max_label: Very helpful
  - annotation_type: process_reward
    name: step_check
    description: Mark each step correct or incorrect.
    steps_key: steps
    mode: per_step

用 potato start config.yaml -p 8000 启动服务器。Potato 在启动时需要一个数据文件,data/traces.jsonl 中已有的轨迹会与轮询到的轨迹一同进入队列。文件中的轨迹只有带有 steps 列表时,才会出现在上面的显示中。

trace_ingestion.sources 下每个 type: langfuse 的条目会启动一个轮询器。轮询器读取以下键:

键默认值含义
api_urlhttps://cloud.langfuse.comLangfuse 主机。自托管服务器使用自己的 URL
public_key、secret_key无项目的 API 密钥
poll_interval30两次轮询之间的秒数
lookback_hours24第一次轮询回溯多远
settle_seconds30轨迹结束后,等待多少秒再取走该轨迹
max_trace_seconds3600超过该秒数后,未结束的轨迹也会被取走

轮询器取走的轨迹

轮询器在给定时间窗口内读取 GET /api/public/v2/observations,并按轨迹 ID 对各行分组。没有该端点的服务器会改用旧版 GET /api/public/traces API。Langfuse 迁移指南指出,Langfuse Cloud 会提供旧版读取端点直到 2026 年 11 月 16 日,轮询器无需为切换做任何更改。当根观测有了结束时间且已过 settle_seconds 秒,这条轨迹就算已结束。根观测始终不结束的轨迹会在 max_trace_seconds 之后被取走,并把 metadata.complete 设为 false。Potato 会跳过条目库中已有的轨迹,因此重启不会重复添加任何轨迹。

每条轨迹成为一个条目,ID 为 langfuse_<trace id>。条目的 steps 列表按开始顺序保存子观测,每个步骤保留该观测的输入、输出、类型、名称、模型和观测 ID。轨迹名称成为 task_description,没有名称的轨迹则使用轨迹输入。会话 ID、用户 ID、标签、输入和输出放在 metadata 下。轮询器会取回窗口内的每条轨迹,不按名称、标签或环境过滤。自动化规则和分诊队列决定评审者先看到哪些被轮询到的轨迹。

转换 Langfuse 导出文件

langfuse 转换器读取一个轨迹对象,或由轨迹对象组成的列表,格式为 JSON 或 JSONL。每条轨迹都需要一个由完整观测对象组成的 observations 列表。旧版 GET /api/public/traces/{traceId} 端点返回的轨迹就是这种形状,而列表端点 GET /api/public/traces 返回的是观测 ID,不是对象。来自 v2 Observations API 的各行,在转换前必须按 traceId 分组成每条轨迹一个对象,观测放在 observations 下。转换文件的命令如下。

bash
python -m potato.trace_converter \
  --input langfuse_traces.json \
  --input-format langfuse \
  --output data/traces.jsonl

该命令为每条轨迹写出一行 JSON,--auto-detect 会根据首项带有 type 的 observations 列表识别该格式。转换器对各字段的映射如下。

Langfuse 字段Potato 字段
轨迹 idid
轨迹 nameagent_name
轨迹 input.query、input.input、input.question,或字符串形式的 inputtask_description
GENERATION 观测的输出(content、text 或 completion)conversation 中的一个 Agent (Thought) 轮次
SPAN 观测的名称和输入一个 Agent (Action) 轮次,写作 name(key=value, ...)
SPAN 观测的输出(result、output 或整个对象)一个 Environment 轮次
EVENT 观测的输出,没有输出时取输入一个 System (<name>) 轮次
观测数量、轨迹名称和模型metadata_table 中的行

转换器按文件顺序保留观测,并丢弃其他所有观测类型。Langfuse 的观测类型还包括 agent、tool、chain、retriever、evaluator、embedding 和 guardrail,使用这些类型埋点的轨迹在转换时会丢失相应步骤。轨迹输出、元数据、会话 ID 和观测级别同样不会被带过来。当轨迹依赖这些类型或字段时,请改为轮询项目。

对于转换得到的文件,请让显示和任何步骤级标注方案指向 conversation。在轮询配置中,把 instance_display 块以及 annotation_schemes 下的 step_check 条目换成下面的内容。

yaml
instance_display:
  fields:
    - key: task_description
      type: text
      label: User request
    - key: conversation
      type: agent_trace
      label: Langfuse trace
 
annotation_schemes:
  - annotation_type: process_reward
    name: step_check
    description: Click the first step that went wrong.
    steps_key: conversation
    mode: first_error

用于 Langfuse 轨迹的显示与标注方案

agent_trace 显示把每个步骤呈现为带类型徽标的卡片,既能读取轮询器产生的 steps 列表,也能读取转换器产生的 conversation 列表。三窗格 eval_trace 显示会把同样的步骤拆分为推理、函数调用和最终答案。请根据轨迹的来源路径,把显示的 key 设为 steps 或 conversation。

写回会发送来自 radio、likert、slider、number、select 和 multiselect 标注方案的轨迹级答案。步骤级问题使用 process_reward、trajectory_eval、tool_call_review、failure_attribution 或 gui_trajectory。对被轮询到的轨迹,凡是有 min_annotators 名评审者给出的步骤标签,都会成为该步骤自身观测上的一个分数。文本框记录的理由只进入 Potato 的导出,不会被发送。对于涉及整个 Langfuse 会话的问题,设置 enabled: true 的 sessions 块会按 metadata.session_id 对被轮询到的轨迹分组,带有 session_level: true 的标注方案在 /sessions 页面上每个会话只回答一次(见会话级评分)。

MAST 故障分类法和 τ-bench 智能体评估设计为智能体故障提供了现成的轨迹级标注方案。

Langfuse 中的分数

一旦有 min_annotators 名评审者作答,写回就会为每个标注方案发送一个分数。设置 trigger: adjudicated 时,Potato 改为等待裁决者的决定,而且无论哪种触发方式,裁决得到的值都会取代投票结果。每个分数命名为 <score_prefix>.<scheme name>,所带的值见下表。

标注方案分数的值Langfuse 数据类型
radio、select、带 labels 的 likert多数评审者选择的标签分类
multiselect超过半数评审者选择的标签,以逗号分隔分类
不带 labels 的 likert中位数分值数值
slider、number平均值数值
步骤级标注方案多数评审者给该步骤的标签,发送到该步骤的观测上分类

平票时不会发送任何内容,直到裁决者做出决定或另一名评审者打破平局。评论说明该值是如何得出的,例如 Potato: 2 of 2 annotator(s) chose correct。元数据包含评审者人数、各答案的计数,以及评审者两两之间意见一致的比例。不会发送任何用户名。设置 langfuse.score_configs: true 后,Potato 在第一次发送某个分数时,会创建一个以该分数命名的 Langfuse 分数配置。随后 Langfuse 会在筛选器中列出 resolved 的各个类别以及 1 到 5 的 helpfulness 范围。

当 LANGFUSE_PUBLIC_KEY 和 LANGFUSE_SECRET_KEY 两个变量都已设置时,写回从这两个变量读取 Langfuse 密钥,否则从 langfuse 来源读取。Potato 为每条轨迹和每个标注方案的分数赋予固定的 ID。只有当 ID、名称和日期都一致时,Langfuse 才会替换分数(见防止重复分数)。因此,在之后某一天发生变化的答案,会新增一个带当天日期的第二个分数。可以用管理员 API 密钥检查发送情况。

bash
curl -H "X-API-Key: $KEY" http://localhost:8000/admin/api/trace_writeback

响应会统计待发送、已发送、失败和被搁置的分数数量,并给出每条被搁置轨迹正在等待的原因。

Potato 导出中的结果

每条轨迹,无论是轮询到的还是转换得到的,其标注都保存在 annotation_output/<user>/user_state.json 中。agent_eval 导出会按轨迹汇总答案,为每个单选标注方案给出标签分布和多数结果,为每个数值标注方案给出平均值。

bash
python -m potato.export --config config.yaml --format agent_eval --output results/

导出格式列出了 jsonl、parquet、huggingface 和其他格式。会话级答案写入 annotation_output/session_annotations.jsonl。/admin/iaa 仪表板报告每个标注方案下评审者之间的一致性,标注者间一致性详解介绍了如何选择系数。数据集页面通过 Import ingested traces 从被轮询到的轨迹构建带版本的评估集。修正后的轨迹经由轨迹编辑成为 SFT 和 DPO 记录。

限制

  • 写回只覆盖被轮询到的轨迹。 来自数据文件的轨迹,无论是否经过转换,其结果都保留在 Potato 中。
  • 文本答案留在 Potato 中。 写回不发送任何自由文本答案,会话级答案同样不会发送。
  • 轮询器取走整个项目。 轨迹到达之后,采样和优先级由自动化规则和分诊队列决定。

常见问题

Langfuse 是什么?

Langfuse 是一个开源的 AI 工程平台,用于追踪 LLM 和智能体应用、评估这些轨迹并管理提示词。该仓库在 ee 文件夹之外采用 MIT 许可,Langfuse 可以作为 Langfuse Cloud 使用,也可以运行在你自己的服务器上。Potato 连接 Langfuse,让多名人工评审者审阅每条轨迹,并把他们合并后的答案作为分数发回 Langfuse。

Langfuse 为智能体可观测性记录了什么?

Langfuse 把每次智能体运行记录为一条轨迹,把运行中的每个步骤记录为嵌套在该轨迹下的观测。一个步骤可以是一次 LLM 调用、一次工具调用、一个检索步骤,或运行中的其他操作。观测有十种类型,包括 generation、agent、tool、retriever 和 guardrail,会话则把同一次用户交互的多条轨迹归在一起。当一条轨迹包含 span、event、generation 之外的观测类型时,Langfuse 会根据这些观测的时间和嵌套关系绘制一张智能体图。Potato 的轮询器读取这些观测,并把每个观测转为评审者可以标注的一个步骤。

延伸阅读