交互式用够了?用 Agent SDK 把 Claude 塞进 Python Web 服务

CLI调用Claude做产品级服务?我试过,翻车了

去年我接过一个活,把Claude Code CLI包成HTTP接口给上层业务调用,subprocess起进程、stdin塞prompt、stdout收文本,简单粗暴。第一周跑得欢,第二周产品提需求:能不能记住上一次问的内容?能不能限定只读?能不能输出JSON?我一一在CLI外面糊补丁,越糊越脏,最后代码长得像一碗意大利面。后来Anthropic出了Agent SDK,我把那堆subprocess代码全删了,三百行Python搞定。

这一篇就讲怎么用Agent SDK把Claude塞进Python Web服务。书里对应第8章,M7里程碑------控制粒度演进的终点:CLAUDE.md(声明式)→ Skills(半声明式)→ Hooks(事件式)→ Agent SDK(编程式)。前三层还在配置范畴,到SDK这一层,Harness本身被当成一个库来调用。

从工具到组件:SDK的定位

CLI是"工具",SDK是"组件"。工具靠shell调用、靠stdin/stdout通信;组件靠import、靠对象方法通信。前者面向人,后者面向代码。

bash 复制代码
# Python
pip install claude-agent-sdk

# TypeScript/Node.js
npm install @anthropic-ai/claude-agent-sdk

两个语言版本API基本对齐,下面以Python为主。

Quick Start:五分钟跑起来

python 复制代码
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def analyze_code():
    options = ClaudeAgentOptions(
        max_turns=5,
        allowed_tools=["Read", "Grep", "Glob"],
        system_prompt="你是一名代码架构分析师。",
    )

    async for message in query(
        prompt="分析 src/auth/ 目录的实现架构",
        options=options
    ):
        if message.type == "assistant":
            for block in message.content:
                if hasattr(block, 'text'):
                    print(block.text, end="", flush=True)
        elif message.type == "result":
            print(f"\n\n完成。费用:${message.total_cost_usd:.4f}")

asyncio.run(analyze_code())

query()是异步生成器,吐出来的不是一坨字符串,是结构化消息。TS版本几乎一模一样,把async for换成for await、把hasattr换成'text' in block即可。

等一下,这里我漏说一个前提------query()吐的消息不止两种,一共四类,搞不清楚状态机后面会迷糊。

四种消息类型:对话状态机

json 复制代码
// 1. system_init:会话初始化,给 session_id
{
  "type": "system", "subtype": "init",
  "session_id": "550e8400-...",
  "model": "claude-sonnet-4-6",
  "tools": ["Read", "Grep", "Glob"]
}

// 2. assistant:Claude 的响应,既能有文本又能有 tool_use
{
  "type": "assistant",
  "message": { "role": "assistant",
    "content": [
      {"type": "text", "text": "让我先看看目录结构..."},
      {"type": "tool_use", "id": "toolu_xxx", "name": "Glob",
       "input": {"pattern": "src/auth/**/*"}}
    ]}
}

// 3. user:工具执行结果回灌
{
  "type": "user",
  "message": { "role": "user",
    "content": [{"type": "tool_result", "tool_use_id": "toolu_xxx",
      "content": "src/auth/\n├── login.ts\n├── session.ts"}]}
}

// 4. result:任务完成,带成本/耗时/session_id

system_init给session_id,assistant里既能有文本又能有tool_use,user是工具结果回灌,result收尾给费用和元数据。我第一次写的时候把result当成了"最终答案",漏掉了assistant流里也可能有最终文本,结果输出残缺------记住,最终文本在assistant流里,result只是元数据。

ClaudeAgentOptions:精细控制从这开始

CLI时代我想要的控制项,这里都有:

python 复制代码
from claude_agent_sdk import ClaudeAgentOptions

options = ClaudeAgentOptions(
    model="claude-sonnet-4-6",
    max_turns=10,
    max_budget_usd=1.0,              # 成本上限,超了就停

    allowed_tools=["Read", "Grep", "Glob", "Write"],
    disallowed_tools=["Bash"],
    permission_mode="default",       # default / acceptEdits / plan / bypassPermissions

    system_prompt="你是一名高级代码审查员。",
    append_system_prompt="务必检查 SQL 注入漏洞。",  # 追加不覆盖

    cwd="/path/to/project",
    env={"PROJECT_NAME": "MyApp"},

    resume="session-id-to-resume",   # 续接会话
    output_format={"type": "json_schema", "schema": my_schema},
    mcp_servers=[{"name": "db", "command": "python", "args": ["./db_server.py"]}],
)

max_budget_usd这条我必须有------给客户做项目的时候,没有成本上限的Agent能在测试循环里把预算烧光,亲身经历,一个晚上烧了四十刀。

工具权限还能做模式匹配,颗粒度到命令参数:

python 复制代码
options = ClaudeAgentOptions(
    allowed_tools=[
        "Read", "Grep", "Glob",
        "Bash(git diff *)",     # 只允许 git diff
        "Bash(npm test *)",     # 只允许 npm test
        "mcp__database__query", # 只允许这个 MCP 工具
    ]
)

Bash(git diff *)这种写法比disallowed_tools=["Bash"]然后自己写正则过滤命令行优雅多了------CLI时代我就是这么糊的,这里是SDK原生支持。

session_id:会话延续和分支

python 复制代码
# 第一轮:分析问题,拿到 session_id
session_id = None
async for message in query(prompt="分析 src/auth 的安全问题", options=options):
    if message.type == "system" and message.subtype == "init":
        session_id = message.session_id

# 第二轮:在上一轮上下文里继续
resume_options = ClaudeAgentOptions(**options.__dict__, resume=session_id)
async for message in query(
    prompt="重点分析你发现的第一个 SQL 注入风险",
    options=resume_options
):
    ...

要分支探索两个方向,加fork_session=True

python 复制代码
options_fork = ClaudeAgentOptions(
    **options.__dict__, resume=session_id, fork_session=True
)

# 方向 A:重构为微服务
async for message in query(prompt="如果重构为微服务,需要改哪些?", options=options_fork):
    ...

# 方向 B:原架构加固,同一个 session_id 再 fork
async for message in query(prompt="如果保持现有架构,怎么加固安全?", options=options_fork):
    ...

fork_session不污染原会话------做A/B方案对比的时候特别好用。

@tool 装饰器:自定义工具 + Pydantic

python 复制代码
from claude_agent_sdk import tool, create_sdk_mcp_server
from pydantic import BaseModel, Field

class DatabaseQueryParams(BaseModel):
    table: str = Field(..., description="Table name")
    columns: list[str] = Field(default=["*"], description="Columns to select")
    where: str | None = Field(default=None, description="WHERE clause")
    limit: int = Field(default=100, ge=1, le=1000)   # 1到1000之间

@tool(
    name="safe_query",
    description="Execute a safe, parameterized database query",
    parameters=DatabaseQueryParams        # 直接传 Pydantic 类
)
async def safe_query(args: DatabaseQueryParams):
    # args 已经通过 Pydantic 验证,类型安全
    rows = await db.execute(args.table, args.columns, args.where, args.limit)
    return {"content": [{"type": "text", "text": json.dumps(rows)}]}

# 用 MCP 服务器承载这些工具
tools_server = create_sdk_mcp_server(
    name="app-tools", version="1.0.0",
    tools=[safe_query]
)

options = ClaudeAgentOptions(
    mcp_servers={"app-tools": tools_server},
    allowed_tools=["Read", "Grep", "Glob", "mcp__app-tools__safe_query"]
)

parameters直接传Pydantic类,SDK会自动转成JSON Schema喂给Claude,Claude回传的参数也会被Pydantic校验------limit超1000直接拒。CLI时代我得自己写参数校验,还经常漏边界。

等一下,这里我又漏了一个前提------光有参数校验不够,还得有运行时拦截。

四道安全防线:脱离CLI沙箱也得住

CLI有沙箱兜底,SDK脱离了CLI,得自己搭防线。书上给了四道:

python 复制代码
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

# 防线一: PreToolUse Hook------执行前拦截
async def block_dangerous_bash(input_data, tool_use_id, context):
    if input_data["tool_name"] != "Bash":
        return {}
    command = input_data["tool_input"].get("command", "")
    dangerous = ["rm -rf", "sudo", "chmod 777", "> /dev/", "mkfs", "dd if="]
    for pattern in dangerous:
        if pattern in command:
            return {"hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": f"Blocked: {pattern}"
            }}
    return {}

# 防线二: can_use_tool------运行时权限检查
async def can_use_tool(tool_name: str, tool_input: dict) -> dict:
    if tool_name in ["Write", "Edit"]:
        file_path = tool_input.get("file_path", "")
        if ".env" in file_path or "secrets" in file_path:
            return {"allowed": False, "reason": "Access to sensitive files denied"}
    if tool_name == "Bash":
        command = tool_input.get("command", "")
        if any(cmd in command for cmd in ["curl", "wget", "ssh"]):
            return {"allowed": False, "reason": "Network commands not allowed"}
    return {"allowed": True}

# 防线三: PostToolUse------执行后审计
async def audit_all_tools(input_data, tool_use_id, context):
    import json
    from datetime import datetime
    entry = {
        "timestamp": datetime.now().isoformat(),
        "tool": input_data["tool_name"],
        "input": input_data["tool_input"],
    }
    with open("agent-audit.jsonl", "a") as f:
        f.write(json.dumps(entry) + "\n")
    return {}

options = ClaudeAgentOptions(
    permission_mode="acceptEdits",                            # 防线零: 权限模式
    allowed_tools=["Read", "Write", "Edit", "Grep", "Glob"],  # 防线一: 工具白名单
    can_use_tool=can_use_tool,                                 # 防线二: 运行时检查
    hooks={                                                    # 防线三: Hooks 拦截+审计
        "PreToolUse": [HookMatcher(matcher="Bash", hooks=[block_dangerous_bash])],
        "PostToolUse": [HookMatcher(matcher="*", hooks=[audit_all_tools])],
    }
)

四道防线:permission_mode(粗粒度模式)+ allowed_tools(工具白名单)+ can_use_tool(运行时检查)+ PreToolUse/PostToolUse Hooks(事件拦截+审计)。我做生产服务时这四道全开,少一道都睡不着。

结构化输出:强制JSON Schema

python 复制代码
from pydantic import BaseModel

class SecurityReport(BaseModel):
    summary: str
    issues: list[dict]     # [{severity, file, line, description}]
    risk_score: float       # 0.0 - 10.0

options = ClaudeAgentOptions(
    output_format={
        "type": "json_schema",
        "schema": SecurityReport.model_json_schema()
    },
    max_turns=10,
    allowed_tools=["Read", "Grep", "Glob"],
)

async for message in query(prompt="对 src/ 进行安全审查", options=options):
    if message.type == "result" and message.structured_output:
        report = SecurityReport.model_validate(message.structured_output)
        print(f"风险评分:{report.risk_score}")
        for issue in report.issues:
            print(f"  [{issue['severity']}] {issue['file']}:{issue.get('line', '?')}")

message.structured_output是SDK帮你validate好的dict,再用Pydantic包一层就是类型安全对象。CLI时代我用正则解析Claude的Markdown输出,写到崩溃。

完整Web服务:FastAPI + SSE

把上面这些拼起来,一个能跑的代码分析服务:

python 复制代码
#!/usr/bin/env python3
"""代码分析 Agent 服务------一个可运行的完整示例"""
import asyncio, json
from claude_agent_sdk import query, ClaudeAgentOptions

async def analyze_codebase(directory: str, focus: str = "general"):
    focus_prompts = {
        "security": "专注于安全漏洞:SQL 注入、XSS、敏感信息硬编码、权限控制。",
        "performance": "专注于性能问题:N+1 查询、内存泄漏、缺少缓存。",
        "quality": "专注于代码质量:命名规范、DRY 原则、复杂度、测试覆盖。",
        "general": "全面分析:安全、性能、质量、架构。"
    }
    options = ClaudeAgentOptions(
        model="claude-sonnet-4-6",
        max_turns=15,
        max_budget_usd=0.50,
        allowed_tools=["Read", "Grep", "Glob"],
        permission_mode="plan",       # 只读模式
        cwd=directory,
        append_system_prompt=focus_prompts.get(focus, focus_prompts["general"]),
    )

    output_text, tools_used, metadata = [], [], {}
    async for message in query(
        prompt=f"分析当前项目的代码。输出 Markdown 格式的分析报告。",
        options=options,
    ):
        if message.type == "assistant":
            for block in message.content:
                if hasattr(block, 'text'):
                    output_text.append(block.text)
                elif hasattr(block, 'name'):
                    tools_used.append(block.name)
        elif message.type == "result":
            metadata = {
                "session_id": message.session_id,
                "cost_usd": message.total_cost_usd,
                "turns": message.num_turns,
                "duration_ms": message.duration_ms,
                "success": not message.is_error,
            }
    return {"report": "\n".join(output_text), "tools_used": tools_used, "metadata": metadata}

包成FastAPI接口,加SSE流式输出:

python 复制代码
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

@app.post("/api/analyze")
async def analyze(request: AnalyzeRequest):
    async def event_stream():
        async for message in query(prompt=request.prompt, options=options):
            if message.type == "assistant":
                for block in message.content:
                    if hasattr(block, 'text'):
                        yield f"data: {json.dumps({'type': 'text', 'content': block.text})}\n\n"
            elif message.type == "result":
                yield f"data: {json.dumps({'type': 'done', 'cost': message.total_cost_usd})}\n\n"
    return StreamingResponse(event_stream(), media_type="text/event-stream")

StreamingResponseasync for的每一段文本立刻推给前端,用户体验比"等三十秒再一次性返回"好太多。

顺便一提,我自己的雷达鸭App(华为应用市场+微信小程序,收录中国一人公司赚钱案例)的AI分析接口,也是用Agent SDK包成FastAPI服务挂在内网,前端Uni-app+ArkTS直接SSE消费,比之前subprocess那套稳定多了。

下一版我想要什么

我对下一版SDK有两个期待:一是can_use_tool支持异步流式审批------现在一次只能返回allow/deny,复杂策略要写一堆if-else;二是output_format能原生支持流式部分JSON------现在结构化输出必须等result才能拿到完整对象,长报告场景下用户得干等。

Agent SDK把Harness从"开发者用的工具"变成了"产品里的组件",控制粒度到命令参数、会话分支、运行时拦截这一层。CLI的活,本来就不该让产品服务去干。


关于作者:雷达鸭App独立开发者,10+年软件开发经验,软件设计师,人工智能应用工程师,专注鸿蒙ArkTS+Web前端,探索AI自动化。

版权声明:本文基于《Claude Code 实战:Harness 工程之道》(黄佳 著)第8章内容整理,原书内容版权归原作者所有。本文采用 MIT 协议发布,转载请保留本声明。

相关推荐
智圣新创0117 小时前
高校共生型学生社区生态圈搭建 核心路径与效能升级高频实操答疑
大数据·人工智能
table study17 小时前
LzzCad:一款开源的 AI + CAD/CAM 软件
人工智能
rain_sxr17 小时前
浏览器跑大模型:WebGPU 端侧推理的加载、回退与性能工程
人工智能
薛定e的猫咪17 小时前
Codex全套科研技能汇总
人工智能·深度学习
zhanghaha131418 小时前
Python语言基础:4_数据类型转换
java·前端·python
码农学院18 小时前
跨平台AI搜索优化的统一策略与引擎差异适配方法
人工智能
is8118 小时前
编程小白也可以做项目,AI 辅助开发太香了
ai编程
kyriewen18 小时前
别再写useMemo了——2026年这5个React性能优化已经是反模式
前端·react.js·ai编程
奇牙coding18 小时前
gpt-5.6-sol 接入指南:reasoning_effort 参数配置、推理链验证与常见报错排查
前端·css·gpt·ai