第9章:结构化输出 —— 让 Agent 按格式返回数据
一句话:让 Agent 返回 JSON 等结构化数据,方便程序直接处理,不用再自己"猜"Agent 到底说了啥。
本章目标
- 理解为什么需要结构化输出,以及它解决了什么问题
- 学会用 JSON Schema 定义输出格式
- 学会用 Zod(TypeScript)和 Pydantic(Python)简化 Schema 定义
- 学会从 Agent 返回的结果中提取和使用结构化数据
- 学会处理结构化输出的错误情况
- 通过三个实战示例掌握各种场景下的用法
前置知识
- 需要先看完第4章(query() 函数基础用法)
- 需要先看完第7章(流式输出,理解消息类型)
- 了解 JSON 基本格式
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}")
注意事项
outputFormat只支持type: "json_schema",目前没有其他类型schema必须是一个合法的 JSON Schema 对象- Agent 会先正常干活(读文件、搜索代码等),最后把结果按你的 Schema 格式输出
- 结构化输出出现在
result类型消息的structured_output字段中
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 的好处总结
- 写起来简单 —— 比手写 JSON Schema 省一半的代码
- 类型安全 ——
z.infer<typeof Schema>自动推导出 TypeScript 类型,编辑器有提示 - 运行时验证 ——
safeParse()可以在运行时检查数据是否合法 - 可组合 —— 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 的好处
- Pythonic —— 用类定义数据模型,Python 开发者天然熟悉
- 类型提示 —— 编辑器有完整的类型提示和自动补全
- 验证严格 —— 自动检查数据类型、范围、格式等
- 生态强大 —— 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 的数据。这通常发生在以下情况:
- Schema 太复杂了,Agent "理解"不了
- 任务本身太模糊,Agent 不知道该填什么
- Agent 尝试了多次修正后还是通不过验证
当出错时,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 分析你自己的某个项目,返回一份结构化的分析报告。
要求:
- 定义一个包含以下字段的 Schema:
name: 项目名称tech_stack: 技术栈列表file_count: 文件总数lines_of_code: 代码行数(估算)test_coverage: 测试情况描述score: 总体评分 1-10recommendations: 改进建议列表(至少 3 条)
- 用 Zod 或 Pydantic 定义 Schema
- 调用
query()并获取结构化结果 - 把结果打印成漂亮的格式
提示: 给 Agent 足够的工具(Read、Glob、Grep、Bash),让它能全面了解项目。
练习2:提取文章关键信息,返回 JSON
目标: 让 Agent 读取一个文本文件(或一个网页),提取关键信息。
要求:
- 准备一篇文章(可以是本地 .txt 文件或一个网页 URL)
- 定义 Schema 包含:
title: 标题word_count: 字数(估算)language: 文章语言key_points: 核心要点列表(3-5 条)entities: 提到的重要实体(人名、公司名、产品名等)sentiment: 情感倾向one_line_summary: 一句话总结
- 对比结构化输出和非结构化输出的区别 —— 不设
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 的结构化输出功能。回顾一下关键知识点:
为什么要用:Agent 默认返回自然语言文本,不方便程序处理。结构化输出让 Agent 返回规范的 JSON 数据。
怎么用:在
query()的options中设置outputFormat(TypeScript)或output_format(Python),指定type: "json_schema"和一个 JSON Schema。简化定义:
- TypeScript 用 Zod +
zodToJsonSchema()—— 写起来像写类型,还能自动推导 TypeScript 类型 - Python 用 Pydantic +
.model_json_schema()—— 写起来像写数据类,验证功能强大
- TypeScript 用 Zod +
获取结果:结构化数据在
result消息的structured_output字段中,可以直接当对象使用。错误处理:结构化输出可能失败,检查
subtype字段判断成功还是失败,建议用 Zod/Pydantic 做二次验证。Schema 设计技巧:善用
description帮助 Agent 理解字段含义,合理使用可选字段和枚举类型,Schema 不要太复杂。
一句话总结:outputFormat + JSON Schema = 让 Agent 从"写作文"变成"填表格",程序处理起来爽多了。
下一章预告
下一章我们进入第三篇"学跑步",学习自定义工具 —— 内置工具不够用?没问题,自己给 Agent 造新工具!你会学到如何创建自定义的 MCP 工具,让 Agent 能查天气、操作数据库、调用任何你想要的 API。