首页 › tutorials › 用-p模式构建CI代码审查闭环

用 -p 模式构建 CI 代码审查闭环

概述

本教程聚焦 Claude Code 的 Headless(无人值守)自动化能力,目标是用 -p(--print)模式把 Claude Code 织进 GitHub Actions,搭建一条「PR 触发 → 拉取 diff → Claude 评审 → 回写评论 → 质量门禁」的端到端代码审查闭环。适合已经能在终端里用 Claude Code 完成单次任务、想把它升级为 CI 常驻评审员的工程师与 DevOps 同学。学完你将掌握:headless 三种输出格式的取舍、stream-json 的逐事件解析、Hooks 作为质量门禁的配置方法,以及一个可直接复制进仓库的完整 workflow。

前置条件


第一步:理解 Headless 模式的三个关键参数

交互式 Claude Code 会启动一个 TUI,等待你逐轮输入。而 headless 模式把这一切变成「一次 prompt 进、一次结果出」的命令,天然适配脚本与 CI。最小用法:

claude -p "用一句话总结当前目录的 README"

在 CI 里,你会反复用到三个参数的组合,先记住这张对照表:

参数 作用 典型取值
-p / --print 进入 headless,不启动 TUI 无值,开关型
--output-format 控制 stdout 输出形态 text / json / stream-json
--input-format 控制 stdin 输入形态 text / stream-json
--allowedTools 限定可用工具白名单 Read,Bash(git:*)
--max-turns 限制 agent 最大循环轮数 1~N,防失控
--add-dir 授予额外目录访问权 绝对路径

预期结果:执行后终端直接打印模型回复,进程结束码为 0。如果出现 interactive mode requires a TTY 之类的报错,多半是少了 -p。

常见问题:CI 里直接 claude -p 偶尔会因缺少 TTY 而 hang。稳妥写法是同时显式声明输入输出格式,避免它去探测终端:

claude -p --output-format text --input-format text "你的 prompt"

第二步:三种输出格式,何时用哪个

headless 的灵魂在于「输出可被程序消费」。三种格式各有定位:

text(默认)——纯文本回复,适合人类阅读或 >> 追加进文件、PR 描述。最简单,但无法区分「模型在思考」还是「模型在调用工具」。

json——把整次会话压成一个 JSON 对象返回,包含 result(最终文本)、cost_usd(花费)、duration_ms(耗时)、is_error 等字段。适合「跑完一次拿总结」的场景:

claude -p --output-format json "列出 src 下所有 TODO" \
  | jq '{todo: .result, cost: .cost_usd}'

stream-json——按事件流逐行(NDJSON)输出,每个事件一行 JSON。这是 CI 自动化的首选,因为你能在「工具调用发生时」就介入,而不必等整轮结束。典型事件包括 system(初始化)、assistant(模型消息)、tool_use(发起工具调用)、tool_result(工具返回)、result(终态汇总)。

一个最小的流式消费脚本,用 jq 边读边过滤:

claude -p --output-format stream-json "评审 git diff HEAD~1 的改动" \
  | jq -c 'select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text'

常见问题:stream-json 模式下若同时给了 --input-format stream-json 却没按协议逐行喂输入,进程会卡住等待(对应社区 issue #53584 的典型复现)。CI 里只设 --output-format stream-json、--input-format 保持 text,是最稳的组合。


第三步:用 Hooks 给 Headless 加质量门禁

headless 模式下没有人盯着「确认执行」按钮,所以护栏必须前置到配置里。Hooks 就是这套护栏——在特定事件点自动执行的 shell 命令,配置在 .claude/settings.json。

最常用的是两个事件:

下面这段配置做两件事:① 禁止 headless 模式下 git push --force;② 每次 Edit/Write 之后自动跑 eslint --fix。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' | grep -q 'push --force' && { echo '禁止 force push' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "npx eslint --fix $(jq -r '.tool_input.file_path')" }
        ]
      }
    ]
  }
}

关键约定:Hook 通过 stdin 收到一段 JSON(含 session_id、tool_name、tool_input);退出码 0 放行,退出码 2 阻断并把 stderr 反馈给模型让其改道,其他非零码视为错误。CI 里务必用 jq 解析 stdin,别用 grep 硬切。

预期结果:当 headless Claude 试图执行 git push --force 时,命令被拦下,模型收到反馈后会改用普通 git push。

常见问题:PostToolUse 里直接 npx eslint 每次都冷启动很慢。优化方案是把它包成一个脚本,先 test -f node_modules/.bin/eslint 走本地、否则才 fallback 到 npx,CI 里通常已 npm ci 装好依赖。


第四步:把闭环装进 GitHub Actions

现在把前三步拼成一条 workflow。设计目标:每个 PR 打开或更新时,Claude 自动评审 diff,并以评论形式贴回 PR。

先在仓库 Settings → Secrets and variables → Actions 添加 ANTHROPIC_API_KEY。然后新建 .github/workflows/claude-review.yml:

name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write   # 回写评论必须

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # 需要完整历史才能取 diff

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Fetch PR diff
        id: diff
        run: |
          git diff origin/${{ github.base_ref }}...HEAD > pr.diff
          echo "size=$(wc -c < pr.diff)" >> $GITHUB_OUTPUT

      - name: Run Claude review
        if: steps.diff.outputs.size != '0'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p \
            --output-format json \
            --max-turns 3 \
            --allowedTools "Read" \
            "你是资深代码评审。阅读 pr.diff,针对每个文件给出:1) 风险等级(高/中/低) 2) 具体问题 3) 修改建议。用 Markdown 输出,不要客套话。" \
            | jq -r '.result' > review.md

      - name: Post review comment
        if: steps.diff.outputs.size != '0'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = '## 🤖 Claude Code 评审\n\n' + fs.readFileSync('review.md', 'utf8');
            await github.rest.issues.createComment({
              ...context.repo,
              issue_number: context.issue.number,
              body
            });

预期结果:PR 创建后约 30~60 秒,会自动出现一条带「风险等级/问题/建议」的评论。

常见问题与对策:

  1. 评论过长被截断——GitHub 单条评论上限约 65536 字符。在 jq 后加 head -c 60000 截断,或按文件拆成多条评论。
  2. 大 diff 烧 token——加 if: steps.diff.outputs.size < 50000 的前置判断,超大 PR 改为只评审变更过的关键文件。
  3. 评审重复刷屏——用 github.rest.issues.listComments 先查是否已有 bot 评论,有则 updateComment 而非新建。

第五步:批量任务编排——从单 PR 到多仓库

单条 workflow 跑通后,下一步是批量。两种典型形态:

形态 A:一次评审多个文件。把文件列表喂给一个 headless 进程,让它串行处理,输出用 stream-json 区分每个文件的结果:

printf '%s\n' src/a.ts src/b.ts src/c.ts | while read f; do
  claude -p --output-format json --allowedTools Read \
    "只评审 $f 的安全性问题" \
    | jq --arg f "$f" '{file: $f, result: .result}'
done > batch-review.jsonl

形态 B:多仓库定时巡检。用 workflow_dispatch + schedule 触发,遍历一组仓库清单。关键是用 --max-turns 兜底,防止某个仓库让 agent 陷入死循环:

on:
  schedule:
    - cron: '17 2 * * 1'   # 每周一凌晨(避开整点高峰)
  workflow_dispatch:

批量通用护栏:


实战案例:可一键复现的「风险分级评审机器人」

把上面的零件组装成一个最小但完整的项目。目录结构:

.ai-reviewer/
├── settings.json          # Hooks 护栏
├── prompt.md              # 评审提示词
└── run.sh                 # 本地预演脚本
.github/workflows/
└── claude-review.yml      # CI 入口

.ai-reviewer/prompt.md(评审提示词,单文件维护便于迭代):

你是一名注重工程实效的代码评审。对给定 diff 按下列规则评审:

## 评审范围
- 只看本次变更(diff),不评审未改动代码
- 优先级:安全 > 正确性 > 可读性 > 风格

## 输出格式(严格遵守)
对每个文件输出一个区块:
### `文件路径`
- **风险**:高 | 中 | 低
- **问题**:(编号列表,每条 ≤ 2 行)
- **建议**:(对应修复,给最小可行代码片段)

## 边界
- 不输出"整体看起来不错"等无信息量的话
- 风格类问题合并为一条,不逐条罗列

.ai-reviewer/run.sh(本地预演,CI 前 dry-run):

#!/usr/bin/env bash
set -euo pipefail

DIFF_FILE="${1:-pr.diff}"
PROMPT="$(cat .ai-reviewer/prompt.md)"

# 本地预演:用 HEAD~1 的 diff 当样本
[ -f "$DIFF_FILE" ] || git diff HEAD~1 > "$DIFF_FILE"

claude -p \
  --output-format json \
  --max-turns 4 \
  --allowedTools "Read,Bash(cat:*)" \
  --setting .ai-reviewer/settings.json \
  "$(printf '%s\n\n待评审 diff:\n```diff\n%s\n```' "$PROMPT" "$(cat "$DIFF_FILE")")" \
  | jq -r '.result'

预期结果:本地执行 bash .ai-reviewer/run.sh 即可在终端看到结构化的风险分级评审,无需推送。确认效果满意后,CI 直接复用同一段 prompt——这是「本地与 CI 同源」的关键,避免两边提示词漂移。


工作流程图

flowchart TD
    A[PR 打开/更新] --> B[Actions 触发]
    B --> C[checkout + 取 diff]
    C --> D{diff 为空?}
    D -- 是 --> Z[跳过评审]
    D -- 否 --> E[headless Claude -p]
    E --> F{PreToolUse Hook}
    F -- 命中护栏 --> G[阻断+反馈]
    F -- 放行 --> H[模型调用 Read 等工具]
    H --> I[PostToolUse: eslint --fix]
    I --> J[生成评审结果]
    J --> K[output-format json 解析]
    K --> L[jq 提取 .result]
    L --> M[回写 PR 评论]
    G --> E
    M --> N[人工 review + merge]

关键设计决策与权衡

为什么 CI 里优先 json 而非 stream-json? 评审是一次性总结任务,不需要在工具调用中途介入;json 拿到完整对象后一次 jq 取 .result 最干净。stream-json 留给需要「边跑边消费」的管道,比如把工具调用实时转成进度日志。

为什么用 --allowedTools 而不是全开? CI 环境里 Claude 拥有的能力越窄越安全。评审场景只需 Read(顶多加 Bash(cat:*)),不给 Edit/Write 就从物理上杜绝了「评审机器人顺手改了线上代码」的风险。

为什么 Hooks 配在仓库内而不是用户级? 仓库级 .claude/settings.json 会随代码版本化,团队所有人——包括 CI——共享同一套护栏。用户级配置只在你的本机生效,CI 里读不到。

成本控制三件套:① --max-turns 限轮数;② diff 体积预判跳过超大 PR;③ json 输出里的 cost_usd 字段做账,超预算阈值时在评论里标注当次花费。


延伸阅读


训练营适用性评估

参考来源

  1. Claude Code Headless Mode: Non-Interactive & CI Usage —— --print 标志、管道输入、CI/CD 自动化与批处理的实践指南
  2. Run Claude Code programmatically(官方) —— Agent SDK 在脚本与 CI/CD 中的编程式调用权威说明
  3. Hooks reference(官方) —— Hook 事件、配置 schema、JSON 输入输出与退出码的完整参考
  4. Headless --print --output-format=stream-json Issue #53584 —— 社区对 stream-json 卡死问题的复现与根因讨论
  5. Building Headless Automation with Claude Code —— 基于 SDK 与官方 GitHub Action 搭建 headless 自动化的实战笔记

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入