本文面向想让 LLM 输出可靠结构化 JSON 的开发者。预计阅读时间10 分钟最终效果掌握 Zod Schema generateObject 的结构化输出方案理解 Schema 与 System Prompt 协同设计的方法。LLM 天生擅长生成自然语言文本但当我们需要把它的输出接入程序逻辑时自由文本就变成了障碍。你需要的是一个可以JSON.parse()的结构化对象而不是一段散文。本文以 ChatCrystal 的两个真实场景为例介绍两种让 LLM 输出结构化 JSON 的方案。为什么需要结构化输出假设你让 LLM “总结这段对话”它可能返回这段对话讨论了如何配置 Ollama。用户遇到了模型加载的问题 最终通过修改 config.json 解决了。建议以后注意端口冲突。这段文字对人来说很好理解但程序无法直接从中提取标题、标签、代码片段等字段。你需要的是{title:Ollama 模型加载配置问题,tags:[ollama,config,debugging],key_conclusions:[端口冲突会导致模型加载失败]}这就是结构化输出要解决的问题。下面介绍两种方案。方案一Prompt 约束 后处理最朴素的做法是在 prompt 中明确告诉 LLM “请以 JSON 格式返回”然后在代码里用正则或 JSON 解析提取结果。这种方法的优点是兼容任何模型包括不支持 function calling 的小模型缺点是 LLM 不一定严格遵守格式需要额外的容错逻辑。ChatCrystal 早期版本的摘要生成就采用过这种方式。在 prompt 中指定 JSON 字段然后从响应中提取 JSON 块// 从 LLM 响应中提取 JSONconstjsonMatchrawResponse.match(/\{[\s\S]*\}/);if(jsonMatch){constresultJSON.parse(jsonMatch[0]);}这种方式的问题很明显LLM 可能在 JSON 前后加上解释文字可能输出格式有误尾部逗号、注释等需要写大量的 try-catch 和正则兜底方案二AI SDK 的 generateObjectVercel AI SDK 提供了generateObject函数它利用模型的 structured output 能力在 API 层面保证返回合法的 JSON。你只需要定义一个 Zod schemaSDK 会负责 prompt 注入、输出解析和验证。ChatCrystal 当前版本的摘要生成和关系发现都使用了这个方案。基本用法import{generateObject}fromai;import{z}fromzod;// 1. 定义 schemaconstMySchemaz.object({title:z.string(),score:z.number().min(0).max(10),});// 2. 调用 generateObjectconst{object}awaitgenerateObject({model:getLanguageModel(),schema:MySchema,system:你是一个评分助手,prompt:请对以下内容打分...,maxRetries:2,});// 3. 直接使用 — object 的类型已经由 Zod 推断console.log(object.title);// stringconsole.log(object.score);// numbergenerateObject背后做了这些事将 Zod schema 转换为 JSON Schema通过模型的 structured output / function calling 接口传递 schema模型返回的 JSON 自动通过 Zod 验证如果验证失败自动重试最多maxRetries次Zod Schema 定义输出格式Zod 是 TypeScript 生态中最流行的运行时验证库。用它定义 schema既能在编译期提供类型推断又能在运行时做数据验证。ChatCrystal 摘要生成的 schema 定义如下constSummarizeSchemaz.object({title:z.string().describe(简洁的标题概括对话主题20字以内),summary:z.string().describe(2-4 段 markdown 摘要涵盖决策上下文和可复用知识),key_conclusions:z.array(z.string()).describe(3-5 个关键结论或决策),code_snippets:z.array(z.object({language:z.string(),code:z.string(),description:z.string(),})).describe(0-3 个最关键的代码片段),tags:z.array(z.string()).describe(3-6 个小写英文标签),});注意.describe()方法。它不是 Zod 原生的功能而是 AI SDK 扩展的——这些描述会被自动注入到发给模型的 schema 中帮助模型理解每个字段的含义和约束。这对输出质量有直接影响。枚举和数值约束关系发现的 schema 展示了更多技巧constRelationElementSchemaz.object({target_note_id:z.number().describe(目标笔记的 ID),relation_type:z.enum([CAUSED_BY,LEADS_TO,RESOLVED_BY,SIMILAR_TO,CONTRADICTS,DEPENDS_ON,EXTENDS,REFERENCES,]).describe(关系类型),confidence:z.number().min(0).max(1).describe(置信度0.0-1.0),description:z.string().describe(简短说明关系20字以内),});z.enum([...])限定模型只能从给定的枚举值中选择杜绝了拼写错误或自造类型.min(0).max(1)对数值做范围约束模型返回 1.5 这种越界值时会被 SDK 拒绝并触发重试System Prompt 设计技巧Schema 约束了输出什么格式而 system prompt 约束了输出什么内容。好的 system prompt 能显著提升结构化输出的质量。角色定义constSYSTEM_PROMPT你是一个技术对话分析专家擅长从 AI 编程助手的对话中提炼结构化知识。;一句话定义角色和能力边界。不需要长篇大论模型会根据角色调整用语风格和分析深度。每个字段的详细说明constSYSTEM_PROMPT... ### 标题 用一句话概括对话的核心主题。使用与对话相同的语言。 ### 摘要 写 2-4 段 markdown 格式的摘要需要涵盖 - 决策上下文遇到了什么问题考虑了哪些方案 - 实施要点具体做了什么改动 - 可复用知识可以提炼出什么通用经验 ### 关键结论 提取 3-5 个最重要的结论或决策每条应当独立可理解。 ### 标签 3-6 个小写英文标签涵盖技术栈、问题类型、领域。;虽然 schema 的.describe()已经提供了字段说明但在 system prompt 中用自然语言展开描述效果更好。这相当于给了模型写作指南而不仅仅是格式规范。约束和注意事项constSYSTEM_PROMPT... ## 注意事项 - 使用与对话相同的语言撰写总结 - 如果对话记录标注了中间省略了 N 条消息基于可见内容总结 - 技术术语、函数名、包名保留原文不翻译;负面约束“不要做什么”和边界条件处理同样重要。这些细节决定了输出在边缘情况下是否仍然可用。maxRetries 重试机制即使用了 structured output模型偶尔也会返回不符合 schema 的结果。maxRetries参数让 SDK 自动重试失败的请求const{object}awaitgenerateObject({model:getLanguageModel(),schema:SummarizeSchema,system:SYSTEM_PROMPT,prompt:transcript,maxOutputTokens:4096,maxRetries:3,// 最多重试 3 次});ChatCrystal 的摘要生成设置为maxRetries: 3关系发现设置为maxRetries: 2。为什么不同摘要是一次性操作重试成本低关系发现是自动触发的需要更快失败以避免阻塞队列。重试时 SDK 会将验证失败的原因反馈给模型让它在下一次尝试中修正。这比你手动写重试循环要优雅得多。输出验证和错误处理generateObject通过 Zod 验证保证了数据格式正确但格式正确不等于语义正确。ChatCrystal 在拿到结构化结果后还会做二次过滤。关系发现的代码展示了这种SDK 验证 业务验证的双重保障const{object:rawRelations}awaitgenerateObject({model:getLanguageModel(),output:array,schema:RelationElementSchema,system:RELATION_SYSTEM_PROMPT,prompt,maxRetries:2,});// 业务层二次过滤constfilteredRelationsrawRelations.filter((rel)candidateIdSet.has(rel.target_note_id)rel.confidenceMIN_CONFIDENCE,).slice(0,MAX_RELATIONS);即使 Zod 验证通过target_note_id是数字confidence在 0-1 之间业务逻辑仍需检查target_note_id是否在候选列表中模型可能幻觉出不存在的 IDconfidence是否达到最低阈值0.3 虽然合法但没有实际意义返回数量是否超过上限实际案例摘要生成完整的摘要生成流程constSummarizeSchemaz.object({title:z.string().describe(简洁的标题概括对话主题20字以内),summary:z.string().describe(2-4 段 markdown 摘要涵盖决策上下文和可复用知识),key_conclusions:z.array(z.string()).describe(3-5 个关键结论或决策),code_snippets:z.array(z.object({language:z.string(),code:z.string(),description:z.string(),})).describe(0-3 个最关键的代码片段),tags:z.array(z.string()).describe(3-6 个小写英文标签),});asyncfunctionsummarizeConversation(conversationId:string,transcript:string){const{object}awaitgenerateObject({model:getLanguageModel(),schema:SummarizeSchema,system:SYSTEM_PROMPT,prompt:transcript,maxOutputTokens:4096,maxRetries:3,});// 后处理统一标签为小写return{...object,tags:object.tags.map((t)t.toLowerCase()),raw_response:JSON.stringify(object),};}注意最后的.map(t t.toLowerCase())。虽然 system prompt 里写了小写英文标签但模型偶尔仍会返回大写。schema 和 prompt 的约束是尽量遵守代码层面的归一化才是确定性保障。实际案例关系发现关系发现使用output: array模式让模型返回一个数组而非单个对象const{object:rawRelations}awaitgenerateObject({model:getLanguageModel(),output:array,schema:RelationElementSchema,system:RELATION_SYSTEM_PROMPT,prompt,// 包含新笔记和候选笔记的上下文maxOutputTokens:1024,maxRetries:2,});output: array告诉 SDK 期望的顶层结构是数组schema 定义的是每个元素的格式。这比让模型返回{ relations: [...] }再取.relations更直接。prompt 的构造也很有讲究——不是让模型自由发挥而是给出了明确的输入格式新笔记 标题: xxx 摘要: xxx 标签: xxx 关键结论: xxx 已有笔记 [id1] 标题 - 摘要 [标签] [id2] 标题 - 摘要 [标签]结构化的输入引导结构化的输出。当 prompt 本身格式清晰时模型更容易遵循 schema 约束。Prompt 工程最佳实践Schema 和 Prompt 协同设计。schema 定义格式约束prompt 定义内容约束两者缺一不可。只靠 schema模型可能返回格式正确但内容空洞的结果只靠 prompt输出格式可能不稳定。善用.describe()。Zod schema 的.describe()会直接出现在模型的 system message 中相当于给模型的字段注释。写得越具体输出质量越高。枚举优于自由文本。z.enum([A, B, C])比z.string()安全得多。只要你的场景能穷举选项就用枚举。数值约束要明确。z.number().min(0).max(1)比z.number()好。模型有时会返回不合常理的数值范围约束能触发重试。不要完全信任模型输出。SDK 的 Zod 验证保证了格式正确但语义正确性需要业务层把关。幻觉 ID、无意义的置信度、不合语境的标签——这些都是可能发生的。设置合理的 maxRetries。重试有成本时间 token一般 2-3 次足够。如果连续失败可能是 schema 定义过于复杂考虑简化。后处理不可省略。标签统一小写、数组去空值、字符串 trim——这些确定性的归一化操作不应该依赖模型遵守。下一步LLM 和 Embedding 不能混用 — 理解不同 AI 模型的分工Ollama vs OpenAI vs Claude 摘要横评 — 不同模型的结构化输出能力对比Vercel AI SDK 文档: generateObject — 官方 API 参考项目地址github.com/ZengLiangYi/ChatCrystal