# Claude Code Agent SDK 生产部署

> Claude Agent SDK 生产部署指南: 会话模式、隔离、观测、成本、结构化输出、tool search、缓存 TTL,以及 TypeScript/Python 配置片段。

URL: https://docs.passion8.cc/docs/claude-code/sdk-production
Language: zh-CN
Publisher: Passion8

Agent SDK 适合把 Claude Code 的 agent loop 嵌入服务、后台任务、CI 或多租户产品。生产部署时要先接受一个核心事实: `query()` 不是一次纯无状态 API 调用,而是启动并监管一个 `claude` Claude Code CLI 子进程,SDK 通过 stdio 和它通信。




每个运行中的 agent session 都有自己的进程树、工作目录和本地 transcript。你需要像部署有状态 worker 一样规划文件系统、隔离、观测和成本控制。




如果你要先理解 SDK 权限评估、Hooks、MCP、SessionStore 和成本字段,看 [Agent SDK 能力矩阵](https://docs.passion8.cc/docs/claude-code/sdk-capabilities)。TypeScript/Python API、session API 和迁移差异见 [Agent SDK API 参考速查](https://docs.passion8.cc/docs/claude-code/sdk-api-reference)。Agent loop、settingSources、skills、subagents、todo tracking 和 checkpointing 见 [Agent SDK Agent 能力](https://docs.passion8.cc/docs/claude-code/sdk-agent-features)。Custom tools、system prompt、streaming、structured output、tool search、用户审批和 SDK slash commands 的运行时设计见 [Agent SDK 运行时模式](https://docs.passion8.cc/docs/claude-code/sdk-runtime-patterns)。本页聚焦部署拓扑、多租户隔离和运行时运维。

## 运行模型

| 项目 | 生产含义 |
| --- | --- |
| `query()` | 启动 Claude Code CLI 子进程,不是直接把 prompt 发给无状态 API wrapper |
| 子进程 | 拥有 shell、工具调用、当前工作目录和本地 session 文件 |
| 并发 | N 个并发 session 通常意味着 N 个子进程,需要按 CPU、内存、磁盘和 API 限额规划 |
| `cwd` | 默认继承宿主应用工作目录;多 session 或多租户必须显式传入 |
| 本地状态 | 容器重启、扩缩容、迁移节点时会丢失,除非你单独持久化 |

本地默认状态主要有三类:

| 状态 | 默认位置 | 生产处理 |
| --- | --- | --- |
| Session transcripts | `~/.claude/projects` 或 `CLAUDE_CONFIG_DIR` 下的 `projects/` | 需要跨主机恢复时用 `SessionStore` 镜像 |
| Memory files | 用户层 `~/.claude/CLAUDE.md`,项目层工作目录内 `CLAUDE.md` | 不会被 `SessionStore` 替代,需要独立卷、对象存储或禁用策略 |
| 工作产物 | session 的 `cwd` | 用每租户/每任务目录、卷或对象存储同步 |

## 会话模式

| 模式 | 适合场景 | 关键设计 |
| --- | --- | --- |
| Ephemeral | 一次性修复、分析、转换、CI job | 每个任务一个容器或 sandbox,结束即销毁;只保留你显式导出的结果 |
| Long-running | Slack bot、邮件 agent、持续站点构建器 | 容器长期运行,HTTP/WebSocket 入口把同一 session 路由到同一 worker |
| Hybrid + `SessionStore` | 用户间歇回来继续的研究、项目管理、客服工单 | 空闲时释放容器,下次用 session ID 加 `SessionStore` 恢复 transcript |
| Multi-agent container | 多 agent 协作或仿真 | 同容器内多个 SDK 子进程,每个 agent 单独 `cwd`、配置目录和权限边界 |

`SessionStore` 只镜像 transcript,不是本地状态的替代品。Claude Code 子进程仍然先写本地 transcript,SDK 再把批次转发到 store。`CLAUDE.md`、auto memory、文件 checkpoint blob 和工作目录产物都不由 `SessionStore` 接管。




如果 `SessionStore.append()` 失败,SDK 会重试有限次数,最终失败时继续运行并在消息流中发出 `mirror_error`。生产环境要监控 `{ type: "system", subtype: "mirror_error" }`,否则外部存储可能悄悄缺 transcript 批次。




## 多租户隔离

默认 SDK 会读取本机的 user、project、local settings 和 memory。共享容器里如果不隔离,一个租户的 `CLAUDE.md`、MCP、命令或 auto memory 可能进入另一个租户的上下文。

| 隔离点 | TypeScript | Python | 目的 |
| --- | --- | --- | --- |
| 禁用文件系统 settings | `settingSources: []` | `setting_sources=[]` | 不加载 user/project/local settings |
| 禁用 auto memory | `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` | `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` | 避免 `~/.claude/projects/<project>/memory/` 注入系统提示 |
| 每租户配置目录 | `CLAUDE_CONFIG_DIR=/srv/claude-config/<tenant>` | 同左 | 隔离 `~/.claude.json`、transcripts 和缓存状态 |
| 明确工作目录 | `cwd: tenantDir` | `cwd=tenant_dir` | 隔离文件读写、命令执行和产物 |
| 租户 egress/proxy | 在网关或网络层配置 | 同左 | 独立 outbound IP、凭据注入、domain allowlist 和审计 |

Python SDK 旧版本曾把 `setting_sources=[]` 当作未设置处理。依赖空列表隔离时,先升级到当前版本再上线。

## 观测与成本

Agent 是长生命周期进程,一次用户任务可能跨多轮 API、工具调用、MCP 请求和子代理。生产环境至少要导出 OpenTelemetry,并从 result message 记录 token 与成本字段。

```bash
CLAUDE_CODE_ENABLE_TELEMETRY=1
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.example.com:4318
OTEL_RESOURCE_ATTRIBUTES=service.name=agent-runtime,team=platform
```

默认不要记录 prompt 原文、工具输入输出或 raw API body。只有在隔离排障环境短期开启这些高敏字段。

| 字段 | 含义 | 用途 |
| --- | --- | --- |
| `message.total_cost_usd` | SDK 汇总的客户端成本估算 | 计入每 session/租户账单和预算告警 |
| `message.usage.input_tokens` | 本轮标准输入 token | 判断上下文膨胀 |
| `message.usage.output_tokens` | 本轮输出 token | 判断回复和工具规划成本 |
| `message.usage.cache_creation_input_tokens` | 写入 prompt cache 的 token | 缓存写入成本,通常高于标准输入 |
| `message.usage.cache_read_input_tokens` | 从 prompt cache 读取的 token | 缓存命中收益,通常按较低输入价计费 |

成功和失败的 result message 都可能带 `usage` 与 `total_cost_usd`。不要只在 success 分支记录成本。

## 结构化输出与工具规模

结构化输出适合把 agent 结果写入数据库、任务系统或 UI。配置 `outputFormat` / `output_format` 后,最终 result message 会带 `structured_output`;SDK 会按 JSON Schema 校验,不匹配时重试,最终失败则返回错误结果。

Custom tools 和 MCP 是生产 agent 的主要扩展方式:

| 能力 | 用法 |
| --- | --- |
| Custom tools | 用 SDK 的 in-process MCP server 包装应用内函数、数据库访问或内部 API |
| Remote MCP | 连接 HTTP/SSE/stdio MCP server,把 Slack、GitHub、DB、工单系统接入 agent |
| `allowedTools` / `allowed_tools` | 预批准明确工具或 `mcp__server__*` 通配符 |
| Tool search | 工具很多时延迟加载定义,避免每轮都把全部工具 schema 放进上下文 |

`ENABLE_TOOL_SEARCH` 常用值:

| 值 | 行为 |
| --- | --- |
| 未设置 | 默认启用;在 Vertex AI 或非一方 `ANTHROPIC_BASE_URL` 下可能回退到 upfront 工具定义 |
| `true` | 强制启用,代理或模型不支持 `tool_reference` 时请求可能失败 |
| `auto` | 工具定义超过上下文窗口 10% 时启用 |
| `auto:5` | 工具定义超过 5% 时启用,更早进入搜索模式 |
| `false` | 禁用 tool search,每轮加载全部工具定义 |

工具少于约 10 个时,全部 upfront 加载通常更简单。工具库达到几十、几百甚至上千个时,tool search 通常能显著降低上下文占用并改善工具选择。

## Prompt cache TTL

Agent SDK 会自动使用 prompt caching。你通常不需要手写 cache 控制,但要知道 5 分钟和 1 小时 TTL 的成本差异。

| 规则 | 说明 |
| --- | --- |
| 默认 API key / Bedrock / Vertex / Foundry | 缓存写入通常是 5 分钟 TTL |
| `ENABLE_PROMPT_CACHING_1H=1` | 可选请求 1 小时 TTL,适合短会话反复加载相同系统提示和上下文 |
| Claude subscription | 计划额度内通常自动使用 1 小时 TTL |
| 1 小时写入 | 写入价格更高,适合能换来更多 cache read 的工作负载 |
| Cache hit | 命中会刷新 TTL;TTL 是闲置过期时间,不是总寿命 |
| 前缀变化 | 换模型、换 effort、工具定义变化、MCP 连接变化、`/compact` 等都可能降低命中 |

把 `cache_creation_input_tokens` 和 `cache_read_input_tokens` 分开看。creation 每轮都很高,通常说明 prompt 前缀在变;read 持续升高,说明同一前缀被复用。

## TypeScript 配置片段

```typescript
import path from "node:path";
import { query, type SessionStore } from "@anthropic-ai/claude-agent-sdk";

declare const prompt: string;
declare const sessionId: string | undefined;
declare const sessionStore: SessionStore;

const tenantId = "tenant_123";
const tenantDir = path.join("/srv/agent-work", tenantId);
const configDir = path.join("/srv/claude-config", tenantId);

for await (const message of query({
  prompt,
  options: {
    cwd: tenantDir,
    resume: sessionId,
    sessionStore,
    maxTurns: 30,
    maxBudgetUsd: 5,
    permissionMode: "dontAsk",
    settingSources: [],
    allowedTools: ["Read", "Grep", "Glob", "mcp__enterprise-tools__*"],
    mcpServers: {
      "enterprise-tools": {
        type: "http",
        url: "https://tools.example.com/mcp"
      }
    },
    outputFormat: {
      type: "json_schema",
      schema: {
        type: "object",
        properties: {
          summary: { type: "string" },
          actions: {
            type: "array",
            items: { type: "string" }
          }
        },
        required: ["summary"]
      }
    },
    env: {
      ...process.env,
      CLAUDE_CONFIG_DIR: configDir,
      CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
      CLAUDE_CODE_ENABLE_TELEMETRY: "1",
      CLAUDE_CODE_ENHANCED_TELEMETRY_BETA: "1",
      OTEL_TRACES_EXPORTER: "otlp",
      OTEL_METRICS_EXPORTER: "otlp",
      OTEL_LOGS_EXPORTER: "otlp",
      OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
      OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector.example.com:4318",
      ENABLE_TOOL_SEARCH: "auto:5"
    }
  }
})) {
  if (message.type === "system" && message.subtype === "mirror_error") {
    console.error("SessionStore mirror failed", message);
  }

  if (message.type === "result") {
    console.log({
      subtype: message.subtype,
      cost: message.total_cost_usd,
      usage: message.usage,
      structured: message.structured_output
    });
  }
}
```

TypeScript 的 `env` 会替换子进程环境,不是 merge。生产代码里通常要展开 `...process.env`,否则 `PATH`、`ANTHROPIC_API_KEY` 或 provider 变量可能丢失。

## Python 配置片段

```python
import asyncio
from pathlib import Path

from claude_agent_sdk import ClaudeAgentOptions, query

session_store = ...


async def run_agent(prompt: str, session_id: str | None = None) -> None:
    tenant_id = "tenant_123"
    tenant_dir = Path("/srv/agent-work") / tenant_id
    config_dir = Path("/srv/claude-config") / tenant_id

    options = ClaudeAgentOptions(
        cwd=tenant_dir,
        resume=session_id,
        session_store=session_store,
        max_turns=30,
        max_budget_usd=5,
        permission_mode="dontAsk",
        setting_sources=[],
        allowed_tools=["Read", "Grep", "Glob", "mcp__enterprise-tools__*"],
        mcp_servers={
            "enterprise-tools": {
                "type": "http",
                "url": "https://tools.example.com/mcp",
            }
        },
        output_format={
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                    "actions": {
                        "type": "array",
                        "items": {"type": "string"},
                    },
                },
                "required": ["summary"],
            },
        },
        env={
            "CLAUDE_CONFIG_DIR": str(config_dir),
            "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1",
            "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
            "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
            "OTEL_TRACES_EXPORTER": "otlp",
            "OTEL_METRICS_EXPORTER": "otlp",
            "OTEL_LOGS_EXPORTER": "otlp",
            "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
            "OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector.example.com:4318",
            "ENABLE_TOOL_SEARCH": "auto:5",
        },
    )

    async for message in query(prompt=prompt, options=options):
        if getattr(message, "type", None) == "system" and getattr(message, "subtype", None) == "mirror_error":
            print("SessionStore mirror failed", message)

        if getattr(message, "type", None) == "result":
            print(
                {
                    "subtype": getattr(message, "subtype", None),
                    "cost": getattr(message, "total_cost_usd", None),
                    "usage": getattr(message, "usage", None),
                    "structured": getattr(message, "structured_output", None),
                }
            )


asyncio.run(run_agent("分析这个租户工作区并返回结构化摘要"))
```

Python 的 `env` 会叠加到继承环境上,但仍建议把认证和代理变量交给容器 secret 或网关统一注入。

## 上线检查

| 检查项 | 通过标准 |
| --- | --- |
| 子进程边界 | 每个 session 有明确 `cwd`、资源上限、最大轮数和预算 |
| Session 恢复 | 需要跨主机恢复的 session 已配置 `SessionStore`,并监控 `mirror_error` |
| 状态持久化 | memory files、工作目录产物和 transcript 分别有清楚的保留策略 |
| 租户隔离 | `settingSources: []` / `setting_sources=[]`、`CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`、每租户 `CLAUDE_CONFIG_DIR` 和 egress policy 都已落地 |
| 观测 | OTEL traces/metrics/logs 进 collector,敏感日志默认关闭 |
| 成本 | result message 的 `total_cost_usd`、usage 和 cache read/write tokens 都按租户归档 |
| 工具 | custom tools/MCP 有权限白名单,大工具集启用或评估 tool search |
| 缓存 | 已根据会话间隔选择默认 5m 或 `ENABLE_PROMPT_CACHING_1H=1` |

## 官方参考

- [Hosting the Agent SDK](https://code.claude.com/docs/en/agent-sdk/hosting)
- [Persist sessions to external storage](https://code.claude.com/docs/en/agent-sdk/session-storage)
- [TypeScript SDK reference](https://code.claude.com/docs/en/agent-sdk/typescript)
- [Python SDK reference](https://code.claude.com/docs/en/agent-sdk/python)
- [Get structured output from agents](https://code.claude.com/docs/en/agent-sdk/structured-outputs)
- [Scale to many tools with tool search](https://code.claude.com/docs/en/agent-sdk/tool-search)
- [Track cost and usage](https://code.claude.com/docs/en/agent-sdk/cost-tracking)
