---
title: AI SDK 结构化笔记：统一调用、Schema 与业务校验
description: 对照 ChatCrystal 的 AI SDK 7 调用，分清 provider 适配、输出类型与业务事实，用离线 Zod 练习验证提示描述的边界。
image: "https://zengliangyi.com/og/blog/ai-sdk-structured-notes.png"
---

统一模型接口能减少接入代码，却不能消除不同服务的协议和能力差异；结构化输出能验证形状，也不能证明内容属实。本文从 ChatCrystal 固定版本的调用链出发，解释这两条边界。它不是模型排名，也不要求读者为练习调用付费服务。

## 当前摘要调用使用 AI SDK 7

[server/package.json](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/package.json) 固定 `ai: 7.0.97`，项目要求 Node 24+。[摘要实现](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/summarize.ts) 使用 `generateText` 配合 `Output.object`，从结果读取 output；原稿的 generateObject/system/object 写法不描述这个版本。

下面是实际调用片段，依赖应用已有的模型工厂、Schema、提示词和转录，不能单独复制后运行：



```ts
const   {   output   }   =   await   generateText  ({  
    model:   getLanguageModel  (),  
    output: Output.  object  ({ schema: SummarizeSchema }),  
    instructions:   SYSTEM_PROMPT  ,  
    prompt: transcript,  
    maxOutputTokens:   4096  ,  
    maxRetries:   3  ,  
  });
```



`instructions` 与 `prompt` 分别传入任务要求和可见转录；五个字段是 title、summary、key_conclusions、code_snippets、tags。之后应用把 tags 转成小写，并把原输出 JSON 保存到 raw_response。官方 [结构化数据说明](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) 对 Output.object 的作用是输出结构验证，不能据此保证调用永远成功或生成的结论都有原文依据。

## 工厂统一类型，调用协议仍有区别

[providers.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/providers.ts) 当前注册七种生成 provider：Ollama、OpenAI、OrcaRouter、Anthropic、Google、Azure 和 custom。Embedding 由其中五种支持：Ollama、OpenAI、Google、Azure 和 custom；并不是所有生成模型都可拿来做向量。

`getLanguageModel` 从 llm 配置选择工厂，Embedding 从独立的 embedding 配置选择工厂。Ollama 在清理尾部斜杠后追加 `/v1`，生成模型显式使用 `.chat(model)`；OpenAI/custom 的工厂调用 `openai(model)`，使用 SDK 的默认 Responses 路径。[OpenAI 适配器文档](https://ai-sdk.dev/providers/ai-sdk-providers/openai) 说明了不同模型工厂。因此“兼容 OpenAI”不能只检查 URL 看起来相似，还要核对端点、模型能力、鉴权和所用适配器版本。

配置切换与实际兼容性测试见 [云端 LLM 配置](/blog/cloud-llm-configuration/)，向量配置见 [Embedding 模型配置](/blog/embedding-model-configuration/)。这里关注统一调用与校验的职责，不再重复各服务的设置流程。

## describe 是期望，不是长度或数量约束

实际 SummarizeSchema 使用 `z.string()` 和 `z.array(...)`，再用 describe 写“20 字以内”“3–5 个结论”等期望，没有对应的 max/min 数量限制。下面是缩减为三个字段的离线教学 schema，**不是应用完整 Schema**。在新的临时目录安装 Zod 4.6.2，保存为 `lesson.mjs`，用 Node 24+ 运行；不调用模型，不访问数据库。



```js
import   assert   from   'node:assert/strict'  ;  
  import   { z }   from   'zod'  ;  
  
  const   noteSchema   =   z.  object  ({  
    title: z.  string  ().  describe  (  'A concise title within 20 characters'  ),  
    summary: z.  string  (),  
    key_conclusions: z.  array  (z.  string  ()).  describe  (  '3–5 conclusions'  ),  
  });  
  const   constructed   =   {  
    title:   'x'  .  repeat  (  80  ),  
    summary:   'Invented example: this code has not been checked against a source.'  ,  
    key_conclusions: [],  
  };  
  assert.  equal  (noteSchema.  safeParse  (constructed).success,   true  );  
  assert.  equal  (noteSchema.  safeParse  ({   ...  constructed, title:   123   }).success,   false  );  
  assert.  equal  (  
    noteSchema.  safeParse  ({   ...  constructed, key_conclusions:   'one conclusion'   }).success,  
    false  
  );  
  
  const   stricter   =   noteSchema.  extend  ({  
    title: z.  string  ().  max  (  20  ),  
    key_conclusions: z.  array  (z.  string  ()).  min  (  3  ).  max  (  5  ),  
  });  
  assert.  equal  (stricter.  safeParse  (constructed).success,   false  );
```



这份构造对象的长标题、空数组和无来源摘要都能通过原教学 schema：它们符合字段类型。添加 max/min 后，长度与数量才成为可执行限制；事实是否来自原文仍需另查。刻意给原教学 title 加上 max(20)，第一个断言应失败；恢复 describe-only 后应通过。这不是要求直接修改应用的生产 Schema，而是用反例说明验证职责。

## 模型输出后还有业务筛选与持久化

[关系发现](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/relations.ts) 用 `Output.array`、八种关系枚举及 0–1 数值约束，随后只保留候选 ID 集合中的目标、confidence 至少 0.5，并截取最多五条。候选存在、业务阈值和返回数量并非仅靠提示词描述完成；confidence 也不是经过校准的正确概率。关系投影细节见 [笔记关系与图谱](/blog/note-relations-and-graph/)。

摘要链路先查已有笔记，再做 experience gate；接受后生成摘要并保存笔记，之后才生成向量和发现关系。向量失败会标记 embedding failed，关系失败会记录错误，它们不会自动把已成功的摘要变成失败。继续点击同一会话，也不会覆盖已有笔记后重新评测模型，评测方法见 [摘要质量评估](/blog/llm-summary-evaluation/)。

## 重试次数不是输出正确性的保证

该摘要调用 maxRetries 为 3，关系调用为 2；[generateText 参考](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) 定义了重试参数。不能把它写成“任何 Schema 校验失败都会自动重新生成三次”。错误种类、适配器和失败阶段不同，输出解析错误与可重试的传输/API 错误需要分别处理。

应用任务队列还有启动间隔与部分 429 重试，但一个任务能发起门控、摘要、多次 embed 和关系调用，不能用“队列每秒一个任务”推导“全系统每秒一个模型请求”。验证接入时应分别记录调用协议、输出形状、来源事实、业务筛选、保存状态和失败类型；其中任何一项通过都不能替代其余项。

分享：

##  相关文章

[##  实现 Embedding 服务：文本、分块与提交边界

对照 ChatCrystal 的真实 Embedding 流程，区分文本选择、软分块、向量校验与提交，并用离线反例验证边界。          曾亮懿       2026年10月6日      阅读约 4 分钟

#ChatCrystal

#AI

#Node.js](/blog/embedding-service-design)    [##  结构化 JSON Prompt：把输出字段与来源证据一起设计

以 ChatCrystal 摘要和关系发现为例，设计字段契约、可见输入与反例样本，并用离线 Zod 练习区分格式验证和证据验证。          曾亮懿       2026年10月6日      阅读约 4 分钟

#ChatCrystal

#AI

#Node.js](/blog/structured-json-prompts)    [##  Vectra 本地搜索：用固定向量验证排序、过滤与持久化

在临时目录运行 Vectra 0.15.0 的确定性练习，检查余弦分数、元数据过滤、重新打开索引和取消更新。          曾亮懿       2026年10月6日      阅读约 4 分钟

#ChatCrystal

#AI

#Node.js](/blog/vectra-local-search)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "AI SDK 结构化笔记：统一调用、Schema 与业务校验",
  "description": "对照 ChatCrystal 的 AI SDK 7 调用，分清 provider 适配、输出类型与业务事实，用离线 Zod 练习验证提示描述的边界。",
  "url": "https://zengliangyi.com/blog/ai-sdk-structured-notes/",
  "image": "https://zengliangyi.com/images/blog/ai-sdk-structured-notes.svg",
  "datePublished": "2026-10-05T00:00:00.000Z",
  "dateModified": "2026-10-05T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/ai-sdk-structured-notes/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "AI SDK 结构化笔记：统一调用、Schema 与业务校验",
      "item": "https://zengliangyi.com/blog/ai-sdk-structured-notes/"
    }
  ]
}
```
