在 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 API | Observations 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。
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_url | https://cloud.langfuse.com | Langfuse 主机。自托管服务器使用自己的 URL |
public_key、secret_key | 无 | 项目的 API 密钥 |
poll_interval | 30 | 两次轮询之间的秒数 |
lookback_hours | 24 | 第一次轮询回溯多远 |
settle_seconds | 30 | 轨迹结束后,等待多少秒再取走该轨迹 |
max_trace_seconds | 3600 | 超过该秒数后,未结束的轨迹也会被取走 |
轮询器取走的轨迹
轮询器在给定时间窗口内读取 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 下。转换文件的命令如下。
python -m potato.trace_converter \
--input langfuse_traces.json \
--input-format langfuse \
--output data/traces.jsonl该命令为每条轨迹写出一行 JSON,--auto-detect 会根据首项带有 type 的 observations 列表识别该格式。转换器对各字段的映射如下。
| Langfuse 字段 | Potato 字段 |
|---|---|
轨迹 id | id |
轨迹 name | agent_name |
轨迹 input.query、input.input、input.question,或字符串形式的 input | task_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 条目换成下面的内容。
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 密钥检查发送情况。
curl -H "X-API-Key: $KEY" http://localhost:8000/admin/api/trace_writeback响应会统计待发送、已发送、失败和被搁置的分数数量,并给出每条被搁置轨迹正在等待的原因。
Potato 导出中的结果
每条轨迹,无论是轮询到的还是转换得到的,其标注都保存在 annotation_output/<user>/user_state.json 中。agent_eval 导出会按轨迹汇总答案,为每个单选标注方案给出标签分布和多数结果,为每个数值标注方案给出平均值。
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 的轮询器读取这些观测,并把每个观测转为评审者可以标注的一个步骤。
延伸阅读
- LLM 与智能体轨迹的人工审核队列和AI 智能体评估工具对比
- 智能体标注,介绍其他 14 种轨迹转换器
- 如何衡量 LLM 评判者与人工标注者之间的一致性,用于比较 Langfuse 的 LLM 评判分数与评审者标签
- Potato 与 LangSmith 和 Langfuse 的智能体评估对比
- 轨迹分数写回,位于 Potato 的源码文档中