在这里插入图片描述
还在让用户对着白屏干等?LangGraph四大流式模式values、updates、messages、custom,搞懂一个就能让AI应用交互体验原地起飞!本文一次说清四种流的底层逻辑、新手最容易踩的五个坑,以及不同业务场景下的黄金选型法则,读完直接上手写代码。

四种流式处理模式

"values模式:状态全量快照"

"核心原理与数据流"

"新手误区:把快照当增量"

"正确姿势与State瘦身"

"updates模式:增量更新投递"

"核心原理与Diff机制"

"新手误区:直接读取不完整State"

"正确姿势:前端Merge策略"

"messages模式:消息实时对话"

"核心原理与Token级流式"

"新手误区:非LLM节点也等消息"

"正确姿势:混合流式编排"

"custom模式:自定义流编排"

"核心原理与自定义Payload"

"新手误区:格式混乱不敢用"

"正确姿势:Schema标准化"

"实战选型与调试心法"

"四种模式对比矩阵"

"混合使用的正确姿势"

"调试技巧与避坑指南"

文字目录

  1. values模式:状态全量快照的流式推送
  2. updates模式:增量更新的精准投递
  3. messages模式:消息粒度的实时对话体验
  4. custom模式:自定义流的自由编排
  5. 实战选型:四种模式的黄金抉择法则

嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《LangChain核心技术与LLM项目实践》

“看着API文档觉得自己行了,一写业务代码就原形毕露。”这句话是不是戳中了你的膝盖?学LangGraph的时候,官方demo跑起来行云流水,一轮对话唰唰出结果。可一到真实业务里,用户那边疯狂催“怎么还没输出”,你这边盯着黑乎乎的终端直冒汗——是模型卡了?是节点挂了?还是流式配置压根没生效?别慌,今天咱们就把LangGraph里最影响用户体验的“流式处理”彻底掰开揉碎。搞不清这四种stream_mode,你做出来的AI应用就跟2G网速刷短视频一样,用户忍不了三秒就卸载。来吧,学长带你把这四个家伙治得服服帖帖。

values模式:状态全量快照的流式推送

咱们先从最“实在”的values模式聊起。这个模式简单粗暴,你在调用graph.stream的时候带上stream_mode=“values”,LangGraph就会在每一个节点执行完毕之后,把当前整个State的全量快照一股脑儿推给你。注意,是全量,不是增量,是咔嚓一张全景照片,不是局部特写。

这玩意儿最适合什么场景呢?举个例子。你在做一个多Agent协作的智能运营后台,页面上有一个巨大的数据看板,要实时显示当前走到哪个节点了、订单状态变成啥了、上下文里积累了哪些中间结论。这种时候,values模式就是你的上帝视角。你不需要知道上一个节点到底改了哪个字段,你只想拿到截至目前为止的完整上下文,直接渲染到页面上。配合LangGraph的checkpointer一起用,你甚至能看到整个工作流在每个检查点的全貌。

用户输入Query

检索节点

State快照V1全量推送

生成节点

State快照V2全量推送

听起来很美好对不对?但坑,往往就藏在美好背后。新手最容易犯的错,我总结为三个字:不瘦身。有些同学恨不能把整个世界塞进State里。五十轮历史对话、十篇长文档的全文、好几张Base64编码的图片,甚至上一次请求的日志,全往State里堆。然后开了values模式,前端每秒钟收到一个好几MB的JSON巨兽,浏览器直接卡成PPT,内存占用飙到天际。用户那边看到的是一个不断转圈然后直接崩溃的页面。

还有更隐蔽的误区:把values当增量。有些新手收到第一个chunk,里面有messages数组;收到第二个chunk,里面也有messages数组。他误以为第二个chunk里只有新增的消息,结果直接用第二个去覆盖前端的state。这下可好,前面的数据全丢了,用户看到的回答缺胳膊少腿。更有甚者,在State里塞了超大列表,前端每次都用JSON.stringify往页面上怼,界面直接冻结。

看看这段灾难代码:

# 灾难级State设计,啥都往里塞
class State(TypedDict):
    history: Annotated[list, add_messages]  # 攒了50轮对话
    documents: list  # 10篇长文档全文
    images: list     # 3张Base64图片
    logs: list       # 调试日志也塞进来

# 前端狂暴渲染
async for snapshot in graph.astream(
    {"query": "总结一下吧"}, 
    stream_mode="values"
):
    # 每次都是一个巨大的State!
    render_chat(snapshot["history"])      # 浏览器:我扛不住了
    render_docs(snapshot["documents"])    # 内存:我想报警
    console.log(JSON.stringify(snapshot)) # 控制台:我想死

那怎么治?第一招,State设计必须遵循“最小必要原则”。大对象、历史归档、二进制文件,统统放外部存储,State里只存引用ID或者摘要。别让values背着沉重的包袱上路。第二招,前端收到values后,心态要摆正:这是快照,适合做全局刷新和状态监控,不适合做增量追加。如果你的State实在不小,但又需要全局视图,可以给前端加上虚拟滚动或者分页加载,别傻乎乎地一次性全量渲染。第三招,善用条件渲染,只提取你关心的字段做diff更新,而不是把整个State树重新挂载到DOM上。

修正后的思路长这样:

# 精简State,大对象外置
class State(TypedDict):
    query: str
    summary: str
    doc_ids: list  # 只存ID,全文走对象存储
    status: str

# 前端优雅消费
async for snapshot in graph.astream(user_input, stream_mode="values"):
    # 只更新真正变化的区域
    if snapshot.get("summary"):
        update_summary_card(snapshot["summary"])
    
    # 用轻量级指示器代替全量刷新
    update_workflow_status(snapshot.get("status", "running"))

values模式是全局瞭望塔,视野最广,但也最吃资源。State越小,它越香;State越肥,它越呛。记住,做监控大盘它是神器,做高并发聊天机器人它是灾难。

updates模式:增量更新的精准投递

如果说values是拍全景照,那updates模式就是发快递,而且是那种只送“变更单”的精准快递。当你把stream_mode设成"updates"时,LangGraph在每个节点跑完之后,不会给你整个State,而是只把这个节点产生的修改部分——也就是delta——推给你。数据结构通常类似这样:{"node_name": {"new_key": "new_value"}}。前端拿到这些补丁之后,像拼拼图一样把它们合并到本地状态里,就能还原出完整画面。

这个模式在带宽敏感、State体积大的场景下,简直是救命稻草。想象一下,你的State里躺着几十KB的上下文,但当前节点只是更新了一个status字段。用updates,这次推送的payload可能只有几十字节。这在高并发场景下,省下来的带宽和延迟可不是一星半点。而且因为不需要序列化整个State,后端的压力也会小很多。

推送updates

推送updates

初始State

节点A执行

变更单A

节点B执行

变更单B

前端合并为完整State

但是!新手面对updates的第一反应往往是懵逼的。他们习惯了REST API返回一个完整的JSON,突然收到一个孤零零的{"counter": 5},第一反应是:“这啥?数据结构怎么不完整?是不是LangGraph出bug了?”然后就是疯狂的调试,怀疑人生,甚至在群里大喊“我收到的是空数据”。

第二个大坑是合并逻辑写错。有些同学直接把updates赋值给本地变量:current_state = chunk。结果呢?这不是合并,这是覆盖!之前节点积累下来的数据全被抹了,前端展示直接回到解放前。第三个坑藏在并行节点里。LangGraph支持节点并行执行,这时候多个节点可能同时产出updates,推送的顺序取决于哪个节点先跑完。新手如果前端逻辑没做好隔离,UI就会跳来跳去,甚至出现闪烁,用户以为中病毒了。

看看这段让人血压升高的代码:

# 新手迷惑行为大赏
async for chunk in graph.astream(input_state, stream_mode="updates"):
    # chunk 可能是 {"extractor": {"entities": ["北京"]}}
    # 里面根本没有 query 字段!
    display_user_query(chunk["query"])  # KeyError,当场爆炸
    
    # 或者是这样覆盖的
    current_state = chunk  # 完了,之前的数据全丢了
    render_page(current_state)  # 用户:我上一秒看到的数据呢?

怎么破局?核心就四个字:Patch思维。updates本质上是状态补丁,不是状态本身。前端必须准备一个merge策略。在Python里处理的话,可以用安全合并;在前端React或者Vue里,用不可变更新。千万别直接赋值,要的是merge,不是replace。

来,看看正确打开方式:

# 后端或前端累积状态
current_state = {}
async for delta in graph.astream(input_state, stream_mode="updates"):
    # delta 的结构是 {node_name: node_output}
    for node_name, node_update in delta.items():
        if node_name not in current_state:
            current_state[node_name] = {}
        # 安全合并:只更新收到的键
        current_state[node_name].update(node_update)
    
    # 现在 current_state 就是逐步构建的完整视图
    render_incremental(current_state)

在并行节点的场景下,建议给每个节点分配独立的UI卡片或者状态槽位。节点A的更新只更新卡片A,节点B的更新只更新卡片B,互不干扰。LangGraph保证updates是按节点完成顺序推送的,只要你的前端做好隔离,就不会有闪烁问题。如果你做的是日志流、实时通知中心,updates就是不二之选。

updates模式是精准制导导弹,只送干货。但前提是,你得学会自己“拼装”。它考验的不是框架,是你的状态管理基本功。

messages模式:消息粒度的实时对话体验

好,接下来这个模式,是离终端用户最近、最直观的一种流式体验,也是新手最感兴趣的一个——messages模式。stream_mode="messages"专门用于捕获LLM生成过程中的消息分片,通常是AIMessageChunk。当你调用一个大模型节点,并且模型本身支持流式输出时,LangGraph会把这个token一个一个地吐出来。前端收到后,就能呈现出那种丝滑的“打字机效果”,让用户感觉AI正在实时思考、实时作答。

注意,这个模式是专款专用的。它只关注Message对象,不关心你的State里其他字段变了没有。换句话说,它是LLM的专属话筒,其他节点别想抢麦。

token1

token2

token3

LLM节点生成中

用户屏幕

痛点方面,第一个大坑就是:把messages当成万能流。很多新手在Graph里加了一个工具节点,然后开了messages模式,满心期待地等着看工具执行的实时输出。结果呢?工具执行那几秒,前端一片空白!因为工具节点不产生AIMessageChunk,messages模式自然就没东西吐。用户看着白屏干着急,还以为程序卡死了,疯狂点重试,甚至给你打差评。

第二个坑是不知道如何处理混合内容。LLM先流式输出一段思考过程,然后突然来一波工具调用,新手傻傻分不清楚,把工具调用的JSON也直接渲染成文字,页面顿时出现一堆乱码,用户体验直接归零。第三个坑是忽略metadata。messages流通常会附带metadata,告诉你这条消息是从哪个节点来的。如果你忽略了这个信息,在多Agent系统里,你就不知道这句话是规划Agent说的,还是执行Agent说的,对话气泡的颜色和头像都没法区分,整个界面一团糟。

看看这段典型的错误示范:

# 错误示范:在工具节点执行时傻等messages
async for msg in graph.astream(
    {"messages": [HumanMessage(content="查北京天气")]},
    stream_mode="messages"
):
    # LLM思考时有token,但调用天气API时这里完全没动静!
    print(msg.content)  
    # 用户等了5秒没反应,以为挂了,直接刷新页面

怎么解决?messages模式要配合场景使用。纯对话场景,用它做主输出通道没毛病。但一旦涉及工具调用、代码执行、外部API请求,一定要给它配个助手。最常用的是混合模式,比如同时开启messages和updates,或者用values来兜底状态。

正确姿势如下:

# 混合模式:既看LLM打字,也感知状态变化
async for chunk in graph.astream(
    input_state,
    stream_mode=["messages", "updates"]
):
    if hasattr(chunk, "content") and chunk.content is not None:
        # 这是 AIMessageChunk,实时打字机
        append_streaming_text(chunk.content)
    elif isinstance(chunk, dict):
        # 这是 updates,可能包含工具执行状态
        if "tool_node" in chunk:
            show_tool_loading_spinner(chunk["tool_node"])

另外,一定要善用metadata来区分消息来源:

async for chunk, metadata in graph.astream(..., stream_mode="messages"):
    node_name = metadata.get("langgraph_node", "unknown")
    # 不同节点用不同颜色气泡展示
    render_bubble(chunk.content, color=agent_color_map.get(node_name, "blue"))

messages是用户体验的门面担当,打字机效果确实很酷。但记得给它配个助理处理杂活,别让它单打独斗。没有状态感知的messages流,就像没有字幕的电影,看得人云里雾里。

custom模式:自定义流的自由编排

前面三种模式,是LangGraph给你准备好的套餐。但有时候,套餐吃不饱,你想自己下厨。这时候,custom模式就登场了。stream_mode="custom"允许你在Graph的节点内部,向外推送完全自定义格式的数据。比如你想推送一个进度百分比{"progress": 45},或者一张中间生成的图片URL,甚至一段语音的Base64编码,都可以走这个通道。它把流式的定义权完全交还给了开发者。任何你觉得值得实时推送给前端的数据,都能走custom通道。

这在复杂AI工作流里特别有用。比如你做了一篇长文的深度分析,整个过程可能要两分钟。如果只用values,前端只能等两分钟看到一个结果,用户早跑了;如果用了custom,你可以每完成一个阶段就推送一次进度,让用户心里有底,体验感直接拉满。

进度30

图片URL

完成通知

自定义节点

前端进度条

前端图片框

前端提示音

但一听到“自定义”,很多新手本能地退缩。觉得这是高级玩家的玩具,自己hold不住。有些同学硬着头皮用,结果节点里yield的数据格式随心所欲:有时候是字符串“处理中…”,有时候是字典{"progress": 30},有时候又是个列表。前端同学拿到这种数据,哭晕在工位,写了一大堆if-else来判断类型,代码臭得不能再臭。

还有一个坑是混淆custom和updates。有人在custom通道里塞State的变更数据,结果破坏了LangGraph本身的状态管理语义。custom应该是业务层面的通知,是“我给你看个东西”,而不是“我要修改你的核心数据”。如果你把状态更新和自定义事件混在一起,后面调试的时候绝对会抓狂。

看看这段让人头皮发麻的代码:

# 节点里的随心所欲
def heavy_analysis_node(state: State):
    yield "开始处理..."  # 字符串?
    yield {"progress": 30}  # 字典?
    yield [1, 2, 3]  # 列表?
    # 前端:这我该怎么解析?
    return {"result": "done"}

解决之道在于:契约先行。在写第一行业务代码之前,先把custom事件的Schema定下来。团队里前后端对齐:我们所有的custom事件,统一走这个结构。type字段表示事件类型,payload字段放具体数据,node字段标明来源。前后端按契约办事,世界就清净了。

来,看看标准化的写法:

# 定义统一自定义事件Schema
def make_event(event_type: str, payload: dict, node: str):
    return {
        "type": event_type,
        "payload": payload,
        "node": node
    }

def heavy_analysis_node(state: State):
    yield make_event("progress", {"percent": 10, "stage": "读取数据"}, "analysis")
    # ... 执行耗时操作 ...
    yield make_event("progress", {"percent": 50, "stage": "深度分析"}, "analysis")
    # ... 生成中间图表 ...
    yield make_event("asset", {"image_url": "/tmp/chart.png"}, "analysis")
    yield make_event("progress", {"percent": 100, "stage": "完成"}, "analysis")
    
    return {"analysis_result": "completed"}

# 前端解析稳如老狗
async for event in graph.astream(..., stream_mode="custom"):
    if event["type"] == "progress":
        update_progress_bar(event["payload"]["percent"], event["payload"]["stage"])
    elif event["type"] == "asset":
        show_generated_image(event["payload"]["image_url"])

custom是留给架构师的画笔。画得好是艺术品,画不好是鬼画符。定好规矩,它就是你手里最灵活的一张牌。进度条、多媒体流、中间产物展示,统统都能搞定。

实战选型:四种模式的黄金抉择法则

学到这儿,你肯定想问:大仙,我能不能全都要?LangGraph确实支持传入列表,比如stream_mode=["values", "messages"]。但混合使用就像调火锅底料,比例不对会翻车。这一节咱们聊聊架构层面的选型思维,这才是从“会写代码”到“会设计系统”的跨越。

先说说最常见的坑:选择困难症导致的过度混合。有些同学为了保险,同时开启values、messages、updates、custom四种模式。结果前端逻辑写成了一团意大利面。数据来了,先判断是dict还是Message,再判断有没有content字段,再判断是不是来自特定节点,无穷无尽的if-else。维护这种代码,比维护祖传屎山还痛苦,下一个接手的同事可能想顺着网线过来揍你。

还有些同学该用custom的时候硬用values,在State里塞一堆临时进度字段。这不仅污染了核心业务状态,还导致checkpoint变得臃肿,每次持久化都慢半拍。反过来,该用updates的时候用了values,带宽和序列化开销直接把服务拖垮。

看看这段反面教材:

# 灾难级混合使用,前端解析地狱
async for chunk in graph.astream(
    input_state,
    stream_mode=["values", "messages", "updates", "custom"]
):
    if hasattr(chunk, "content"):
        # 可能是 messages
        pass
    elif isinstance(chunk, dict) and "type" in chunk:
        # 是 custom 还是 values?傻傻分不清
        pass
    elif isinstance(chunk, dict):
        # 可能是 updates 或 values
        pass
    # 维护这段代码的人已经提桶跑路

那怎么选?记住这张决策树。

需要流式输出

内容来自LLM

messages模式

需要完整上下文

values模式

需要自定义数据

custom模式

updates模式

第一问:用户最需要看到什么?

  • 如果是LLM实时生成的文字,用户体验优先 → messages。
  • 如果是业务流程的进度条、中间产物,需要丰富交互 → custom。
  • 如果是完整的数据看板、全局上下文,管理后台类需求 → values。
  • 如果是高效的状态同步、日志流,对带宽敏感 → updates。

第二问:你的State有多大?

  • State很小(<10KB),且前端需要完整视图 → 大胆用values。
  • State很大(>100KB),或者包含大量历史上下文 → 优先updates,否则带宽和序列化开销会让你哭。
  • 不在乎State,只在乎模型输出 → messages独美,别给自己加戏。

第三问:要不要混合?
我的建议是:非必要不混合。如果必须混合,最多两种。

  • 标准聊天机器人:messages + updates。messages负责打字机,updates负责感知工具执行和状态变更。
  • 复杂Agent平台:updates + custom。updates同步核心状态,custom推送进度和中间资产。
  • 监控大盘:values单独使用就够了,别折腾。

最后,无论你选哪种,都建议在团队里封装一个StreamAdapter,把LangGraph的底层流式事件翻译成前端友好的统一事件。别让前端同学直接面对LangGraph的原始流,那是后端该干的活。

class StreamAdapter:
    def __init__(self):
        self.state = {}
    
    async def consume(self, graph, input_state, config):
        async for chunk in graph.astream(
            input_state, 
            config,
            stream_mode=["updates", "messages", "custom"]
        ):
            # 统一分发,前端只需要监听三种事件
            if hasattr(chunk, "content"):
                yield {"event": "token", "data": chunk.content}
            elif isinstance(chunk, dict) and "type" in chunk:
                yield {"event": "custom", "data": chunk}
            elif isinstance(chunk, dict):
                self.state.update(chunk)
                yield {"event": "state", "data": self.state}

流式模式没有高低之分,只有合不合适。好的架构师不是全都要,而是知道什么时候要什么。把这四种模式的脾气摸透了,你的LangGraph应用才能真正做到“行云流水”。

写在最后

好啦,咱们今天把LangGraph的四种流式处理模式从头到尾捋了一遍。values给你上帝视角,updates给你精准打击,messages给你用户口碑,custom给你无限可能。这四个家伙就像你工具箱里的四把螺丝刀,十字的、一字的、电动的、万用的,场景对了,拧螺丝就是一种享受;场景错了,再好的螺丝刀也只能干瞪眼。

我知道,看着这些模式的时候,你可能还是有点晕。没关系,我刚学LangGraph那会儿,对着stream_mode参数也是发呆半小时。但编程这事儿,不就是一个从“看不懂”到“手到擒来”的过程吗?先把这篇文章收藏了,下次写代码的时候翻出来对照着选型,写错三次,你就彻底记住了。调试流式代码确实磨人,但每次看到前端丝滑的打字机效果,那种成就感是实实在在的。

编程之路从来不易,但每一步成长都算数。保持好奇,持续动手,你也能成为那个在用户面前云淡风轻、背后玩转流式的大神。学长在前面等你,咱们下回接着聊!

关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐