ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

airi 中的 xsAI 结构化输出:从 generateObject、streamObject 到 tool() 与 rawTool 的实践

airi 中的 xsAI 结构化输出:从 generateObject、streamObject 到 tool() 与 rawTool 的实践 airi 中的 xsAI 结构化输出:从 generateObject、streamObject 到 tool() 与 rawTool 的实践【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi本篇以 airi 仓库中.agents/skills/xsai/references/structured-output.md参考文档为核心,系统讲解 xsAI(一个极小型 OpenAI 兼容 LLM 运行时)的结构化输出体系:generateObject/streamObject两种 API、基于xsschema的 schema 库选型、tool()与rawTool()两个工具助手,以及推荐规则;并结合 airi 仓库中 Spark Notify 与 Web 搜索工具的真实源码,展示这些 API 在生产代码中如何与 Zod、JSON Schema 转换和 provider 归一化协作落地。读完本文,你可以为任意 OpenAI 兼容端点写出带校验的结构化输出、流式对象解析与类型安全工具,并理解 airi 中“schema 即契约”的实现细节。一、结构化输出 API 总览:generateObject 与 streamObjectstructured-output.md开篇即给出范围声明:该参考文档服务于generateObject、streamObject、tool()、rawTool()以及 schema 选型指导。文档将两个核心 API 做了清晰分工:generateObject:一元(unary)结构化输出,返回经过 schema 校验的最终对象。适用于“等模型一次性吐完、拿到可信结果再往下走”的场景。streamObject:增量式结构化输出,边生成边产出部分对象。适用于 UI 需要逐步渲染、或工作流希望尽早消费中间结果的场景。两者的共同底座是xsschema:schema 的转换统一经由xsschema完成。这正是理解 xsAI 结构化输出的关键——它不直接绑定某个 schema 库,而是通过 Standard Schema 风格(一种跨库的标准接口)接入各家类型库,再统一转换为 OpenAI 兼容端点能消费的 JSON Schema。最小可运行示例airi 仓库内的姊妹文档 recipes.md 给出了规范的最小示例。结构化输出的标准起点如下(Valibot 示例,项目需额外安装valibot/to-json-schema):import { env } from node:process import { generateObject } from xsai/generate-object import * as v from valibot const { object } await generateObject({ apiKey: env.OPENAI_API_KEY!, baseURL: https://api.openai.com/v1/, messages: [ { content: Extract the event information., role: system, }, { content: Alice and Bob are going to a science fair on Friday., role: user, }, ], model: gpt-4o, schema: v.object({ date: v.string(), name: v.string(), participants: v.array(v.string()), }), })从示例可以确认三个实践约束(与 SKILL.md 中的“Key constraints”一致):baseURL与model在实际调用中通常必填,必须显式写出;apiKey视 provider 而定:托管端点需要(在 Node.js 中推荐从process.env读取,在浏览器中推荐从localStorage读取),本地/代理端点可能不需要;示例中一律不硬编码密钥;schema传入的是 Standard Schema 风格的类型定义(此处为 Valibot 的v.object),由xsschema负责转成 JSON Schema。为什么优先 generateObject 而非自由 JSON参考文档的“Recommendation rules”第一条就是:优先使用generateObject,而不是让模型输出自由格式 JSON。理由很直接:自由 JSON 只能靠调用方事后JSON.parse再手工校验,字段缺失、类型漂移、幻觉字段都无解;而generateObject的输出在返回前已经过 schema 校验,调用方拿到的是可断言类型的对象。这也是 airi 内部代码的一致选择——仓库里几乎看不到“让模型吐 JSON 字符串再解析”的旧式写法,取而代之的是 schema 驱动的rawToolxsschema校验路径(见第四节)。二、generateObject的关键选项文档列出了generateObject的五个重要选项:选项作用schema必填。目标对象的类型定义(Standard Schema 风格),同时决定校验规则schemaName为该对象 schema 命名,便于 provider 端标识与调试schemaDescription描述 schema 的整体用途,帮助模型理解要产出什么strict严格模式。开启后模型输出中的额外字段会被拒绝/裁剪,保证对象形状与 schema 完全一致output: array(可选)将输出形态从“单个对象”切换为“数组”,适用于一次抽取多条同类记录(如多个人名、多条事件)strict与output两个选项的组合空间覆盖了绝大多数抽取任务:单条记录用默认对象模式;“从一段文本中抽取 N 条同构记录”时用output: array;对下游字段消费方敏感的场景(如直接写入数据库或触发调度)则打开strict防止模型夹带未知字段。这些选项最终都体现在发往 OpenAI 兼容端点的请求里,由 provider 侧的 structured output / JSON Schema 约束机制保证输出形状。三、streamObject:流式结构化输出与两种结果形态当 UI 或工作流受益于“部分结构化输出”时,参考文档推荐切换到streamObject。文档明确指出两种模式对应的结果形态:对象模式(object mode):消费partialObjectStream,每个 tick 拿到一个不断趋近完整的部分对象;数组模式(array mode):配合output: array使用elementStream,逐元素产出,适合“一条条卡片/条目”式的实时渲染。recipes.md 的“Streaming structured output”给出了对象模式的标准写法:import { env } from node:process import { streamObject } from xsai/stream-object import * as v from valibot const { partialObjectStream } await streamObject({ apiKey: env.OPENAI_API_KEY!, baseURL: https://api.openai.com/v1/, messages: [ { content: Extract the event information., role: system, }, { content: Alice and Bob are going to a science fair on Friday., role: user, }, ], model: gpt-4o, schema: v.object({ date: v.string(), name: v.string(), participants: v.array(v.string()), }), }) for await (const partialObject of partialObjectStream) { console.log(partialObject) }注意这里的一个容易踩坑的细节:streamObject本身是 async 的,调用处必须await才能拿到partialObjectStream。原因在参考文档中有一句关键解释——“schema 转换发生在文本流开始之前”:xsAI 需要先把 Standard Schema 经xsschema转换成 JSON Schema、再把带 schema 约束的请求发出去,流才能启动。这与streamText(同步返回、调用方异步消费textStream/fullStream)的行为形成对照,调用方如果忘记await会拿不到任何流。在 airi 这类虚拟主播/桌面伴侣应用中,数组模式(elementStream)尤其有价值:例如让模型逐条生成“日程卡片”“提醒条目”时,前端可以边生成边追加,而不是等待整个数组闭合后才一次性呈现。四、Schema 选型:Standard Schema 生态与 xsschema参考文档明确 xsAI 通过xsschema支持 Standard Schema 风格库,官方列出的四家为:ZodValibotArkTypeEffect并且给出了两条重要的依赖提示——某些 schema 厂商需要额外的 JSON Schema 转换包才能接入:Zod v3:zod-to-json-schemaValibot:valibot/to-json-schema“Use these when the user wants typed structured output” 这句话点出了设计意图:只要用户要“有类型的结构化输出”,就应沿用项目已有的类型库,让 xsAI 通过xsschema适配层消化库间差异,而不是强制统一成某一家。仓库实证:airi 里的实际选型从源码结构看,airi 选择了Zod xsschema的组合。packages/core-agent/package.json 与 apps/stage-tamagotchi/package.json、integrations/telegram-bot/package.json 等均声明了xsai/shared-chat、xsai/tool(通过 pnpm catalog 统一版本),而xsschema与zod出现在这些包的实际 import 中。两处典型用法:1)rawTooltoJsonSchema:把 Zod 定义编译成 provider 可消费的 JSON Schema。packages/stage-ui/src/tools/web-search.ts 是教科书式的例子:// packages/stage-ui/src/tools/web-search.ts(节选) import { rawTool } from xsai/tool import { toJsonSchema } from xsschema import { z } from zod/v4 const webSearchParameters z.object({ query: z.string().min(2).max(400).describe(The search query. Be specific; this is sent to a web search engine.), max_results: z.union([z.number().int().min(MIN_MAX_RESULTS).max(MAX_MAX_RESULTS), z.null()]).describe(How many results to return (1-10), or null for the default of 5.), time_range: z.union([z.enum([day, week, month, year]), z.null()]).describe(Restrict results to a recent time window when freshness matters, or null for no restriction.), include_domains: z.union([z.array(z.string()).max(10), z.null()]).describe(Only return results from these domains, or null.), exclude_domains: z.union([z.array(z.string()).max(10), z.null()]).describe(Never return results from these domains, or null.), }) export async function createWebSearchTools(options: { apiKey: string, timeoutMs?: number }): PromiseTool[] { // Keep the generated JSON Schema provider-neutral. const parameters await toJsonSchema(webSearchParameters) return [ rawTool({ name: web_search, description: Search the web for current or unfamiliar information and return a list of results with source URLs., parameters, execute: async (rawInput, { abortSignal }: ToolExecuteOptions) { // Keep the runtime range check because rawTool does not validate input. const input rawInput as WebSearchInput const maxResults Math.min(Math.max(MIN_MAX_RESULTS, Math.trunc(input.max_results ?? DEFAULT_MAX_RESULTS)), MAX_MAX_RESULTS) // ... }, }), ] }这里能看到两条与参考文档呼应、且文档本身未展开的工程细节:parameters是await出来的:再次印证“schema 转换是前置异步步骤”这一行为特征,rawTool的构造因此通常发生在 async 工厂函数里;rawTool不做入参校验:源码注释明说 “Keep the runtime range check because rawTool does not validate input”,所以execute内部手动做了Math.min/Math.max的边界钳制。这与参考文档把rawTool()定位为“已有 JSON Schema / 零 schema 库耦合时使用”是一致的——你把类型库的工作接走了,运行时防御也得自己补。2)rawTooltoJsonSchemavalidate:工具执行时的二次校验。packages/core-agent/src/agents/spark-notify/tools.ts 中,builtIn_sparkCommand工具把模型可能多次调用的“子代理指令”用 Zod 描述(sparkNotifyCommandSchema),执行时走 Standard Schema 的validate做运行时验证,失败则把错误以AIRI System: Error - invalid spark_command parameters: ...的形式回传给模型,让其自我修正:// packages/core-agent/src/agents/spark-notify/tools.ts(节选) tools.push(rawTool({ name, description: Issue a spark:command to sub-agents. You can call this tool multiple times., parameters: normalizeNullableAnyOf(await toJsonSchema(sparkNotifyCommandSchema) as any), execute: async (rawPayload, context) { try { const payload rawPayload as z.infertypeof sparkNotifyCommandSchema const validated await validate(sparkNotifyCommandSchema, payload) options.onCommands(validated.commands.map(normalizeSparkNotifyCommand)) // ... } catch (error) { // ... return AIRI System: Error - invalid spark_command parameters: ${errorMessageFrom(error)} } return AIRI System: Acknowledged, command fired. }, }))这段代码展示了generateObject“校验后再消费”理念在工具调用链路里的等价物:模型输出 → schema 校验 → 校验通过才进入下游调度,不通过则把结构化错误信息喂回模型。五、tool() 与 rawTool():两种工具注册路径参考文档对两个工具助手的分工只有一句话,但选型含义明确:tool():配合 Standard Schema 库(Zod、Valibot 等)使用;rawTool():配合现成的 JSON Schema 使用。推荐规则进一步收敛了选择:“用户项目里已有 schema 库时优先tool();用户手里已经有 JSON Schema、或希望与 schema 库零耦合时改用rawTool()。”两者的行为差异可以在 airi 源码中得到印证:tool()路径下,parameters传 Standard Schema,入参在执行前可被库本身校验,execute收到的是已验证的对象;rawTool()路径下,parameters是裸 JSON Schema(通常由toJsonSchema从 Zod 生成),xsAI 只负责把它挂到请求的tools字段上,入参校验与类型收窄由调用方完成(如上节web-search.ts中的手动钳制与as WebSearchInput断言)。一个值得注意的命名细节:web_search工具刻意使用 snake_case 且不加builtIn_前缀,源码注释解释为它是“面向用户能力的、模型可识别的名字”,区别于常驻的基础设施工具(builtIn_前缀的 MCP/调试/spark 工具)。这提示在使用rawTool自定义工具时,工具名是模型可见的契约,应与描述、参数命名一起设计。六、provider 侧的 JSON Schema 归一化:文档没写但源码里的必修课参考文档声明 xsAI 是 OpenAI 兼容优先的,但“兼容”在 schema 层面并不总是一帆风顺——不同兼容端点对 JSON Schema 形态的容忍度不同。airi 仓库里沉淀了两处针对这一问题的归一化工具,恰好是rawTooltoJsonSchema组合落地的最后一公里:1) 可空标量的 anyOf 折叠。packages/core-agent/src/agents/spark-notify/schema.ts 中的normalizeNullableAnyOf说明了问题:xsschema对可空联合(如z.union([z.string(), z.null()]))产出的是{ anyOf: [{ type: string }, { type: null }] }形态,而部分 OpenAI 兼容校验器拒绝该形态、只接受type: [string, null]。该函数递归遍历properties/items/anyOf/oneOf,把“纯标量 anyOf”折叠为 type 数组:// Before: { anyOf: [{ type: string }, { type: null }] } // After: { type: [string, null] }2) provider 适配层级的折叠 required 修正。packages/stage-ui/src/libs/providers/tool-schema.ts 的collapseToolSchemaPrimitiveAnyOf做了更完整的版本:除折叠 anyOf 外,还会把 number/integer 分支上的minimum/maximum/multipleOf等约束合并到目标节点,并在properties变化后清理失效的required键(required为空时整键删除)。函数注释明确其使用前提:“只在确认某个 provider 拒绝了标准 anyOf 形态后,才在该 provider 适配器里使用”——即保持生成出的 JSON Schema 默认 provider-neutral,按需降级。从源码结构看,这套分层对应一个清晰的职责划分:xsschema负责“类型库 → JSON Schema”,xsAI 负责“JSON Schema → OpenAI 兼容请求”,而 airi 的 provider 适配器负责“不同兼容端点的方言差异”。理解这条链路后,遇到某端点 400 拒绝工具 schema 时,排查顺序就是:先看 Zod 定义是否过严,再看是否缺了 anyOf 折叠,最后才怀疑网络或鉴权。七、选型决策清单把参考文档的“Recommendation rules”与 SKILL.md 的“API selection rules”合并,可以得到一张覆盖结构化输出与工具调用的完整决策表:需求首选 API依据需要一个最终校验过的对象generateObject优先于让模型产出自由 JSONUI/工作流要逐步拿到结构化中间结果streamObject对象模式用partialObjectStream,数组模式用elementStream项目已有 Zod/Valibot 等 schema 库tool()Standard Schema 路径,入参自带库级校验已有 JSON Schema,或要求零 schema 库耦合rawTool()注意自行补运行时校验(rawTool 不验证入参)流式文本/工具事件/轻量 agent 循环streamTextstopWhen与结构化输出互补,非替代配套的约束提醒(均出自 SKILL.md 的 Key constraints):generateObject()、streamObject()、tool()均依赖xsschema,个别 schema 厂商需要额外安装 JSON Schema 转换包(Zod v3 →zod-to-json-schema,Valibot →valibot/to-json-schema);xsAI 刻意保持极小,若只为结构化输出一个能力,应选用粒化的xsai/generate-object/xsai/stream-object/xsai/tool包,而不是伞形xsai包。小结xsAI 的结构化输出体系可以用一条主线串起来:类型库(Zod/Valibot/ArkType/Effect)→xsschema转换 →generateObject/streamObject/tool()/rawTool()四类 API → OpenAI 兼容端点。generateObject解决“可信的最终对象”,streamObject解决“增量可见的部分对象”,tool()与rawTool()分别覆盖“有 schema 库”与“裸 JSON Schema”两条工具注册路径。airi 仓库中的 web-search.ts、spark-notify/tools.ts 与 tool-schema.ts 则补上了参考文档留白的另一半:异步 schema 转换的实际调用位置、rawTool不校验入参时的运行时防御,以及跨兼容端点的 JSON Schema 归一化策略。按这条主线选型与排障,即可在 airi 或任意 OpenAI 兼容栈上稳定落地带校验的结构化输出。【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表