《Claude Code 工程化实战》第 24 讲 Agent SDK 高级应用

📌 本讲摘要

第 23 讲学了 SDK 基础------query() / ClaudeCodeOptions 11 字段 / 4 类消息 / 3 个基础场景。本讲进入高级应用:自定义工具(@tool 装饰器 + create_sdk_mcp_server)、多 Agent 编排(主 Agent + SubAgent 协同)、流式会话(resume 多轮 + 流式输出)、错误处理(try/except + 重试 + fallback)。项目实战:自动化测试修复 Agent(分析 → 修复两阶段工作流)、4 层权限管理(只读分析 → 编辑修复 → Hook 拦截 → 审计日志)。

本讲的三条主线:

第一、自定义工具 + 多 Agent 编排------@tool 装饰器把 Python 函数变 Claude 工具(自动生成 JSON schema)、create_sdk_mcp_server 打包 in-process MCP server;主 Agent 调 SubAgent 协同。

第二、流式会话 + 错误处理------resume 续跑旧会话 / 流式输出(逐块打印)/ 3 层错误处理(try/except + 指数退避重试 + fallback)。

第三、自动化测试修复 Agent 实战------分析(只读、plan 模式)→ 修复(acceptEdits 模式、Edit 改代码 + 跑测试);4 层权限管理(只读 / 编辑 / Hook / 日志)。

学完本讲、你应该能用 @tool 装饰器写自定义工具、能用 create_sdk_mcp_server 打包 in-process MCP server、能用 resume 续跑多轮对话、能做 3 层错误处理、能写完整的测试修复 Agent 两阶段工作流。

📖 详细内容

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

第 19 讲学了 MCP server------用 npx 启动 stdio 进程暴露工具。本讲学 in-process MCP server------用 @tool 装饰器把 Python 函数直接变成工具、在 SDK 进程内运行、无需 npx / 进程隔离。

@tool 装饰器

@tool 装饰器是 SDK 提供的装饰器、把 Python 函数变成 Claude 可调用的工具、自动生成 JSON schema(从 type hints 推断参数类型)。

用法:

复制代码
from claude_code_sdk import tool

@tool
def get_user(user_id: int) -> dict:
    """根据用户 ID 获取用户对象。"""
    return db.query("SELECT * FROM users WHERE id = ?", user_id)

装饰器自动:

  1. 从 type hints 推断参数 schema(user_id: int{"type": "integer"})。

  2. 从 docstring 推断工具 description(给 Claude 看)。

  3. 注册到 SDK 工具列表、Claude 推理时能看到 get_user(user_id: int) 工具。

create_sdk_mcp_server

create_sdk_mcp_server 把多个 @tool 打包成 in-process MCP server、无需 stdio 进程。

用法:

复制代码
from claude_code_sdk import create_sdk_mcp_server

server = create_sdk_mcp_server(
    name="my-tools",
    version="1.0.0",
    tools=[get_user, create_user, update_user]
)

然后在 ClaudeCodeOptions.mcp_servers 里引用这个 server,Claude 就能调 my-tools 暴露的所有工具。

in-process MCP vs stdio MCP

in-process 优势:无需 npx / 启动快(进程内函数调用)/ 易调试(普通 Python 调试器)/ 单语言(纯 Python)。

stdio 优势:跨语言(任何语言写 server)、跨进程(隔离)、跨机器(远程 MCP)。

选择:工具用 Python 写 + 不需要跨进程 → in-process(本讲重点)。工具用其他语言写 + 需要隔离 / 跨机器 → stdio。

维度 @tool 装饰器(in-process) stdio MCP server
集成方式 Python 装饰器(同进程) 命令行启动独立进程(npx / uvx)
性能 ~ms 级(进程内函数调用) ~5-10ms(stdio IPC 开销)
部署 同 Python 进程一起打包 需要 npm / npx 启动
调试 普通 Python 调试器(pdb / pydev) stdio echo + mcp-inspector
适用场景 工具用 Python 写 / 不需要跨进程 工具用其他语言写 / 需要隔离 / 跨机器

多 Agent 编排:主 Agent + SubAgent 协同

第 6 讲学过 SubAgent------独立上下文角色。本讲用 SDK 编排主 Agent + SubAgent------主 Agent 负责整体流程、SubAgent 负责隔离任务。

2 种协同模式

模式 1:顺序协同------主 Agent 调 SubAgent A → 等 A 完成 → 调 SubAgent B → 汇总结果。例:主 Agent 调 code-reviewer SubAgent 做代码审查 → 调 test-runner SubAgent 跑测试 → 汇总报告。

模式 2:并行协同------主 Agent 调多个 SubAgent 同时跑(用 asyncio.gather)→ 等所有完成 → 汇总结果。例:主 Agent 同时调 3 个 SubAgent(lint / format / test)→ 3 个并行跑 → 汇总报告、快 3 倍。

2 种模式的选择:SubAgent 之间有依赖(A 完成后 B 才能跑)→ 顺序。SubAgent 之间独立 → 并行。

主 Agent 汇总 SubAgent 报告

主 Agent 调 SubAgent 后、SubAgent 完成会输出报告(写到 .claude/state/.json 或 stdout)。主 Agent 读报告、整合到自己的输出。

3 个 SubAgent 报告整合:code-reviewer 报告(代码问题)+ test-runner 报告(测试结果)+ lint 报告(代码风格)→ 主 Agent 输出统一报告。

完整代码见后面多 Agent 编排完整代码代码块。

流式会话:resume + 流式输出

第 23 讲讲过 query() 是 async generator、本节深入两个高级用法:resume + 流式输出。

resume:续跑旧会话

query() 用 resume=session_id 续跑旧会话(从 ResultMessage 拿到 session_id)。

用法:query(prompt="继续上次的任务", options=ClaudeCodeOptions(resume="session_abc123")。Claude 加载旧会话的上下文(之前的对话 / 工具结果 / 推理状态)、继续执行。

适用场景:

• 多轮对话------用户多次问、Claude 累加上下文。

• 中断续跑------长任务跑一半中断(超时 / 错误)、后续跑时 resume 旧会话。

• 会话持久化------把 session_id 存数据库、下次读出来 resume。

流式输出:逐块打印

query() 异步流式返回 AssistantMessage、每条消息包含多个 content block(文本 / 工具调用)。逐块处理 = 实时反馈。

典型代码:async for msg in query(...): print(msg.content[0].text, end="", flush=True)------逐字符打印、用户实时看到 Claude 输出。

流式 vs 非流式:流式反馈好(用户等得不焦虑)/ 适合长任务;非流式简单 / 适合短任务。

中断与续跑

长任务(timeout / 错误)中断时、捕获 session_id 存数据库。下次调用时 resume 旧会话、从断点继续。避免长任务中断重头跑。

完整代码见后面流式会话 + 错误处理完整代码代码块。

错误处理:try/except + 重试 + fallback

SDK 跑 Claude 不可避免遇到错误(网络超时 / API 报错 / 任务失败)。生产环境必须有 3 层错误处理。

第 1 层:try/except 捕获异常

SDK 抛的异常类型:

CLINotFoundError------Claude Code CLI 没安装。

ProcessError------Claude Code 子进程错误。

CLIJSONDecodeError------消息 JSON 解析失败。

TimeoutError------超时(asyncio.TimeoutError)。

用 try/except 捕获、记录到日志 + 友好错误提示用户。

第 2 层:指数退避重试

网络超时 / API 临时错误(5xx)适合重试。指数退避策略:第 1 次重试等 1 秒、第 2 次等 2 秒、第 3 次等 4 秒......避免雪崩。

限制:重试 ≤ 3 次、过多会导致长任务变超长任务。

第 3 层:fallback 降级

Claude 跑失败(超过重试次数)时、降级到简单规则:

• 跑代码审查失败 → 回退到提示用户人工 review。

• 跑测试修复失败 → 回退到列出失败原因、让人修。

• 跑自动 commit 失败 → 回退到提示用户 commit。

fallback 不是放弃、而是用简单方式解决。3 层叠加 = 健壮的错误处理。

项目实战:自动化测试修复 Agent

综合运用 24 讲学的所有内容、做一个完整项目------自动化测试修复 Agent。

完整代码见后面自动化测试修复 Agent 完整代码代码块。

两阶段工作流

阶段 1:分析(只读)------用 plan 模式(只读不写),Claude 读测试失败信息 + 读代码 + 分析失败原因、输出修复计划。

阶段 2:修复(编辑)------用 acceptEdits 模式(允许 Edit),Claude 根据修复计划 Edit 改代码 + 跑测试验证。

2 阶段工作流:先分析后修复、避免边改边分析导致乱改。

4 层权限管理

层 1:只读分析 ------阶段 1 用 permission_mode="plan"(plan 模式只读),Claude 只能 Read / Grep / Glob、不能 Edit / Write / Bash。

层 2:编辑修复 ------阶段 2 用 permission_mode="acceptEdits"(自动接受 Edit),Claude 能 Edit / Write / Bash(白名单)。

层 3:Hook 拦截------用 .claude/settings.json 的 PreToolUse HW 拦危险命令(rm -rf / curl | sh),即使 acceptEdits 模式也拦。

层 4:审计日志------PostToolUse HW 记录所有 Edit / Write / Bash 到 audit log、出事后可追溯。

4 层叠加形成纵深防御------只读分析 → 编辑修复 → Hook 拦截 → 审计日志、任何一层失效其他层兜底。

权限层 机制 作用
层 1:只读分析 permission_mode="plan" 阶段 1 只读不写、只输出修复计划
层 2:编辑修复 permission_mode="acceptEdits" 阶段 2 允许 Edit/Write、实际改代码
层 3:HW 拦截 PreToolUse deny-pipe-exec / deny-extra-dangerous 危险命令根本跑不出来、纵深防御
层 4:审计日志 PostToolUse audit-log 所有 Edit/Write/Bash 记录到日志、出事后可追溯

SDK × 5 种扩展机制协同

SDK 是调用者侧------Python 代码调 Claude。5 种扩展机制是被调用者侧------Claude 推理时用。SDK + 5 种机制协同工作。

SDK + Skills------SDK 启动时加载 .claude/skills/ 的 SKILL.md,Claude 推理时自动发现本地能力。

SDK + SubAgent------SDK 启动时加载 .claude/agents/ 的 *.md,Claude 推理时用 @name 调 SubAgent。

SDK + Hooks------SDK 启动时加载 .claude/settings.json 的 hooks 字段、工具调用前后触发拦截 / 记录。

SDK + MCP------SDK 启动时加载 .mcp.json,Claude 推理时调 mcp__xxx__yyy 工具。

SDK + Rules------SDK 启动时加载 .claude/rules/ 的 *.md,Claude 推理时遵守软规范。

SDK + 5 机制 = 完整工程化体系。SDK 是骨架,5 机制是血肉。

🛠️ 实战代码

📄 第 24 讲配套:自定义工具 + in-process MCP server 完整代码(@tool 装饰器 + 2 个 tool + create_sdk_mcp_server)

复制代码
#!/usr/bin/env python3
# custom_tools.py
# 自定义工具 + in-process MCP server
import asyncio
from claude_code_sdk import (
    query, ClaudeCodeOptions, tool, create_sdk_mcp_server
)

# === 自定义工具 1:查数据库 ===
@tool
def get_user(user_id: int) -> dict:
    """根据用户 ID 获取用户对象。

    Args:
        user_id: 用户 ID(整数)

    Returns:
        包含 id, name, email 的字典
    """
    return {
        "id": user_id,
        "name": f"User {user_id}",
        "email": f"user{user_id}@example.com"
    }

# === 自定义工具 2:发邮件 ===
@tool
def send_email(to: str, subject: str, body: str) -> str:
    """发送邮件。

    Args:
        to: 收件人邮箱
        subject: 邮件主题
        body: 邮件正文

    Returns:
        发送结果("success" 或错误信息)
    """
    # 实际场景调 SMTP
    return f"Email sent to {to} with subject '{subject}'"

# === 打包成 in-process MCP server ===
custom_server = create_sdk_mcp_server(
    name="my-custom-tools",
    version="1.0.0",
    tools=[get_user, send_email]
)

async def main():
    options = ClaudeCodeOptions(
        # 加载 in-process MCP server
        mcp_servers={
            "my-custom-tools": custom_server
        },
        max_turns=5
    )

    # Claude 调 get_user(1) 工具
    async for msg in query(
        prompt="查询用户 ID 1 的信息",
        options=options
    ):
        from claude_code_sdk import AssistantMessage
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if hasattr(block, "text"):
                    print(block.text, end="", flush=True)

asyncio.run(main()

# === @tool 装饰器自动做的事 ===
# 1. 从 type hints 推断参数 schema
#    user_id: int → {"type": "integer"}
#    to: str → {"type": "string"}
# 2. 从 docstring 推断 description
#    "根据用户 ID 获取用户对象" → tool description
# 3. 注册到 SDK 工具列表
#    Claude 看到 mcp__my-custom-tools__get_user(user_id: int)

# === in-process MCP vs stdio MCP ===
# in-process: 进程内函数调用,启动快,易调试,单语言
# stdio:      跨进程,跨语言,跨机器,隔离

📄 第 24 讲配套:多 Agent 编排完整代码(主 Agent + code-reviewer SubAgent + test-runner SubAgent + 汇总)

复制代码
#!/usr/bin/env python3
# multi_agent.py
# 多 Agent 编排:主 Agent + 3 个 SubAgent
import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage

async def run_subagent(name: str, prompt: str, options: ClaudeCodeOptions) -> str:
    """跑一个 SubAgent,返回报告"""
    report = ""
    async for msg in query(prompt=prompt, options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if hasattr(block, "text"):
                    report += block.text
    return report

async def main():
    # === 阶段 1:并行调 3 个 SubAgent(快 3 倍)===
    code_review_task = run_subagent(
        name="code-reviewer",
        prompt="审查 src/api/users.py 的代码",
        options=ClaudeCodeOptions(
            allowed_tools=["Read", "Grep"],
            system_prompt="你是 code-reviewer,只输出代码问题",
            max_turns=10
        )
    )

    test_runner_task = run_subagent(
        name="test-runner",
        prompt="跑 pytest tests/test_users.py,报告结果",
        options=ClaudeCodeOptions(
            allowed_tools=["Bash(pytest:*)", "Read"],
            system_prompt="你是 test-runner,只输出测试结果",
            max_turns=10
        )
    )

    lint_task = run_subagent(
        name="linter",
        prompt="跑 ruff check src/api/users.py,报告问题",
        options=ClaudeCodeOptions(
            allowed_tools=["Bash(ruff:*)"],
            system_prompt="你是 linter,只输出 lint 问题",
            max_turns=5
        )
    )

    # 并行跑(asyncio.gather)
    review_report, test_report, lint_report = await asyncio.gather(
        code_review_task,
        test_runner_task,
        lint_task
    )

    # === 阶段 2:主 Agent 汇总 ===
    print("=== code-reviewer 报告 ===")
    print(review_report)
    print("\n=== test-runner 报告 ===")
    print(test_report)
    print("\n=== linter 报告 ===")
    print(lint_report)

    # 写到 .claude/state/reports/latest.md
    with open(".claude/state/reports/latest.md", "w") as f:
        f.write(f"# 综合报告\n\n")
        f.write(f"## 代码审查\n{review_report}\n\n")
        f.write(f"## 测试\n{test_report}\n\n")
        f.write(f"## Lint\n{lint_report}\n\n")

asyncio.run(main()

# === 2 种协同模式 ===
# 顺序: A 完成后 B,等所有完成
# 并行: asyncio.gather 并行跑,快 N 倍

# === 主 Agent 汇总 SubAgent 报告 ===
# 1. SubAgent 写报告到 .claude/state/<name>.json
# 2. 主 Agent 读报告
# 3. 主 Agent 整合到自己的输出
# 4. SubAgentStop HW 验证报告完整性(第 18 讲)

📄 第 24 讲配套:流式会话 + 错误处理完整代码(resume 续跑 + 流式输出 + try/except + 指数退避重试 + fallback)

复制代码
#!/usr/bin/env python3
# streaming_errors.py
# 流式会话 + 错误处理(resume + 流式输出 + 3 层错误处理)
import asyncio
import time
from claude_code_sdk import (
    query, ClaudeCodeOptions,
    AssistantMessage, ResultMessage,
    CLINotFoundError, ProcessError, CLIJSONDecodeError
)

async def stream_query(prompt: str, options: ClaudeCodeOptions):
    """流式 query():逐块打印 AssistantMessage"""
    async for msg in query(prompt=prompt, options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if hasattr(block, "text"):
                    # 流式输出:逐字符打印 + flush
                    print(block.text, end="", flush=True)
        elif isinstance(msg, ResultMessage):
            # 返回 session_id 供下次 resume
            return msg.session_id, msg.total_cost_usd

async def query_with_retry(
    prompt: str,
    options: ClaudeCodeOptions,
    max_retries: int = 3
):
    """3 层错误处理:try/except + 指数退避重试 + fallback"""
    for attempt in range(max_retries):
        try:
            # 尝试跑
            session_id, cost = await stream_query(prompt, options)
            return session_id, cost

        except (ProcessError, CLIJSONDecodeError) as e:
            # 临时错误,指数退避重试
            if attempt < max_retries - 1:
                wait_time = 2 ** attempt  # 1s, 2s, 4s
                print(f"\n⚠️ 第 {attempt + 1} 次失败: {e}")
                print(f"   {wait_time}秒后重试...")
                await asyncio.sleep(wait_time)
            else:
                # 重试耗尽,fallback
                print(f"\n❌ 重试 {max_retries} 次仍失败,fallback")
                return await fallback_handler(prompt)

        except CLINotFoundError as e:
            # CLI 没安装,不能重试
            print(f"\n❌ Claude Code CLI 没安装: {e}")
            return None, 0.0

        except asyncio.TimeoutError as e:
            # 超时
            print(f"\n⚠️ 超时: {e}")
            if attempt < max_retries - 1:
                await asyncio.sleep(2 ** attempt)
            else:
                return await fallback_handler(prompt)

async def fallback_handler(prompt: str):
    """Fallback:降级到简单规则"""
    print(f"\n🔄 Fallback:Claude 跑失败,降级到简单规则")
    # 例:如果任务是代码审查,fallback 是"列出文件让人工审"
    if "审查" in prompt:
        return "session_fallback", 0.0  # 标记是 fallback
    return "session_fallback", 0.0

async def main():
    options = ClaudeCodeOptions(
        allowed_tools=["Read", "Grep"],
        max_turns=10
    )

    # 第 1 次:流式跑(实时反馈)
    session_id, cost = await query_with_retry(
        prompt="分析这个项目结构",
        options=options
    )

    if session_id == "session_fallback":
        print("使用了 fallback,跳过 resume")
        return

    # 第 2 次:resume 续跑(用第 1 次的 session_id)
    options_resume = ClaudeCodeOptions(
        resume=session_id,  # ← 关键:续跑旧会话
        allowed_tools=["Read", "Grep"],
        max_turns=10
    )

    print("\n\n=== 续跑(基于上轮上下文)===\n")
    session_id_2, cost_2 = await query_with_retry(
        prompt="继续:具体说说 main.py 做了什么",
        options=options_resume
    )

    print(f"\n\n📊 累计 cost: ${(cost + cost_2):.4f}")

asyncio.run(main()

# === resume 续跑 ===
# 1. 第 1 次 query() 拿到 session_id(ResultMessage.session_id)
# 2. 第 2 次 query() 用 resume=session_id 续跑
# 3. Claude 加载旧会话的上下文(对话历史 / 工具结果 / 推理状态)

# === 3 层错误处理 ===
# 第 1 层: try/except 捕获异常(4 种异常类型)
# 第 2 层: 指数退避重试(1s, 2s, 4s, ≤ 3 次)
# 第 3 层: fallback 降级(简单规则处理)

# === 流式输出 ===
# print(block.text, end="", flush=True)
# flush=True 关键,否则等缓冲区满才输出

📄 第 24 讲配套:自动化测试修复 Agent 完整代码(两阶段工作流 + 4 层权限管理 + 完整 Python)

复制代码
#!/usr/bin/env python3
# test_fixer_agent.py
# 自动化测试修复 Agent(两阶段工作流 + 4 层权限管理)
import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage

async def analyze_phase(test_output: str) -> str:
    """阶段 1:分析(只读,plan 模式)"""
    options = ClaudeCodeOptions(
        # 层 1:只读权限(plan 模式)
        permission_mode="plan",
        allowed_tools=["Read", "Grep", "Glob", "Bash(pytest:*)"],
        system_prompt="""你是测试分析专家。分析测试失败原因:
1. 读测试输出,理解失败模式
2. 读相关源码,定位 bug
3. 输出修复计划(具体到 file:line + 修复方案)
不要修改任何文件,只分析。""",
        max_turns=10
    )

    plan = ""
    async for msg in query(
        prompt=f"分析这个测试失败:\n{test_output}",
        options=options
    ):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if hasattr(block, "text"):
                    plan += block.text

    return plan

async def fix_phase(plan: str) -> bool:
    """阶段 2:修复(编辑,acceptEdits 模式)"""
    options = ClaudeCodeOptions(
        # 层 2:编辑权限(acceptEdits 模式)
        permission_mode="acceptEdits",
        allowed_tools=["Read", "Edit", "Write", "Bash(pytest:*)", "Bash(ruff:*)"],
        system_prompt="""你是测试修复专家。根据修复计划:
1. 用 Edit 改代码(只改 plan 提到的位置)
2. 改完跑 pytest 验证
3. 如果测试还失败,继续修(最多 5 轮)
不要做 plan 之外的事。""",
        max_turns=20  # 给修复更多轮次
        # 层 3:HW 拦截(在 .claude/settings.json 配置 PreToolUse)
        # 层 4:HW 审计日志(PostToolUse 记录所有 Edit/Write)
    )

    print("=== 阶段 2:修复 ===\n")
    async for msg in query(
        prompt=f"根据这个修复计划修复代码:\n\n{plan}",
        options=options
    ):
        from claude_code_sdk import AssistantMessage
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if hasattr(block, "text"):
                    print(block.text, end="", flush=True)
                elif hasattr(block, "name"):
                    # 工具调用
                    print(f"\n🔧 工具: {block.name}")

    # 验证测试通过
    print("\n\n=== 验证 ===")
    proc = await asyncio.create_subprocess_exec(
        "pytest", "tests/", "-q",
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await proc.communicate()
    return proc.returncode == 0

async def main():
    # 1. 跑测试,看失败
    proc = await asyncio.create_subprocess_exec(
        "pytest", "tests/", "-q",
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await proc.communicate()
    test_output = stdout.decode() + stderr.decode()

    if proc.returncode == 0:
        print("✅ 所有测试通过,无需修复")
        return

    print(f"❌ 测试失败,启动修复 Agent\n")

    # 2. 阶段 1:分析
    print("=== 阶段 1:分析(只读) ===\n")
    plan = await analyze_phase(test_output)
    print(f"\n\n📋 修复计划:\n{plan}\n")

    # 3. 阶段 2:修复
    success = await fix_phase(plan)

    if success:
        print("\n✅ 测试修复成功")
    else:
        print("\n❌ 修复失败,需要人工介入")

asyncio.run(main()

# === 配套 .claude/settings.json(层 3 + 层 4)===
# {
#   "hooks": {
#     "PreToolUse": [
#       {
#         "matcher": "Bash",
#         "hooks": [
#           {"type": "command", "command": "bash .claude/hooks/deny-pipe-exec.sh"},
#           {"type": "command", "command": "bash .claude/hooks/deny-extra-dangerous.sh"}
#         ]
#       }
#     ],
#     "PostToolUse": [
#       {
#         "matcher": "Edit|Write",
#         "hooks": [
#           {"type": "command", "command": "bash .claude/hooks/audit-log.sh"}
#         ]
#       }
#     ]
#   }
# }

# === 4 层权限管理 ===
# 层 1: 只读分析(permission_mode="plan")
# 层 2: 编辑修复(permission_mode="acceptEdits")
# 层 3: HW 拦截(PreToolUse deny-pipe-exec / deny-extra-dangerous)
# 层 4: 审计日志(PostToolUse audit-log)

# === 两阶段工作流 ===
# 阶段 1: 只读分析(plan 模式)→ 输出修复计划
# 阶段 2: 编辑修复(acceptEdits 模式)→ Edit 改代码 + 跑测试

⚠️ 常见坑

⚠️ @tool 装饰器忘写 type hints(Claude 看不到参数 schema、工具无法调用、所有参数必须 type hints + docstring)

type hints 缺失陷阱:@tool 装饰器依赖 type hints 生成 JSON schema,如果函数参数没写 type hints(def get_user(user_id) 而非 def get_user(user_id: int)),Claude 看不到参数类型、工具无法调用。修正:所有 @tool 函数必须 type hints(参数 + 返回值)+ docstring(描述工具用途)。判断标准:你的 @tool 函数有 type hints + docstring 吗?参数 / 返回值都标了吗?没标、补 type hints。

⚠️ 流式输出忘 flush(实时反馈没生效、用户等很久才看到结果、print 加 flush=True)

流式变全量陷阱:流式输出时 print 没加 flush=True,Python print 默认行缓冲(等换行才输出),如果 Claude 输出的文本没换行、用户等很久才看到结果(等缓冲区满才输出)。修正:流式输出必须 print(block.text, end="", flush=True)------end="" 避免换行、flush=True 立即输出。判断标准:你的流式 print 有 flush=True 吗?没有、加 flush。

⚠️ 重试没限次数(网络持续失败导致死循环、重试 ≤ 3 次后 fallback)

死循环重试陷阱:网络持续失败时(Anthropic API 持续 5xx)、无限重试导致长任务变超长任务------用户等几小时还看不到结果。修正:重试 ≤ 3 次、第 4 次 fallback 降级。指数退避:1s / 2s / 4s、避免持续打 API。判断标准:你的重试有限次数吗?≤ 3 次?无限 = 改限次。

⚠️ SubAgent 报告没汇总(主 Agent 看不到 SubAgent 完整结果、SubAgentStop HW + 显式汇总)

SubAgent 报告丢失陷阱:主 Agent 调 3 个 SubAgent 并行跑、3 个 SubAgent 完成、主 Agent 只看到任务完成信号、看不到具体报告(报告写在 .claude/state/.json 但主 Agent 没读)。修正:主 Agent 显式读 .claude/state/.json + 汇总;或者配 SubAgentStop HW 验证报告完整性 + 自动汇总。判断标准:你的 SubAgent 报告汇总了吗?主 Agent 看到完整结果吗?没有、显式汇总。

💡 一句话备忘

Agent SDK 高级应用把 SDK 从基础调用升级到工程化项目------@tool 装饰器 + create_sdk_mcp_server 自定义工具、主 Agent + SubAgent 多 Agent 编排、resume + 流式输出 + 3 层错误处理、自动化测试修复 Agent(分析 → 修复两阶段 + 4 层权限管理)是完整项目实战。

相关推荐
coderklaus2 小时前
RAG 与 LLM Wiki
agent
codingWhat2 小时前
从“模型会说话”到“Agent 可管理”:一套 AI 资产平台的前端工程实践分享
agent
花千树_0102 小时前
插件加载方式选不好,迟早被同事吐槽——四种姿势全解析
langchain·agent
疯狂学习GIS2 小时前
AI Agent 操作 QGIS 实测:Codex 与 Claude Code 能自己出图吗
人工智能·ai·大模型·gis·agent·qgis·codex
大模型真好玩4 小时前
LangChain DeepAgents 速通指南(十一)—— DeepAgents Code 记忆与状态管理
人工智能·langchain·agent
名不经传的养虾人5 小时前
从0到1:企业级AI项目迭代日记 Vol.67|能运行,和能商用
人工智能·agent·ai编程·企业ai·多agent协作
杠杠的blog6 小时前
企业级提示词管理
python·agent·提示词·生产级实践
武子康6 小时前
LingBot 四线全拆:Video / World / VLA / VA 按最终输出选型 + 开源许可证避坑表
人工智能·llm·agent
为你学会写情书7 小时前
RAG 性能总卡在检索?先搞懂 Milvus 向量数据库再说
llm·agent
TrisighT7 小时前
Agent 并行工具调用快了 0.8 秒——200 组实测发现,多花的那 3100 token 全打了水漂
aigc·agent·ai编程