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 实例,而非全局单例。

原因

二、安全设计(四层防御)

这是整个项目最核心的经验。安全必须从架构层面设计,不能事后补丁。

2.1 四层模型

层级机制防御目标可靠性
L1 SDK 配置disallowedTools禁用 Write/Edit高,但可被 Bash 绕过
L2 应用 HookPreToolUse 白名单命令/路径/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 的创造力。以下绕过都曾被发现或预见到:

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

已知缺陷

三、会话管理

3.1 三层恢复机制

 
 
 
用户发消息
  ├─ Layer 1: 内存缓存命中 → 直接 resume SDK session
  ├─ Layer 2: 缓存未命中 → 从 DB 读取 sdkSessionData (.jsonl)
  │           → 写回文件系统 → resume SDK session
  └─ Layer 3: 无 SDK session → 从 DB 读历史摘要
              → 注入 system prompt → 开始新 session

为什么需要三层

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',
});

设计原则

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 代替
  - 不确定有哪些字段时,先传错误字段名,错误信息会列出可用字段

经验

五、流式输出与 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 个关键维度:

  1. 时间戳 — 自动由 pino 添加
  2. 会话 ID — 关联同一用户的所有日志
  3. 阶段标签[Chat][Security][Session]
  4. 耗时 — 每个操作的 durationMs
  5. 工具名 — 哪个工具在执行
  6. 错误详情 — 截断后的错误消息
  7. 安全上下文 — 被拦截的命令/路径

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/configclone 后必须清除凭证

8.3 System Prompt 编写原则

  1. 明确工作目录和路径格式 — agent 经常用错路径
  2. 列出工具的参数示例 — 比抽象描述有效 10 倍
  3. 说明“不要做什么” — 如 “不要用 Bash 调用 git”
  4. 列出允许和禁止的 Bash 命令 — 减少试错
  5. 包含业务上下文 — 业务指标定义、关键实验标识、核心数据表结构等
  6. 安全规则显式声明 — “绝对禁止输出 API Key、Token、密码”

写在最后

这些经验不是理论推演,而是把一个 agent 系统从想法推到真实用户的过程中,踩过的每一个坑。

如果你也在构建 Claude Agent SDK 的生产级 agent,希望能帮你少走一些弯路。

工具在快速进化,这些做法也会过时,但安全优先于便利白名单优于黑名单基础设施强制优于代码校验这几条底层原则,应该会长期有效。

皮成,2026-04-26