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 |
text 或 candidates[].content.parts |
⚠️ JSON_SCHEMA 模式强制 strict: true,Zod schema 必须 strict-safe(无 .optional() 键),否则 Rubric 在 prepareRequest 阶段本地报错。遇到此限制改用 TOOLS 或 MD_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 应用。