AI Agent 教程

第9章:结构化输出 —— 让 Agent 按格式返回数据

一句话:让 Agent 返回 JSON 等结构化数据,方便程序直接处理,不用再自己"猜"Agent 到底说了啥。

本章目标

前置知识


9.1 为什么需要结构化输出?

默认情况下,Agent 返回的是"自由发挥"的文本

你让 Agent 分析一段代码的质量,它可能会这样回复:

这段代码整体质量不错,我给它打 8 分(满分10分)。
主要问题有以下几个:
1. 变量命名不够清晰,比如第 15 行的 `a` 和 `b`
2. 缺少错误处理
3. 没有写注释

对人来说,这段文字读起来很舒服。但如果你写的是一个自动化程序,需要把分数存到数据库、把问题列表展示在前端页面上,那你就头疼了 —— 你得自己写代码去"解析"这段文字,从里面把分数和问题列表"抠"出来。

这就像你去医院体检,医生口头跟你说"血压有点高,血糖正常,胆固醇偏高……"你听完就忘了。但如果医生给你一张体检报告表,每项指标都有明确的数值,你一眼就能看明白,还能拿去给其他医生看。

结构化输出 = 告诉 Agent"请填表,别写作文"

结构化输出就是这个意思:你给 Agent 一张"表格模板"(JSON Schema),Agent 按照模板格式返回数据。返回的不是自由文本,而是一个规规矩矩的 JSON 对象。

没有结构化输出时:

用户: 分析这段代码的质量
Agent: 这段代码整体不错,我给 8 分...(一大段文字)
程序: 😵 我怎么从这段话里把分数提取出来??

有结构化输出时:

用户: 分析这段代码的质量
Agent: { "score": 8, "issues": ["变量命名不清晰", "缺少错误处理", "没有注释"] }
程序: 太好了!score 是 8,issues 有 3 条,直接存数据库!

什么时候需要用结构化输出?

简单来说,只要你的 Agent 输出是要给程序处理(而不是直接给人看)的,就应该考虑用结构化输出:

场景 需要结构化输出吗? 原因
聊天机器人直接回复用户 不需要 直接显示文本就行
Agent 分析代码后存入数据库 需要 程序要解析数据
Agent 提取文章摘要存入系统 需要 需要字段对应
Agent 生成 API 文档 需要 需要结构化的文档格式
Agent 做数据分析返回图表数据 需要 前端要用 JSON 画图
Agent 和用户闲聊 不需要 自然语言就好

9.2 使用 JSON Schema 定义输出格式

JSON Schema 是什么?

JSON Schema 就是一个用来描述"JSON 数据长什么样"的规范。你可以把它理解为 JSON 的"模具" —— 你先用 JSON Schema 做好一个模具,Agent 就会按照这个模具来"倒"出数据。

比如,你想让 Agent 返回这样的数据:

{
  "score": 8,
  "issues": ["变量命名不清晰", "缺少错误处理"]
}

对应的 JSON Schema 就是:

{
  "type": "object",
  "properties": {
    "score": {
      "type": "number",
      "description": "代码质量评分,1-10 分"
    },
    "issues": {
      "type": "array",
      "items": { "type": "string" },
      "description": "发现的问题列表"
    }
  },
  "required": ["score", "issues"]
}

JSON Schema 基础语法速查

别被 JSON Schema 吓到,常用的就这几个关键字:

关键字 作用 例子
type 数据类型 "string", "number", "boolean", "object", "array"
properties 对象的属性定义 { "name": { "type": "string" } }
required 哪些字段是必须的 ["name", "age"]
items 数组里每个元素的类型 { "type": "string" }
description 对字段的说明 "用户的年龄"
enum 允许的值列表 ["high", "medium", "low"]

在 Claude Agent SDK 中使用 outputFormat

好了,现在把 JSON Schema 塞给 Agent。在 SDK 中,你只需要在 options 里加一个 outputFormat 字段就行:

TypeScript 版本:

import { query, type MessageStream } from "@anthropic-ai/claude-code";

// 定义输出的 JSON Schema
const codeReviewSchema = {
  type: "object",
  properties: {
    score: {
      type: "number",
      description: "代码质量评分,1-10 分"
    },
    issues: {
      type: "array",
      items: { type: "string" },
      description: "发现的问题列表"
    },
    suggestions: {
      type: "array",
      items: { type: "string" },
      description: "改进建议列表"
    }
  },
  required: ["score", "issues", "suggestions"]
};

// 调用 Agent,指定输出格式
for await (const message of query({
  prompt: "分析当前项目的 index.ts 文件的代码质量",
  options: {
    allowedTools: ["Read", "Glob", "Grep"],
    outputFormat: {
      type: "json_schema",
      schema: codeReviewSchema
    }
  }
})) {
  if (message.type === "result") {
    // message.structured_output 就是解析好的 JSON 对象
    const result = message.structured_output;
    console.log(`代码评分: ${result.score}`);
    console.log(`发现 ${result.issues.length} 个问题:`);
    result.issues.forEach((issue: string, i: number) => {
      console.log(`  ${i + 1}. ${issue}`);
    });
  }
}

Python 版本:

from claude_code_sdk import query, ClaudeCodeOptions, OutputFormat

# 定义输出的 JSON Schema
code_review_schema = {
    "type": "object",
    "properties": {
        "score": {
            "type": "number",
            "description": "代码质量评分,1-10 分"
        },
        "issues": {
            "type": "array",
            "items": {"type": "string"},
            "description": "发现的问题列表"
        },
        "suggestions": {
            "type": "array",
            "items": {"type": "string"},
            "description": "改进建议列表"
        }
    },
    "required": ["score", "issues", "suggestions"]
}

# 调用 Agent,指定输出格式
async for message in query(
    prompt="分析当前项目的 index.ts 文件的代码质量",
    options=ClaudeCodeOptions(
        allowed_tools=["Read", "Glob", "Grep"],
        output_format=OutputFormat(
            type="json_schema",
            schema=code_review_schema
        )
    )
):
    if message.type == "result":
        result = message.structured_output
        print(f"代码评分: {result['score']}")
        print(f"发现 {len(result['issues'])} 个问题:")
        for i, issue in enumerate(result["issues"]):
            print(f"  {i + 1}. {issue}")

注意事项


9.3 用 Zod(TypeScript)简化 Schema 定义

手写 JSON Schema 太累了

上面的 JSON Schema 其实已经算简单的了。但如果你的输出格式比较复杂,比如有嵌套对象、可选字段、枚举值等等,手写 JSON Schema 会非常痛苦,而且容易写错。

比如你想定义这样一个项目分析报告:

{
  "project_name": "my-app",
  "language": "TypeScript",
  "dependencies": [
    { "name": "react", "version": "18.2.0", "type": "production" },
    { "name": "vitest", "version": "1.0.0", "type": "development" }
  ],
  "complexity": {
    "score": 7,
    "level": "medium",
    "details": "中等复杂度,有一些嵌套逻辑"
  }
}

如果手写 JSON Schema,你需要写几十行嵌套的 JSON。但用 Zod,几行代码就搞定了。

Zod 是什么?

Zod 是 TypeScript 生态里最流行的数据验证库。它让你用 TypeScript 代码来定义数据格式,然后可以自动转换成 JSON Schema。

安装:

npm install zod zod-to-json-schema

用 Zod 定义 Schema

import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

// 用 Zod 定义输出格式 —— 像写 TypeScript 类型一样直观
const ProjectReportSchema = z.object({
  project_name: z.string().describe("项目名称"),
  language: z.string().describe("主要编程语言"),
  description: z.string().describe("项目简介,一两句话"),
  dependencies: z.array(
    z.object({
      name: z.string().describe("依赖包名"),
      version: z.string().describe("版本号"),
      type: z.enum(["production", "development"]).describe("依赖类型")
    })
  ).describe("主要依赖列表"),
  complexity: z.object({
    score: z.number().min(1).max(10).describe("复杂度评分 1-10"),
    level: z.enum(["low", "medium", "high"]).describe("复杂度等级"),
    details: z.string().describe("复杂度详细说明")
  }).describe("项目复杂度评估")
});

// 自动转换为 JSON Schema
const jsonSchema = zodToJsonSchema(ProjectReportSchema);

看到没?用 Zod 写起来就像写 TypeScript 类型一样自然。z.string() 对应字符串,z.number() 对应数字,z.array() 对应数组,z.object() 对应对象,z.enum() 对应枚举。每个字段后面加 .describe() 就是说明。

完整示例:用 Zod + query() 分析项目

import { query } from "@anthropic-ai/claude-code";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

// 第一步:用 Zod 定义输出格式
const ProjectReportSchema = z.object({
  project_name: z.string().describe("项目名称"),
  language: z.string().describe("主要编程语言"),
  description: z.string().describe("项目简介"),
  dependencies: z.array(
    z.object({
      name: z.string(),
      version: z.string(),
      type: z.enum(["production", "development"])
    })
  ).describe("前 5 个最重要的依赖"),
  complexity: z.object({
    score: z.number().min(1).max(10),
    level: z.enum(["low", "medium", "high"]),
    details: z.string()
  })
});

// 从 Zod Schema 推导出 TypeScript 类型 —— 这是 Zod 的杀手锏!
type ProjectReport = z.infer<typeof ProjectReportSchema>;

// 第二步:转换为 JSON Schema
const jsonSchema = zodToJsonSchema(ProjectReportSchema);

// 第三步:调用 Agent
async function analyzeProject(projectPath: string): Promise<ProjectReport> {
  for await (const message of query({
    prompt: `分析 ${projectPath} 目录下的项目,给出一份项目报告`,
    options: {
      allowedTools: ["Read", "Glob", "Grep", "Bash"],
      cwd: projectPath,
      outputFormat: {
        type: "json_schema",
        schema: jsonSchema
      }
    }
  })) {
    if (message.type === "result" && message.structured_output) {
      // 用 Zod 验证返回的数据 —— 双重保险
      const parsed = ProjectReportSchema.safeParse(message.structured_output);
      if (parsed.success) {
        return parsed.data;  // 这里 parsed.data 是完全类型安全的!
      } else {
        console.error("数据验证失败:", parsed.error);
        throw new Error("Agent 返回的数据格式不对");
      }
    }
  }
  throw new Error("Agent 没有返回结果");
}

// 第四步:使用
const report = await analyzeProject("/path/to/my-project");
console.log(`项目: ${report.project_name}`);
console.log(`语言: ${report.language}`);
console.log(`复杂度: ${report.complexity.level} (${report.complexity.score}/10)`);
// TypeScript 编辑器会自动提示所有可用的字段 —— 爽!

Zod 的好处总结

  1. 写起来简单 —— 比手写 JSON Schema 省一半的代码
  2. 类型安全 —— z.infer<typeof Schema> 自动推导出 TypeScript 类型,编辑器有提示
  3. 运行时验证 —— safeParse() 可以在运行时检查数据是否合法
  4. 可组合 —— Schema 可以像乐高一样拼装复用

9.4 用 Pydantic(Python)简化 Schema 定义

Pydantic 是什么?

Pydantic 是 Python 生态里最流行的数据验证库(FastAPI 就是基于它的)。跟 Zod 在 TypeScript 中的作用一样,Pydantic 让你用 Python 类来定义数据格式。

安装:

pip install pydantic

用 Pydantic 定义 Schema

from pydantic import BaseModel, Field
from typing import Literal
from enum import Enum

class DependencyType(str, Enum):
    PRODUCTION = "production"
    DEVELOPMENT = "development"

class Dependency(BaseModel):
    name: str = Field(description="依赖包名")
    version: str = Field(description="版本号")
    type: DependencyType = Field(description="依赖类型")

class ComplexityLevel(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

class Complexity(BaseModel):
    score: int = Field(ge=1, le=10, description="复杂度评分 1-10")
    level: ComplexityLevel = Field(description="复杂度等级")
    details: str = Field(description="复杂度详细说明")

class ProjectReport(BaseModel):
    project_name: str = Field(description="项目名称")
    language: str = Field(description="主要编程语言")
    description: str = Field(description="项目简介")
    dependencies: list[Dependency] = Field(description="前 5 个最重要的依赖")
    complexity: Complexity = Field(description="项目复杂度评估")

完整示例:用 Pydantic + query() 分析项目

from pydantic import BaseModel, Field
from claude_code_sdk import query, ClaudeCodeOptions, OutputFormat

# 第一步:用 Pydantic 定义输出格式
class CodeReviewResult(BaseModel):
    score: int = Field(ge=1, le=10, description="代码质量评分 1-10")
    issues: list[str] = Field(description="发现的问题列表")
    suggestions: list[str] = Field(description="改进建议列表")
    summary: str = Field(description="一句话总结")

# 第二步:把 Pydantic 模型转换为 JSON Schema
json_schema = CodeReviewResult.model_json_schema()
# Pydantic v2 用 model_json_schema()
# Pydantic v1 用 schema()

# 第三步:调用 Agent
async def review_code(file_path: str) -> CodeReviewResult:
    async for message in query(
        prompt=f"审查 {file_path} 文件的代码质量,给出评分和建议",
        options=ClaudeCodeOptions(
            allowed_tools=["Read", "Glob", "Grep"],
            output_format=OutputFormat(
                type="json_schema",
                schema=json_schema
            )
        )
    ):
        if message.type == "result" and message.structured_output:
            # 用 Pydantic 验证和解析
            return CodeReviewResult.model_validate(message.structured_output)

    raise RuntimeError("Agent 没有返回结果")

# 第四步:使用
result = await review_code("src/main.py")
print(f"评分: {result.score}/10")
print(f"总结: {result.summary}")
for issue in result.issues:
    print(f"  - {issue}")

Pydantic 的好处

  1. Pythonic —— 用类定义数据模型,Python 开发者天然熟悉
  2. 类型提示 —— 编辑器有完整的类型提示和自动补全
  3. 验证严格 —— 自动检查数据类型、范围、格式等
  4. 生态强大 —— FastAPI、SQLAlchemy 等主流框架都支持 Pydantic

Zod vs Pydantic 对比

特性 Zod (TypeScript) Pydantic (Python)
定义方式 函数链式调用 类继承
类型推导 z.infer<typeof Schema> 天然就是类
转 JSON Schema zodToJsonSchema() .model_json_schema()
运行时验证 .safeParse() .model_validate()
安装 npm install zod pip install pydantic

两者的核心思路是一样的:用代码定义格式,自动转成 JSON Schema。选哪个取决于你用什么语言。


9.5 从结果中提取结构化数据

结构化数据在哪里?

当你设置了 outputFormat 后,Agent 的最终结果会出现在 result 类型消息的 structured_output 字段中。

for await (const message of query({ /* ... */ })) {
  // 中间消息(Agent 的思考和工具调用过程)
  if (message.type === "assistant") {
    console.log("Agent 正在工作...");
  }

  // 最终结果
  if (message.type === "result") {
    // structured_output 就是解析好的 JSON 对象
    // 可以直接当普通对象使用
    const data = message.structured_output;

    if (data) {
      console.log("拿到结构化数据了:", data);
    }
  }
}

处理错误情况

结构化输出并不是 100% 能成功的。有时候 Agent 可能无法生成符合你 Schema 的数据。这通常发生在以下情况:

当出错时,result 消息的 subtype 字段会告诉你出了什么问题:

for await (const message of query({
  prompt: "提取文档中的联系人信息",
  options: {
    allowedTools: ["Read"],
    outputFormat: {
      type: "json_schema",
      schema: contactSchema
    }
  }
})) {
  if (message.type === "result") {
    if (message.subtype === "success" && message.structured_output) {
      // 成功了!直接用
      const contacts = message.structured_output;
      console.log("提取到的联系人:", contacts);
    } else {
      // 失败了,需要处理
      console.error("结构化输出失败:", message.subtype);
      // 可能的策略:
      // 1. 用更简单的 Schema 重试
      // 2. 回退到非结构化输出,手动解析文本
      // 3. 记录日志,通知人工处理
    }
  }
}

Python 版本:

async for message in query(
    prompt="提取文档中的联系人信息",
    options=ClaudeCodeOptions(
        allowed_tools=["Read"],
        output_format=OutputFormat(
            type="json_schema",
            schema=contact_schema
        )
    )
):
    if message.type == "result":
        if message.subtype == "success" and message.structured_output:
            contacts = message.structured_output
            print("提取到的联系人:", contacts)
        else:
            print(f"结构化输出失败: {message.subtype}")

最佳实践:防御性编程

即使 Agent 返回了 structured_output,也建议做一次本地验证,双重保险:

import { z } from "zod";

const ContactSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  phone: z.string().optional()
});

// ... 在拿到 structured_output 后 ...
const parsed = ContactSchema.safeParse(message.structured_output);

if (parsed.success) {
  // 类型安全,放心使用
  const contact = parsed.data;
  console.log(contact.name, contact.email);
} else {
  // 数据不符合预期
  console.error("验证失败:", parsed.error.issues);
}

9.6 实战示例

示例一:分析 GitHub 仓库,返回结构化项目报告

这个示例让 Agent 分析一个本地项目目录,返回一份完整的项目分析报告。

import { query } from "@anthropic-ai/claude-code";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

// ---- 定义报告格式 ----

const FileStatsSchema = z.object({
  total_files: z.number().describe("文件总数"),
  by_extension: z.record(z.string(), z.number()).describe("按扩展名统计,如 { '.ts': 15, '.json': 3 }")
});

const ProjectReportSchema = z.object({
  name: z.string().describe("项目名称"),
  version: z.string().describe("版本号,如 package.json 中的 version"),
  language: z.string().describe("主要编程语言"),
  framework: z.string().describe("使用的主要框架,如 React、Express、FastAPI 等"),
  description: z.string().describe("项目描述,2-3 句话"),
  file_stats: FileStatsSchema.describe("文件统计信息"),
  dependencies: z.array(z.object({
    name: z.string(),
    version: z.string(),
    purpose: z.string().describe("这个依赖是干什么的,一句话说明")
  })).describe("前 10 个最重要的依赖"),
  architecture: z.object({
    pattern: z.string().describe("架构模式,如 MVC、分层架构、微服务等"),
    entry_point: z.string().describe("入口文件路径"),
    key_directories: z.array(z.object({
      path: z.string(),
      purpose: z.string()
    })).describe("关键目录及其用途")
  }),
  quality_score: z.number().min(1).max(10).describe("代码质量总评分"),
  highlights: z.array(z.string()).describe("项目亮点,最多 3 条"),
  concerns: z.array(z.string()).describe("值得关注的问题,最多 3 条")
});

type ProjectReport = z.infer<typeof ProjectReportSchema>;

// ---- 执行分析 ----

async function analyzeRepo(repoPath: string): Promise<ProjectReport> {
  const schema = zodToJsonSchema(ProjectReportSchema);

  for await (const message of query({
    prompt: `请全面分析 ${repoPath} 目录下的项目。
阅读 package.json(或其他配置文件)、浏览目录结构、查看关键源文件。
然后给出一份完整的项目分析报告。`,
    options: {
      allowedTools: ["Read", "Glob", "Grep", "Bash"],
      cwd: repoPath,
      outputFormat: {
        type: "json_schema",
        schema: schema
      }
    }
  })) {
    if (message.type === "result" && message.structured_output) {
      const parsed = ProjectReportSchema.safeParse(message.structured_output);
      if (parsed.success) {
        return parsed.data;
      }
      throw new Error(`验证失败: ${JSON.stringify(parsed.error.issues)}`);
    }
  }
  throw new Error("未获得结果");
}

// ---- 使用 ----

const report = await analyzeRepo("/path/to/my-project");

console.log(`=== ${report.name} v${report.version} ===`);
console.log(`语言: ${report.language} | 框架: ${report.framework}`);
console.log(`质量评分: ${report.quality_score}/10`);
console.log(`\n${report.description}`);
console.log(`\n文件统计: ${report.file_stats.total_files} 个文件`);
console.log(`\n项目亮点:`);
report.highlights.forEach(h => console.log(`  + ${h}`));
console.log(`\n关注问题:`);
report.concerns.forEach(c => console.log(`  ! ${c}`));

运行后你可能会得到类似这样的输出:

=== my-awesome-app v1.2.0 ===
语言: TypeScript | 框架: Next.js
质量评分: 7/10

一个基于 Next.js 的全栈 Web 应用,提供用户管理和数据分析功能。使用了 Prisma 作为 ORM,Tailwind CSS 做样式。

文件统计: 127 个文件

项目亮点:
  + 目录结构清晰,分层合理
  + TypeScript 严格模式,类型覆盖好
  + 有完善的 API 路由设计

关注问题:
  ! 测试覆盖率不足,只有 3 个测试文件
  ! 部分组件缺少错误边界处理
  ! 没有 CI/CD 配置文件

示例二:提取文章关键信息

这个示例让 Agent 阅读一篇文章(网页或文件),提取结构化的关键信息。

import { query } from "@anthropic-ai/claude-code";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

// 定义文章信息格式
const ArticleInfoSchema = z.object({
  title: z.string().describe("文章标题"),
  author: z.string().describe("作者,如果找不到则填'未知'"),
  publish_date: z.string().describe("发布日期,格式 YYYY-MM-DD,找不到则填'未知'"),
  main_points: z.array(z.string()).describe("文章的 3-5 个核心观点"),
  summary: z.string().describe("200 字以内的文章摘要"),
  sentiment: z.enum(["positive", "negative", "neutral", "mixed"]).describe("文章整体情感倾向"),
  topics: z.array(z.string()).describe("文章涉及的主题标签,3-5 个"),
  key_quotes: z.array(z.object({
    quote: z.string().describe("原文引用"),
    context: z.string().describe("这句话的上下文说明")
  })).describe("文章中最重要的 2-3 句引用")
});

type ArticleInfo = z.infer<typeof ArticleInfoSchema>;

async function extractArticleInfo(articleUrl: string): Promise<ArticleInfo> {
  const schema = zodToJsonSchema(ArticleInfoSchema);

  for await (const message of query({
    prompt: `请访问并阅读这篇文章: ${articleUrl}
提取文章的关键信息,包括标题、作者、核心观点、摘要等。`,
    options: {
      allowedTools: ["WebFetch", "WebSearch"],
      outputFormat: {
        type: "json_schema",
        schema: schema
      }
    }
  })) {
    if (message.type === "result" && message.structured_output) {
      const parsed = ArticleInfoSchema.safeParse(message.structured_output);
      if (parsed.success) {
        return parsed.data;
      }
      throw new Error("验证失败");
    }
  }
  throw new Error("未获得结果");
}

// 使用
const info = await extractArticleInfo("https://example.com/some-article");

console.log(`标题: ${info.title}`);
console.log(`作者: ${info.author}`);
console.log(`情感: ${info.sentiment}`);
console.log(`标签: ${info.topics.join(", ")}`);
console.log(`\n核心观点:`);
info.main_points.forEach((p, i) => console.log(`  ${i + 1}. ${p}`));
console.log(`\n摘要: ${info.summary}`);

示例三:从源代码生成结构化 API 文档

这个示例让 Agent 读取源代码文件,生成结构化的 API 文档 JSON,可以直接用来渲染文档网站。

from pydantic import BaseModel, Field
from claude_code_sdk import query, ClaudeCodeOptions, OutputFormat

# ---- 定义 API 文档格式 ----

class Parameter(BaseModel):
    name: str = Field(description="参数名")
    type: str = Field(description="参数类型")
    required: bool = Field(description="是否必填")
    description: str = Field(description="参数说明")
    default: str | None = Field(default=None, description="默认值")

class APIEndpoint(BaseModel):
    name: str = Field(description="函数/方法名")
    description: str = Field(description="功能说明")
    parameters: list[Parameter] = Field(description="参数列表")
    return_type: str = Field(description="返回值类型")
    return_description: str = Field(description="返回值说明")
    example: str = Field(description="使用示例代码")
    tags: list[str] = Field(description="标签,如 ['async', 'public']")

class APIDocumentation(BaseModel):
    module_name: str = Field(description="模块名称")
    module_description: str = Field(description="模块整体说明")
    version: str = Field(description="版本号")
    endpoints: list[APIEndpoint] = Field(description="所有 API 端点/函数列表")
    dependencies: list[str] = Field(description="外部依赖列表")

# ---- 执行 ----

async def generate_api_docs(source_file: str) -> APIDocumentation:
    schema = APIDocumentation.model_json_schema()

    async for message in query(
        prompt=f"""请阅读源代码文件 {source_file},为其中所有的公开 API(public 函数/方法/类)
生成结构化的 API 文档。每个 API 都需要包含参数说明、返回值说明和使用示例。""",
        options=ClaudeCodeOptions(
            allowed_tools=["Read", "Glob", "Grep"],
            output_format=OutputFormat(
                type="json_schema",
                schema=schema
            )
        )
    ):
        if message.type == "result" and message.structured_output:
            return APIDocumentation.model_validate(message.structured_output)

    raise RuntimeError("未获得结果")

# ---- 使用 ----

import asyncio

async def main():
    docs = await generate_api_docs("src/utils.py")

    print(f"# {docs.module_name} API 文档")
    print(f"\n{docs.module_description}")
    print(f"\n版本: {docs.version}")

    for endpoint in docs.endpoints:
        print(f"\n## {endpoint.name}")
        print(f"\n{endpoint.description}")
        print(f"\n参数:")
        for param in endpoint.parameters:
            required = "必填" if param.required else "可选"
            print(f"  - `{param.name}` ({param.type}, {required}): {param.description}")
        print(f"\n返回: {endpoint.return_type} - {endpoint.return_description}")
        print(f"\n示例:\n```\n{endpoint.example}\n```")

asyncio.run(main())

9.7 复杂 Schema 技巧

技巧一:可选字段

有时候你不确定 Agent 能不能找到某个信息,可以把字段设为可选的:

// Zod 版本
const Schema = z.object({
  name: z.string(),                    // 必填
  email: z.string().optional(),        // 可选
  age: z.number().nullable(),          // 可以是 null
  bio: z.string().default("无")        // 有默认值
});
# Pydantic 版本
class UserInfo(BaseModel):
    name: str                                    # 必填
    email: str | None = None                     # 可选
    age: int | None = None                       # 可以是 None
    bio: str = "无"                              # 有默认值

技巧二:枚举限制值的范围

当某个字段只有几种可能的值时,用枚举来限制:

// Zod
const severity = z.enum(["critical", "warning", "info"]);

// 完整例子
const IssueSchema = z.object({
  message: z.string(),
  severity: z.enum(["critical", "warning", "info"]),
  category: z.enum(["security", "performance", "style", "logic"])
});

技巧三:嵌套和递归

Schema 可以嵌套使用,也可以互相引用:

// 先定义子模块
const AddressSchema = z.object({
  city: z.string(),
  country: z.string()
});

const CompanySchema = z.object({
  name: z.string(),
  address: AddressSchema,          // 嵌套使用
  subsidiaries: z.array(z.object({ // 数组中嵌套对象
    name: z.string(),
    address: AddressSchema          // 复用同一个 Schema
  }))
});

技巧四:description 很重要

description 不只是给人看的文档 —— Agent 也会读这些描述来理解每个字段该填什么。写好 description 能显著提高输出质量:

// 差的写法 —— Agent 不知道你要什么
const BadSchema = z.object({
  s: z.number(),
  i: z.array(z.string())
});

// 好的写法 —— Agent 一看就懂
const GoodSchema = z.object({
  score: z.number()
    .min(1).max(10)
    .describe("代码质量总评分,1分最差10分最好,严格按照以下标准评分:1-3差、4-6中等、7-9良好、10优秀"),
  issues: z.array(z.string())
    .describe("发现的具体问题列表,每条用一句话描述,包含问题所在的文件名和行号")
});

动手练习

练习1:分析项目,返回结构化 JSON 报告

目标: 让 Agent 分析你自己的某个项目,返回一份结构化的分析报告。

要求:

  1. 定义一个包含以下字段的 Schema:
    • name: 项目名称
    • tech_stack: 技术栈列表
    • file_count: 文件总数
    • lines_of_code: 代码行数(估算)
    • test_coverage: 测试情况描述
    • score: 总体评分 1-10
    • recommendations: 改进建议列表(至少 3 条)
  2. 用 Zod 或 Pydantic 定义 Schema
  3. 调用 query() 并获取结构化结果
  4. 把结果打印成漂亮的格式

提示: 给 Agent 足够的工具(Read、Glob、Grep、Bash),让它能全面了解项目。

练习2:提取文章关键信息,返回 JSON

目标: 让 Agent 读取一个文本文件(或一个网页),提取关键信息。

要求:

  1. 准备一篇文章(可以是本地 .txt 文件或一个网页 URL)
  2. 定义 Schema 包含:
    • title: 标题
    • word_count: 字数(估算)
    • language: 文章语言
    • key_points: 核心要点列表(3-5 条)
    • entities: 提到的重要实体(人名、公司名、产品名等)
    • sentiment: 情感倾向
    • one_line_summary: 一句话总结
  3. 对比结构化输出和非结构化输出的区别 —— 不设 outputFormat 时 Agent 怎么回复?设了之后又是什么样?

练习3:批量处理 + 结构化输出

进阶目标: 让 Agent 批量分析多个文件,每个文件返回一份结构化报告,最后汇总。

提示:

// 对多个文件分别分析
const files = ["src/a.ts", "src/b.ts", "src/c.ts"];
const reports = [];

for (const file of files) {
  const report = await analyzeFile(file); // 你写的函数
  reports.push(report);
}

// 汇总
const avgScore = reports.reduce((sum, r) => sum + r.score, 0) / reports.length;
console.log(`平均分: ${avgScore.toFixed(1)}`);

本章小结

这一章我们学了 Agent 的结构化输出功能。回顾一下关键知识点:

  1. 为什么要用:Agent 默认返回自然语言文本,不方便程序处理。结构化输出让 Agent 返回规范的 JSON 数据。

  2. 怎么用:在 query()options 中设置 outputFormat(TypeScript)或 output_format(Python),指定 type: "json_schema" 和一个 JSON Schema。

  3. 简化定义

    • TypeScript 用 Zod + zodToJsonSchema() —— 写起来像写类型,还能自动推导 TypeScript 类型
    • Python 用 Pydantic + .model_json_schema() —— 写起来像写数据类,验证功能强大
  4. 获取结果:结构化数据在 result 消息的 structured_output 字段中,可以直接当对象使用。

  5. 错误处理:结构化输出可能失败,检查 subtype 字段判断成功还是失败,建议用 Zod/Pydantic 做二次验证。

  6. Schema 设计技巧:善用 description 帮助 Agent 理解字段含义,合理使用可选字段和枚举类型,Schema 不要太复杂。

一句话总结:outputFormat + JSON Schema = 让 Agent 从"写作文"变成"填表格",程序处理起来爽多了。


下一章预告

下一章我们进入第三篇"学跑步",学习自定义工具 —— 内置工具不够用?没问题,自己给 Agent 造新工具!你会学到如何创建自定义的 MCP 工具,让 Agent 能查天气、操作数据库、调用任何你想要的 API。

← 上一章8. 会话管理