接入大模型 API 之后很多团队遇到的第一个线上事故往往不是模型回答错了而是 JSON 解析失败。模型返回了一段看起来像 JSON、却混着解释文本的字符串解析器直接抛异常或者字段从 string 悄悄变成了 number批量入库时触发类型错误。问题表面是“模型不听话”根源却是应用层缺少一份数据契约。今天的题目 “Types with AI: Working with LLMs Through Types”本质要讨论的是当大语言模型LLM真正进入业务系统之后编程语言的类型系统如何从一个“编译期检查工具”升级为 AI 应用的数据契约层。这篇文章不会只讲抽象概念我会给出一个明确判断类型系统不能消灭 LLM 的不确定性但能把它围堵在可控边界内。读完你可以掌握三件事——理解 Type、Schema、Structured Output 三者之间的关系跑通 TypeScript、Python、Java 三种语言下的类型化 LLM 调用知道线上遇到解析失败时该怎么定位和修复。这个方向不是空谈。最近一年AI 应用开发里最热门的几个关键词——LLM 框架、AI Agent、RAG、结构化输出——底层都在反复处理同一件事把模型生成的自由文本转成系统能直接消费的强类型数据。谁能把这一步做得稳谁的 Agent 就不容易失控。1. LLM 应用开发的结构化困境为什么类型系统开始变重要先还原一个真实开发场景。你负责一个天气查询 AI 助手产品要求用户问一句“北京今天冷不冷”系统调用大模型然后从返回结果里提取城市、温度、湿度和穿衣建议写入数据库再回复用户。没有类型约束时你的代码大概长这样response llm.chat(北京天气怎么样请返回 JSON) data json.loads(response) # 很可能会失败 city data[city] # 很可能是 KeyError temperature data[temperature] # 可能是 25°C也可能是 25这就是问题的源头。大模型是一个概率生成器它不是数据库不是 REST API不会保证返回字段永远齐全、类型永远正确。你每一次调用得到的都是一段“看起来合理”的文本。传统开发里编译器会在运行前帮你发现字段拼错、类型不匹配但在 LLM 场景下这些错误全部推迟到了运行时而且是以线上故障的形式出现。引入类型系统之后流程变成了这样先用 TypeScript 的 Zod、Python 的 Pydantic 或 Java 的 Record 定义好返回结构把类型定义转成 JSON Schema随请求一起发给大模型模型返回后SDK 自动把 JSON 反序列化成强类型对象校验失败时直接抛出带字段路径的错误而不是等到data[city]才崩。这里的关键变化是错误发现的位置从“业务逻辑里”提前到了“数据入口处”。类型系统成了 AI 应用的第一道防线它不负责判断模型回答得对不对只负责判断“返回的数据形状能不能被系统安全消费”。我的判断是在纯对话、纯娱乐场景里类型化不是刚需一旦 LLM 要接入业务流程、数据库、外部 API类型系统就是必需品。这也正是“Types with AI”这个方向最近快速升温的根本原因。2. 核心概念Type、Schema、Structured Output 到底约束了什么这几个术语经常混着出现但职责完全不同。我们先做一个概念拆分。2.1 Type编程语言里的数据契约Type 是你在代码里定义的数据结构。比如 TypeScript 的 interface、Python 的 dataclass / Pydantic Model、Java 的 Record 或 POJO。interface WeatherReport { city: string; temperature: number; humidity: number; suggestion?: string; }Type 解决的是“数据进到代码里之后长什么样”。它让编译器、IDE、静态检查工具能帮你发现字段拼错、类型不匹配、未定义属性等问题。2.2 Schema跨语言的描述方式Schema 解决的是“怎么把类型定义传给另一个系统”。JSON Schema 是目前最通用的描述语言它不关心你用的是 TypeScript 还是 Python它只描述 JSON 数据的形状。{ type: object, properties: { city: { type: string }, temperature: { type: number }, humidity: { type: integer }, suggestion: { type: string } }, required: [city, temperature, humidity] }大模型无法理解 TypeScript 的 interface但它能理解 JSON Schema。所以主流的类型化 LLM 开发第一步都是把代码里的 Type 编译成 JSON Schema。2.3 Structured Output给 LLM 的格式约束协议Structured Output 是大模型服务商提供的生成约束能力。OpenAI 等厂商允许在请求参数里指定 response_format声明“你必须返回符合这个 JSON Schema 的 JSON”。更严格的模式还会启用 strict mode禁止模型输出多余的 key。2.4 四者关系对比概念作用对象解决的问题典型代表Type代码层编译期发现数据形状错误TypeScript interface、Java RecordSchema协议层跨语言描述数据结构JSON Schema、Zod 编译结果Structured Output模型层约束 LLM 输出格式OpenAI response_formatPrompt 约束指令层让模型“尽量”按格式输出“请返回严格 JSON”实际项目中这四个层次会叠加使用。只写 Prompt 约束最省事但可靠性最低只用 JSON Schema类型转换还需要自己写只有把类型系统与 Structured Output 结合才真正减少样板代码和解析类故障。3. 技术方案全景从 JSON Mode 到 Function Calling 到类型安全 SDK类型化 LLM 开发不是只有一种做法。从底层到顶层大致有四层方案越往下越接近模型能力越往上越接近业务开发体验。3.1 第一层Prompt 约束在 Prompt 里写“请返回 JSON”然后自己JSON.parse。这是最原始的做法缺点非常明显模型可能在 JSON 前后加解释、用单引号、丢字段。只适合做 demo不建议上生产。3.2 第二层JSON Mode / JSON Schema 约束在 API 参数里声明response_format要求模型返回合法 JSON甚至要求必须符合一个 JSON Schema。这一层已经能解决大部分格式问题但类型转换、缺失字段兜底、异常重试仍然要自己写。3.3 第三层Function Calling / Tool Calling函数调用机制本质上是把工具函数签名转成 JSON Schema让模型决定调用哪个函数并生成参数。它天然具备结构化约束模型输出的参数必须匹配函数签名否则 API 层就会报错。Agent 系统里大量用到这种机制但它的主要目标是“让模型调用工具”而不是“解析业务数据”所以不适合完全替代结构化输出。3.4 第四层类型安全 SDK 与框架这是目前开发体验最好的一层。SDK 自动完成“类型定义 → JSON Schema → 请求 → 响应解析 → 强类型对象”的全链路。语言典型方案核心类型库核心理念TypeScriptAI SDK ZodZodschema 即类型类型即校验PythonInstructor PydanticPydantic模型即 schem自动重试修正JavaSpring AI Structured OutputRecord / POJO类型反射生成 Schema接下来三章我用三个最小示例分别演示这三种语言下如何用类型约束 LLM 输出。示例都以跑通流程为目标版本细节以你实际安装的 SDK 为准。4. 代码实战TypeScript Zod 类型化 LLM 调用TypeScript 生态里最成熟的类型化 LLM 方案是 Vercel AI SDK 配合 Zod。Zod 是 TypeScript 阵营最流行的声明式校验库AI SDK 能把 Zod schema 编译成 JSON Schema 发送给模型并在返回后自动校验和解析类型。4.1 初始化项目先准备一个 Node.js 环境安装依赖mkdir types-with-ai-demo cd types-with-ai-demo npm init -y npm install ai ai-sdk/openai zod npm install -D tsx typescriptnpx tsc --init --target ES2020 --module NodeNext --moduleResolution NodeNext说明ai是核心 SDKai-sdk/openai是 OpenAI 模型适配器zod负责定义 schema。tsx用于直接运行 TypeScript 文件。4.2 用 Zod 定义输出类型创建scripts/weather.ts// file: scripts/weather.ts import { generateObject } from ai; import { openai } from ai-sdk/openai; import { z } from zod; // 1. 用 Zod 定义业务数据结构 const WeatherReportSchema z.object({ city: z.string().describe(城市名称), temperature: z.number().describe(当前温度摄氏度), humidity: z.number().describe(相对湿度百分比), windLevel: z.enum([无风, 微风, 大风, 台风]).describe(风力等级), suggestion: z.string().describe(给用户的穿衣或出行建议), }); async function main() { // 2. 调用 generateObject把 schema 传给模型 const result await generateObject({ model: openai(gpt-4o-mini), // 模型名称请以实际可用模型为准 schema: WeatherReportSchema, prompt: 请查询北京的天气情况并返回结构化结果。, }); // 3. 此时 result.object 已经是强类型对象 console.log(城市, result.object.city); console.log(温度, result.object.temperature); console.log(湿度, result.object.humidity); console.log(风力, result.object.windLevel); console.log(建议, result.object.suggestion); } main().catch((error) { console.error(调用失败, error); process.exit(1); });运行npx tsx scripts/weather.ts4.3 关键逻辑解释这段代码真正重要的一句话是zod schema的定义就是整个数据契约。字段名、类型、描述、枚举值、必填项全部写在类型里既给前端类型推导又给模型生成提示。模型返回后AI SDK 会做两层工作先检查 JSON 是否符合 schema再把它解析成 TypeScript 类型对象。如果模型漏了必填字段或者把temperature返回成字符串会直接抛错错误信息会告诉你具体是哪个字段校验失败。这个方案的坑在于describe()里的中文描述会真实进入请求影响模型返回质量所以描述要写清楚不要写模棱两可的话。5. 代码实战Python Pydantic 定义 Agent 数据契约Python 生态里Pydantic 是事实上的数据校验标准。在此基础上Instructor 库把“定义 Pydantic 模型 → 发送给 LLM → 校验结果 → 失败自动重试”整个过程封装成了一个极其顺畅的 API。5.1 安装依赖pip install instructor pydantic openai5.2 定义 Pydantic 模型并调用创建examples/instructor_demo.py# file: examples/instructor_demo.py from typing import Literal from pydantic import BaseModel, Field import instructor from openai import OpenAI class WeatherReport(BaseModel): 天气查询结果的数据契约 city: str Field(description城市名称) temperature: float Field(description当前温度摄氏度) humidity: int Field(description相对湿度百分比) wind_level: Literal[无风, 微风, 大风, 台风] Field( description风力等级 ) suggestion: str Field(description给用户的穿衣或出行建议) # 关键给 OpenAI 客户端安装 instructor 扩展 client instructor.from_openai(OpenAI()) report client.chat.completions.create( modelgpt-4o-mini, # 模型名称请以实际可用模型为准 response_modelWeatherReport, messages[ { role: user, content: 请查询北京的天气情况并返回结构化结果。, } ], ) print(f城市{report.city}) print(f温度{report.temperature}) print(f湿度{report.humidity}) print(f风力{report.wind_level}) print(f建议{report.suggestion})运行python examples/instructor_demo.py5.3 关键逻辑解释相比自己写 JSON parsingInstructor 最大的价值是“自动修正”。当模型第一次返回的 JSON 无法通过 Pydantic 校验时Instructor 可以把错误信息拼进新的 Prompt让模型自己修正后重新生成。也就是说你的业务代码里不需要再写一遍“如果解析失败就重试”的逻辑这个机制被框架吸收掉了。在 Agent 项目里所有的工具函数入参、状态流转、最终输出都可以用 Pydantic 模型来定义。每次模型调用本质上都是在“生产一个 Pydantic 实例”数据在到达业务代码之前已经被验证过了。需要提醒的是自动修正机制会消耗额外 token生产环境要设计好重试次数和熔断策略避免一次请求反复修正导致成本失控。6. 代码实战Java 生态如何用类型吸收 LLM 返回Java 生态里Spring AI 提供了类似的能力。Java 的强类型特点在这里反而是优势一个 Record 类型既能做接口契约又能反射生成 JSON Schema。6.1 添加依赖在pom.xml中引入 Spring AI 相关依赖具体版本以你的 Spring Boot 项目匹配版本为准dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency6.2 定义输出类型创建WeatherReport.java// file: src/main/java/com/example/demo/WeatherReport.java package com.example.demo; import jakarta.annotation.Nullable; public record WeatherReport( String city, Double temperature, Integer humidity, String windLevel, Nullable String suggestion ) { }注意这里我给suggestion标注了Nullable。在实际项目中模型偶尔会漏掉某些非核心字段如果你不声明可空反序列化时会直接抛异常。很多 IDE 的静态检查会提示 “null annotation types have been detected in the project”这其实是在提醒你在 LLM 返回数据这个场景里可空性声明不是可有可无而是必须处理的问题。6.3 调用并转换类型创建WeatherService.java// file: src/main/java/com/example/demo/WeatherService.java package com.example.demo; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.converter.BeanOutputConverter; import org.springframework.stereotype.Service; Service public class WeatherService { private final ChatClient chatClient; public WeatherService(ChatClient chatClient) { this.chatClient chatClient; } public WeatherReport queryWeather(String city) { // 1. 从 Java 类型反射生成 JSON Schema BeanOutputConverterWeatherReport converter new BeanOutputConverter(WeatherReport.class); String jsonSchema converter.getJsonSchema(); // 2. 把 JSON Schema 写入 Prompt要求模型返回匹配的 JSON String prompt 请查询%s的天气情况并按以下 JSON Schema 返回严格 JSON %s .formatted(city, jsonSchema); // 3. 调用模型并拿到原始 JSON 字符串 String response chatClient.prompt() .user(prompt) .call() .content(); // 4. 反序列化为强类型 Java 对象 return converter.convert(response); } }Spring AI 的 API 演进比较快不同版本的类名和调用方式可能略有差异这里演示的是核心思路类型转 SchemaSchema 约束模型模型返回后再转换回类型。无论 API 怎么变这条链路是稳定的。6.4 关键逻辑解释Java 方案的优势在于类型系统本身就非常强。一个 Record 既定义了字段又带来了 equals、hashCode、toString还方便做参数校验。缺点也很明显样板代码比 TypeScript 和 Python 多而且 Spring AI 的版本更新频繁从早期 OpenAI 直连到现在的 ChatClient 抽象接口一直在调整。如果团队里没有 Java 背景较强的成员建议先做技术调研再评估是否引入。7. 运行结果与效果验证怎么判断类型安全真的生效了代码能跑通只是第一步。在真实项目里你要验证的不是“这一次调用成功”而是“这个链路在长时间运行中是否稳定”。我建议至少做三层验证。7.1 单测断言在测试代码里直接断言返回对象的字段类型和值域。以 Python 为例# file: tests/test_weather.py from examples.instructor_demo import WeatherReport def test_weather_report_type(): report WeatherReport( city北京, temperature26.5, humidity40, wind_level微风, suggestion适合穿薄外套, ) assert isinstance(report.temperature, float) assert report.wind_level in {无风, 微风, 大风, 台风}Java 和 TypeScript 的测试思路相同构造一个合法的强类型实例再构造一个非法 JSON 字符串确认反序列化真的会抛错。7.2 线上指标监控建议接入四个核心指标结构化解析失败率调用总量中 JSON 校验失败的占比重试率触发自动修正或者业务重试的次数平均 token 消耗因为格式错误导致的重试会显著提高成本Schema 校验耗时模型推理和 JSON 校验的性能开销。正常情况下解析失败率应该在 1% 以下。如果超过 5%首先检查 Prompt 是否清晰、schema 是否过于复杂其次考虑模型选择是否合适。7.3 失败注入测试可以在测试环境准备一组“坏样本”缺字段、类型错误、多出字段、包含 Markdown 代码块标记。把这些样本直接塞给解析函数确认每一类问题都有明确的错误提示而不是静默吞掉或返回默认值。实际开发中我见过不少团队只测了“好消息路径”上线后一次字段缺失就把整条链路打崩。类型系统的意义恰恰在于它让这些边界情况在开发阶段就暴露出来而不是留给监控告警去发现。8. 常见问题与排查方法问题现象可能原因排查方式解决方案模型输出无法通过 JSON Schema 校验schema 过于复杂或 Prompt 指令冲突打印模型原始返回确认差在哪简化 schema拆分字段给字段加描述字段类型频繁被转成字符串模型未严格遵守 type 约束查看完整响应使用 strict mode必要时去掉additionalProperties中文响应偶尔被截断导致 JSON 不完整token 上限或 max_tokens 设置过小检查 usage 里的 token 数据调大 token 上限失败后重试可空字段缺失导致 NPE 或 undefined字段被声明为必填但模型没有返回检查 schema 的 required 列表明确标注 Nullable 或 optional并给默认值Function Calling 参数里出现多余 key模型生成了非预期字段打开 JSON 解析日志使用严格 JSON Schema禁止额外字段重试次数过多、成本上升自动修正机制被频繁触发查看重试日志与错误原因优化 Prompt减少重试次数上限增加熔断排查这类问题第一步永远是“看原始返回”。很多 SDK 会把解析后的对象直接抛给你但如果只看解析后的结果你根本不知道原始 JSON 是什么样。我建议在开发环境始终记录一次完整的原始响应线上日志至少保留错误案例的原始响应这是定位问题最快的路径。9. 类型化 LLM 开发的最佳实践与工程建议最后这部分是从实际项目里沉淀下来的一些建议按优先级排序。9.1 契约先行先定义好所有输入输出的 Type 和 Schema再写 Prompt。不要在代码里散落一堆字符串字段靠肉眼对齐。类型定义就是接口文档也应该是 Code Review 的核心对象。9.2 把可空性当作一等公民LLM 输出天生不稳定字段缺失是常态。所有非核心字段都必须显式标注可空并在业务代码里做默认值兜底。这一条看着简单却是很多线上事故的根源。尤其是 Java 项目静态检查工具提示 null annotation 时不要忽略它。9.3 不要信任 LLM 输出的任何内容反序列化成功只代表“形状对”不代表“内容安全”。LLM 生成的数据本质上和用户输入一样是不可信数据入库前要做内容层校验。比如金额字段不能是负数日期字段必须符合格式枚举字段必须存在于白名单里。9.4 给重试机制设置边界自动修正和业务重试都很有效但必须有上限。建议设置最大重试次数 1~2 次超过后降级到默认结果或人工处理。同时记录重试原因方便分析是不是 Prompt 设计有问题。9.5 用日志保留原始响应日志里同时记录 Prompt、原始响应、解析结果、错误信息四段内容。遇到线上问题这四段缺一不可。特别是原始响应一旦被截断或覆盖很多问题就无法复现。9.6 关注成本因素结构化输出、自动修正、schema 约束都会增加 token 消耗。在模型选择上简单任务不要用大模型硬扛复杂的结构化输出任务要评估单次调用的边际成本。给团队一个成本看板比事后追责更有效。9.7 渐进式迁移如果现有系统已经跑了一段裸 JSON 解析不用一次性全改。挑一个故障率最高的接口接入类型化 SDK跑通后比较数据质量再做推广。类型化改造的核心收益是“减少线上故障”不是“换一个写代码的姿势”。10. 总结类型系统是 AI 应用的隐形骨架回到标题 “Types with AI: Working with LLMs Through Types”。这个题目真正想表达的是和大模型协作时决定一个应用能不能从 demo 走向生产的往往不是模型有多聪明而是数据在系统边界处有多可靠。类型系统不会让模型变聪明但它能让不可控的模型输出在进入业务逻辑之前先接受一次严格的安检。对 TypeScript 团队从 Zod AI SDK 入手对 Python 团队从 Pydantic Instructor 入手对 Java 团队从 Spring AI 的 Structured Output 入手。三种方案背后是同一套思想定义类型、生成 Schema、约束模型、校验结果、兜底异常。下一步如果你想在一个真实项目里体会这套流程可以找一个开源 AI 模拟器或 AI Agent 项目在里面把某个自由文本输出改成类型化输出然后对比改造前后的单元测试和线上异常数量。把这个流程跑通一遍比看十篇文章都更有价值。建议收藏备用等你的团队接入 LLM 时这份实践清单可以直接拿来做技术方案评审的底稿。