NestJS 用 Zod 接住 Agent 结构化结果
教程在 NestJS 的 LangChain Agent 调用中,将 Zod schema 传给 run() 的 output 参数,把客户消息转换成带类型校验的订单对象。示例约束 SKU 和正整数数量,同时提醒结构固定不代表字段内容确定。
AI 深度解读
让 AI 帮忙生成订单,难点不只是识别用户想买什么,还在于后端能否直接使用结果。作者给出的办法很具体:在 NestJS 中调用 LangChain agent 时,把 Zod schema 传给 run() 的 output 参数,让返回值成为经过校验、带类型的对象。
这篇教程解决的是接口输出的结构问题。它能省掉从自然语言里提取 SKU、数量的胶水代码,但不能保证商品一定找对。理解这条边界,比照着代码接通接口更有用。
固定返回结构,能解决什么问题
假设客户说「请给我两把 K2 键盘」,没有 schema 时,agent 可能返回一句确认语,后端还得从句子里提取商品编号和数量。有了 schema,调用方拿到的是包含 sku 和 quantity 的对象。

结构化输出约束的是答案的形状,字段内容仍由模型决定。
因此,要把两件事分开:quantity 是不是正整数,可以交给 schema 校验;sku 是不是客户真正想买的商品,仍依赖检索结果和模型判断。
作者标注的测试环境是 NestJS 12、LangChain 1.x、Zod 4、nestjs-langchain 2.1 和 Node 22。这是系列第 3 篇,示例依赖第 2 篇已经接入的商品目录工具,单独复制当前代码还不够。
一份 Zod schema,连接运行时校验和类型检查
作者在 orders/order-draft.schema.ts 中定义订单草稿,同时导出对应的 TypeScript 类型:
import { z } from 'zod';
export const OrderDraft = z.object({
sku: z.string().describe('The SKU of the product in the catalog.'),
quantity: z.number().int().positive(),
});
export type OrderDraft = z.infer<typeof OrderDraft>;
这里要看两处:.describe() 告诉模型这个字符串应该是目录里的 SKU;.int().positive() 则把数量限制为正整数。描述用于帮助模型理解,校验规则用于判断结果是否符合约定,两者作用不同。
z.infer<typeof OrderDraft> 从 schema 推导类型,避免手写一份接口后,还要维护另一份校验规则。作者还提到 .transform() 和 .refine() 可用,但没有在正文中展示具体行为,不能据此推断复杂转换、校验失败时如何处理。
schema 属于这次调用,商品事实仍要查目录
下面的控制器把客户消息交给 agent,并要求它先检索目录,再生成订单草稿:
import { Body, Controller, Post } from '@nestjs/common';
import { Agent } from 'nestjs-langchain';
import { OrderDraft } from './order-draft.schema.js';
@Controller('orders')
export class OrdersController {
constructor(private readonly agent: Agent) {}
@Post('draft')
async draft(@Body('message') message: string): Promise<OrderDraft> {
const { output } = await this.agent.run(
`Find the product of this customer message in the catalog, then draft the order.\n\n${message}`,
{ output: OrderDraft },
);
return output;
}
}
关键是 { output: OrderDraft }:它限定当前这次 run() 的输出。原有的 POST /ask 仍然可以返回文本,同一个 agent 不必统一改成对象输出。控制器还需要加入 AppModule 的 controllers。
启动命令是 npm run start:dev。示例请求为 curl -X POST http://localhost:3000/orders/draft -H 'Content-Type: application/json' -d '{"message": "Two K2 keyboards, please."}',返回 { "sku": "KB-101", "quantity": 2 }。
作者说明,SKU 来自第 2 篇的 searchProducts 工具。提示中要求检索目录,是为了减少模型凭记忆填写 SKU 的情况。不过,这段代码没有展示「必须调用工具」的执行约束,不能把提示要求等同于强制保证。
我的判断
我认可这种接口设计:在 AI 调用边界完成结构校验,让后续业务代码使用明确的对象,比散落着字符串截取逻辑更容易维护。output.sku 能通过类型检查,拼错属性名会报错,这个收益也很直接。
但当前 schema 对 SKU 只检查字符串,并不检查商品是否存在;数量符合正整数要求,也不代表库存足够。返回值适合作为「订单草稿」,真正创建订单前仍需业务校验。
作者没有提供成功率、延迟或成本的对照数据,因此不能据此认定这套方案比手动解析 JSON 更快、更省或更准确。本文展示的是接入方式,以及类型和结构约束带来的工程便利。
测试也要区分层次。作者预告第 4 篇用 LangChain 的 fakeModel() 回放预设工具调用和答案,在无 API key、无 provider、无网络的条件下测试目录工具、端点、重试和 422,并指出测试已在 orders/orders.controller.spec.ts 中。这能验证应用如何处理预设结果,不能证明真实模型理解客户需求的准确率。
能拿来做什么
- 在 NestJS 的订单草稿接口中,用 Zod 同时定义返回结构和 TypeScript 类型,让业务代码直接读取
sku、quantity。 - 保留聊天接口的文本输出,只在需要对象结果的
run()调用上传入 schema,按接口用途选择输出形式。 - 把商品目录检索和下单校验接完整,分别检查 SKU 是否存在、数量是否可售,避免把结构合法当成订单有效。
- 按作者给出的测试方向,用
fakeModel()验证端点和异常处理,再通过真实模型调用检查商品识别效果。