S
Codex SDK 教程 TypeScript
第 05 章

结构化输出

结构化输出 — 让 AI 返回 JSON

这章学会让 AI 不再返回自由文本,而是乖乖返回你定义好格式的 JSON 数据。

为什么需要结构化输出

先看一个痛点:

typescriptconst turn = await thread.run("分析这段代码有什么问题");
console.log(turn.finalResponse);
// "这段代码有3个问题:第一,变量命名不规范...第二,缺少错误处理...第三,..."

AI 回了一段自由文本。如果你想提取"有几个问题""每个问题是什么",就得自己写正则或者字符串处理——又累又不稳定。

结构化输出的思路:给 AI 一个 JSON 模板,它按模板填空,你直接 JSON.parse() 拿数据。

打个比方:不给模板,AI 写自由作文;给了模板,AI 填表格。填表格的结果你当然更好处理。

基础用法:JSON Schema

thread.run() 的第二个参数 turnOptions 里有个 outputSchema,传入一个 JSON Schema 对象:

typescript// structured-basic.ts
import { Codex } from "@openai/codex-sdk";

async function main() {
  const codex = new Codex();
  const thread = codex.startThread({ skipGitRepoCheck: true });

  // 定义 JSON Schema — AI 必须按这个格式返回
  const schema = {
    type: "object",
    properties: {
      summary: {
        type: "string",
        description: "一句话总结",
      },
      issues: {
        type: "array",
        items: {
          type: "object",
          properties: {
            severity: {
              type: "string",
              enum: ["low", "medium", "high", "critical"],
              description: "严重程度",
            },
            description: {
              type: "string",
              description: "问题描述",
            },
            file: {
              type: "string",
              description: "涉及的文件",
            },
          },
          required: ["severity", "description"],
          additionalProperties: false,
        },
      },
      score: {
        type: "number",
        description: "代码质量评分 0-100",
      },
    },
    required: ["summary", "issues", "score"],
    additionalProperties: false,
  } as const;

  const turn = await thread.run("分析当前项目的代码质量", {
    outputSchema: schema,
  });

  // turn.finalResponse 现在是 JSON 字符串!
  const result = JSON.parse(turn.finalResponse);

  console.log(`📊 评分: ${result.score}/100`);
  console.log(`📝 总结: ${result.summary}`);
  console.log(`🔍 发现 ${result.issues.length} 个问题:`);

  for (const issue of result.issues) {
    const icon = { low: "💬", medium: "⚠️", high: "❌", critical: "🔥" }[issue.severity];
    console.log(`  ${icon} [${issue.severity}] ${issue.description}`);
  }
}

main().catch(console.error);

关键点

  • additionalProperties: false 必须加 — 防止 AI 返回多余字段
  • required 必须列全 — 确保所有字段都有
  • description 强烈建议加 — AI 能看到,帮它理解每个字段该填什么

Zod 进阶:类型安全的结构化输出

手写 JSON Schema 又臭又长,一不留神就写错。Zod 是更优雅的方案:用 TypeScript 代码定义 schema,自带类型推导。

安装:

bashnpm install zod zod-to-json-schema

改写上面的例子:

typescript// structured-zod.ts
import { Codex } from "@openai/codex-sdk";
import { z } from "zod";
import zodToJsonSchema from "zod-to-json-schema";

// 用 Zod 定义 schema — 又短又清晰
const ReviewSchema = z.object({
  summary: z.string().describe("一句话总结"),
  issues: z.array(
    z.object({
      severity: z.enum(["low", "medium", "high", "critical"]).describe("严重程度"),
      description: z.string().describe("问题描述"),
      file: z.string().optional().describe("涉及的文件"),
    })
  ),
  score: z.number().min(0).max(100).describe("代码质量评分"),
});

// 自动推导 TypeScript 类型 — 不用手写 interface!
type Review = z.infer<typeof ReviewSchema>;

async function main() {
  const codex = new Codex();
  const thread = codex.startThread({ skipGitRepoCheck: true });

  const turn = await thread.run("分析当前项目的代码质量", {
    // 关键:target 必须设为 "openAi"
    outputSchema: zodToJsonSchema(ReviewSchema, { target: "openAi" }),
  });

  // 用 Zod 解析 — 如果格式不对会直接报错
  const review: Review = ReviewSchema.parse(JSON.parse(turn.finalResponse));

  console.log(`📊 评分: ${review.score}/100`);
  console.log(`📝 ${review.summary}`);
  review.issues.forEach((issue) => {
    console.log(`  [${issue.severity}] ${issue.description}`);
  });
}

main().catch(console.error);

Zod vs 手写 JSON Schema 对比

对比项 手写 JSON Schema Zod
代码量 30+ 行 10 行
类型安全 需要手写 interface 自动推导 z.infer
运行时校验 需要自己写 .parse() 自带
可读性 JSON 嵌套,眼花缭乱 TypeScript 代码,一目了然
建议 简单 schema 可以用 ✅ 推荐所有场景

Schema 设计技巧

1. 给字段加 description

AI 能看到 description,这就像给它写了个填表说明:

typescriptz.string().describe("用中文回答,不超过 50 字")

2. 用 enum 限制选项

typescriptz.enum(["bug", "security", "performance", "style"])

AI 只能从这几个里选,不会瞎编。

3. 控制嵌套深度

Schema 别搞太复杂,3 层嵌套以内为佳。太深了 AI 容易出错。

4. 用 optional() 标记非必填字段

typescriptz.object({
  name: z.string(),                  // 必填
  email: z.string().optional(),      // 可选
  age: z.number().optional(),        // 可选
})

错误处理

虽然有了 Schema,还是建议做好错误处理:

typescript// 方案一:try-catch
try {
  const result = ReviewSchema.parse(JSON.parse(turn.finalResponse));
} catch (err) {
  if (err instanceof z.ZodError) {
    console.error("AI 返回的格式不对:", err.issues);
  } else {
    console.error("JSON 解析失败:", err);
  }
}

// 方案二:safeParse(推荐)
const parsed = ReviewSchema.safeParse(JSON.parse(turn.finalResponse));
if (parsed.success) {
  const review = parsed.data;  // 类型安全
  console.log(review.score);
} else {
  console.error("格式错误:", parsed.error.issues);
}

safeParse() 不会抛异常,返回一个 { success, data, error } 对象,更适合生产环境。

实战小例子:让 AI 做技术选型

typescriptconst TechDecisionSchema = z.object({
  recommendation: z.string().describe("推荐的技术方案"),
  reason: z.string().describe("推荐理由,100字以内"),
  alternatives: z.array(
    z.object({
      name: z.string(),
      pros: z.array(z.string()),
      cons: z.array(z.string()),
    })
  ).describe("备选方案"),
  confidence: z.enum(["low", "medium", "high"]).describe("推荐置信度"),
});

const turn = await thread.run(
  "我需要给 Node.js 项目选一个 ORM,数据库是 PostgreSQL,要支持 TypeScript",
  { outputSchema: zodToJsonSchema(TechDecisionSchema, { target: "openAi" }) }
);

const decision = TechDecisionSchema.parse(JSON.parse(turn.finalResponse));
console.log(`推荐: ${decision.recommendation} (置信度: ${decision.confidence})`);
console.log(`理由: ${decision.reason}`);

小结

  • 结构化输出 = 给 AI 一个 JSON Schema,它按格式返回
  • 基础用法:run(prompt, { outputSchema: schema })
  • 推荐用 Zod:代码短、类型安全、自带运行时校验
  • 记得加 { target: "openAi" }zodToJsonSchema
  • 生产环境用 safeParse() 做安全解析

下一章高级特性 — 全局配置、线程恢复、多模态输入、取消控制。