Skip to main content

04 · 输出解析与流式:StrOutputParser 与打字机

本片目标:解决”输出”两个痛点——① 剥壳取文本(AIMessage → str);② 流式输出(逐字显示,SSE 网络模型)。同时第一次接触 LCEL 管道(|,为第 09 篇 RAG 组装埋伏笔。 新增规定性:4(输出用 StrOutputParser;流式用 .stream()| 管道开始出现数据字典:StrOutputParser / AIMessageChunk。 进程线程模型stream() 是生成器——逐块 yield,每块到达就处理,不等待全部。 网络模型:流式 = SSE(Server-Sent Events,HTTP 长连接逐事件推送)。

1. 上集回顾

第 03 篇我们有模板了。但两个输出问题没解决:
  1. model.invoke(messages) 返回 AIMessage,我想要纯字符串——每次 .content 太啰嗦,而且后面接 RAG 链时每个环节都要”字符串进、字符串出”,AIMessage 这个壳会碍事。
  2. 模型生成 100 个 token 要 2 秒,invoke憋到最后才返回。用户盯着空白 2 秒——体验极差。ChatGPT 那种逐字蹦出来是怎么做到的?

2. StrOutputParser:剥壳取文本

2.1 StrOutputParser 返回值(1.x 注意!)

⚠️ 1.5.x 实测类型StrOutputParser().invoke(ai_msg) 返回的不是裸 str,而是 langchain_core.messages.base.TextAccessor。它是 str 的子类(isinstance(x, str) == Truestr(x) 正常),但 type(x).__name__'TextAccessor'所有字符串操作(拼接、切片、正则)都照常工作,只是类型名不同——别被调试输出吓到。这是 langchain-core 1.0 引入的过渡类(为了兼容旧的 .text() 方法调用)。
它为什么存在StrOutputParser 实现了一个通用接口 BaseOutputParser。以后你要自定义解析器(比如第 05 篇的 Pydantic 解析),只需实现 parse 方法。它让”模型输出 → 应用所需格式”这件事有了统一的抽象。

3. 流式输出:.stream() 与 AIMessageChunk

3.1 数据字典:AIMessageChunk

流式协议特征(实测)
  • 第一块是”空壳”id 已就位、content 为空——用于先确定这次 run 的 id。
  • 中间块content 是碎片(几个字),usage_metadataNone
  • 末块content 可能为空,但 response_metadata["stop_reason"] == "end_turn"usage_metadata 有值——表示生成结束

3.2 手动累积(你通常不用做)

框架层面,invoke = 把 stream 所有块拼起来。你不需要手动拼,但要知道:invoke 和 stream 是同一个底层的两种视图

4. LCEL 管道(|)——预告

第 03 篇的模板 + 第 04 篇的解析器,可以串起来:
| 是什么:LangChain 表达式语言(LCEL)。a | b 表示”a 的输出作为 b 的输入”。它要求两边的对象都实现 Runnable 协议(都有 invoke)。 类型流
现在只需要眼熟。第 09 篇 RAG 组装会用到完整的 | 组合(含 RunnablePassthrough)。先记住:prompt | model | parser 是 LangChain 1.x 的标准三段式

5. 进程线程模型

关键stream() 是惰性生成器。它不是”一次性取完再返回”,而是”来一块给一块”。阻塞发生在每次迭代(等待网络),但每次只等一小段(几十毫秒),所以用户体验是”逐字蹦”。

6. 网络模型:SSE 长连接

本机已装 httpx-sse 0.4.3——就是 SDK 解析 SSE 流的底层库(第 12 篇细讲)。
实测时序(智谱 glm-4.7,2026-08)
  • 正常时:stream 首个内容块 ≈ 0.5s < invoke 总耗时 ≈ 2.0s——首 token 到达更快,这是流式最核心的体验优势。
  • 偶发:首 token 延迟可能飙到 10s+(API 侧波动)。结论:流式降低的是”首字等待”,不保证”总时间”更短——invokestream 生成同样内容的总耗时差不多,流式的价值是早开始显示

7. 验证:跑起来

配套代码 code/04_streaming.py
  1. StrOutputParser 剥壳演示;
  2. model.stream() 逐块打印 content + usage(看到空壳/碎片/末块三态);
  3. prompt | model | parser 管道第一次完整跑通;
  4. 计时对比 invoke vs stream 完成时间(验证 invoke 憋到最后)。
预期输出(节选)

8. 边界

  • chunk.content 是碎片不是累积——想拿完整答案,要么自己 ''.join,要么直接用 invoke
  • invoke 也能拿到流式数据——invoke 内部就是消费 stream。区别只在”要不要逐块处理”。
  • SSE 需要长连接——代理/网关配置不当会切断流式。生产环境要注意超时设置(第 15 篇)。
  • StrOutputParser 对多模态 content 无能为力——content 是块列表(图片等)时,解析器只取字符串部分。

推荐资料(延伸阅读)


9. 未完待续

“输出是文本”解决了,但业务往往要的是结构化数据:客服工单要 {"intent": "投诉", "order_id": "2026..."}、RAG 要带引用来源、报表要数字。让模型”保证输出 JSON 且字段正确”——这就是结构化输出。 05 · 结构化输出