合集

Claude Agent SDK 实战

已更新 27 章
阅读进度 74%

第 19 章

时延:用户等的是哪一段?

首字 2 秒,结果 8 秒:中间发生了什么?

用户说“你的 Agent 好慢”,通常不是一个数字。

有人指“我打完字,多久看到第一个字”;有人指“整个任务多久跑完”;有人指“点停止,多久真的停”。

这一章拆一次真实调用,把“慢”拆成几段。


19.1 一次真实时间线

ch19_latency.py 做一件只读的事:读 README,回答测试命令,不改文件。

实际输出(2026-10-22,SDK 0.2.168 + CLI 2.1.296 + deepseek-flash):

text
subtype success turns 6 cost 0.033111
total 7.82s
  init          1.35s
  first_stream  2.05s
  first_msg     2.64s
  result        7.82s

拆开看:

时间点耗时发生了什么
init1.35sCLI 子进程启动、initialize 握手
first_stream2.05s第一个增量事件(首字)
first_msg2.64s第一条完整 AssistantMessage
result7.82s6 轮结束,ResultMessage

这段时间线不是“感觉”,是打点打出来的:

python
async def main():
    t0 = time.perf_counter()
    marks: dict[str, float] = {}

    async for msg in query(...):
        now = time.perf_counter() - t0
        if isinstance(msg, SystemMessage) and msg.subtype == "init":
            marks["init"] = now
        elif isinstance(msg, StreamEvent):
            marks.setdefault("first_stream", now)
        elif isinstance(msg, AssistantMessage):
            marks.setdefault("first_msg", now)
        elif isinstance(msg, ResultMessage):
            marks["result"] = now
            print(msg.subtype, msg.num_turns, msg.total_cost_usd)

setdefault 是为了只记第一次:一个任务可能有多个 AssistantMessage,首条才有意义。

两个关键比例:

  • 首字 2.05s,总时延 7.82s。 首字只占总时延的 26%;后面 5.8 秒花在工具调用、后续轮次上。
  • 启动/握手 1.35s,占总时延 17%。 这不是“模型慢”,是进程边界。

如果你只优化模型,最多优化中间那段;启动和轮次还在。


19.2 端到端时延拆成四段

一次 Agent 任务的时延,大概由四段组成:

text
进程启动/握手
  + 模型首字
  + 工具执行
  + 后续轮次(模型 + 工具)
  + 审批等待(如果有)

这次的时间线:

  • 启动/握手:1.35s;
  • 首字:0.70s(2.05 - 1.35);
  • 第一条完整消息:0.59s(2.64 - 2.05);
  • 到结果:5.18s(7.82 - 2.64),主要是工具和后续轮次。

不同任务,四段的比例完全不同:只读问答的大头是启动 + 1–2 轮模型;修测试是多轮工具 + 测试;长任务是轮次 + 压缩;审批型是人等。先量比例,再决定优化哪段。别一上来就“换更快的模型”。


19.3 首字 vs 总时延

首字 2.05s,总时延 7.82s。

如果 UI 是“等 8 秒,一次性出现答案”,用户感知是 8 秒;如果 UI 是“2 秒开始逐字输出”,用户感知是 2 秒开始有反馈。

开流式:

python
options = ClaudeAgentOptions(
    include_partial_messages=True,
    # 其他 options
)


async def main():
    async for msg in query(...):
        if isinstance(msg, StreamEvent):
            # msg.event 里有 text_delta / content_block_delta 等
            ...

要注意:

  • 流式不减少总时延;总时延还是 7.82s;
  • 流式减少“空白等待”的感知;
  • 流式让用户能提前判断“它有没有走错”。

适合:聊天、长回答、需要“打字机效果”的界面。 不适合:CI、批处理、只看最终结果的场景。

流式不是“性能优化”,是“体验优化”。别把两者混为一谈。

流式也有代价:

  • 事件更多,UI/网络处理更复杂;
  • 你需要处理“半句话/半个工具调用”的状态;
  • 如果模型/端点不支持稳定增量,体验反而更差。

所以流式不是默认全开;聊天/长回答开,批处理/CI 不开。


19.4 轮数是乘数

这次只读问答 6 轮,7.82s。

第 17 章评测里的修测试任务,baseline 两次是 10.5s/13.65s(7/9 轮),workflow 是 15.21s/15.01s(9/11 轮)。轮数 7–11,时延 10.5–15.2s。轮数每多一轮,就多一次模型调用 + 工具执行;如果工具是测试/构建,还会多几秒。

所以“减少轮数”通常比“换更快的模型”更有效:

  • 工具面更准,少选错;
  • 提示更清楚,少反复读;
  • Stop 条件明确,少无效重试;
  • 子 Agent 并行,减少串行等待。

19.5 进程启动不是免费的

这次 init 1.35s。它包含:

  • 启动 CLI 子进程;
  • initialize 握手;
  • 加载设置/工具/技能;
  • 准备会话。

如果你每一轮都新建 query(),就是每一轮都付这 1.35s。一次性任务/CI 用 query() 可以接受;多轮聊天用 ClaudeSDKClient,只付一次;每轮新建 query() 不推荐,既付启动,又丢运行期控制。

算一笔启动账:10 轮,每轮新建 query() 是 10 × 1.35s ≈ 13.5s;一个 ClaudeSDKClient 是 1.35s。差 12 秒。用户感知的“慢”,可能不是模型,是你每轮都在重启 CLI。

长连接的写法:

python
async def main():
    client = ClaudeSDKClient(options=options)
    await client.connect()
    try:
        await client.query("第一轮")
        async for msg in client.receive_response():
            ...
        await client.query("第二轮")
        async for msg in client.receive_response():
            ...
    finally:
        await client.disconnect()

它不只省启动时间,还保留会话状态和运行期控制(interrupt/set_model)。


19.6 工具时延与并行

工具执行是时延里最不可控的一段:

  • Read 很快;
  • Bash 跑测试可能几秒到几分钟;
  • 网络工具受外部服务影响;
  • 审批要等人,可能无限长。

三个手段:

  • 只读工具并行:多个 Read/Grep 可以同时跑;
  • 子 Agent 并行:多个独立调研同时跑,墙钟约等于最慢的那个;
  • 给工具设超时:别让一个卡住的命令拖垮整次运行。

第 8 章的并行 demo:三个 researcher,wall 0.20s vs serial 0.60s;主历史还是 1 条。并行买的是时延,不是成本。

一次测试命令的时延,往往比模型还长:Read 一个文件几十 ms,Grep 一个仓库几百 ms,unittest 全量 1–10s,安装依赖/构建 10s 到几分钟。所以“工具并行”只对只读、独立、快的工具有效;测试/构建要串行,或者放进后台任务。

给工具设超时:

python
import anyio


async def call_tool():
    with anyio.fail_after(120):
        return await run_tool(name, args)

超时后别直接重试;先看工具有没有执行到一半(文件、外部请求)。时延优化不能以“重复副作用”为代价。


19.7 别用平均掩盖长尾

这次 7.82s;第 17 章评测 10.5–15.2s。平均值会掩盖长尾。

线上更该看:

  • p50 / p95:一半用户等多久,最慢的 5% 等多久;
  • 超时率:多少任务超过 30s/60s;
  • 重试率:失败重试把时延翻倍;
  • 审批等待:人等的时间要单独统计,别混进“模型时延”。

一个平均 10s、p95 30s 的 Agent,比平均 12s、p95 14s 的更让人难受。

这次只有 4 个样本,不够算 p95;但至少能看到 10.5–15.2s 的跨度。样本小的时候,报范围,不报“平均 12.3s”就完事。

每次运行至少记四个数:init、first_stream、first_msg、result。没有这四个点,你只能看到“总共 15 秒”,不知道是启动、模型还是工具慢。

第 17 章的 run_eval.py 只记了总时延;如果要排查时延,把它扩成四个时间点。

给一个交互式 Agent 的时间预算:首字 < 3s,简单任务总时延 < 15s,复杂任务 < 60s,审批等待 < 30s。超了就分别用流式、减轮数/工具、后台任务/进度反馈、超时默认拒绝/异步。

预算是设计出来的,不是“跑完看看”。


19.8 一次 30 秒的慢请求

假设一个修测试任务跑了 30 秒。拆开:

段耗时能不能省
init/启动1.3s复用 client
读文件/模型3s窄工具/流式
跑测试20s只在关键点跑;后台/超时
重试5s减少无效重试

大头不是模型,是测试和重试。

可以做的:

  • 测试只在“改完”和“提交前”跑,不要每改一行跑一次;
  • 给测试设超时(比如 120s),卡住就停;
  • 把长测试放后台,先给用户“正在跑”的反馈;
  • 失败后先读输出,不要立刻重跑。

如果只优化模型,30 秒可能只降到 28 秒;优化轮次/工具,才可能降到 15 秒。


19.9 一次慢请求的排查顺序

用户说“慢”,按这个顺序查:

  1. 启动/init:是不是每轮新建 query()?
  2. 首字:有没有开流式?模型/端点首字多少?
  3. 轮数:是不是反复读/反复试?
  4. 工具:有没有慢命令/网络调用/审批等待?
  5. 长尾:p95 和超时率多少?

这次的时间线就是第 1、2、4 项的组合:1.35s 启动、0.70s 首字、5.18s 工具/后续轮次。


19.10 Harness 视角:时延是控制面的结果

首字/总时延不是模型单方面的属性:进程启动是 Harness 的会话管理,工具时延是工具面/沙箱,轮数是循环/停止条件,审批等待是权限管线。

优化时延,先问“哪一段是 Harness 控制的”,再决定换模型还是改配置。


19.11 下一章:可靠性

时延是“等多久”,可靠性是“能不能等到”。

第 20 章讲失败分类、重试、超时、断点、降级:哪些失败可以重试,哪些重试会重复副作用,resume 为什么不是“重试”。


本章代码 时延测量:https://github.com/kevinkelin/claude-agent-sdk-book/blob/master/code/sdkagent/ch19_latency.py 实测环境:SDK 0.2.168 + CLI 2.1.296 + deepseek-flash;一次运行,不是统计。 说明:first_stream 依赖 include_partial_messages=True;不同端点/模型的流式行为可能不同。