Chapter 07

工具链实战:Braintrust / LangSmith / Promptfoo

自己写评估脚本很快会遇到数据集管理、实验版本、回归对比、CI 集成的痛点。本章带你用当下最主流的三个平台把这些苦活变成一行命令。

三大平台定位对比

平台定位强项弱项
Braintrust商业化评估平台,SaaSUI 对比最清晰;trace/eval 一体化;实验版本管理优秀收费,国内访问慢
LangSmithLangChain 同门,偏可观测Trace 观测强;与 LangChain 生态无缝Evaluation 相对轻;绑定 LC 生态较深
Promptfoo开源 CLI,本地优先完全本地;YAML 配置;CI 最友好;免费UI 相对朴素;scale 到大团队需自建

选型决策树

你要什么? │ ┌────────────┼────────────┐ ▼ ▼ ▼ 开源本地 团队协作 已用 LangChain │ │ │ ▼ ▼ ▼ Promptfoo Braintrust LangSmith │ 预算有限? │ Promptfoo + 自建看板
实用建议 小团队/个人:Promptfoo 起步,CI 集成,本地可控。
中大团队:Braintrust 做协作,对比/回归/分享有完整 workflow。
LangChain 重度用户:LangSmith,省掉对接成本。
三者可共存:Promptfoo 跑 CI 冒烟 + Braintrust 做主评估库。

Promptfoo 实战

最小例子

# promptfooconfig.yaml
description: "客服意图分类评估"

prompts:
  - "你是一个客服意图分类器,把下面的用户输入归类为:query/complaint/refund/other。只返回标签。\n\n用户: {{input}}"

providers:
  - openai:gpt-4o-mini
  - openai:gpt-4o

tests:
  - vars: { input: "我的订单怎么还没到?" }
    assert:
      - type: equals
        value: "query"
  - vars: { input: "这质量也太差了吧!" }
    assert:
      - type: equals
        value: "complaint"
  - vars: { input: "我想退货" }
    assert:
      - type: equals
        value: "refund"
  - vars: { input: "能给我推荐点好吃的吗" }
    assert:
      - type: equals
        value: "other"

# 运行: npx promptfoo eval
# 可视化: npx promptfoo view

断言类型(assertions)

assert:
  - type: equals              # 精确匹配
    value: "positive"
  - type: contains            # 包含子串
    value: "发票"
  - type: regex               # 正则
    value: "^\\{.*\\}$"
  - type: is-json             # JSON 合法
  - type: contains-json       # 包含合法 JSON
  - type: similar             # 语义相似
    value: "退款流程说明"
    threshold: 0.8
  - type: llm-rubric          # LLM 裁判
    value: "回答是否专业且简洁"
  - type: latency             # 延迟
    threshold: 2000
  - type: cost                # 成本
    threshold: 0.01
  - type: javascript          # 自定义 JS
    value: "output.length < 500 && !output.includes('竞品')"

CI 集成

# .github/workflows/evals.yml
name: Evals
on: [pull_request]
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20" }
      - run: npx promptfoo eval --no-progress-bar --max-concurrency 4
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      - run: npx promptfoo share  # 生成可分享链接

Braintrust 实战

核心概念

Dataset
评估集。在 UI 或 API 里管理 input/expected 字段,支持版本和 tag。
Experiment
一次跑 eval 的结果。每次跑生成新的 experiment,与历史对比 diff。
Scorer
打分函数。内置 ExactMatch、Levenshtein、Factuality、Moderation,也支持自定义。
Project
组织维度。一个应用一个 project,内含多个 dataset/experiment/log。

SDK 例子

from braintrust import Eval, init_dataset
from autoevals import Factuality, LevenshteinScorer

# 评估集可以本地,也可以从 Braintrust 拉
dataset = init_dataset(project="customer-support", name="prod-samples-v3")

def my_agent(input: str) -> str:
    # 你的 LLM 应用
    rsp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": input}],
    )
    return rsp.choices[0].message.content

Eval(
    "customer-support",
    data=dataset,
    task=my_agent,
    scores=[Factuality(), LevenshteinScorer()],
    experiment_name="gpt4o-mini-v7-tone-tweak",
    metadata={"model": "gpt-4o-mini", "prompt_version": "v7"},
)

运行完,Braintrust UI 会显示:

自定义 Scorer

from autoevals import Score

def has_citation(output, expected=None):
    score = 1.0 if "[source:" in output else 0.0
    return Score(
        name="HasCitation",
        score=score,
        metadata={"output_length": len(output)},
    )

LangSmith 实战

起点:观测再评估

LangSmith 最强的是 trace 观测。接入即送全链路记录:

import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "ls_..."
os.environ["LANGSMITH_PROJECT"] = "customer-support"

from langsmith import traceable

@traceable(run_type="chain")
def my_agent(q):
    # 每次调用自动记录到 LangSmith
    return llm_call(q)

从 trace 构建评估集

LangSmith 的独特工作流:先观测生产流量,挑代表性/问题 case 一键加入 dataset,然后跑评估。

from langsmith import Client
from langsmith.evaluation import evaluate

client = Client()

def my_agent(inputs):
    return {"output": llm_call(inputs["question"])}

def correctness_evaluator(run, example):
    pred = run.outputs["output"]
    ref  = example.outputs["answer"]
    score = llm_judge(pred, ref)
    return {"key": "correctness", "score": score}

evaluate(
    my_agent,
    data="customer-support-v3",       # 数据集名
    evaluators=[correctness_evaluator],
    experiment_prefix="sonnet-4-6-v2",
    max_concurrency=8,
)

三个平台的 diff 视图对比

对比"同一条 case 在 v6 vs v7 的输出"是日常最高频操作:

Braintrust

  • 列表模式:左右两列横向 diff,直接看哪些红哪些绿
  • 关注"regressions"视图:只看退步 case
  • 带 metadata 筛选(按 model、prompt_version)

LangSmith / Promptfoo

  • LangSmith:实验对比页,按 score 差值排序
  • Promptfoo:表格模式,多 provider 并列同一行
  • Promptfoo CLI 可直接生成 HTML 报告

多平台混用模式

# 典型架构
Dev 本地:        Promptfoo  (快速迭代 prompt)
CI (每 PR):      Promptfoo  (快跑 50 条冒烟)
主评估 (每晚):   Braintrust (全量 1000 条,带 Judge)
生产 trace:      LangSmith  (持续观测)

本地自建方案

如果因为合规/成本/网络不能用 SaaS,最小本地栈:

# 用 SQLite + Streamlit 搭一个最小评估看板
import sqlite3, json
from datetime import datetime

conn = sqlite3.connect("evals.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS runs (
  id INTEGER PRIMARY KEY,
  ts TEXT,
  experiment TEXT,
  case_id TEXT,
  input TEXT,
  output TEXT,
  expected TEXT,
  scores TEXT,      -- JSON
  metadata TEXT     -- JSON
)""")

def log_run(exp, case_id, inp, out, expected, scores, meta):
    conn.execute(
        "INSERT INTO runs (ts, experiment, case_id, input, output, expected, scores, metadata) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
        (datetime.utcnow().isoformat(), exp, case_id, inp, out, expected,
         json.dumps(scores), json.dumps(meta)),
    )
    conn.commit()

搭配 Streamlit 做看板,200 行能做出"能用的 mini Braintrust"。

选型的 5 个判断题

  1. 合规:数据能不能出境?能 → 三个都行;不能 → Promptfoo 本地 + 自建
  2. 团队规模:个人/2-3 人 → Promptfoo;5+ → Braintrust
  3. 是否用 LangChain:深度使用 → LangSmith;否则 → Braintrust
  4. 是否需要和 trace 深度集成:需要 → LangSmith / Braintrust;只做离线 eval → Promptfoo
  5. 预算:可花 → 选付费;零预算 → Promptfoo + LangSmith 免费额度

本章小结