结构化输出 —— JSON Schema 约束、格式化输出、Schema 验证
简介
在前一篇文章中,我们探讨了如何将 Codex CLI 集成到 CI/CD 流水线。但这里有一个关键问题:
AI 返回的自由格式文本,下游系统无法直接处理。
如果你的 CI 流水线需要:
- 自动将审查结果发布到 PR 评论
- 将发现的安全问题创建为 GitHub Issues
- 将代码指标写入数据库
- 触发基于特定条件的后续流程
那么你需要的不是"一大段文字",而是结构化的、可解析的数据。
这就是本文要解决的问题:如何约束 Codex CLI 的输出为严格的 JSON Schema 格式,并确保输出的数据可以直接被下游系统消费。
一、为什么需要结构化输出
1.1 自由格式输出的问题
假设你让 Codex 审查一个 PR:
codex exec --full-auto "Review this PR and tell me what's wrong."AI 可能会返回:
好的,让我看看这个 PR...
我发现了一些问题。首先是安全方面的,在第 42 行有一个潜在的 SQL 注入。
另外代码风格也不太统一,有些地方用了 tab,有些地方用了空格。
哦对了,还有一个 bug,如果用户输入为空字符串,第 88 行的逻辑会抛出异常。
总体来说,这个 PR 需要修复这些问题才能合并。这段文本对人类来说没问题,但如果你想自动提取问题列表,就会非常困难。
1.2 结构化输出的优势
同样的审查请求,使用结构化输出:
{
"review": {
"status": "changes_requested",
"summary": "3 issues found: 1 security, 1 bug, 1 style",
"issues": [
{
"type": "security",
"severity": "high",
"file": "src/db/query.ts",
"line": 42,
"description": "Potential SQL injection via string concatenation",
"suggestion": "Use parameterized queries"
},
{
"type": "bug",
"severity": "medium",
"file": "src/handler.ts",
"line": 88,
"description": "Null pointer exception on empty string input",
"suggestion": "Add null/empty check before processing"
},
{
"type": "style",
"severity": "low",
"file": "src/utils.ts",
"line": null,
"description": "Inconsistent indentation (tabs vs spaces)",
"suggestion": "Configure editor to use spaces consistently"
}
]
}
}这样的输出可以被直接解析、过滤、聚合和展示。
二、JSON Schema 基础
2.1 什么是 JSON Schema
JSON Schema 是一种用于描述 JSON 数据格式的规范。它定义了:
- 数据必须是对象还是数组
- 每个字段的类型(字符串、数字、布尔值等)
- 哪些字段是必需的
- 字符串的格式和模式
- 数值的范围
2.2 代码审查的 JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodeReview",
"type": "object",
"required": ["status", "summary", "issues"],
"properties": {
"status": {
"type": "string",
"enum": ["approved", "changes_requested", "commented"]
},
"summary": {
"type": "string",
"minLength": 10,
"maxLength": 500
},
"issues": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "severity", "description"],
"properties": {
"type": {
"type": "string",
"enum": ["security", "bug", "performance", "style", "documentation"]
},
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low", "info"]
},
"file": {
"type": "string",
"pattern": "^[a-zA-Z0-9_./-]+$"
},
"line": {
"type": ["integer", "null"],
"minimum": 1
},
"description": {
"type": "string",
"minLength": 5
},
"suggestion": {
"type": "string"
}
}
}
},
"score": {
"type": "integer",
"minimum": 0,
"maximum": 100
}
}
}2.3 将 Schema 保存到文件
# 保存 schema 到文件
cat > review-schema.json << 'EOF'
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CodeReview",
"type": "object",
"required": ["status", "summary", "issues"],
"properties": {
"status": { "type": "string", "enum": ["approved", "changes_requested"] },
"summary": { "type": "string" },
"issues": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "severity", "file", "description"],
"properties": {
"type": { "type": "string", "enum": ["security", "bug", "performance", "style"] },
"severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] },
"file": { "type": "string" },
"line": { "type": ["integer", "null"], "minimum": 1 },
"description": { "type": "string" },
"suggestion": { "type": "string" }
}
}
}
}
}
EOF三、在 Codex CLI 中使用 Schema
3.1 通过 Prompt 约束输出
最直接的方式是在 Prompt 中明确要求 JSON 格式:
codex exec --full-auto \
"Review the code changes and output ONLY a JSON object following this schema:
{
\"status\": \"approved\" | \"changes_requested\",
\"summary\": \"brief summary\",
\"issues\": [
{
\"type\": \"security\" | \"bug\" | \"performance\" | \"style\",
\"severity\": \"critical\" | \"high\" | \"medium\" | \"low\",
\"file\": \"file path\",
\"line\": 42 or null,
\"description\": \"what's wrong\",
\"suggestion\": \"how to fix\"
}
]
}
Output valid JSON only, no markdown, no explanation."注意: 这种方式依赖于 AI 遵循指令的能力,不保证 100% 正确。
3.2 使用 OpenAI 的 response_format 参数
如果使用 OpenAI API 的 response_format 功能,可以强制模型输出符合 Schema 的 JSON:
# 创建 API 请求
cat > api-request.json << 'EOF'
{
"model": "o3",
"messages": [
{
"role": "system",
"content": "You are a code review assistant. Analyze the code and return findings in the specified JSON format."
},
{
"role": "user",
"content": "Review the following code changes:\n$(cat /tmp/pr_diff.txt)"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "code_review",
"schema": {
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["approved", "changes_requested"]},
"summary": {"type": "string"},
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["security", "bug", "performance", "style"]},
"severity": {"type": "string", "enum": ["critical", "high", "medium", "low"]},
"file": {"type": "string"},
"line": {"type": ["integer", "null"]},
"description": {"type": "string"},
"suggestion": {"type": "string"}
},
"required": ["type", "severity", "file", "description"]
}
}
},
"required": ["status", "summary", "issues"]
},
"strict": true
}
}
}
EOF
# 发送请求
curl -s https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d @api-request.json | jq '.output[0].content[0].text'strict: true 是关键 —— 它告诉 API 强制输出符合 Schema 的 JSON。
3.3 通过 Codex CLI 的管道集成 Schema
# 方式 1:使用 jq 后验证
codex exec --full-auto "Review and output JSON..." | \
jq '.' && echo "Valid JSON" || echo "Invalid JSON"
# 方式 2:完整的验证管道
codex exec --full-auto "Review and output JSON..." | \
python3 -c "
import json, sys
from jsonschema import validate, ValidationError
schema = json.load(open('review-schema.json'))
data = json.load(sys.stdin)
try:
validate(instance=data, schema=schema)
print('✅ Schema validation passed')
except ValidationError as e:
print(f'❌ Schema validation failed: {e.message}')
sys.exit(1)
"四、格式化输出
4.1 不同场景的输出格式
# 场景 1:CI/CD 管道 —— JSON 格式
codex exec --full-auto "Review..." --output-format json
# 场景 2:终端展示 —— 表格格式
codex exec --full-auto "Review..." --output-format table
# 场景 3:文档生成 —— Markdown 格式
codex exec --full-auto "Review..." --output-format markdown4.2 自定义输出模板
# 创建一个输出模板
cat > review-template.tmpl << 'EOF'
# Code Review Report
## Status: {{.status}}
## Summary
{{.summary}}
## Issues Found: {{len .issues}}
| # | Type | Severity | File | Line | Description |
|---|------|----------|------|------|-------------|
{{range .issues}}| {{.type}} | {{.severity}} | {{.file}} | {{.line}} | {{.description}} |
{{end}}
## Recommendations
{{range .issues}}
- [{{.severity}}] {{.file}}:{{.line}} - {{.suggestion}}
{{end}}
EOF
# 使用模板渲染
codex exec --full-auto "Review and output JSON..." | \
jq -r '"# Code Review Report\n\n## Status: \(.status)\n\n## Summary\n\(.summary)\n\n## Issues\n\(.issues | map("- [\" + .severity + \"] " + .file + ":" + (.line|tostring) + " - " + .description) | join("\n"))"'4.3 多格式输出管道
#!/bin/bash
# review-pipeline.sh —— 一次审查,多种输出
RESULT=$(codex exec --full-auto "Review and output JSON...")
# 1. 保存原始 JSON
echo "$RESULT" > review-result.json
# 2. 提取严重问题创建 GitHub Issue
echo "$RESULT" | jq -r '.issues[] | select(.severity == "critical" or .severity == "high")' | \
while read -r issue; do
echo "$issue" | jq -r '.description' | \
xargs -I{} gh issue create --title "🚨 {}" --label "bug"
done
# 3. 生成 PR 评论
COMMENT=$(echo "$RESULT" | jq -r '"## Review Summary\n\nStatus: \(.status)\n\n\(.summary)\n\n### Issues\n\n" + (.issues | map("- **[\(.severity)]** \(.file):\(.line) - \(.description)") | join("\n"))')
gh pr comment $PR_NUMBER --body "$COMMENT"
# 4. 生成 HTML 报告
echo "$RESULT" | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f'<h1>Code Review: {data[\"status\"]}</h1>')
print(f'<p>{data[\"summary\"]}</p>')
print('<ul>')
for issue in data['issues']:
print(f'<li><b>[{issue[\"severity\"]}]</b> {issue[\"file\"]}:{issue[\"line\"]} - {issue[\"description\"]}</li>')
print('</ul>')
" > review-report.html五、Schema 验证
5.1 使用 ajv 验证(Node.js)
# 安装 ajv
npm install ajv ajv-formats
# 创建验证脚本
cat > validate.mjs << 'EOF'
import Ajv from "ajv";
import addFormats from "ajv-formats";
import fs from "fs";
const ajv = new Ajv({ strict: true });
addFormats(ajv);
const schema = JSON.parse(fs.readFileSync("review-schema.json", "utf8"));
const validate = ajv.compile(schema);
// 从 stdin 读取 JSON
let input = "";
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => {
try {
const data = JSON.parse(input);
const valid = validate(data);
if (valid) {
console.log("✅ JSON Schema validation passed");
process.exit(0);
} else {
console.error("❌ Schema validation failed:");
validate.errors.forEach(err => {
console.error(` ${err.instancePath}: ${err.message}`);
});
process.exit(1);
}
} catch (e) {
console.error("❌ Invalid JSON:", e.message);
process.exit(1);
}
});
EOF
# 使用
codex exec --full-auto "Review and output JSON..." | node validate.mjs5.2 使用 jsonschema 验证(Python)
# 安装
pip install jsonschema
# 创建验证脚本
cat > validate.py << 'EOF'
import json
import sys
from jsonschema import validate, ValidationError, Draft7Validator
def main():
# 加载 schema
with open("review-schema.json") as f:
schema = json.load(f)
# 从 stdin 或文件读取数据
if len(sys.argv) > 1:
with open(sys.argv[1]) as f:
data = json.load(f)
else:
data = json.load(sys.stdin)
# 验证
validator = Draft7Validator(schema)
errors = list(validator.iter_errors(data))
if errors:
print("❌ Schema validation failed:")
for error in errors:
path = ".".join(str(p) for p in error.absolute_path)
print(f" {path}: {error.message}")
sys.exit(1)
else:
print("✅ Schema validation passed")
# 输出验证后的数据摘要
print(f"\n Status: {data['status']}")
print(f" Summary: {data['summary'][:100]}...")
print(f" Issues: {len(data['issues'])}")
by_severity = {}
for issue in data['issues']:
sev = issue['severity']
by_severity[sev] = by_severity.get(sev, 0) + 1
for sev, count in sorted(by_severity.items()):
print(f" {sev}: {count}")
if __name__ == "__main__":
main()
EOF
# 使用
codex exec --full-auto "Review and output JSON..." | python3 validate.py5.3 使用 jsonschema 验证(Go)
# 安装
go get github.com/santhosh-tekuri/jsonschema/v5
# 创建验证脚本
cat > validate.go << 'EOF'
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/santhosh-tekuri/jsonschema/v5"
)
func main() {
schema, err := jsonschema.Compile("review-schema.json")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to compile schema: %v\n", err)
os.Exit(1)
}
var data interface{}
if err := json.NewDecoder(os.Stdin).Decode(&data); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse JSON: %v\n", err)
os.Exit(1)
}
if err := schema.Validate(data); err != nil {
fmt.Fprintf(os.Stderr, "❌ Schema validation failed: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Schema validation passed")
}
EOF5.4 CI 流水线中的验证集成
- name: Run Codex and Validate
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
# 执行审查并保存结果
codex exec --full-auto "Review and output JSON..." > review.json
# 验证 JSON 格式
echo "$review" | jq . > /dev/null || {
echo "❌ Invalid JSON output"
exit 1
}
# 验证 Schema
python3 validate.py review.json || {
echo "❌ Schema validation failed"
exit 1
}
# 如果有 critical 级别的问题,标记 PR 为 failed
CRITICAL=$(jq '[.issues[] | select(.severity == "critical")] | length' review.json)
if [ "$CRITICAL" -gt 0 ]; then
echo "Found $CRITICAL critical issues"
exit 1
fi
echo "✅ Review passed with no critical issues"六、进阶:复杂 Schema 设计
6.1 嵌套 Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"review": {
"type": "object",
"properties": {
"metadata": {
"type": "object",
"properties": {
"reviewer": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"model": { "type": "string" }
},
"required": ["reviewer", "timestamp"]
},
"analysis": {
"type": "object",
"properties": {
"files_reviewed": { "type": "array", "items": { "type": "string" } },
"total_changes": { "type": "integer" },
"complexity_score": { "type": "number", "minimum": 0, "maximum": 10 }
}
},
"findings": {
"type": "array",
"items": { "$ref": "#/definitions/finding" }
}
}
}
},
"definitions": {
"finding": {
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["security", "bug", "performance", "style"] },
"severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] },
"location": {
"type": "object",
"properties": {
"file": { "type": "string" },
"line": { "type": "integer" },
"column": { "type": "integer" }
}
},
"description": { "type": "string" },
"remediation": { "type": "string" }
},
"required": ["category", "severity", "description"]
}
}
}6.2 带条件的 Schema
{
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["approved", "changes_requested"] },
"issues": { "type": "array" }
},
"allOf": [
{
"if": {
"properties": { "status": { "const": "changes_requested" } }
},
"then": {
"properties": {
"issues": { "minItems": 1 }
}
}
}
]
}这个 Schema 规定:如果状态是 changes_requested,则 issues 数组必须至少有一个元素。
七、实战:完整的结构化审查系统
#!/bin/bash
# codex-structured-review.sh
# 完整的结构化 PR 审查脚本
set -euo pipefail
PR_NUMBER=$1
REPO="${2:-$(git remote get-url origin | sed 's/.*://' | sed 's/.git$//')}"
# 1. 获取 PR diff
echo "📥 Fetching PR #${PR_NUMBER} diff..."
gh pr diff $PR_NUMBER -R $REPO > /tmp/pr_diff.txt
# 2. 执行结构化审查
echo "🤖 Running Codex review..."
RESULT=$(codex exec --full-auto \
"Analyze the following PR diff and return a JSON object with this exact structure:
{
\"status\": \"approved\" or \"changes_requested\",
\"summary\": \"one-line summary\",
\"score\": 0-100 integer,
\"issues\": [{
\"type\": \"security|bug|performance|style|documentation\",
\"severity\": \"critical|high|medium|low|info\",
\"file\": \"relative file path\",
\"line\": integer or null,
\"description\": \"detailed description\",
\"suggestion\": \"how to fix\"
}],
\"positive_notes\": [\"list of good things\"]
}
PR Diff:
$(cat /tmp/pr_diff.txt)")
# 3. 验证输出
echo "🔍 Validating output..."
echo "$RESULT" | python3 validate.py
# 4. 保存结果
echo "$RESULT" > "review-pr-${PR_NUMBER}.json"
# 5. 生成 PR 评论
SCORE=$(echo "$RESULT" | jq '.score')
STATUS=$(echo "$RESULT" | jq -r '.status')
SUMMARY=$(echo "$RESULT" | jq -r '.summary')
COMMENT="## 🤖 AI Code Review (Score: ${SCORE}/100)
**Status:** ${STATUS}
**Summary:** ${SUMMARY}
### Issues
$(echo "$RESULT" | jq -r '.issues[] | "- **[\(.severity)]** \`" + .file + ":" + (.line|tostring) + "\` - " + .description + "\n 💡 " + .suggestion')
### Positive Notes
$(echo "$RESULT" | jq -r '.positive_notes[] | "- " + .')
"
# 6. 发布评论
gh pr comment $PR_NUMBER --body "$COMMENT" -R $REPO
echo "✅ Review complete for PR #${PR_NUMBER}"总结
结构化输出是将 AI Agent 从"聊天工具"转变为"生产力工具"的关键一步。本文覆盖了:
1. JSON Schema 约束: 如何定义严格的输出格式,确保 AI 返回的数据结构一致。
2. 格式化输出: 不同场景(CI/CD、终端、文档)下的输出格式选择与自定义模板。
3. Schema 验证: 使用 ajv(Node.js)、jsonschema(Python)等工具验证输出是否符合预期。
关键要点:
- 使用 OpenAI 的
response_format+strict: true可以获得最可靠的结构化输出 - 始终在后端验证 AI 的输出,不要信任任何未经校验的数据
- 设计 Schema 时要考虑下游系统的实际需求
- 为不同的使用场景设计不同的 Schema
下篇预告
下一篇我们将探讨 Codex CLI 的 Session 管理:如何使用 --continue 标志继续之前的会话,会话 ID 的工作原理,会话恢复的最佳流程,以及定期清理策略。当你的 AI Agent 需要执行多步骤、长周期的任务时,Session 管理就变得至关重要。