如果你正在用 Opik Agent Optimizer 做 Prompt 优化,那么有一件事比选哪个优化算法更重要------数据集。优化器再聪明,如果喂给它的数据不对,或者数据划分不合理,最后得到的 Prompt 很可能只是"背答案",一到真实场景就翻车。
这篇文章会从零开始,把 Opik 中"定义数据集"这件事讲透。包括数据集长什么样、怎么创建、怎么过滤、怎么和 Hugging Face 打通、怎么做训练/验证拆分,以及哪些坑一定要避开。内容主要参考 Opik 官方文档,但我会尽量用大白话和实际例子来说明。

一、数据集在优化器里到底扮演什么角色
Opik 的优化器在评估候选 Prompt 时,会拿数据集里的每一条数据去跑。每跑一条,就根据你定义的 Metric 打一个分。优化器根据这些分数来判断:哪个 Prompt 更好,哪个方向值得继续改。
所以,数据集是优化器的"题库"。没有题库,优化器就不知道什么叫好、什么叫坏。你给它的数据越能代表真实场景,优化出来的 Prompt 就越可能在线上表现好。
官方文档里有一句话很关键:"Without datasets, it's not possible to steer the optimizer on what is good and bad." 翻译过来就是:没有数据集,你根本没法告诉优化器什么是好、什么是坏。
在 Opik 2.0 里,数据集和实验都是项目级别的。创建数据集时一定要指定 project_name,否则它可能跑到别的项目下面去,后面找起来麻烦。
二、数据集的结构:每条数据就是一个 JSON 对象
Opik 里的数据集,本质上就是一堆 JSON 对象。每个对象包含哪些字段,取决于你的 Prompt 模板和 Metric 需要什么。官方给了一个简单的表格:
| 字段 | 用途 |
|---|---|
inputs(比如 question、context) |
这些值会被替换到 ChatPrompt 的占位符里 |
answer / label |
标准答案,Metric 会拿它来对比 |
metadata |
任意字典,用来打标签,比如场景、拆分、难度 |
这里要注意:Schema 不是强制的。你不需要预先定义一套完整的结构。只定义你的 Prompt 和 Metric 真正会用到的字段就行。 比如你的 Prompt 里有一个 {question} 占位符,那么每条数据里就必须有一个 question 键。如果你的 Metric 需要标准答案,那就再加一个 answer 键。其他的字段,想加就加,不加也不影响运行。
metadata 特别有用。比如你可以用 metadata["split"] = "train" 来标记这条数据属于训练集,或者用 metadata["difficulty"] = "hard" 来标记难度。后面分析结果的时候,这些标签能帮你快速筛选和定位问题。
三、创建或加载数据集的三种方式
Opik 提供了几种创建数据集的途径,你可以根据手头的数据情况来选择。
方式一:直接用 SDK 创建
这是最直接的方式。用 opik.Opik() 拿到客户端,然后 get_or_create_dataset 创建或获取一个数据集,最后用 insert 插入数据。
python
import opik
client = opik.Opik()
dataset = client.get_or_create_dataset(name="agent-opt-support", project_name="my-project")
dataset.insert([
{"question": "Summarize Opik.", "answer": "Opik is an LLM observability platform."},
{"question": "List two optimizer types.", "answer": "MetaPrompt and HRPO."},
])
这个例子创建了一个叫 agent-opt-support 的数据集,里面放了两条问答数据。project_name 指定为 my-project,这样数据集就归属到正确的项目下。
方式二:从文件上传
如果你已经有一份 CSV 或者 Parquet 文件,列名和 Prompt 变量对应,那就可以用 Python 读进来,然后调用 dataset.insert(...) 批量插入。Opik 的 Dataset SDK 提供了相关辅助方法。
上传之前,建议先确认几件事:
- 列名是否和 Prompt 里的占位符一致?比如 Prompt 里是
{question},文件里列名也得是question。 - 如果打算用
metadata做过滤,文件里是否包含了对应的列? - 上传之后,去 Opik UI 的 Datasets 标签页里扫一眼,确认行数和内容都对。
方式三:使用内置示例数据集
如果你只是想快速跑通流程,或者做个小实验,Opik 优化器 SDK 自带了一些现成的数据集:
python
from opik_optimizer import datasets
hotpot = datasets.hotpot(count=300)
tiny = datasets.tiny_test()
这些数据集放在 sdks/opik_optimizer/src/opik_optimizer/datasets 目录下,和 notebook 示例是对应的。大多数辅助函数都支持一些常见的切片控制参数:
split:比如"train"、"validation"count和start:切片大小和偏移量(打乱之后)seed:确定性的随机种子filter_by:切片前先过滤行
有些辅助函数还支持 prefer_presets,用来覆盖数据集自带的预设。
内置数据集的好处是省事,适合验证想法。但真正做项目时,还是建议用自己的数据,因为只有你自己的数据才能代表真实场景。
四、过滤数据集行:只取你想要的那部分
有时候你不想用整个数据集,只想取某个子集。比如只取某个任务 ID 的数据,或者只取某种类型的数据。这时候就可以用 filter_by。
filter_by 支持三种过滤方式:
- 精确匹配 :
{"task_id": "e57337a4"} - 成员关系 :
{"type": {"bridge", "comparison"}},表示 type 是 bridge 或 comparison - 可调用对象 :
{"task_id": lambda value: value.startswith("e57")},用函数来自定义逻辑
举个例子:
python
from opik_optimizer.datasets import arc_agi2, hotpot
dataset = arc_agi2(
split="train",
count=1,
prefer_presets=False,
filter_by={"task_id": "e57337a4"},
)
dataset = hotpot(
split="validation",
count=100,
filter_by={"type": {"bridge", "comparison"}},
)
第一个例子从 arc_agi2 里只取 task_id 为 e57337a4 的那一条。第二个例子从 hotpot 的验证集里取 100 条,且类型必须是 bridge 或 comparison。
过滤的好处是,你可以针对特定场景做优化。比如你发现模型在"比较类"问题上表现差,就可以专门过滤出这类数据来训练。
五、把 Hugging Face 数据集接入 Opik
很多开发者手里已经有 Hugging Face 数据集。Opik 完全可以和它打通:先用 datasets.load_dataset 加载,然后把需要的字段整理成 Opik 能接受的格式,最后插入 Opik 数据集。
python
from datasets import load_dataset
import opik
hf = load_dataset("your-org/your-dataset", split="train")
records = [
{"question": row["question"], "answer": row["answer"]}
for row in hf.select(range(200))
]
client = opik.Opik()
dataset = client.get_or_create_dataset("your-hf-train", project_name="my-project")
dataset.insert(records)
这个例子从 Hugging Face 加载数据集,取前 200 条,只保留 question 和 answer 两个字段,然后插入 Opik。
如果你希望 Hugging Face 数据集也能像内置数据集那样支持 split、count、filter_by 这些接口,可以按照 sdks/opik_optimizer/src/opik_optimizer/datasets 里的模式,用 DatasetSpec 和 DatasetHandle 写一个自定义辅助函数。这样用起来会更顺手。
六、训练/验证拆分:防止过拟合的关键一步
这是整篇文章最重要的部分之一。过拟合是指:优化后的 Prompt 在训练数据上表现很好,但遇到新数据就不行了。原因很简单------优化器可能把训练数据里的噪声或特例也学进去了。
要避免过拟合,就必须把数据拆开:
- 训练数据集(70%--80%):优化器用它来分析失败、生成改进方案。
- 验证数据集(20%--30%):用来评估和排名候选 Prompt,帮助选出泛化能力好的那个。
- 测试数据集(可选,单独留出):完全不动,等优化结束后再用来测最终的真实表现。
优化器用训练集"学习",用验证集"选择",这样选出来的 Prompt 才更可能在没见过的数据上也表现好。
来看一个拆分的代码示例:
python
import opik
client = opik.Opik()
# 创建训练数据集(占 70-80%)
training_dataset = client.get_or_create_dataset(name="agent-opt-train", project_name="my-project")
training_dataset.insert([
{"question": "What is Opik?", "answer": "Opik is an LLM observability platform."},
{"question": "List optimizer types.", "answer": "MetaPrompt, Evolutionary, etc."},
# ... 更多训练样本
])
# 创建验证数据集(占 20-30%)
validation_dataset = client.get_or_create_dataset(name="agent-opt-val", project_name="my-project")
validation_dataset.insert([
{"question": "Explain Opik's purpose.", "answer": "Opik helps monitor LLMs."},
{"question": "Name two optimizers.", "answer": "GEPA and Few-Shot Bayesian."},
# ... 更多验证样本
])
# 优化时同时传入训练集和验证集
result = optimizer.optimize_prompt(
prompt=my_prompt,
dataset=training_dataset,
validation_dataset=validation_dataset,
metric=my_metric,
)
拆分时要注意几点:
- 70/30 或 80/20 是比较标准的比例。
- 两个集合都要有多样性,覆盖不同场景。
- 验证集在 Prompt 开发过程中要保持"未见",不能拿来做训练。
- 两个集合的数据分布要一致,否则验证结果没有意义。
在留出数据上做最终测试
优化结束后,别急着上线。拿一个完全没参与过优化的测试数据集,再跑一遍最终 Prompt,看看它在"陌生数据"上的表现。
python
from opik.evaluation import evaluate_prompt
# 优化完成后,在未见过的数据上测试
test_dataset = client.get_dataset(name="agent-opt-test")
test_results = evaluate_prompt(
prompt=result.prompt, # 优化得到的最佳 Prompt
dataset=test_dataset,
scoring_metrics=[my_metric],
task_threads=4,
project_name="my-project",
)
print(f"Test score: {test_results.mean_scores}")
这个最终得分,才是你对"上线后效果"最有参考价值的估计。如果测试得分和验证得分差距很大,说明可能还是过拟合了,需要回头调整数据或优化策略。
七、最佳实践:让数据集真正为优化服务
官方文档列了一些最佳实践,我结合自己的理解展开说一下。
1. 优化过程中保持数据集不可变。
一旦开始跑优化,就不要往数据集里加数据或改数据。否则不同轮次的评估标准就不一致了,结果没法比较。如果需要加数据,创建一个新的数据集版本。
2. 一定要用验证数据集。
这是避免过拟合最有效的手段。不要为了省事只用一个数据集。70/30 或 80/20 的拆分是最低要求。
3. 如果是 RAG 类 Prompt,记得记录 context 字段。
RAG 场景下,模型回答不好很可能是因为检索到的上下文缺失或不对。把 context 记下来,失败分析时就能一眼看出是不是检索环节出了问题。
4. 用 metadata 跟踪拆分信息。
即使你已经把训练集和验证集分成了两个数据集,也可以在 metadata 里再标一下 metadata["split"] = "eval",方便后续做更细粒度的组织。
5. 写清楚数据集的所有者。
用数据集描述说明这个集合是谁维护的、用来做什么的。团队协作时,这一点非常重要,否则过几个月没人知道这个数据集是干嘛的。
6. 保持 Schema 和 Prompt 同步。
如果你的 Prompt 里用到了 {context},那么每条数据里都必须有 context 这个键。要么在数据里补上,要么在优化器里提供默认值。否则跑起来就会报错。
八、验证清单:上传前后检查这些事项
为了避免低级错误,建议在上传数据集前后做以下检查:
- 在 Opik 的 Datasets 标签页里确认行数是否正确。也可以在 Python 里用
len(dataset.get_items())来查。 - 在 Dashboard 的数据集查看器里抽查几行,看看内容对不对。
- 如果数据里包含多模态资源或工具调用负载,跑一次优化后确认它们是否出现在 trace 树里。
- 先用少量数据跑一次小批量优化,端到端验证整个流程是否通畅。
这些小检查花不了几分钟,但能帮你省下大量调试时间。
九、下一步:定义 Metric,然后开始优化
数据集准备好之后,下一步就是定义 Metric。Metric 决定了优化器怎么打分,也就决定了它往哪个方向努力。Opik 文档里有一篇"Define metrics"专门讲这个。
定义好 Metric,就可以按照"Optimize prompts"的指引启动实验了。如果你需要领域特定的打分方式,还可以扩展数据集,加一些额外字段,然后在自定义 Metric 里引用它们。
写在最后
数据集这件事,看起来简单,实际上非常关键。它决定了优化器能学到什么、能泛化到什么程度。一个设计良好的数据集,应该覆盖真实场景的多样性,有清晰的训练/验证拆分,并且和你的 Prompt、Metric 保持同步。
Opik 在数据集管理上提供了不少便利:SDK 创建、文件上传、内置示例、Hugging Face 接入、过滤、拆分,基本覆盖了常见需求。花点时间把数据集整理好,后面优化起来会顺畅很多。
希望这篇指南能帮你把"定义数据集"这一步走扎实。接下来,就去定义你的 Metric,然后跑起第一轮优化吧。