本教程聚焦 Claude Code 从"命令行工具"升级为"可编程运行时"的两大能力:Agent SDK(TypeScript/Python 编程式调用)与 Routines(保存的配置+多仓库工作集)。读者将学会用 30 行代码驱动 Claude 自主读文件、跑命令、改代码,并用 xargs/parallel 把同一套指令批量投放到几十个仓库并行执行。适用人群:已熟悉交互式 Claude Code、希望把它嵌入脚本/服务/批处理流水线的后端工程师、平台/SRE、研发效能团队。
claude --version 有输出)claude)或 ANTHROPIC_API_KEY 环境变量交互式的 claude 适合人在回路(human-in-the-loop)的场景:你打字、它思考、你审核。但真实工程里有一类需求它力不从心——把 Claude 当成一个被调用的函数:
这类场景的共同点是:没有人在屏幕前敲回车。Claude 必须以子进程或库的形式被唤起、被喂参数、被收集输出、被关闭。Claude Code 提供了三层能力来满足它,复杂度递增:
| 层次 | 形态 | 适用 | 本章 |
|---|---|---|---|
| L1 | claude -p 无头模式 |
简单脚本、一次性任务 | 第一节 |
| L2 | Agent SDK(TS/Python) | 服务化、流式、复杂编排 | 第二、三节 |
| L3 | Routines + 批处理 | 多仓库、可复用配置 | 第四、五节 |
下面逐层展开。
-p 无头模式——最轻量的集成-p(--print)让 Claude 不启动 TUI,而是接收一个 prompt、自主执行若干轮工具调用、把最终答案打印到 stdout 后退出。这是把 Claude 嵌入 shell 脚本最直接的方式。
cd ~/repos/my-service
claude -p "用三句话总结这个仓库的架构,并指出最可能的技术债。"
预期输出:一段纯文本落到 stdout,进程退出码为 0。把输出重定向到文件即可留档:
claude -p "..." > review.txt
claude -p \
--output-format json \ # 结构化输出,便于 jq 解析
--verbose \ # json 模式下附带每一步的会话信息
--max-turns 15 \ # 限制自主轮数,防止失控
--allowedTools "Read,Grep,Glob,Bash"\ # 收紧权限白名单
--model claude-sonnet-4-6 \ # 指定模型
"为 src/ 下所有导出的函数补上 JSDoc 注释,只改注释不改逻辑"
参数说明:
--output-format json:返回一个 JSON 对象,包含 result(最终文本)、cost_usd(本次花费)、duration_ms、num_turns、session_id 等。做成本核算和任务追踪时务必用它。--output-format stream-json:以换行分隔的 JSON 流输出每个事件(工具调用、文本块、结果),适合实时展示进度条或事件驱动处理。--max-turns:限制 Agent 自主循环次数。生产环境强烈建议设置,避免 prompt 不清晰时模型反复试错烧 token。--allowedTools:在 CLI 已授予权限的基础上再做一次白名单收紧。例如只想让它"读和分析",就只给 Read,Grep,Glob,不给 Edit,Write。--permission-mode:可选 acceptEdits、bypassPermissions、plan 等,控制它能否自动执行写操作。批量无人值守场景常用 acceptEdits。claude -p --output-format stream-json --verbose "列出所有 TODO 注释的位置" | \
jq -r 'select(.type=="tool_use" and .name=="Grep") | .input.pattern'
每个流事件是一个独立 JSON 对象,按类型(system、assistant、tool_use、tool_result、result)过滤即可。这在构建"实时进度看板"时非常有用。
| 现象 | 原因 | 解决 |
|---|---|---|
| 提示需要授权,脚本卡住 | 默认权限模式会等待用户确认 | 加 --permission-mode acceptEdits,或预先在 settings.json 配 allow 列表 |
| 退出码非 0 但看不出错 | stderr 被吞了 | 用 2>err.log 单独捕获 stderr |
| JSON 解析失败 | 默认输出是纯文本 | 必须显式加 --output-format json |
| 重复登录态丢失 | 每个子进程独立认证 | 用 ANTHROPIC_API_KEY 而非 OAuth,或确保共享同一 ~/.claude |
当 -p 不够用时(比如需要把 Claude 嵌进一个长期运行的服务、需要在同一会话里多轮交互、需要拿到结构化对象而非解析文本),就轮到 Agent SDK 上场。它把 Claude Code 的整套运行时(工具系统、权限、会话、流式事件)封装成一个库函数 query()。
# TypeScript / Node
npm install @anthropic-ai/claude-agent-sdk
# Python
pip install claude-agent-sdk
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
const messages = [{ role: "user", content: "列出 src/ 下所有 TypeScript 文件并统计每个文件的行数" }];
// query() 是一个异步生成器,逐个 yield 事件
for await (const message of query({
prompt: "分析本仓库的测试覆盖率现状,给出提升到 80% 的优先动作清单",
options: {
cwd: "/home/me/repos/my-service", // 工作目录,决定 Claude 能操作哪个仓库
allowedTools: ["Read", "Grep", "Glob", "Bash"], // 工具白名单
maxTurns: 15,
model: "claude-sonnet-4-6",
permissionMode: "acceptEdits",
},
})) {
// 每条 message 是一个流事件:assistant 文本、tool_use、tool_result、result...
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") process.stdout.write(block.text);
}
}
}
}
main();
import anyio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
cwd="/home/me/repos/my-service",
allowed_tools=["Read", "Grep", "Glob", "Bash"],
max_turns=15,
model="claude-sonnet-4-6",
permission_mode="acceptEdits",
)
async for message in query(
prompt="找出所有未处理的 Promise rejection 和 bare except,列出文件:行号",
options=options,
):
if getattr(message, "type", None) == "assistant":
for block in message.message.content:
if block.type == "text":
print(block.text, end="")
anyio.run(main)
SDK 并非只有"一次性 prompt"一种用法,理解这三种形态决定了你如何编排:
形态 A:单次查询(one-shot)。一个 prompt,自主跑完,返回结果。对应上面的示例,最适合"分析类""报告类"任务。
形态 B:多轮对话(multi-turn)。保留 session_id,下一次 query() 时传入 resume,在同一上下文里继续追问。适合需要人工中途介入审核、再让它继续的场景:
# 第一轮:让它调研
result = await query(prompt="调研迁移到 ESM 的可行性", options=opts)
session_id = result.... # 从结果事件中取出
# (人工审核中间结论后)
# 第二轮:基于同一会话继续
await query(prompt="好,现在按你建议的方案动手改 package.json 和 tsconfig",
options={**opts, "resume": session_id})
形态 C:自定义工具(custom tools)。SDK 允许你注册宿主进程里的函数作为 MCP 工具,让 Claude 在执行过程中回调你的代码。例如把"查公司内部监控平台""查 Jira 工单"封装成工具,Claude 就能在分析代码时顺手拉取线上指标。这是把 Claude 与企业内部系统打通的关键闸门。
// 注册一个自定义工具:查内部 SLO 面板
const options = {
mcpServers: {
"slo-panel": {
type: "stdio",
command: "node",
args: ["./slo-mcp-server.js"], // 你自己实现的 MCP server
},
},
// Claude 会像调用 Read/Bash 一样调用 slo-panel 暴露的工具
};
query() 一直不返回:多半是 maxTurns 没设、模型在死循环。务必设上限,并在外层加超时(如 Python 的 anyio.fail_after)。settings.json 的权限;若想完全程序化,用 permissionMode: "bypassPermissions"(仅在可信沙箱内),或注册 canUseTool 回调自定义审批逻辑。session_id 保存在 cwd 下的会话存储里,宿主进程要有读写权限,且 cwd 要稳定。当同一套指令要在多个仓库上反复跑时,把 prompt、工具白名单、模型、目标仓库列表打包成一个Routine,是最干净的复用方式。Routines 是 Claude Code 官方提供的"保存的配置"机制:一个 Routine = 一个 prompt + 一个或多个仓库 + 一组配置约束。
裸用 claude -p 的问题在于:参数散落在各处脚本里,改一次要改 N 个地方;不同人跑出的结果不一致(有人忘了加 --max-turns,有人忘了限制工具)。Routine 把这些固化下来,让"团队统一的代码体检流程"成为一个可分发、可版本化的对象。
Routines 通常通过 Claude Code 的配置文件或 claude 交互式命令创建/保存(具体语法以你本机版本为准)。一个 Routine 的逻辑结构如下:
# 概念性结构(实际以本机版本配置格式为准)
name: weekly-security-scan
prompt: |
扫描本仓库的依赖清单与代码,找出已知 CVE、硬编码密钥、
不安全的反序列化点。输出一份 Markdown 报告,按严重程度排序。
repositories:
- /home/me/repos/billing-service
- /home/me/repos/auth-gateway
- /home/me/repos/notification-worker
options:
allowedTools: [Read, Grep, Glob, Bash]
maxTurns: 20
model: claude-sonnet-4-6
permissionMode: acceptEdits
outputFormat: json
运行一个 Routine 等价于:对其中每一个仓库,用相同的 prompt 和 options 跑一次独立的 Claude 会话,并汇总结果。仓库之间的执行天然隔离——各自的工作目录、各自的上下文,互不污染。
-p vs SDK 的取舍| 需求 | 推荐方案 |
|---|---|
| 一次性、临时任务 | claude -p |
| 团队共享、重复执行的固定流程 | Routine |
| 需要嵌入自有服务、复杂事件处理 | Agent SDK |
| 需要自定义工具、与企业系统打通 | Agent SDK + MCP |
这是本教程的重头戏。目标:对一组仓库并行执行同一套 Claude 指令,收集结构化结果,控制并发与成本。
关键认知:Claude Code 的每次 -p 调用或 SDK query() 都是一个独立的 OS 进程/会话。这意味着多仓库天然适合用进程级并行——不存在共享状态、不需要锁、不会互相干扰。并发度只受两个约束:
经验值:中小仓库(< 10 万行)的"分析类"任务,并发 4~8 是一个安全的起点;写操作类任务降到 2~4。
#!/usr/bin/env bash
set -euo pipefail
REPO_LIST="repos.txt" # 每行一个仓库绝对路径
MAX_PARALLEL=4
OUT_DIR="reports/$(date +%Y%m%d)"
mkdir -p "$OUT_DIR"
# 关键:-P 控制并发数;每个仓库一个独立 Claude 进程
cat "$REPO_LIST" | xargs -I {} -P "$MAX_PARALLEL" bash -c '
repo="$1"
name=$(basename "$repo")
out="'"$OUT_DIR"'/${name}.json"
cd "$repo"
claude -p \
--output-format json \
--max-turns 15 \
--allowedTools "Read,Grep,Glob,Bash" \
--permission-mode acceptEdits \
"审查本仓库的安全性,列出高/中/低危问题各不超过 5 条,给出文件:行号" \
> "$out" 2> "${out%.json}.err"
echo "✓ $name (exit=$?)"
' _ {}
要点:
xargs -P 4 让最多 4 个仓库同时跑_ {} 把当前仓库名作为 $1 传进内联脚本jq 汇总#!/usr/bin/env bash
set -euo pipefail
OUT_DIR="reports/$(date +%Y%m%d)"
mkdir -p "$OUT_DIR"
export OUT_DIR
# parallel 支持 --jobs、--load(按系统负载限流)、--retries(失败重试)
parallel --jobs 4 --retries 1 --joblog "$OUT_DIR/joblog.txt" '
repo={}
name=$(basename "$repo")
cd "$repo"
claude -p --output-format json --max-turns 15 \
--allowedTools "Read,Grep,Glob" \
"生成本仓库的架构概述(500 字内)" \
> "'"$OUT_DIR"'/${name}.json"
' :::: repos.txt
# joblog 记录每个任务的退出码、耗时,便于事后审计
GNU parallel 相比 xargs 的优势:--joblog 自动记录每任务状态、--retries 自动重试被 429 打断的任务、--bar 显示进度条。
当 xargs/parallel 不够时——例如要按仓库大小动态分配并发、要在结果间做去重合并、要对失败仓库做差异化重试——就用 SDK 自己写编排器:
import asyncio, json, os
from pathlib import Path
from claude_agent_sdk import query, ClaudeAgentOptions
REPOS = Path("repos.txt").read_text().splitlines()
MAX_CONCURRENCY = 4
PROMPT = "审查安全性,按严重程度列出问题,输出 JSON:{issues:[{severity,file,line,desc}]}"
async def scan_one(repo: str, sem: asyncio.Semaphore):
async with sem:
opts = ClaudeAgentOptions(
cwd=repo,
allowed_tools=["Read", "Grep", "Glob", "Bash"],
max_turns=15,
model="claude-sonnet-4-6",
permission_mode="acceptEdits",
)
final_text = ""
try:
async for msg in query(prompt=PROMPT, options=opts):
if getattr(msg, "type", None) == "result":
final_text = msg.result
return {"repo": Path(repo).name, "ok": True, "raw": final_text}
except Exception as e:
return {"repo": Path(repo).name, "ok": False, "error": str(e)}
async def main():
sem = asyncio.Semaphore(MAX_CONCURRENCY) # 信号量控并发
results = await asyncio.gather(*[scan_one(r, sem) for r in REPOS])
# 按严重程度排序输出总报告
sev_order = {"high": 0, "medium": 1, "low": 2}
all_issues.sort(key=lambda x: sev_order.get(x.get("severity", "low"), 3))
Path("reports/summary.json").write_text(json.dumps(all_issues, indent=2, ensure_ascii=False))
print(f"完成:{len(REPOS)} 个仓库,{len(all_issues)} 条问题")
asyncio.run(main())
这段编排器的关键设计:
asyncio.Semaphore 把并发钉死在 MAX_CONCURRENCY,避免一次性把 50 个仓库全打出去触发 429asyncio.gather 让所有任务并行推进,先完成的先返回批量跑最容易翻车的就是账单和 429。三条铁律:
maxTurns。一次失控的自主循环可以烧掉几十美元。cost_usd,跑完后累加,超出预算阈值自动熔断:total=$(cat reports/*.json | jq -s '[.[].cost_usd // 0] | add')
echo "本次总花费: $total USD"
--retries,SDK 自己写 tenacity 装饰器:from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=2, min=4, max=60),
retry=lambda e: "429" in str(e) or "529" in str(e))
async def scan_one_with_retry(repo, sem):
...
flowchart TB
subgraph 输入
R[repos.txt<br/>仓库清单]
P[Prompt / Routine<br/>统一指令]
C[配置: maxTurns,<br/>allowedTools, model]
end
subgraph 编排层
SEM{并发控制器<br/>Semaphore -P 4}
DISP[任务分发器<br/>xargs / parallel / SDK]
end
subgraph 执行层 每仓库一个独立进程
W1[Claude 进程 1<br/>repo-A 独立会话]
W2[Claude 进程 2<br/>repo-B 独立会话]
W3[Claude 进程 3<br/>repo-C 独立会话]
WN[Claude 进程 N<br/>repo-N 独立会话]
end
subgraph 结果层
J1[repo-A.json<br/>含 cost_usd]
J2[repo-B.json]
J3[repo-C.json]
AGG[汇总器<br/>jq / Python]
RPT[总报告<br/>summary.json]
end
R --> DISP
P --> DISP
C --> DISP
DISP --> SEM
SEM --> W1
SEM --> W2
SEM --> W3
SEM --> WN
W1 --> J1
W2 --> J2
W3 --> J3
WN --> AGG
J1 --> AGG
J2 --> AGG
J3 --> AGG
AGG --> RPT
style SEM fill:#ffe4b5
style AGG fill:#e0ffe0
style RPT fill:#e0ffe0
图中虚线框住的"并发控制器"是整个架构的命门:它决定了你是 4 倍速跑完还是被 429 雪崩。
把前面所有概念串起来,构建一个可投入生产的"每周依赖漏洞巡检"机器人。它每周一早上对一组仓库做依赖审计,按严重程度汇总,并把高危项提成交付物。
vuln-patrol/
├── repos.txt # 待巡检仓库清单
├── routine.yaml # 巡检指令与配置(Routine 思路)
├── run.sh # xargs 并行入口(方案 A)
├── orchestrator.py # SDK 编排入口(方案 C,含重试与汇总)
└── reports/
└── 20260615/
├── billing-service.json
├── auth-gateway.json
└── summary.md
/home/me/repos/billing-service
/home/me/repos/auth-gateway
/home/me/repos/notification-worker
/home/me/repos/inventory-api
/home/me/repos/search-service
你是依赖安全审计员。请完成以下步骤:
1. 读取本仓库的依赖清单文件(package.json / requirements.txt / go.mod / pom.xml 等,按实际存在为准)
2. 用 Grep 搜索代码中是否使用了已知有漏洞的模式(eval、pickle.loads、不安全的反序列化、硬编码密钥)
3. 对每个发现,给出:severity(high/medium/low)、file、line、description、suggested_fix
4. 严格输出 JSON:{"repo":"<仓库名>","issues":[...],"scanned_files":N}
不要输出 JSON 以外的内容。
import asyncio, json, sys
from pathlib import Path
from datetime import date
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from claude_agent_sdk import query, ClaudeAgentOptions
REPOS = [l.strip() for l in Path("repos.txt").read_text().splitlines() if l.strip()]
PROMPT = Path("audit_prompt.txt").read_text()
MAX_CONCURRENCY = 4
BUDGET_USD = 5.0 # 整批预算上限
class RateLimited(Exception): pass
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=2, min=4, max=60),
retry=retry_if_exception_type(RateLimited),
)
async def audit_one(repo: str, sem: asyncio.Semaphore):
async with sem:
opts = ClaudeAgentOptions(
cwd=repo,
allowed_tools=["Read", "Grep", "Glob", "Bash"],
max_turns=20,
model="claude-sonnet-4-6",
permission_mode="acceptEdits",
)
result_text, cost = "", 0.0
async for msg in query(prompt=PROMPT, options=opts):
t = getattr(msg, "type", None)
if t == "result":
result_text = getattr(msg, "result", "")
cost = getattr(msg, "cost_usd", 0.0) or 0.0
if t == "system" and "429" in str(getattr(msg, "subtype", "")):
raise RateLimited(repo)
return {"repo": Path(repo).name, "cost": cost, "raw": result_text}
async def main():
sem = asyncio.Semaphore(MAX_CONCURRENCY)
spent = 0.0
all_issues = []
tasks = [audit_one(r, sem) for r in REPOS]
for coro in asyncio.as_completed(tasks): # 先完成先处理,便于提前熔断
res = await coro
spent += res["cost"]
if spent > BUDGET_USD:
print(f"⚠️ 预算熔断:已花费 ${spent:.2f} > ${BUDGET_USD}", file=sys.stderr)
# 取消尚未完成的任务
for t in tasks:
t.cancel()
break
try:
data = json.loads(res["raw"])
for iss in data.get("issues", []):
iss["repo"] = res["repo"]
all_issues.append(iss)
except json.JSONDecodeError:
print(f"⚠️ {res['repo']} 输出非 JSON,跳过", file=sys.stderr)
# 生成 Markdown 总报告
sev = {"high": [], "medium": [], "low": []}
for i in all_issues:
sev.setdefault(i.get("severity", "low"), []).append(i)
md = [f"# 依赖漏洞巡检报告 {date.today()}\n",
f"扫描仓库 {len(REPOS)} 个,发现问题 {len(all_issues)} 个,花费 ${spent:.2f}\n"]
for level in ["high", "medium", "low"]:
md.append(f"\n## {level.upper()} ({len(sev[level])})\n")
for i in sev[level]:
md.append(f"- **{i['repo']}** `{i['file']}:{i.get('line','?')}` — {i['description']}")
out = Path(f"reports/{date.today()}/summary.md")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text("\n".join(md))
print(f"报告已生成:{out}")
asyncio.run(main())
用 cron 每周一早 7 点跑(注意避开整点,减少与全网定时任务的撞车):
# 每周一 06:17 跑依赖巡检
17 6 * * 1 cd /home/me/vuln-patrol && /usr/bin/python3 orchestrator.py >> logs/cron.log 2>&1
reports/2026-06-15/summary.md:按 high/medium/low 分级的总报告,含每条问题的仓库、文件、行号、修复建议经过多轮批量任务验证,以下是最常踩的坑:
cwd 必须显式指定。SDK 不传 cwd 会用宿主进程目录,批量时极易把不同仓库的结果写到同一处。每个任务务必把自己的仓库路径作为 cwd。--permission-mode plan 先出方案、人工确认后再 acceptEdits 执行。maxTurns 是安全阀不是性能参数。设太小任务完不成,设太大一次失控破产。先小批量试跑,观察"成功任务"的 num_turns 中位数,再设为它的 1.5~2 倍。code.claude.com/docs/en/agent-sdk/overview —— TypeScript/Python 完整 API、自定义工具、流式事件参考code.claude.com/docs/en/routines —— 保存配置与多仓库工作集的官方机制code.claude.com/docs/en/best-practices —— 环境配置、prompt 工程、子 Agent 编排的官方建议-p 无头模式与批量命令的系统化对照| 维度 | 评估 |
|---|---|
| 适用训练营类型 | 研发效能(DevEx)、平台工程、SRE 自动化、AI 工程化进阶营 |
| 学员基础要求 | 已掌握交互式 Claude Code,熟悉一门脚本语言(Python/Shell),理解进程与并发 |
| 不适用 | 零基础营(应先修"入门篇")、纯前端营(cwd/并发概念较远) |
| 建议教学时长 | 4 课时:L1 -p(1h)+ L2 SDK(1.5h)+ L3 Routines(0.5h)+ 批量实战(1h) |
| 实操占比 | 建议 60% 以上,每层配一个可跑通的 mini 任务 |
| 考核点 | 能独立写出带并发控制、成本熔断、结果汇总的批量编排器 |
-p 无头模式与批量命令的系统化对照指南[!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 等场景全覆盖。
