tomnio/rubric · 上手攻略

  • 仓库:tomnio/rubric
  • 链接:https://github.com/tomnio/rubric
  • 分类:LLM 应用 / 结构化提取
  • 作者:Tom
  • 更新:2026-09-15

是什么

Rubric 是一个 Schema-first 结构化提取库:你定义一个 Zod schema,它把这个 schema 挂到 LLM 请求上,从回复中抽出 JSON,校验其是否符合 schema,不符合就自动 reask(重问),直到返回合规结果或重试次数耗尽。

核心能力:定义 schema → 提取 JSON → 校验 → reask 循环(最多 3 次),全程类型安全,原生 SDK 不被修改。

解决什么问题

LLM 返回结构化数据是个老大难问题:

  • 纯 prompt 引导 JSON:输出不稳定,模型可能逸出格式边界
  • Force JSON mode:模型仍可能返回非法 JSON 或不符合 schema 的字段
  • 手动解析 + 重试:每个项目都要写一套 try/catch/retry 逻辑,重复劳动
  • 类型丢失:LLM 输出是字符串,拿不到 TypeScript 类型

Rubric 把「提取 → 校验 → 不合规就重问」封装成一次 API 调用,你拿到手的直接是 Zod 校验后的类型对象。

快速安装

# 必需
pnpm add @tomnio/rubric
pnpm add zod@^3.24.0    # ⚠️ 必须是 3.x,Zod 4 不满足 peer 依赖

# 可选(按 provider)
pnpm add openai
pnpm add @anthropic-ai/sdk
pnpm add @google/genai

⚠️ 需要 Node 20+,不支持 Deno / Bun(截至当前版本)。

核心用法

基本提取(OpenAI + TOOLS 模式)

import OpenAI from "openai"
import { z } from "zod"
import { wrap } from "@tomnio/rubric"

const User = z.object({
  name: z.string(),
  age: z.number().int(),
})

const client = wrap(new OpenAI(), { mode: "TOOLS" })

const user = await client.create({
  model: "gpt-5.6-luna",
  schema: User,
  messages: [{ role: "user", content: "John is 25 years old" }],
})

console.log(user) // { name: "John", age: 25 }

wrap() 不 patch SDK,原生 client 完全不受影响,随时可取回用。

Anthropic(ANTHROPIC_TOOLS 模式)

import Anthropic from "@anthropic-ai/sdk"
import { wrap } from "@tomnio/rubric"

const client = wrap(new Anthropic(), { mode: "ANTHROPIC_TOOLS" })

const user = await client.create({
  model: "claude-sonnet-4-6",
  schema: User,
  messages: [{ role: "user", content: "John is 25 years old" }],
})

Google Gemini(GEMINI_JSON 模式)

import { GoogleGenAI } from "@google/genai"
import { wrap } from "@tomnio/rubric"

const client = wrap(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }), {
  mode: "GEMINI_JSON",
})

const result = await client.create({
  model: "gemini-2.5-pro",
  schema: User,
  messages: [{ role: "user", content: "John is 25 years old" }],
})

兼容 OpenAI 格式网关(DeepSeek / Groq / OpenRouter / Moonshot 等)

import OpenAI from "openai"
import { compatible, wrap } from "@tomnio/rubric"

wrap(
  new OpenAI({
    apiKey: process.env.DEEPSEEK_API_KEY,
    baseURL: compatible.deepseek.baseURL, // "https://api.deepseek.com"
  }),
  { mode: compatible.deepseek.mode }, // "TOOLS"
)

⚠️ baseURL 必须是 /v1 根路径(https://api.deepseek.com/v1),不要填完整 chat/completions 路径(SDK 会自己追加,变成 /v1/chat/completions/chat/completions,返回 404)。

cited() 引用校验

import { cited } from "@tomnio/rubric"

const UserWithCitation = cited(User)

const user = await client.create({
  model: "gpt-5.6-luna",
  schema: UserWithCitation,
  messages: [
    { role: "user", content: "John is 25 years old and lives in Beijing." },
  ],
  context: "John is 25 years old and lives in Beijing.", // 来源文本
})
// 若模型引用了文本中不存在的内容,会触发 reask

maybe() 安全结果

import { maybe } from "@tomnio/rubric"

const result = maybe(user)
if (result.error) {
  console.log(result.message) // 提取失败原因
} else {
  console.log(result.result) // 合规结果
}

llmRefine() LLM 评判校验

import { llmRefine } from "@tomnio/rubric"

const UserWithJudge = User.extend({
  name: llmRefine("name should be a common human name, not a nickname", client),
})

tokenBudget 防超额

const result = await client.create({
  model: "gpt-5.6-luna",
  schema: User,
  messages: [{ role: "user", content: "..." }],
  tokenBudget: 20_000, // 累计达到预算上限即停止 reask 循环
})

流式提取(不完整对象)

// createPartial: 实时获取不完整对象(流式 SSE)
for await (const partial of client.createPartial({
  model: "gpt-5.6-luna",
  schema: User,
  messages: [{ role: "user", content: "John is 25 years old" }],
})) {
  console.log(partial) // { name: "John" } → { name: "John", age: 25 }
}

// createIterable: 流式列表,每项完整后立即返回
for await (const item of client.createIterable({
  model: "gpt-5.6-luna",
  schema: z.array(Item),
  messages: [{ role: "user", content: "..." }],
})) {
  console.log(item) // 每次拿到一个完整 Item
}

Hooks 生命周期

const client = wrap(new OpenAI(), {
  mode: "TOOLS",
  onRequest: (kwargs, attempt) => console.log("request", attempt),
  onError: (err, attempt) => console.log("error", attempt, err.message),
  onParseError: (raw, attempt) => console.log("parse error", attempt),
  onSuccess: (result, attempt) => console.log("success", attempt),
  onUsage: (usage, attempt) => console.log("tokens used", usage),
})

测试注入(fake client)

wrap({
  async chatCompletionsCreate(kwargs) {
    return { choices: [{ message: { tool_calls: [/* ... */] } }] }
  },
})

模式详解

模式 Schema 挂载方式 JSON 来源
TOOLS(默认) tools + tool_choice: required tool_calls[].function.arguments
JSON_SCHEMA response_format.json_schema(strict:true) message.content
MD_JSON system prompt + markdown fence 最后一个完整 JSON span
ANTHROPIC_TOOLS input_schema + tool_choice content[].tool_use.input
GEMINI_JSON config.responseJsonSchema textcandidates[].content.parts

⚠️ JSON_SCHEMA 模式强制 strict: true,Zod schema 必须 strict-safe(无 .optional() 键),否则 Rubric 在 prepareRequest 阶段本地报错。遇到此限制改用 TOOLSMD_JSON

⚠️ DeepSeek 等 thinking 模型(如 flash 变体)可能拒绝 tool_choice: required(400 Thinking mode does not support this tool_choice),建议用 MD_JSON 模式。

典型适用场景

场景 说明
表单/问卷提取 LLM 读取自然语言表单,返回结构化字段
文档解析 发票、合同、简历 → JSON 对象
RAG 结果结构化 检索结果 → cited() 校验引用准确性
多模型统一接口 一个代码库切换 OpenAI/Anthropic/Gemini
数据清洗/验证管道 LLM 输出 → Zod 校验 → reask → 合规数据
列表/批量提取 z.array() + createIterable() 流式处理大批量

坑与注意

⚠️ Zod 版本锁定:peer 依赖声明 zod@^3.24.0,裸 pnpm add zod 会拉 Zod 4(不满足 peer),导致运行时错误。必须显式指定版本pnpm add zod@^3.24.0

⚠️ baseURL 填写规范:填到 /v1 根,不要包含 /chat/completions,SDK 自动追加。

⚠️ thinking 模型 + TOOLS 模式:部分 flash 变体拒绝 tool_choice,改用 MD_JSON 模式。

⚠️ tokenBudget 不支持流式createPartial() / createIterable() 流式场景无 reask 循环,tokenBudget 对流式调用无效。

⚠️ provider 必须返回 usage metadata:tokenBudget 依赖 usage 字段计量;若 provider 响应不含 usage,抛出 TokenUsageUnavailableError 而不是 blind retry。

⚠️ 数组根类型传输:Zod z.array(T) 传输时被包装为 { items: T[] },提取后需自行解包(或直接用 z.array(T) 解构,Rubric 负责处理)。

⚠️ maxRetries: 0:表示仅一次尝试(原请求本身),不算重试。耗尽重试次数后抛出 RetryExhaustedError

与同类对比

特性 Rubric Pydantic AI (Python) Instructor (Python) Outlines (Python)
Schema 定义 Zod Pydantic Pydantic LM Format / JSON Schema
Reask 机制 ✅ 自动循环 ✅ Refinements ⚠️ 需 grammar 约束
引用校验 ✅ cited()
LLM Judge ✅ llmRefine()
多 provider ✅ OpenAI/Anthropic/Gemini/兼容网关 主要 OpenAI OpenAI/Anthropic
tokenBudget
流式提取 ✅ createPartial/Iterable
运行时语言 TypeScript Python Python Python
SDK patch ❌(wrap 不修改原对象)

Rubric 适合 TypeScript 生态 + 多 provider 场景 + 需要引用校验 的项目。若已在 Python 技术栈,Pydantic AI 或 Instructor 更成熟。

一句话推荐结论

TypeScript 结构化 LLM 提取首选:用 Zod 定义 schema → Rubric 自动处理「挂载 → 提取 → 校验 → reask」全套流程,支持 OpenAI/Anthropic/Gemini 等多 provider,cited() 引用校验和 llmRefine() LLM 评判是额外亮点——适合所有需要稳定结构化 JSON 输出的 LLM 应用。