
LLM 写代码的能力已经不需要再证明真正把这类项目接进工程管线时你会发现最棘手的问题往往不在模型选型而在怎么让一段概率生成的输出落进一个确定性的数据结构里。这次我们直接围绕“编程语言与类型安全在 LLM 时代”这个主题展开。听起来像理论话题实际上是非常工程化的问题LLM 返回的 JSON 经常多字段、少字段、类型错位、枚举值跑飞生成的代码经常有隐式类型错误Agent 工具调用的参数经常不匹配函数签名。类型安全在这一轮 AI 应用中不再是编译器替你检查代码时的“锦上添花”而是把模型输出当作不可信输入之后的一道强制校验闸门。本文会先给出现阶段值得关注的几个方向和核心能力然后分别用 Python Pydantic、TypeScript Zod、TypeChat 三套方案做可复现的代码演示并给出接口化、批量任务、性能观察、常见问题排查这四块实操内容。看完之后你可以直接把自己的 LLM 输出验证机制从裸 JSON 解析升级成带 Schema 校验的完整流水线。1. 核心能力速览我先把这个主题拆成能力矩阵方便快速判断哪些部分与你的项目相关能力项说明项目类型LLM 应用工程基础设施 / 开发方法论不是单一模型要解决的问题LLM 输出不可靠类型系统负责用编译期或运行期约束兜底主要技术方案TypeChat、Pydantic、Zod、JSON Schema、function calling 参数校验硬件门槛逻辑层无需 GPU校验服务用 CPU 即可仅本地跑模型时需要按模型规格考虑显存核心输入LLM 生成的 JSON、代码片段、工具调用参数、结构化文本核心输出强类型对象 / 校验报告 / 失败日志 / 标准化错误信息是否支持批量任务支持同一套校验器可以循环处理 JSONL 或数据库记录是否支持接口 API支持校验器可封装成 FastAPI / Express 中间件或独立验证服务适合场景AI Agent 工具调用、RAG 召回内容抽取、代码生成审校、数据入库前哨、结构化报告生成不适合场景用类型校验代替业务正确性判断把 Schema 当成评测指标这里先强调一个前提类型安全解决的是数据形状与类型问题。它不保证模型“语义正确”更不保证模型“不胡说”。它只是让程序在拿到错误结构、错误类型、异常值的那一刻迅速失败而不是带着脏数据继续往下游跑。从材料看目前最值得学习的类型安全思路有三个方向一是微软开源的 TypeChat直接把 TypeScript 类型定义当成提示词和约束模板二是 Python 生态里 Pydantic 配合 JSON Schema把模型输出从字符串变成类型安全对象三是 TypeScript 生态里 Zod 这把轻量库在编译期之外补一层运行时校验。下文会逐一演示。2. LLM 时代的类型安全到底解决什么问题我们先把场景摆出来。传统程序里类型安全的含义是编译器在编译阶段发现string被当成number使用、函数参数个数不匹配、访问了不存在的成员等错误。进入 LLM 时代情况变了代码的“作者”不再只是人类还包括大模型。模型输出天然具有不确定性和概率性它不遵守你的类型系统也不理解你的业务定义只会根据 token 分布“猜”一个看起来合理的答案。所以 LLM 时代的类型安全必须回答下面这几个问题模型输出的 JSON 结构是否合法是否包含多余字段或缺失字段。模型输出的字段类型是否准确例如confidence: 0.88是字符串而不是数字。模型输出的枚举值是否在预期范围内例如情感只有positive、neutral、negative模型却给出maybe。模型输出的代码片段是否能通过语言自身的类型检查例如用 mypy、pyright、tsc 审一遍。Agent 工具调用的参数是否符合函数签名例如传入的model_name必须是字符串而模型可能传成数字或数组。这些问题的本质是LLM 的输出属于“不可信外部输入”和用户提交的表单数据、第三方回调数据、网络爬虫抓到的页面一样。类型安全是对这一层外部输入进行系统性的约束和清洗。近期也有把 LLM 直接应用在时间序列预测方向上的研究比如 time-llama 一类的动态低秩适应思路。这类任务里模型输出通常是一组数值区间或张量如果缺少强类型约束越界的浮点数、维度不匹配的数组就很容易“静默”流进下游决策模块。类型校验在这里就是最后一道安全闸门只是把“字段”换成了“数值范围”和“维度形状”。所以别把类型安全理解成旧话题它在新的模型应用场景里反而是刚需。3. 适用场景、效果边界与合规边界3.1 适合什么场景先给场景分类。第一类是 RAG 信息抽取。从文档里抽取公司名、时间、金额、风险等级这些字段如果直接靠json.loads()进数据库很容易出现 key 缺失导致 KeyError、金额字段是字符串导致排序失败、日期格式不统一导致计算错误。用强类型模型包一层输出就是固定的CompanyProfile对象缺失字段在第一步就暴露。第二类是 Agent 工具调用。LLM Agent 决定调用哪个工具、传什么参数这是目前最容易出错的环节。模型可能把top_k传成字符串把必填参数省略甚至把工具名写错。现代 LLM 平台都支持 function calling 或 JSON Schema但真正可靠的方案是在模型调用工具之后再做一次参数校验让类型系统卡住非法调用。第三类是代码生成审校。LLM 生成 Python、TypeScript、Rust 代码看起来语法完整但类型是不安全的。把生成结果接入 mypy、pyright、tsc 这类检查器在合并进仓库之前先让编译器和类型系统跑一遍能拦截大量显式类型错误。第四类是结构化报告生成。日报、周报、故障报告、评测结果最终都要以固定格式落库。这类场景不仅要求 JSON 合法还要求字段名、枚举值、嵌套结构完全一致靠自然语言 prompt 无法稳定保证只能靠验证器强制。3.2 类型安全不能解决什么类型安全不能保证模型的输出内容真实。模型输出{is_fraud: false}类型校验只能证明这是一个布尔值不能证明这个判断是对的。类型安全也不能替代语义测试。你可以校验情感分类的结果是三种枚举之一但仍然需要测试基准来判断分类准不准。建议把类型校验看成流水线第一道门把业务校验和效果评测看成第二道门。3.3 合规与授权提醒使用第三方 LLM API 时输入数据可能离开本地环境。涉及用户隐私、商业机密、证件照片、人脸、声音等敏感信息必须在调用前完成脱敏并确认数据合规边界。本文涉及的代码全部用于测试环境验证线下研发请遵循公司数据安全规范。4. 环境准备与技术选型4.1 技术选型参考先给一张选型表方便按团队技术栈选择团队现状推荐方案理由Python 后端使用 FastAPI / DjangoPydantic v2 JSON Schema与 FastAPI 深度集成字段校验和 API 文档一体化Node.js 后端使用 NestJS / ExpressTypeScript Zod编译期有 tsc运行期用 Zod双层校验自然组合需要快速实验、原理解验证TypeChat类型定义即提示词能快速把 LLM 输出变成类型化 JSON对类型安全要求更严格Rust serde_json schemars从语言层面强制但开发成本明显更高本地模型推理Ollama / llama.cpp Pydantic本地推理配合环境内校验适合数据不出内网的场景4.2 基础环境要求这部分以通用清单为主具体版本按实际环境替换。Python 方案建议 Python 3.10 以上安装pydantic2.0可选安装openai或本地推理 SDK。Node 方案建议 Node.js 18 以上安装zod和typescript。本地模型如果只做类型校验演示不跑本地模型完全没有 GPU 压力如果跑本地模型内存和显存以模型规格为准不做统一判断。磁盘空间代码和依赖很小通常是几百 MB 级别只有下载本地模型时才需要大幅预留磁盘。网络调用第三方 LLM API 需要网络可达本地模型不需要外网。4.3 安装命令Python 侧安装pip install pydantic openai jsonschemaNode 侧安装npm install zod typescriptTypeChat 属于微软开源实验项目仓库地址是github.com/microsoft/TypeChat。安装和使用以官方说明为准这里不写死依赖版本。5. 测试一用 Python Pydantic 验证 LLM JSON 输出这是一个能直接跑的最小闭环定义模型 → 模拟 LLM 返回 JSON → 类型校验 → 看失败信息。5.1 定义约束模型from typing import Literal from pydantic import BaseModel, Field, ValidationError class ReviewResult(BaseModel): sentiment: Literal[positive, neutral, negative] confidence: float Field(ge0.0, le1.0) keywords: list[str] Field(min_length1, max_length10) # 情况一所有字段合法 good_raw {sentiment: positive, confidence: 0.88, keywords: [fast, reliable]} # 情况二confidence 是字符串keywords 为空数组 bad_raw {sentiment: positive, confidence: 0.88, keywords: []} for raw in [good_raw, bad_raw]: try: result ReviewResult.model_validate_json(raw) print(校验通过:, result.model_dump()) except ValidationError as e: print(校验失败:, e.errors())输出会很直观第二条数据在confidence上报类型错误在keywords上报长度错误。这就是类型安全的价值——它不关心模型为什么写错只关心错在哪一层、错误结构是什么。5.2 把 Schema 写进 Prompt 或响应格式把ReviewResult.model_json_schema()序列化成 JSON Schema 后可以注入到提示词也可以传给兼容 OpenAI 协议接口的response_format参数。以 OpenAI 兼容接口为例通用写法是import json from pydantic import BaseModel class ReviewResult(BaseModel): sentiment: str confidence: float schema ReviewResult.model_json_schema() # 伪代码实际请求参数因服务商而异 request_payload { model: your-model-name, messages: [ {role: system, content: 你是一个评论分析助手只输出 JSON。}, {role: user, content: 分析这条评论 这个产品很好用} ], response_format: { type: json_schema, json_schema: { name: ReviewResult, schema: schema } } }需要说明的是不同服务商的兼容实现不完全一致部分本地模型对response_format支持不稳定。更保险的办法是把 JSON Schema 直接写进 system prompt让模型先按结构输出再用model_validate_json做兜底。两条线同时做成功率远高于只依赖提示词。5.3 失败处理校验失败后不要直接报错退出。建议把错误明细记录到日志并把原始输出一并保存方便后续分析模型行为。典型做法from pydantic import ValidationError try: result ReviewResult.model_validate_json(raw_output) except ValidationError as e: # 记录原始输出和错误明细后续可重试或降级 error_items e.errors() print(错误字段:, [item[loc] for item in error_items]) print(错误原因:, [item[type] for item in error_items])6. 测试二用 TypeScript Zod 做双层校验TypeScript 的静态类型检查发生在编译期但fetch接口返回的数据、JSON 解析后的数据在运行期不会自动拥有类型。Zod 的价值就在这里先定义 Schema再用safeParse做运行时校验校验通过后才拿到强类型数据。6.1 最小示例import { z } from zod; const ReviewResult z.object({ sentiment: z.enum([positive, neutral, negative]), confidence: z.number().min(0).max(1), keywords: z.array(z.string()).min(1).max(10) }); const raw {sentiment: positive, confidence: 0.88, keywords: []}; const parsed ReviewResult.safeParse(JSON.parse(raw)); if (!parsed.success) { console.error(校验失败:, parsed.error.flatten()); } else { console.log(校验通过:, parsed.data); }这个示例会打印校验失败因为confidence是字符串keywords为空数组。很多团队只在 TypeScript 里依赖interface但interface只是编译期概念对运行期数据没有任何保护力。Zod 这类运行时 Schema 库补齐了这条链路。6.2 组合到业务代码实际业务里模型服务返回的数据会先进入一个工具函数async function fetchReviewResult(raw: unknown): PromiseReviewResult { const parsed ReviewResult.safeParse(raw); if (!parsed.success) { throw new Error(非法模型输出: ${JSON.stringify(parsed.error.flatten())}); } return parsed.data; }这里的unknown很重要。不要写any要把运行期数据先放进unknown强制在边界处做类型收窄。这样写出来的代码类型系统能帮你找出哪些地方没有校验就往下传数据。7. 用 TypeChat 思路理解“类型即提示词”TypeChat 的核心思路是让类型定义同时承担提示词和验证模板的职责。你把一个 TypeScript 类型发给模型模型根据类型定义生成 JSONTypeChat 再负责把模型输出解析和校验成类型化对象。对开发者来说TypeChat 演示了“类型即接口”的 hybrid 工作方式提示词不需要手写一长串字段说明类型定义本身就是最精确的约束描述。典型的 TypeChat 使用流程定义接口类型例如ReviewResult。用createJsonTranslator把类型传给模型。模型返回的文本被解析并验证。验证失败时TypeChat 会把错误信息反馈给模型尝试修复。这个方案的优势是类型定义和实际数据模型天然一致省去“一边维护 Field 定义、一边维护 prompt 描述”的双份维护成本。缺点是它更偏实验性质工程稳定性和复杂类型支持需要按实际项目测试评估。8. 接口化与批量任务把校验器变成服务类型校验不适合只写在某个脚本里更合理的做法是封装成独立接口或者作为流水线中的中间层组件。8.1 用 FastAPI 封装验证服务from fastapi import FastAPI from pydantic import BaseModel, ValidationError app FastAPI() class RawPayload(BaseModel): model_output: str class ValidateResponse(BaseModel): valid: bool data: dict | None None errors: list[dict] | None None app.post(/v1/validate/review, response_modelValidateResponse) def validate_review(payload: RawPayload): try: result ReviewResult.model_validate_json(payload.model_output) return ValidateResponse(validTrue, dataresult.model_dump()) except ValidationError as e: return ValidateResponse(validFalse, errorse.errors())启动uvicorn api_server:app --host 127.0.0.1 --port 8080这样 LLM 应用、Agent 服务、内部调度脚本都能通过 HTTP 调用同一个校验器避免每处重复写校验逻辑。8.2 JSONL 批量任务批量评测是 LLM 应用最常见的场景。把模型返回的原始 JSON 逐行写入raw_outputs.jsonl再批量校验import json from pathlib import Path from pydantic import ValidationError success_count 0 error_list [] with Path(raw_outputs.jsonl).open(encodingutf-8) as f: for line_no, line in enumerate(f, 1): line line.strip() if not line: continue try: result ReviewResult.model_validate_json(line) success_count 1 except ValidationError as e: error_list.append({ line: line_no, raw: line, errors: e.errors() }) print(f成功: {success_count}) print(f失败: {len(error_list)})批量任务一定要带日志、失败原因、原始输出三段信息否则模型表现漂移时你无法定位是批次问题还是单条数据问题。8.3 后续接数据库类型校验通过后的result可以直接交给 ORM 或数据框架落库。中间不要再用dict转一层避免把类型信息丢掉。如果一定要转dict也要在下一步重新声明期望结构。9. 资源占用与性能观察类型校验本身的计算开销非常小。Pydantic v2 是基于 Rust 核心实现Zod 也做了不少性能优化。在通常的 JSON 解析场景里校验耗时基本是亚毫秒到毫秒级数据库连接和 LLM 推理的网络时延相比完全不在一个量级上。真正需要观察的是下面几点。9.1 关注 LLM 推理的延迟分布无论本地模型还是云端 APILLM 生成的延迟都是数百毫秒到数秒甚至更长时间。类型校验只是整个流水线中的一个环节。建议把校验失败率单独统计出来因为你最需要关注的不是校验消耗的毫秒数而是模型产出非法 JSON 的比例。这个比例上升说明模型输出的稳定性在下降需要从提示词、Schema 准确性、模型版本三个方向找原因。9.2 降低校验失败率的方法提示词里给出明确的一句只输出 JSON不要输出 Markdown 代码块。同时把 JSON Schema 嵌入提示词让模型直接按照字段类型生成。另外字段命名不要用缩写confidence比conf更容易让模型生成正确值。枚举值要写全必要时给例子“可选值只有 positive、neutral、negative不要输出其他值。”9.3 观察维度测试时建议记录四个指标指标含义schema 校验失败率JSON 结构和类型不合法的占比全局重试次数校验失败后重试成功需要多少次原始输出样例失败输出的原文方便人工审阅模型 token 用量失败时的total_tokens是否异常高或异常低10. 常见问题与排查方法以下排查表覆盖我在实际项目里最常遇到的几类问题。问题现象可能原因排查方式解决方案模型返回的不是纯 JSON提示词没有强调 JSON模型输出被 Markdown 代码块包裹打印原始返回内容先剥掉 Markdown 围栏再解析或者在提示词中强制要求输出纯 JSONJSON 能解析但字段类型不对提示词未附带 JSON Schema模型按自然语言理解生成打印e.errors()看 loc 和 type把 Schema 注入提示词并启用response_format兼容模式枚举值不受控提示词没列全枚举范围查看模型错误输出中的枚举值在 Schema 中使用严格枚举并在提示词中明确列出可选值长文本输出被截断模型上下文窗口或 max_tokens 不足查看是否出现截断标记分段处理或调高输出上限截断数据不要直接入库批量任务卡住单个请求没有设置超时查看进程和网络连接为每次请求设置超时和重试次数建议 60 秒起相同输入结果时好时坏模型采样温度过高固定随机种子或降低温度评测时把温度设为 0用几次采样做稳定性观察代码生成有隐式类型错误只做静态扫描未跑类型检查器在 CI 中跑 tsc/mypyLLM 生成代码后强制接入本语言类型检查工具工具调用参数未绑定Agent 框架没有做参数校验查看 function call 原始参数在工具执行前插入 schema 校验拒绝非法参数本地模型耗时明显更长硬件和模型规格不匹配观察 GPU/CPU 占用换小模型或调整并发策略下一步再做量化11. 最佳实践与使用建议这里给出可以直接放进团队协作规范里的几条建议。第一把 LLM 输出当成与用户输入同等危险的外部数据。任何进入业务系统的模型输出先过 Schema 校验再谈业务处理。这个认知要写进代码评审清单。第二单独建一个校验层目录。不要在校验函数里混合一个调用 LLM、一个连接数据库的代码块。校验层只做三件事定义模型、解析输入、返回统一错误结构。第三提示词和 Schema 要同步维护。Schema 里新增一个字段提示词里的示例 JSON 也要跟着更新。最好的办法是直接从 Schema 序列化示例减少手工维护。第四批量评测时保留一份“失败样本库”。每次模型版本更新后用同一个验证集跑一遍对比 schema 失败率。这样能快速看出新模型是否更擅长“按格式输出”。第五所有涉及用户数据、隐私、版权素材的输入在进入第三方 LLM 服务前必须评估合规边界。公开模型服务不等于可以随意传输数据生产环境需要建立审批机制。第六接口服务要限制访问范围。校验服务如果开放到内网也要加鉴权否则会成为数据投毒和扫描的目标。从最小化权限开始只允许白名单应用访问。12. 总结与下一步“编程语言与类型安全在 LLM 时代”不是一个研究课题而是一条迫切需要落地的工程底线。类型安全不能替代效果优化但它能让你在模型输出异常时第一时间发现问题而不是让脏数据在下游跑了几天后才在报表里露出端倪。建议你本周就把第一条模型输出接入 Schema 校验。先选一个已经用json.loads硬解析的接口替换成 Pydantic 或 Zod 的严格模型跑一遍测试数据重点观察失败率。你会立刻发现原来模型输出的不确定性问题比预想中更常见也比预想中更容易结构化处理。下一步可以继续往两个方向扩展一是把校验器封装成独立的验证服务接入 Agent 工具调用链路二是把批量评测流程跑起来把 schema 失败率作为模型对比和提示词迭代的常规指标。把这条链路打通之后LLM 应用工程的稳定性会有一个非常可感知的提升。