统一模型接口能减少接入代码,却不能消除不同服务的协议和能力差异;结构化输出能验证形状,也不能证明内容属实。本文从 ChatCrystal 固定版本的调用链出发,解释这两条边界。它不是模型排名,也不要求读者为练习调用付费服务。
当前摘要调用使用 AI SDK 7
server/package.json 固定 ai: 7.0.97,项目要求 Node 24+。摘要实现 使用 generateText 配合 Output.object,从结果读取 output;原稿的 generateObject/system/object 写法不描述这个版本。
下面是实际调用片段,依赖应用已有的模型工厂、Schema、提示词和转录,不能单独复制后运行:
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。官方 结构化数据说明 对 Output.object 的作用是输出结构验证,不能据此保证调用永远成功或生成的结论都有原文依据。
工厂统一类型,调用协议仍有区别
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 适配器文档 说明了不同模型工厂。因此“兼容 OpenAI”不能只检查 URL 看起来相似,还要核对端点、模型能力、鉴权和所用适配器版本。
配置切换与实际兼容性测试见 云端 LLM 配置,向量配置见 Embedding 模型配置。这里关注统一调用与校验的职责,不再重复各服务的设置流程。
describe 是期望,不是长度或数量约束
实际 SummarizeSchema 使用 z.string() 和 z.array(...),再用 describe 写“20 字以内”“3–5 个结论”等期望,没有对应的 max/min 数量限制。下面是缩减为三个字段的离线教学 schema,不是应用完整 Schema。在新的临时目录安装 Zod 4.6.2,保存为 lesson.mjs,用 Node 24+ 运行;不调用模型,不访问数据库。
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,而是用反例说明验证职责。
模型输出后还有业务筛选与持久化
关系发现 用 Output.array、八种关系枚举及 0–1 数值约束,随后只保留候选 ID 集合中的目标、confidence 至少 0.5,并截取最多五条。候选存在、业务阈值和返回数量并非仅靠提示词描述完成;confidence 也不是经过校准的正确概率。关系投影细节见 笔记关系与图谱。
摘要链路先查已有笔记,再做 experience gate;接受后生成摘要并保存笔记,之后才生成向量和发现关系。向量失败会标记 embedding failed,关系失败会记录错误,它们不会自动把已成功的摘要变成失败。继续点击同一会话,也不会覆盖已有笔记后重新评测模型,评测方法见 摘要质量评估。
重试次数不是输出正确性的保证
该摘要调用 maxRetries 为 3,关系调用为 2;generateText 参考 定义了重试参数。不能把它写成“任何 Schema 校验失败都会自动重新生成三次”。错误种类、适配器和失败阶段不同,输出解析错误与可重试的传输/API 错误需要分别处理。
应用任务队列还有启动间隔与部分 429 重试,但一个任务能发起门控、摘要、多次 embed 和关系调用,不能用“队列每秒一个任务”推导“全系统每秒一个模型请求”。验证接入时应分别记录调用协议、输出形状、来源事实、业务筛选、保存状态和失败类型;其中任何一项通过都不能替代其余项。