跳到主要内容

创建日期:2026-09-09 | 最近更新:2026-09-09 本文所有「实验日志」均来自配套实验库 ioi/mini-agent(runtime/simple/complex/stream/traffic.mjs + logs/ 抓包),本机 DeepSeek Anthropic 兼容端点、deepseek-v4-flash[1M] 实测;key 已打码。系列上篇:复杂 Agent

把 Agent 的时序看穿:非流式 vs 流式,用真实日志剖析智能体的本质

一句话:智能体不是一个“跑起来的程序”,而是一连串被宿主编排的“请求 → 响应”回合;模型在每一个回合里只是“读一段静态输入、吐一段输出”的无状态单步函数。非流式与流式,改变的不是这个本质,而是这一次响应的内容在时间上怎么摊开给你。本文用 mini-agent 的真实抓包日志,把两种时序模型画出来、拆开看,最后回答“智能体到底是什么”。

0. 材料:一次真实实验的两份日志

mini-agenttraffic.mjs 会把每次 HTTP 请求/返回落到 logs/。下面 simple-* 会话是非流式简单 Agent 跑「现在几点?请调用 get_current_time」的两回合,stream-* 会话是流式同一问句的原始 SSE。两条都是真实记录。

logs/simple-2026-09-08T15-35-35-083Z/
_meta.json base=https://api.deepseek.com/anthropic model=deepseek-v4-flash[1M]
req-01.json POST /v1/messages body.messages=[user "现在几点?请调用 get_current_time。"]
resp-01.json 200 · 1015ms · content块=[thinking, tool_use] · tool_use=get_current_time{} · stop_reason=tool_use
req-02.json POST /v1/messages body.messages[最后一条].content=[tool_result tool_use_id=call_00_… content="2026/9/8 23:35:35"]
resp-02.json 200 · 595ms · content块=[thinking, text] · text="现在是 2026年9月8日 23:35:35(上海时区)。" · stop_reason=end_turn
logs/stream-2026-09-08T15-23-16-643Z/raw-01.txt(首几帧)
event: message_start
data: {"type":"message_start","message":{…,"usage":{"input_tokens":90,…}}}
event: content_block_start
data: {"type":"content_block_start","content_block":{"type":"thinking",…}}
event: ping
data: {"type":"ping"}
event: content_block_delta
data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"We"}}

后面两节的时序图,就是照着这两份日志画的。

1. 根本前提:模型是「无状态单步函数」

先钉死最关键的一个认知,后面所有讨论都从它来:

  • 模型不会自己“跑”。你给它什么它回什么;它拿到的唯一输入是 messages(整段历史)+ 工具声明,输出的只是一段文本/块
  • 没有内部时钟——上一个回合“算到一半”的状态不会保留,全靠宿主把历史再发一遍;
  • 所以“多步推理”“记忆”“会干活”这些智能感,全是从宿主的一次次往返里长出来的,不在模型内部。

把这一点当坐标:一个 Agent = 宿主的一个循环,每圈 = 一次“请求→响应”。看清了这个循环,非流式和流式就只是「每一圈里那次响应怎么回来」的区别。

2. 非流式的时序模型:回合粒度,整包等

Agent 的最小骨架(本系列篇 0的 while 循环),用真实日志的两回合画出来:

对应真实日志:

  • 第 1 次请求发出去后,宿主阻塞等待1015ms,才收到包含 tool_use 的整包 JSON;
  • 这段时间里模型在“想”,但宿主看不到任何中间产物——要么整包,要么没有;
  • 宿主本地执行工具(毫秒级,几乎不耗时),把结果塞进历史再发第 2 次请求,又等 595ms,拿到最终文本。

非流式的特征一句话:等待是全有或全无——你付的总时长 = 模型这一回合的完整生成时间,且期间一片黑。

3. 流式的时序模型:同一回合被摊成一条时间线

把上面的同一次第 1 次请求加上 stream: true,观察时间轴(左边时间从上到下)——模型还没算完,内容就一段段到达:

本机另做的一次计时实测(同一问句,Anthropic 端点,真实):

Q: 用一句话(15字内)说明 HTTP 是什么,只输出答案。
非流式 total : 5262 ms 正文="HTTP是超文本传输协议。"
流式 首个字节 TTFB : 320 ms
流式 首个 data 帧 : 320 ms
流式 全部收完 total : 6423 ms 正文="HTTP是超文本传输协议。"
→ 打字机窗口(首帧→收完)≈ 6100 ms

读这张表的三个关键:

  1. 流式 TTFB(首字节)320ms ≪ 非流式的 5262ms——用户几乎立刻看到内容开始蹦;这就是打字机体验的来源。模型并没有算得更快,是**“先到的部分”先给用户看了**;
  2. 两者 total 是同量级的(5~6s)——流式不缩短总生成时间,它把同一段时长摊开成可感知的进度
  3. 正文不是第一帧就出现:deepseek 是推理模型,thinking 先到、正文后到——所以在打字机 demo 里正文要等 thinking 块结束才开始显示(llm-format 流式篇 专门讲过)。

4. 对比:一张表看清两种时序模型

维度非流式流式(SSE)
时间单位回合粒度回合内还要按细分
用户首感知全部生成完(TTFT≈total)首字节即开始(TTFT 可小一个数量级)
阻塞期间一片黑,无中间产物内容摊开抵达,可渲染/可中断
结束语义整包 JSON:content + stop_reasonmessage_delta(stop_reason)message_stop
对 Agent 循环收完即用仍要收完整条才可用(见 §5)
信息量全有或全无thinking/text 分块先到(块语义与整包一致)

5. 用时序回答“智能体的本质”

把 §1 的前提和上面两张时序图合起来,本质可以拆成三层:

① 记忆的本质 = 把历史再发一遍。 看 §2 日志:第二次请求的 messages 里躺着第一次的 assistant(tool_use)user(tool_result)。Agent 没有“内部记忆”,它的记忆是宿主侧的一条不断变长的消息序列;所谓上下文窗口,就是这张每次都要整张寄给模型的“黑板”。时序上每回合都重寄全部 → 历史越长、单回合越贵(input_tokens 涨)。

② “干活”的本质 = 模型提议、宿主执行、结果回填。 tool_use 出现在响应的末尾(thinking 之后、宿主能看见它时已经是整包/已收完流),它只是“模型用文本写下的意图”;真正执行(查时间、写文件)的是宿主,结果以 tool_result 回填,让下一次请求能“看见”刚才干了什么。智能体的“迭代智能”,就是这层「看见结果再决定下一步」反复发生——执行权和决定权在时序上是分开的两段,这正是安全白名单能成立的物理基础。

③ 流式只是“呈现层”的优化,不改变协议语义。 这里有个新手最容易踩的坑:即使开了流式,Agent 也必须在 message_stop/[DONE] 之后,才把这一整条 assistant 消息(含可能出现的 tool_use)追加进历史。半截 delta 不能当最终消息用——thinking 可能被 max_tokens 截断、模型可能“说到一半改主意”、tool_useinput 要到块结束才完整。所以:

非流式与流式,改的是“响应的呈现时序”;不改的是“回合的语义”。 智能体依旧是宿主把消息一条条垒高、一次次全量寄出的循环;流式只让循环里每一次等待变得可感知、可打断、体验更顺。

6. 结论:想变强,别指望模型“跑起来”

把「智能体本质」压缩成一句工程判断:你在写的是一个消息序列的编排器,不是一个常驻进程。于是真正决定 Agent 上限的杠杆全在时序的“回合之间”,而不是单次响应内部:

杠杆在时序里加在哪mini-agent 里的体现
更好的工具/回填每个回合的结果质量complex.mjs:错误回填 is_error: true 让模型补救
记忆/上下文管理发送前裁剪/摘要历史messages 越垒越高 → 后续可做摘要或向量检索
规划/多步拆解在“请求前”先让模型产 plan多次往返本质上是同一个循环复用
体验(流式)单回合内部呈现stream.mjs 打字机、可中途中断

想从“能跑的 Agent”走向“聪明的 Agent”,方向是:让宿主在回合之间做对的事——更准的工具、更干净的历史、更可验证的落盘(以工具结果为准)。模型本身,始终只是那个被反复叩问的“无状态单步函数”。

动手

  1. mini-agentnode simple.mjs "现在几点?请调用 get_current_time。",去 logs/simple-*/ 对照本文 §2 的两回合结构;
  2. node stream.mjs "用一句话介绍 HTTP。",对比 raw-01.txtresp-01.json,数一数正文在哪个块才出现;
  3. 自己写个计时脚本(仿本文),量你常用模型/长提示词下的“非流式 total vs 流式 TTFB”,体会差距。

自测

  1. 为什么说模型是“无状态单步函数”?Agent 的“记忆”实际存在哪?
  2. 非流式 Agent 的一回合要等多久才看得到结果?流式把这个等待变成了什么?
  3. 流式与总时长是缩短还是摊开?TTFB 说明了什么?
  4. 为什么 Agent 即使开了流式,也要等 message_stop 才回填历史?
  5. “工具执行权在宿主”这件事,在时序上为什么是安全白名单的根基?

参考:配套实验库 ioi/mini-agent(含抓包 logs/);协议流式细节见本站 llm-format 流式篇