摘录
高级LLM文本生成和结构化JSON提取,包括验证、修复和流式传输。
特性
- 从LLM响应中提取多候选JSON
- 使用jsonrepair自动修复
- Zod模式验证和强制
- 验证失败的可选自我修复
- 流媒体支持
- MCP工具
- 矢量嵌入(兼容OpenAI+Voyage AI)
安装
安装 extrait 使用您首选的包管理器。
bun add extrait
# or
npm install extrait
# or
deno add npm:extrait快速开始
使用自定义的OpenAI兼容传输来指向 extrait 在本地端点。
import { createLLM, prompt, s } from "extrait";
import { z } from "zod";
const llm = createLLM({
provider: "openai-compatible",
model: "mistralai/ministral-3-3b",
transport: {
baseURL: "http://localhost:1234/v1",
apiKey: process.env.LLM_API_KEY ?? "local-demo-key",
},
});
const RecipeSchema = s.schema(
"Recipe",
z.object({
title: s.string().min(1).describe("Short recipe title"),
ingredients: s.array(s.string()).min(1).describe("Ingredient list"),
})
);
const result = await llm.structured(
RecipeSchema,
prompt`Extract a simple recipe from this text: """${text}"""`
);
console.log(result.data);示例一览
这些示例涵盖了存储库中最常见的使用模式。
examples/simple.ts-带流媒体的基本结构化输出examples/generate.ts-高级文本生成examples/streaming.ts-实时部分输出和快照更新examples/calculator-tool.ts-使用MCP工具进行结构化提取examples/streaming-turns-with-tools.ts-流式MCP转换、转换和推理块examples/conversation.ts-多回合提示和多模式内容examples/image-analysis.ts-视觉输入与结构化输出examples/embeddings.ts-嵌入和相似性工作流程
bun run dev simple "Bun.js runtime"
bun run dev generate "Bun.js runtime"
bun run dev streaming
bun run dev calculator-toolAPI 参考
以下部分介绍了图书馆的主要组成部分。
创建LLM客户端
使用 createLLM() 配置提供者、模型、传输和客户端默认值。
const llm = createLLM({
provider: "openai-compatible" | "anthropic-compatible",
model: "gpt-5-nano",
baseURL: "https://api.openai.com", // optional alias for transport.baseURL
apiKey: process.env.LLM_API_KEY, // optional alias for transport.apiKey
transport: {
baseURL: "https://api.openai.com", // optional
apiKey: process.env.LLM_API_KEY, // optional
path: "/v1/chat/completions", // optional; anthropic-compatible usually uses /v1/messages
headers: { "x-trace-id": "docs-demo" }, // optional extra headers
defaultBody: { user: "docs-demo" }, // optional provider body defaults
version: "2023-06-01", // anthropic-compatible only
fetcher: fetch, // optional custom fetch implementation
},
defaults: {
mode: "loose" | "strict", // loose allows repair
selfHeal: 1, // optional retry attempts
debug: false, // optional structured debug output
// or:
// debug: { enabled: true, verbose: true },
systemPrompt: "You are a helpful assistant.",
timeout: {
request: 30_000,
tool: 10_000,
},
},
});baseURL 和 apiKey 在最高层,有缩写别名 transport.baseURL 和 transport.apiKey对于特定请求的选项,例如 stream, request, schemaInstruction,以及解析调优,请参阅下面的部分。
常见的设置模式:
// OpenAI-compatible gateway or local endpoint with top-level aliases
const llm = createLLM({
provider: "openai-compatible",
model: "gpt-4o-mini",
baseURL: process.env.LLM_BASE_URL ?? "http://localhost:1234/v1",
apiKey: process.env.LLM_API_KEY ?? "local-demo-key",
});
// Anthropic-compatible endpoint with explicit API version
const anthropic = createLLM({
provider: "anthropic-compatible",
model: "claude-3-5-sonnet-latest",
transport: {
baseURL: "https://api.anthropic.com",
apiKey: process.env.LLM_API_KEY,
version: "2023-06-01",
},
});定义模式
使用 s Zod的包装器,用于模式名称、描述和更符合人体工程学的创作流程。
import { s } from "extrait";
import { z } from "zod";
const Schema = s.schema(
"SchemaName",
z.object({
// String fields
text: s.string().min(1).describe("Field description"),
optional: s.string().optional(),
withDefault: s.string().default("value"),
// Numbers
count: s.number().int().min(0).max(100),
score: s.number().min(0).max(1),
// Arrays
items: s.array(s.string()).min(1).max(10),
// Nested objects
nested: z.object({
field: s.string(),
}),
// Enums (use native Zod)
category: z.enum(["a", "b", "c"]),
// Booleans
flag: s.boolean(),
})
);进行结构化通话
structured() 接受模式加上标记的提示、流畅的提示构建器或原始消息有效负载。
// Simple prompt
const result = await llm.structured(
Schema,
prompt`Your prompt with ${variables}`
);
// Multi-part prompt
const result = await llm.structured(
Schema,
prompt()
.system`You are an expert assistant.`
.user`Analyze: """${input}"""`
);
// Multi-turn conversation
const conversationResult = await llm.structured(
Schema,
prompt()
.system`You are an expert assistant.`
.user`Hello`
.assistant`Hi, how can I help?`
.user`Analyze: """${input}"""`
);
// With options
const result = await llm.structured(
Schema,
prompt`Your prompt`,
{
mode: "loose",
selfHeal: 1,
debug: true,
systemPrompt: "You are a helpful assistant.",
stream: {
to: "stdout",
onData: (event) => {
if (event.delta.text) {
console.log("New visible text:", event.delta.text);
}
if (event.delta.reasoning) {
console.log("New reasoning text:", event.delta.reasoning);
}
console.log("Current visible text:", event.snapshot.text);
console.log("Current reasoning:", event.snapshot.reasoning);
console.log("Current structured snapshot:", event.snapshot.data);
if (event.done) {
console.log("Streaming done.");
}
},
},
request: {
signal: AbortSignal.timeout(30_000), // optional AbortSignal
reasoningEffort: "medium", // optional reasoning effort hint
},
timeout: {
request: 30_000, // ms per LLM HTTP request
tool: 10_000, // ms per MCP tool call
},
}
);prompt() 构建有序 messages 有效载荷。使用 ` prompt... 用于单个字符串提示,或用于多回合对话的流畅构建器。这 LLMMessage` 如果需要键入自己的消息数组,则导出类型。
在 stream.onData,事件分为两层:
event.delta.text仅是自上次事件以来新接收到的可见文本。event.delta.reasoning只是自上次事件以来新收到的推理文本。event.snapshot.text是迄今为止积累的完整可见文本。event.snapshot.reasoning是迄今为止积累的完全规范化推理。event.snapshot.data是迄今为止可以从流中解析的结构最好的JSON快照。它可能保持不变,同时event.delta.text继续增长。
典型用法是:
- 渲染
event.delta.text直接连接到终端或聊天UI - 可选渲染
event.delta.reasoning在单独的推理小组中 - 使用
event.snapshot.data驱动部分结构化UI状态 - 使用
event.snapshot.text/event.snapshot.reasoning当您需要完整的累积状态,而不仅仅是最新的增量时
您还可以通过以下方式传递提供者请求选项 request:
const result = await llm.structured(
Schema,
prompt`Summarize this document: """${text}"""`,
{
request: {
temperature: 0,
maxTokens: 800,
body: { user: "demo-user" },
},
}
);拨打短信
generate() 是非结构化生成的高级API。它接受与相同的提示形状 structured(),但不注入任何模式或解析输出。
// Simple prompt
const result = await llm.generate(
prompt`Write a short summary of ${topic}.`
);
// Multi-message prompt
const result = await llm.generate(
prompt()
.system`You are a concise assistant.`
.user`Summarize: """${text}"""`
);
// Raw messages payload
const result = await llm.generate({
prompt: {
messages: [
{ role: "user", content: "Say hello in one sentence." },
],
},
});流媒体镜像 structured(),但快照仅包含 text 和 reasoning:
const result = await llm.generate(
prompt`Explain ${topic} in one short paragraph.`,
{
stream: {
enabled: true,
onData: (event) => {
process.stdout.write(event.delta.text);
console.log("Full text so far:", event.snapshot.text);
console.log("Full reasoning so far:", event.snapshot.reasoning);
if (event.done) {
console.log("Streaming done.");
}
},
},
}
);提供商请求选项和MCP工具仍在进行中 request:
const result = await llm.generate(
prompt`Use tools if needed and answer the user clearly.`,
{
request: {
temperature: 0,
maxTokens: 800,
reasoningEffort: "medium",
mcpClients: [calculatorMCP],
maxToolRounds: 10,
},
}
);开 openai-compatible,此内容以如下方式发送 reasoning_effort,与 max 映射到 xhigh.On anthropic-compatible,此内容以如下方式发送 output_config.effort 并自动启用 thinking: { type: "adaptive" }.
对于现有的历史记录或多回合对话,请通过 messages 直接:
const messages = conversation("You are a helpful assistant.", [
{ role: "user", text: "What is the speed of light?" },
{ role: "assistant", text: "Approximately 299,792 km/s in a vacuum." },
{ role: "user", text: "How long does light take to reach Earth from the Sun?" },
]);
const result = await llm.generate({ prompt: { messages } });使用 llm.adapter.complete(...) 或 llm.adapter.stream(...) 仅当您需要原始低级提供者接口时。
图像(多模式)
使用 images() 为支持视觉的模型构建base64图像内容块。
import { images, prompt } from "extrait";
import { readFileSync } from "fs";
const base64 = readFileSync("photo.png").toString("base64");
const img = { base64, mimeType: "image/png" };
// With prompt() builder — pass LLMMessageContent array to .user() or .assistant()
const result = await llm.structured(Schema,
prompt()
.system`You are a vision assistant.`
.user([{ type: "text", text: "Describe this image." }, ...images(img)])
);
// With raw messages array
const result = await llm.structured(Schema, {
messages: [
{
role: "user",
content: [
{ type: "text", text: "Describe this image." },
...images(img),
],
},
],
});
// Multiple images
const content = [
{ type: "text", text: "Compare these two images." },
...images([
{ base64: base64A, mimeType: "image/png" },
{ base64: base64B, mimeType: "image/jpeg" },
]),
];images() 接受单个 { base64, mimeType } 对象或数组,并始终返回 LLMImageContent[] 它直接传播到内容数组中。
对话(多回合历史)
使用 conversation() 建造a LLMMessage[] 从现有的对话历史中。这是将之前的轮次转到法学硕士的惯用方式。
import { conversation } from "extrait";
const messages = conversation("You are a helpful assistant.", [
{ role: "user", text: "What is the speed of light?" },
{ role: "assistant", text: "Approximately 299,792 km/s in a vacuum." },
{ role: "user", text: "How long does light take to reach Earth from the Sun?" },
]);
// High-level text generation
const response = await llm.generate({ prompt: { messages } });
// Or to structured extraction
const result = await llm.structured(Schema, { messages });条目与 images 自动生成多模式内容:
const messages = conversation("You are a vision assistant.", [
{
role: "user",
text: "What is in this image?",
images: [{ base64, mimeType: "image/png" }],
},
]);结果对象
成功的 generate() 调用返回规范化的文本/推理以及请求元数据:
{
text: string,
reasoning: string,
attempts: GenerateAttempt[],
usage?: {
inputTokens?: number,
outputTokens?: number,
totalTokens?: number,
cost?: number,
},
finishReason?: string,
}每 attempts 条目包括:
{
attempt: number,
via: "complete" | "stream",
text: string,
reasoning: string,
usage?: LLMUsage,
finishReason?: string,
}成功的 structured() 调用返回经过验证的数据以及规范化的文本/推理和跟踪元数据。
{
data: T, // Validated data matching schema
text: string, // Visible model text, without inline blocks
reasoning: string, // Normalized reasoning across dedicated fields and inline
json: unknown | null, // Parsed JSON before validation
attempts: StructuredAttempt[], // One entry per parse / self-heal attempt
usage?: {
inputTokens?: number,
outputTokens?: number,
totalTokens?: number,
cost?: number,
},
finishReason?: string, // e.g., "stop"
}每 attempts 条目包括:
{
attempt: number,
selfHeal: boolean,
via: "complete" | "stream",
text: string,
reasoning: string,
json: unknown | null,
candidates: string[],
repairLog: string[],
zodIssues: z.ZodIssue[],
success: boolean,
usage?: LLMUsage,
finishReason?: string,
parsed: ParseLLMOutputResult,
}传统内联 ... 块仍然受支持,但高级 structured() API现在将它们折叠为 reasoning 而不是在内部公开块元数据。
错误处理
抓住 StructuredParseError 当修复和验证仍然失败时。
import { StructuredParseError } from "extrait";
try {
const result = await llm.structured(Schema, prompt`...`);
} catch (error) {
if (error instanceof StructuredParseError) {
console.error("Validation failed");
console.error("Attempt:", error.attempt);
console.error("Zod issues:", error.zodIssues);
console.error("Repair log:", error.repairLog);
console.error("Candidates:", error.candidates);
}
}嵌入
使用生成向量嵌入 llm.embed()它总是会回来的 number[][] --每个输入字符串一个向量。
// Create a dedicated embedder client (recommended)
const embedder = createLLM({
provider: "openai-compatible",
model: "text-embedding-3-small",
transport: { apiKey: process.env.LLM_API_KEY },
});
// Single string
const { embeddings, model, usage } = await embedder.embed("Hello world");
const vector: number[] = embeddings[0];
// Multiple strings in one request
const { embeddings } = await embedder.embed(["text one", "text two", "text three"]);
// embeddings[0], embeddings[1], embeddings[2] — one vector each
// Optional: override model or request extra options per call
const { embeddings } = await embedder.embed("Hello", {
model: "text-embedding-ada-002",
dimensions: 512, // supported by text-embedding-3-* models
body: { user: "user-id" }, // pass-through to provider
});结果形状:
{
embeddings: number[][]; // one vector per input
model: string;
usage?: { inputTokens?: number; totalTokens?: number };
raw?: unknown; // full provider response
}人类学/航海AI
Anthropic不提供原生嵌入API。他们推荐的解决方案是 Voyage AI,它使用相同的OpenAI兼容格式:
const embedder = createLLM({
provider: "openai-compatible",
model: "voyage-3",
transport: {
baseURL: "https://api.voyageai.com",
apiKey: process.env.LLM_API_KEY,
},
});
const { embeddings } = await embedder.embed(["query", "document"]);召唤 llm.embed() 在一个 anthropic-compatible 适配器抛出一个指向Voyage AI的描述性错误。
MCP工具
在请求时连接MCP客户端,以便模型在结构化生成过程中调用工具。
import { createMCPClient } from "extrait";
const mcpClient = await createMCPClient({
id: "calculator",
transport: {
type: "stdio",
command: "bun",
args: ["run", "examples/calculator-mcp-server.ts"],
},
});
const result = await llm.structured(
Schema,
prompt`Calculate 14 + 8`,
{
request: {
mcpClients: [mcpClient],
maxToolRounds: 5,
toolDebug: {
enabled: true,
includeRequest: true,
includeResult: true,
},
onToolExecution: (execution) => {
console.log(execution.name, execution.durationMs);
},
// Optional: transform tool output before it is sent back to the LLM
transformToolOutput: (output, execution) => {
return { ...output, source: execution.name };
},
// Optional: transform tool arguments before the tool is called
transformToolArguments: (args, call) => args,
// Optional: transform the full MCP call payload, including _meta
transformToolCallParams: (params, call) => ({
...params,
_meta: {
source: "extrait-docs",
clientId: call.clientId,
},
}),
// Optional: custom error message when an unknown tool is called
unknownToolError: (toolName) => `Tool "${toolName}" is not available.`,
},
}
);
await mcpClient.close?.();transformToolArguments() 仅接收工具输入对象。 transformToolCallParams() 运行它并接收完整 MCPCallToolParams 将发送到MCP客户端的有效载荷:
type MCPCallToolParams = {
name: string;
arguments?: Record;
_meta?: Record;
};使用 transformToolCallParams() 当您需要附加MCP特定的元数据时,覆盖最终的远程工具名称,或以其他方式更改传递给的完整请求 client.callTool()。此挂钩出口时为 LLMToolCallParamsTransformer.
超时
使用 timeout 无需管理即可设置每个请求和每个工具调用的时间限制 AbortSignal 手动。
const result = await llm.structured(Schema, prompt`...`, {
timeout: {
request: 30_000, // abort the LLM HTTP request after 30s
tool: 5_000, // abort each MCP tool call after 5s
},
});这两个字段都是可选的。 timeout.request 创建一个 AbortSignal.timeout 内部;如果你也通过,它将被忽略 request.signal (您的信号优先)。 timeout.tool 透明地封装每个MCP客户端。
您还可以在客户端设置默认值:
const llm = createLLM({
provider: "openai-compatible",
model: "gpt-5-nano",
transport: { apiKey: process.env.LLM_API_KEY },
defaults: {
timeout: { request: 60_000 },
},
});示例
使用以下命令运行存储库示例 bun run dev .
可用示例:
generate-高级文本生成(generate.ts)streaming-真正的LLM流媒体+快照自检(stream.ts)streaming-with-tools-使用MCP工具进行实时文本流+自检(streaming-with-tools.ts)streaming-turns-with-tools-流式MCP转换、转换和推理块(streaming-turns-with-tols.ts)abort-signal-启动一代,然后快速取消AbortSignal(abort-signal.ts)timeout-通过设置每个请求和每个工具的超时timeout选项(超时.ts)simple-带流媒体的基本结构化输出(simple.ts)sentiment-analysis-枚举验证,严格模式(情感分析)data-extraction-复杂的嵌套模式,自我修复(数据输出)multi-step-reasoning-链式结构化呼叫(多步推理)calculator-tool-MCP工具集成(计算器tool.ts)image-analysis-从图像文件中提取多模态结构(图像分析)conversation-多回合对话历史和内联图像消息(对话.ts)simulated-tools-在没有实际执行的情况下,将虚假的工具调用/结果注入到对话上下文中(模拟tools.ts)embeddings-向量嵌入、余弦相似度和语义比较(嵌入s.ts)
在示例名称后传递参数:
bun run dev generate "Why Bun is fast"
bun run dev streaming
bun run dev streaming-with-tools
bun run dev abort-signal 120 "JSON cancellation demo"
bun run dev timeout 5000
bun run dev simple "Bun.js runtime"
bun run dev sentiment-analysis "I love this product."
bun run dev multi-step-reasoning "Why is the sky blue?"
bun run dev embeddings "the cat sat on the mat" "a feline rested on the rug"环境变量
这些环境变量在示例和常见客户端设置中使用。
LLM_PROVIDER-openai-compatible或anthropic-compatibleLLM_BASE_URL-API端点(可选)LLM_MODEL-型号名称(默认值:gpt-5-nano)LLM_API_KEY-提供程序的API密钥STRUCTURED_DEBUG=1-启用调试输出
默认情况下,结构化调试打印 text (公共可见输出)和 reasoning (标准化推理)。 parseSource (使用的内部来源 解析和自愈)仅在以下情况下打印 debug.verbose 已启用。
测试
与Bun一起运行测试套件。
bun run test