在前一篇文章中,我们探讨了如何将 Codex CLI 集成到 CI/CD 流水线。但这里有一个关键问题:

结构化输出 —— JSON Schema 约束、格式化输出、Schema 验证

简介

在前一篇文章中,我们探讨了如何将 Codex CLI 集成到 CI/CD 流水线。但这里有一个关键问题:

AI 返回的自由格式文本,下游系统无法直接处理。

如果你的 CI 流水线需要:

  • 自动将审查结果发布到 PR 评论
  • 将发现的安全问题创建为 GitHub Issues
  • 将代码指标写入数据库
  • 触发基于特定条件的后续流程

那么你需要的不是"一大段文字",而是结构化的、可解析的数据

这就是本文要解决的问题:如何约束 Codex CLI 的输出为严格的 JSON Schema 格式,并确保输出的数据可以直接被下游系统消费。

一、为什么需要结构化输出

1.1 自由格式输出的问题

假设你让 Codex 审查一个 PR:

bash
codex exec --full-auto "Review this PR and tell me what's wrong."

AI 可能会返回:

text
好的,让我看看这个 PR...

我发现了一些问题。首先是安全方面的,在第 42 行有一个潜在的 SQL 注入。
另外代码风格也不太统一,有些地方用了 tab,有些地方用了空格。

哦对了,还有一个 bug,如果用户输入为空字符串,第 88 行的逻辑会抛出异常。

总体来说,这个 PR 需要修复这些问题才能合并。

这段文本对人类来说没问题,但如果你想自动提取问题列表,就会非常困难。

1.2 结构化输出的优势

同样的审查请求,使用结构化输出:

json
{
  "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

json
{
  "$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 保存到文件

bash
# 保存 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 格式:

bash
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:

bash
# 创建 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

bash
# 方式 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 不同场景的输出格式

bash
# 场景 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 markdown

4.2 自定义输出模板

bash
# 创建一个输出模板
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 多格式输出管道

bash
#!/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)

bash
# 安装 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.mjs

5.2 使用 jsonschema 验证(Python)

bash
# 安装
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.py

5.3 使用 jsonschema 验证(Go)

bash
# 安装
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")
}
EOF

5.4 CI 流水线中的验证集成

yaml
      - 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

json
{
  "$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

json
{
  "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 数组必须至少有一个元素。

七、实战:完整的结构化审查系统

bash
#!/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 管理就变得至关重要。