Claude Agent SDK 工程化笔记:从架构到生产
一份生产级 Claude Agent 系统的工程笔记。
涵盖:分层架构、四层安全模型、会话恢复机制、工具开发模式、SSE 流式输出、可观测性、K8s 部署。
一、架构设计
1.1 分层架构
HTTP 层 (Hono)
└─ 路由层 (routes/) ← 请求/响应、SSE 流、认证
└─ Agent 执行层 (AgentRunner) ← SDK 调用、会话恢复、流式输出
├─ 安全层 (security.ts) ← PreToolUse hooks、路径/命令校验
├─ 工具层 (tools/) ← MCP Server、数据源封装
└─ 持久化层 (session-store) ← PostgreSQL + Drizzle ORM
经验:不要把所有逻辑放在路由中。AgentRunner 作为独立类,封装 SDK 调用、会话管理和流式输出,使得同一个 Agent 可以同时服务 HTTP API 和 IM Webhook。
1.2 Agent 注册表模式
// registry.ts — 集中管理所有 Agent
const runners = new Map<string, AgentRunner>();
export function initializeAgents(config: Config) {
const creds = extractCredentials(config);
runners.set('growth', new AgentRunner(getGrowthAgentConfig(), creds));
// 未来新增 Agent 只需在这里注册
}
经验:Agent 配置(system prompt、工具列表、权限)与运行时(SDK 调用、会话)分离。新增 Agent 只需添加 config.ts + tools.ts + CLAUDE.md,无需修改框架代码。
1.3 MCP Tool Server 工厂模式
// 每次 query 创建独立的 MCP Server 实例
export function createGrowthToolServer(creds: Credentials) {
return createSdkMcpServer({
name: 'growth',
tools: [
createGoogleAdsTool(), // 无需凭证(走签名代理)
createPostHogTool(creds.posthogApiKey), // 闭包封装凭证
createBigQueryTool(creds.gcpServiceAccount),
// ...
],
});
}
关键决策:每次 SDK query 创建新的 MCP Server 实例,而非全局单例。
原因:
- 防止跨请求状态泄漏
- 凭证通过闭包传入,不污染 process.env
- 工具实例无共享状态,天然并发安全
二、安全设计(四层防御)
这是整个项目最核心的经验。安全必须从架构层面设计,不能事后补丁。
2.1 四层模型
| 层级 | 机制 | 防御目标 | 可靠性 |
|---|---|---|---|
| L1 SDK 配置 | disallowedTools | 禁用 Write/Edit | 高,但可被 Bash 绕过 |
| L2 应用 Hook | PreToolUse 白名单 | 命令/路径/URL 校验 | 中,LLM 有创造力绕过 |
| L3 OS 沙箱 | Bubblewrap namespace | 文件系统/网络隔离 | 高,内核级强制 |
| L4 基础设施 | K8s readOnly mount + tmpfs | 物理级只读/凭证删除 | 最高,与 LLM 无关 |
核心教训:L4 > L3 > L2 > L1。越靠近内核/基础设施的限制越可靠。
2.2 从黑名单到白名单的演进
第一版 — 点修复:
// 发现 awk ENVIRON 可以读环境变量,打补丁
/\bENVIRON\b/
第二版 — 架构性重写:
// 改为白名单:只允许这些命令
const ALLOWED_COMMANDS = new Set([
'jq', 'sort', 'uniq', 'wc', 'head', 'tail', 'cat', 'ls', ...
]);
// + 硬拦截模式(即使白名单命令也不能触发)
const HARD_BLOCK_PATTERNS = [/\bpython/, /\bnode\b/, />[^&]/, ...];
教训:黑名单永远追不上 LLM 的创造力。以下绕过都曾被发现或预见到:
awk 'BEGIN{print ENVIRON["API_KEY"]}'— awk 内建变量sed -i 's/old/new/' file— sed 可以写文件git checkout -- .— git 可以修改工作区find -exec— find 可以执行任意命令echo malicious > file— 重定向写入- 生成 Python 脚本再执行 — 绕过 Write 工具禁用
2.3 凭证隔离三原则
原则一:闭包封装,不用 process.env
// ✗ 错误:凭证放在环境变量
process.env.POSTHOG_API_KEY = config.posthogApiKey;
// ✓ 正确:通过闭包传入工具
export function createPostHogTool(apiKey: string) {
return tool('query_posthog', '...', schema, async (input) => {
// apiKey 在闭包中,agent 无法通过 env 读取
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
});
}
将 ANTHROPIC_API_KEY 从 process.env 移除,只通过 SDK subprocess env 传递。这直接堵死了 awk ENVIRON 攻击面。
原则二:启动后删除配置文件
# entrypoint.sh
node /app/dist/index.js &
PID=$!
sleep 5
rm -f /app/config/config.json # 启动后删除
wait $PID
Node 进程已将 config 读入内存缓存,文件删除不影响运行。但 agent 无法再通过 cat /app/config/config.json 读取凭证。
原则三:Git 凭证用后即清
# init-workspace.sh
git clone "$repo_url_with_token" "$repo_dir"
git -C "$repo_dir" remote set-url origin "$clean_url" # 清除 .git/config 中的 token
2.4 Bash 安全 Hook 的完整设计
async function bashSecurityHook(input) {
const command = input.tool_input.command;
// 第 1 层:硬拦截(正则匹配即拒绝,无论命令是什么)
// 解释器、网络工具、文件修改、env 读取、重定向写入...
for (const pattern of HARD_BLOCK_PATTERNS) { ... }
// 第 2 层:命令替换拦截(防 $() 和反引号)
if (/\$\(/.test(command) || /`[^`]+`/.test(command)) { block }
// 第 3 层:白名单校验(每个管道段的首个 token 必须在白名单中)
const cmds = extractCommands(command);
for (const cmd of cmds) {
if (!ALLOWED_COMMANDS.has(cmd)) { block }
}
// 第 4 层:文件路径校验(绝对路径必须在允许目录内)
const paths = command.match(/(?:^|\s)(\/[^\s;|&>]+)/g);
for (const p of paths) {
if (!isPathAllowed(p)) { block }
}
}
已知缺陷:
- 相对路径绕过(
cat ../../dist/index.js)— 需要 L3/L4 兜底 - awk 内部文件操作(
awk '{print > "file"}')— 被重定向正则捕获 - 多行命令分割 — extractCommands 已支持
\n分割
三、会话管理
3.1 三层恢复机制
用户发消息
├─ Layer 1: 内存缓存命中 → 直接 resume SDK session
├─ Layer 2: 缓存未命中 → 从 DB 读取 sdkSessionData (.jsonl)
│ → 写回文件系统 → resume SDK session
└─ Layer 3: 无 SDK session → 从 DB 读历史摘要
→ 注入 system prompt → 开始新 session
为什么需要三层:
- Claude Agent SDK 的 session 状态存在本地
.jsonl文件中 - K8s Pod 重建后文件丢失
- 将
.jsonl内容备份到 PostgreSQL,Pod 重建后可恢复
3.2 关键陷阱:SDK Session 路径
修复了一个隐蔽的 bug:
// SDK 存储路径:${HOME}/.claude/projects/${encodedCwd}/sessions/
// HOME=/app, CWD=/app/workspace
// 实际路径:/app/.claude/projects/-app-workspace/sessions/*.jsonl
// 错误地在 /app/workspace/.claude/ 下搜索,永远找不到文件
教训:SDK 的文件存储路径取决于 HOME 和 CWD 两个变量,必须精确匹配。
3.3 降级与容错
实现了三个关键的降级策略:
// 1. Resume 失败自动降级
if (error.includes('No conversation found')) {
// 清除 sdkSessionId,下次以新 session 方式重试
cached.sdkSessionId = null;
}
// 2. 空结果检测
if (output_tokens === 0 || textContent === '...') {
// 回退到最近一条 reasoning step 作为响应
yield { type: 'text', content: lastReasoningStep };
}
// 3. 上下文压缩阈值
// 从默认 95% 降到 50%,防止上下文爆满导致输出退化
env: { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: '50' }
四、工具开发模式
4.1 标准工具模板
import { tool } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
export function createXxxTool(apiKey: string) {
return tool(
'tool_name',
'工具描述(中文,说明用途和限制)',
{
// Zod schema 定义参数
query: z.string().describe('查询语句'),
limit: z.number().optional().default(100).describe('最大返回行数'),
},
async ({ query, limit }) => {
try {
const result = await fetchData(apiKey, query, limit);
// 大数据保护:截断输出
const text = JSON.stringify(result);
if (text.length > 50_000) {
return { content: [{ type: 'text', text: text.slice(0, 50_000) + '\n...(已截断)' }] };
}
return { content: [{ type: 'text', text }] };
} catch (err) {
// 错误不暴露内部细节
return { content: [{ type: 'text', text: `查询失败: ${sanitize(err.message)}` }] };
}
},
);
}
4.2 安全工具设计(git_query 案例)
// 用 execFileSync 替代 shell 执行,从根本上消除命令注入
execFileSync('git', ['-C', repoDir, command, ...argsList], {
timeout: 30_000,
maxBuffer: 2 * 1024 * 1024,
encoding: 'utf-8',
});
设计原则:
- 枚举验证:repo 和 subcommand 都用
z.enum()限定 - 参数黑名单:
--exec、--output、--config等危险参数被拦截 - 无 shell:
execFileSync直接执行二进制,不经过 shell 解析 - 输出限制:50KB 截断 + 30 秒超时
4.3 工具描述写作
System Prompt(CLAUDE.md)中的工具文档直接影响 Agent 的使用质量:
### query_ahrefs
- 查询 Ahrefs SEO 数据
- **参数说明**:
- `endpoint`: API 端点路径(不含 `/v3/` 前缀)
- `params`: 查询参数,**所有端点都需要 `select` 参数**
- `method`: GET(默认)或 POST(keywords-explorer 必须用 POST)
- **常用端点与示例**:
- `site-explorer/metrics` (GET): `{ target: "example.com", select: "org_traffic" }`
- **重要**:
- `site-explorer/overview` 端点不存在,用 metrics 代替
- 不确定有哪些字段时,先传错误字段名,错误信息会列出可用字段
经验:
- 写明“不要做什么”比写明“要做什么”更重要
- 提供具体的参数示例,而非抽象描述
- 记录常见错误和 workaround(如“先传错误字段名”的技巧)
五、流式输出与 SSE
5.1 SSE 心跳防超时
// 每 20 秒发送心跳,防止 Nginx/Ingress 超时断开
const heartbeat = setInterval(() => {
stream.write(`: heartbeat\n\n`);
}, 20_000);
解决了 SSE 连接在代理层被超时关闭的问题。K8s Ingress 默认 60 秒无数据就断开,Agent 思考时可能超过这个时间。
5.2 结构化流式块
type ChatChunk =
| { type: 'text'; content: string } // 文本输出
| { type: 'reasoning'; content: string } // 推理过程
| { type: 'tool_use'; tool: string; input } // 工具调用
| { type: 'tool_result'; tool: string; content } // 工具结果
| { type: 'thinking'; content: string } // 思考过程
| { type: 'session_meta'; sessionId; title } // 会话元数据
| { type: 'error'; content: string } // 错误
前端可以根据 type 分别渲染:文本流式输出、工具调用折叠展示、推理过程的 UI 风格。
六、可观测性
6.1 结构化日志
// 每个关键路径都有 timing 和 context
log.info('[Chat] SDK query start', {
sessionId, resume: !!cached.sdkSessionId,
systemPromptLen: systemPrompt.length,
prepareMs: Date.now() - startTime,
});
// 工具调用计时
log.info('[Chat] Tool completed', {
sessionId, tool: toolName,
durationMs: Date.now() - toolStartTime,
isError: !!error,
});
// 安全事件
log.warn('[Security] Bash hard-block', {
pattern: pattern.source,
command: command.slice(0, 200),
});
经验:日志的 7 个关键维度:
- 时间戳 — 自动由 pino 添加
- 会话 ID — 关联同一用户的所有日志
- 阶段标签 —
[Chat]、[Security]、[Session] - 耗时 — 每个操作的 durationMs
- 工具名 — 哪个工具在执行
- 错误详情 — 截断后的错误消息
- 安全上下文 — 被拦截的命令/路径
6.2 SDK 卡死检测
// 每 60 秒输出进度日志,排查 SDK 长时间无响应
const progressTimer = setInterval(() => {
log.info('[Chat] Progress', {
sessionId, elapsedMs, silentMs: Date.now() - lastActivityTime,
toolCalls: counter, lastActiveTool,
});
}, 60_000);
七、K8s 部署
7.1 initContainers 模式
initContainers:
- name: init-config
# 将 Secret 复制到 tmpfs(主容器启动后删除)
command: ['sh', '-c', 'cp /secret/config.json /config/config.json']
- name: init-workspace
# 拉取代码仓库(GIT_TOKEN 从 secretKeyRef 获取)
command: ['bash', '/app/scripts/init-workspace.sh']
env:
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: agents-secret
key: GIT_TOKEN
关键点:initContainers 在 Pod 创建时运行一次,容器重启不会重新运行。如果 config.json 被 entrypoint.sh 删除后 Node 崩溃,容器重启会失败——需要删除 Pod 重建。
7.2 Volume 安全策略
volumes:
- name: config-tmpfs # 内存文件系统,Pod 终止即消失
emptyDir: { medium: Memory, sizeLimit: 1Mi }
- name: workspace # 代码仓库
emptyDir: { sizeLimit: 2Gi }
- name: agent-tmp # Agent 临时写入空间
emptyDir: { sizeLimit: 500Mi }
containers:
- volumeMounts:
- name: workspace
mountPath: /app/workspace
readOnly: true # 内核级只读,任何进程都无法写入
- name: agent-tmp
mountPath: /app/tmp # Agent 的唯一可写目录
八、开发流程经验
8.1 迭代节奏
| 阶段 | 重点 |
|---|---|
| 脚手架 | Hono + health check + CI 跑通 |
| 核心架构 | AgentRunner + 核心工具集 + 会话持久化 |
| 安全 + 上线 | 沙箱 + 白名单 + 凭证隔离 + K8s 部署 |
| 可观测性 | 结构化日志 + 心跳 + 降级检测 |
| 加固 + 扩展 | 安全二次审计 + workspace 扩展 |
8.2 犯过的错和修复
| 错误 | 教训 |
|---|---|
| 黑名单拦截 python 但漏掉 awk ENVIRON | 黑名单永远有漏洞,改用白名单 |
| ANTHROPIC_API_KEY 在 process.env 中 | 凭证不应该在父进程环境中 |
| BigQuery CJS/ESM 冲突 | 不要 bundle 依赖,保持外部引用 |
| SDK session 路径搜索错误 | HOME 和 CWD 共同决定路径 |
| SSE 超时断开 | 代理环境必须有心跳 |
| 上下文爆满导致空输出 | 压缩阈值从 95% 降到 50% |
| failIfUnavailable 导致 Pod 启动失败 | 安全配置需要与运维环境匹配 |
| Git 凭证残留在 .git/config | clone 后必须清除凭证 |
8.3 System Prompt 编写原则
- 明确工作目录和路径格式 — agent 经常用错路径
- 列出工具的参数示例 — 比抽象描述有效 10 倍
- 说明“不要做什么” — 如 “不要用 Bash 调用 git”
- 列出允许和禁止的 Bash 命令 — 减少试错
- 包含业务上下文 — 业务指标定义、关键实验标识、核心数据表结构等
- 安全规则显式声明 — “绝对禁止输出 API Key、Token、密码”
写在最后
这些经验不是理论推演,而是把一个 agent 系统从想法推到真实用户的过程中,踩过的每一个坑。
如果你也在构建 Claude Agent SDK 的生产级 agent,希望能帮你少走一些弯路。
工具在快速进化,这些做法也会过时,但安全优先于便利、白名单优于黑名单、基础设施强制优于代码校验这几条底层原则,应该会长期有效。
皮成,2026-04-26