
简介这是一款基于workflow工作流的低代码开发平台源码主要面向希望快速搭建聊天机器人、RAG检索问答、Agent及多智能体应用的开发者技术栈涵盖LangGraph、Langchain、FastAPI与NextJS前后端分离便于二次扩展与自定义编排。包内共566个文件其中包含219个ts与146个tsx前端逻辑组件、111个py后端服务脚本辅以json/yaml配置、Dockerfile部署文件及样式资源压缩包整体约2.12MB目录结构清晰。平台特别新增MCP节点支持通过stdio与SSE两种传输模式连接多个MCP服务器并将MCP工具自动转换为LangChain工具可无缝集成到LangGraph工作流中显著增强Agent的工具调用与外部服务交互能力。已有144人学习浏览适合具备Python与前端基础、想深入理解低代码工作流引擎及模型上下文协议实践的开发者参考。1. workflow低代码平台能做什么聊天机器人、RAG、Agent 只差一张图把聊天机器人、RAG知识库、Agent和Multi-Agent应用塞进同一条workflow流水线是这个低代码平台最核心的价值。它基于LangGraph做图执行、Langchain做节点组件、FastAPI做接口层、NextJS做前端画布把「拖节点、连边、填配置」变成可运行的应用。过去写一个RAG问答要维护检索、拼接prompt、调模型三块代码Agent再套一层循环现在只需在画布上定义谁先跑、数据往哪传。这个方向适合正在做RAG知识库交付、想让Agent应用可维护的团队也适合把重复代码压缩成配置的个人开发者。它解决的问题不是「能不能跑」而是「跑起来之后能不能改、能不能让别人接手」。2. 技术选型拆解LangGraph、Langchain、FastAPI、NextJS 为什么是这套组合这套组合看上去是四个名字的堆叠实际上每一层分工明确LangGraph负责「图怎么走」Langchain负责「节点里用什么工具」FastAPI负责「外部怎么调」NextJS负责「人怎么编辑」。任何一个被替换都会破坏平衡把LangGraph换成普通函数调用循环和状态就要自己写把FastAPI换成同步框架SSE流式输出和并发就要绕路。选型前先理解这四层边界后面所有设计和排障都围绕它展开。2.1 LangGraph与Langchain的分工图编排归谁工具链归谁刚入门的人几乎都会问「LangGraph和Langchain的区别是什么」这也是我在画布设计评审时被问最多的一个问题。答案很简单Langchain是工具箱LangGraph是流水线。Langchain提供ChatOpenAI、ChatPromptTemplate、FAISS、Retriever、memory这些组件解决「调用哪个模型、用什么提示词、从哪个库检索」LangGraph提供StateGraph、node、edge、checkpoint这套图执行机制解决「谁先执行、结果往哪传、要不要循环、中断了怎么办」。平台里这个边界被严格写死在代码结构里节点内部使用Langchain组件节点之间的连接关系全部落在LangGraph的StateGraph上。带来的直接好处是替换模型只改节点config不碰图结构调整流程只改边不碰节点内部逻辑。如果反过来把Langchain的LCEL链当全局编排用一开始线性链还舒服一旦出现条件分支和循环就得自己在链外手写路由逻辑等于把LangGraph的活抢过来干。维度LangChainLangGraph定位组件库与工具链有状态图执行引擎核心对象LLM封装、Retriever、Prompt模板、MemoryStateGraph、Node、Edge、Checkpoint状态管理无靠链式传参TypedDict声明statereducer合并更新分支与循环弱LCEL只支持线性组合条件边、环、递归天然支持在平台里的角色节点内部逐个调用承载整张workflow的执行LangGraph里有个容易踩的机制node必须是一个接收state并返回dict的callableLangchain的Runnable不能直接塞进去当node。常见做法是在节点注册表里写一层适配函数把Runnable的invoke结果映射到state上的指定key。一个LLM节点内部是prompt | llm的LCEL链节点函数负责取state里的question调用链把结果写到answer。这一层适配逻辑收敛在节点工厂里画布上新增一种节点类型只需要注册一个factory。选LangGraph而不是自己写一个有向图引擎核心原因是它把复杂场景做得很稳。条件路由用add_conditional_edges循环靠图的环结构持久化靠checkpointer。低代码平台最怕引擎能力不够业务方提出「先检索、判断要不要追问、再生成」这种分支需求如果引擎不支持条件边就只能退回写死代码背离低代码初衷。2.2 FastAPI做执行层异步、SSE流式与动态编译接口FastAPI是这个平台的workflow执行层。前端画布保存的是JSON定义真正跑起来的是后端编译出来的LangGraph图编译、执行、流式返回都通过FastAPI暴露。选它而不是Flask最直接的原因是异步LangGraph的ainvoke和astream都是异步接口聊天场景要的是token边生成边返回FastAPI的StreamingResponse能直接把astream的事件推成SSE。Pydantic的模型校验在这类平台里是刚需。前端画布拼出的JSON常有字段类型问题top_k传成字符串、edges里少了source字段用手写校验这层代码会膨胀得很快。FastAPI里定义WorkflowDefinition和RunRequest两个模型请求进来先校验再编译错误信息能精确到字段。这比运行到一半报TypeError要友好得多。我一般把FastAPI项目目录结构固定成四层api/routes放路由、services放业务逻辑、core放workflow编译器和节点工厂、models放Pydantic模型。目录清楚以后加节点类型不需要到处找代码。接口层面画布保存走REST执行和流式走SSE管理端实时日志加WebSocket三条通道各管各的。最小接口集是三个POST /v1/workflows保存定义、POST /v1/workflows/{id}/run同步执行、POST /v1/workflows/{id}/stream流式执行。同步接口给测试脚本用流式接口给聊天前端用。run接口一定要接收thread_id它是LangGraph按会话隔离状态的钥匙后面讲并发串号时会再展开。2.3 NextJS做前端为什么画布选了NextJS而不是Vite SPA画布选NextJS而不是Vite搭的纯React SPA很多人觉得小题大做。其实低代码平台的前端不只有画布还承担编辑、预览、运行时状态展示三件事。画布本身用xyflow/reactReact Flow渲染节点和边NextJS的App Router在这里的价值是API Route可以做一个BFF层把后端SSE转发给浏览器顺带处理鉴权和参数拼装。选型时我对比过Vite方案如果纯内部工具Vite完全够甚至冷启动更快。但一旦要让非技术同事通过浏览器编辑workflow、保存发布、看运行日志就需要SSR页面、路由权限、服务端转发这些能力。NextJS内置了这些不用再自己搭一层Node服务。代价是构建心智比Vite重所以我的建议是平台要对外交付就选NextJS纯内部研发自用可以退到Vite。前端画布的核心是把节点类型映射成React组件。内置节点至少有输入、检索、LLM、Agent、条件分支、结束。每种节点对应一个配置面板配置面板输出的字段直接写回节点config。React Flow的useNodesState和useEdgesState管理画布状态保存时把nodes和edges序列化成workflow JSON发给后端运行时后端通过SSE推送节点执行事件前端根据事件把正在执行的节点高亮。这一步做出来产品才算有「低代码」的样子。切换框架的边界也很清楚如果画布需要深度定制节点交互、需要多人实时协作编辑Vite的灵活性反而更好如果重点是交付和部署NextJS的集成度更省心。3. workflow的数据模型与执行原理从画布JSON到LangGraph图3.1 节点、边、状态workflow JSON的数据结构画布上的一切最终落到三样东西节点、边、状态。节点描述「做什么」边描述「谁到谁」状态描述「数据长什么样」。一个workflow JSON的核心字段如下字段类型说明idstring全局唯一对应画布节点idtypestring节点类型对应节点注册表里的factoryconfigobject节点参数如模型名、top_k、prompt模板sourcestring边的起点节点idtargetstring边的终点节点idconditionobject条件边才有的路由判断配置state_schemaobject声明每个状态字段的名称和类型以最简单的LLM链为例画布保存下来的JSON长这样{ version: 1, nodes: [ { id: node_input, type: input, config: {} }, { id: node_llm, type: llm, config: { model: gpt-4o-mini, temperature: 0.2, prompt: 把这段文本翻译成中文{text} } } ], edges: [ { source: node_input, target: node_llm } ], state_schema: { text: { type: string }, answer: { type: string } } }这份JSON是前后端契约。nodes里的id必须和edges里的source、target对应画布保存时不要自己重新生成id否则运行时事件无法回映射到前端节点。config里的prompt用{text}引用状态字段节点工厂在编译时把字段名和state绑定运行时不解析模板变量提前绑定能省掉运行时开销。状态字段的设计决定图能不能跑通。LangGraph要求状态是TypedDict节点函数读到的state只包含state_schema声明过的字段节点返回的key也必须落在这些字段里。常见的翻车是画布上加了节点但忘了把输出字段加进state_schema编译不报错运行时报KeyError或者结果被静默丢弃。我在平台里加了一个保存前校验遍历每个节点的输入输出声明检查是否都能在state_schema里找到。这样问题在画布编辑时就暴露而不是等用户调试到半夜。3.2 动态编译后端如何把workflow定义变成可执行图后端拿到JSON后要把它编译成LangGraph的StateGraph实例。关键机制是节点注册表每种type对应一个工厂函数工厂从config读出参数返回一个接收state并返回dict的callable。编译时按nodes逐个add_node按edges逐条add_edge如果边带condition就add_conditional_edges。from langgraph.graph import StateGraph, END from typing import TypedDict, Dict, Any class GraphState(TypedDict): text: str answer: str def make_llm_node(llm, prompt_template: str): 工厂函数根据config构建一个LangGraph节点callable async def llm_node(state: GraphState) - Dict[str, Any]: # state是当前图的全部状态从这里取上游写入的字段 text state[text] # LangChain组件在这里被调用节点内部不需要关心图结构 response await llm.ainvoke(prompt_template.format(texttext)) # 返回值里只能包含state_schema声明过的key return {answer: response.content} return llm_node def compile_workflow(workflow: dict) - Any: builder StateGraph(GraphState) for n in workflow[nodes]: # registry.get(n[type]) 返回工厂工厂根据n[config]创建节点 node_fn registry.get(n[type]).create(n[config]) builder.add_node(n[id], node_fn) for e in workflow[edges]: if e.get(condition): # 条件边需要额外提供一个路由函数返回目标节点id router make_router(e[condition]) targets {t: t for t in e[condition][targets]} builder.add_conditional_edges(e[source], router, targets) else: builder.add_edge(e[source], e[target]) # 最后要给图一个明确的结束点 builder.add_edge(workflow[nodes][-1][id], END) return builder.compile()逻辑说明节点函数是唯一的执行单元state参数由LangGraph注入返回值会被LangGraph合并进状态再传给下一个节点。make_llm_node是闭包llm和prompt_template在编译时固定运行时不受外部影响这是低代码平台必须做到的点同一份workflow定义在不同时间执行结果要一致。compile_workflow最后强制把最后一个节点连到END防止画布漏掉结束点导致图无法终止。参数说明add_conditional_edges需要传三个参数起点节点、路由函数、目标节点映射表路由函数返回的字符串必须出现在映射表里。实际项目中条件分支是最容易写错的地方映射表少一个keyLangGraph运行时会直接抛InvalidUpdateError。另外compile()默认recursion_limit是25Agent类workflow一次完整推理要跑几十步编译时我习惯显式传compile(..., {recursion_limit: 100})后面避坑章细说。3.3 画布与引擎同步REST保存、WebSocket推送与版本管理前端和后端之间有三条通道。第一条是REST保存画布把nodes和edges序列化后POST到/v1/workflows后端做结构校验和字段校验通过后保存JSON分配workflow_id。第二条是SSE执行运行时后端把每个节点的完成事件推给前端画布据此高亮节点、更新节点状态没有它用户只能干等最后结果。第三条是WebSocket日志广播运行日志和token消耗给调试用。版本管理我采用一个朴素但有效的策略workflow定义带version字段编辑操作永远在草稿版本上进行点「发布」后才生成新的正式版本。正式版本不可编辑回滚只是在版本列表里切换默认版本。这个策略治好了很多次「谁改了我的图」的纠纷每个版本记录编辑者、时间、diff摘要出问题随时切回上一版后悔药是现成的。提示发布版本和草稿版本必须物理隔离。发布时复制一份当前草稿而不是直接更新正式版本否则线上正在跑的流程会被编辑中的半成品影响。4. 落地复现用这套workflow平台跑通一个RAG聊天机器人4.1 项目目录结构与后端依赖准备如果从零开始搭这套平台后端目录我建议固定成下面这个结构把「节点工厂」「编译器」「API」三块拆开加新节点类型不用动路由。app/ main.py # FastAPI入口挂路由和中间件 core/ registry.py # 节点注册表type - factory compiler.py # workflow JSON - LangGraph图 checkpointer.py # Postgres/SQLite checkpointer models/ workflow.py # Pydantic模型WorkflowDefinition、RunRequest services/ graph_service.py # 编译结果缓存、按id取图、执行 api/ routes/ workflows.py # /v1/workflows 路由 stream.py # SSE流式路由依赖上基础包是langchain、langgraph、langchain-openai、fastapi、uvicorn向量库我常用Chroma起步生产环境换pgvector。安装时注意langchain和langgraph是两个独立包很多新手以为装一个langchain就都有跑的时候from langgraph.graph import StateGraph直接ImportError。RAG需要的langchain-chroma、langchain-openai也是独立伴生包少一个都在导入时报错。4.2 定义RAG workflow输入、检索、生成三个节点一个最简RAG聊天机器人画布上只放三个节点输入节点接收用户问题检索节点从知识库取相关片段生成节点把片段和问题拼成prompt交给LLM。workflow JSON如下{ id: rag_demo, version: 1, state_schema: { question: {type: string}, context: {type: list}, answer: {type: string} }, nodes: [ {id: input, type: input, config: {}}, {id: retrieve, type: retriever, config: { collection: kb_001, top_k: 4, score_threshold: 0.3 }}, {id: generate, type: llm, config: { model: gpt-4o-mini, temperature: 0.2, prompt: 基于以下资料回答问题资料{context}\n问题{question}} } ], edges: [ {source: input, target: retrieve}, {source: retrieve, target: generate} ] }几个关键配置。top_k不是越大越好知识库切片质量一般时4到6个片段已经能覆盖答案score_threshold是相似度下限设太高经常返回空设太低会把无关内容送进prompt0.3到0.4是常见起点。prompt里的{context}和{question}必须和state_schema里的字段名一致否则节点工厂格式化时直接KeyError。知识库这一层对应RAG知识库的落地形态本质是collection名加embedding配置在平台里做成下拉选项用户不需要碰代码。4.3 FastAPI执行接口同步返回与SSE流式各一份后端执行接口保留两份同步接口给测试和批处理流式接口给聊天前端。两份都收thread_id这是会话隔离的钥匙。from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel import json, uuid app FastAPI() class RunRequest(BaseModel): inputs: dict # 例如 {question: 什么是向量数据库} thread_id: str None # 不传就自动生成保证每次会话独立 app.post(/v1/workflows/{workflow_id}/run) async def run_workflow(workflow_id: str, req: RunRequest): graph graph_service.get(workflow_id) # 从编译缓存取图 if graph is None: raise HTTPException(404, workflow not found) config {configurable: {thread_id: req.thread_id or uuid.uuid4().hex}} result await graph.ainvoke(req.inputs, configconfig) return result app.post(/v1/workflows/{workflow_id}/stream) async def stream_workflow(workflow_id: str, req: RunRequest): graph graph_service.get(workflow_id) if graph is None: raise HTTPException(404, workflow not found) config {configurable: {thread_id: req.thread_id or uuid.uuid4().hex}} async def event_generator(): # stream_modeupdates 每个节点完成时推一个事件 async for event in graph.astream(req.inputs, configconfig, stream_modeupdates): yield fdata: {json.dumps(event, ensure_asciiFalse)}\n\n return StreamingResponse(event_generator(), media_typetext/event-stream)逻辑说明run接口用ainvoke跑完整张图等全部节点结束返回最终state适合脚本和单测。stream接口用astream加stream_modeupdates每个节点跑完就往前端推一个事件前端拿到事件后高亮对应节点聊天框也能按节点分批显示中间结果。thread_id被放进config.configurableLangGraph的checkpointer按它区分会话传错或漏传就会出现跨用户串状态。参数说明stream_mode有多个取值values每次state更新都推全量stateupdates只推节点返回的增量。聊天场景用updates流量更小如果想做打字机效果需要在LLM节点内部把token逐个yield出去这时用stream_modemessages并让节点内部是astream的LCEL链。平台里两种模式都暴露简单场景用updates效果要求高的场景用messages后者对节点工厂的封装要求更高因为要处理token级事件的格式统一。4.4 NextJS画布上的配置与运行前端画布的最小可用实现是左侧节点面板、中间画布、右侧配置面板。核心状态用React Flow托管保存时把nodes和edges转成workflow JSONuse client; import { useCallback } from react; import { ReactFlow, useNodesState, useEdgesState, addEdge, Background, Controls, } from xyflow/react; import xyflow/react/dist/style.css; import { LLMNode, RetrieverNode, InputNode } from ./nodes; const nodeTypes { input: InputNode, retriever: RetrieverNode, llm: LLMNode }; export default function FlowCanvas({ onSave }: { onSave: (wf: object) void }) { // initialNodes/initialEdges 由props或接口加载这里省略初始化 const [nodes, setNodes, onNodesChange] useNodesState([]); const [edges, setEdges, onEdgesChange] useEdgesState([]); const onConnect useCallback( (conn) setEdges((eds) addEdge(conn, eds)), [setEdges] ); const handleSave () { // 序列化为workflow JSON交给后端校验和编译 const workflow { nodes: nodes.map(n ({ id: n.id, type: n.type, config: n.data?.config ?? {} })), edges: edges.map(e ({ source: e.source, target: e.target })), state_schema: inferStateSchema(nodes, edges), // 前端粗校验后端精校验 }; onSave(workflow); }; return ( div style{{ width: 100%, height: 80vh }} ReactFlow nodes{nodes} edges{edges} nodeTypes{nodeTypes} onNodesChange{onNodesChange} onEdgesChange{onEdgesChange} onConnect{onConnect} fitView Background / Controls / /ReactFlow button onClick{handleSave}保存并发布/button /div ); }逻辑说明每个节点组件接收React Flow的data渲染配置表单onSave把画布状态序列化成后端要求的JSON格式字段名必须和WorkflowDefinition对齐。inferStateSchema是前端辅助函数根据节点类型推导需要的state字段推导不出来就弹窗让用户手填避免所有状态都由后端猜。运行时高亮用的是后端SSE事件前端用EventSource或fetch读流根据事件里的节点id把对应节点样式改成高亮。这里最关键的约定是SSE事件里的节点id必须和画布nodes[].id完全一致。参数说明nodeTypes是React Flow的关键映射必须把画布里的type字符串映射到实际React组件漏掉一个类型画布上就渲染成空白节点。fitView让图自动缩放居中多人协作或大图场景建议关掉自动fit否则用户刚拖到角落的视角会被重置。保存按钮只是草稿保存发布动作要走单独的接口切换正式版本。4.5 跑通后的验证测hit rate、调参数、看traceworkflow发布后先用curl把同步接口打一遍确认基本流程通顺curl -X POST http://localhost:8000/v1/workflows/rag_demo/run \ -H Content-Type: application/json \ -d {thread_id: test_001, inputs: {question: 什么是向量数据库}}通了之后不要急着调生成效果先分开看两段。检索这一段直接影响答案质量指标是hit rate和MRR。把验证集里的每个问题跑一遍检索节点看标准答案对应的文档片段是否出现在top_k结果里。hit rate低优先做三件事知识库切片chunk_size从默认1000降到400到500overlap设50换更适配领域的embedding模型在检索节点前加一个query改写节点把口语问题改写成适合检索的表达。生成这一段主要调两个参数temperature降到0.2以下减少编造prompt模板里明确写「资料不足以回答时直接说不知道」。这两步做完RAG聊天机器人才算能交付而不是只在演示数据上好看。过程中配合看后端日志里每个节点的耗时和token数如果检索占了大头优先优化向量库索引如果生成占大头考虑换更快的模型。RAG效果优化很多是体感玄学唯一可靠的方法是留一份标注好的验证集每次改动跑一遍对比hit rate和回答质量。5. 避坑与排查Agent与Multi-Agent从画布落到生产的高频问题RAG跑通只是第一步Agent和Multi-Agent才是workflow平台真正的试金石。这两个场景的坑比RAG多得多而且不少坑不在代码逻辑上而在对LangGraph运行机制的误解上。这章先说Agent怎么进workflow再把落地中反复踩的五个高频问题按「现象、原因、解决」列清楚。5.1 Agent作为workflow节点的三种落地方式把Agent放进workflow常见做法有三种。第一种是内置Agent节点把ReAct循环封装成一个节点类型节点内部维护Agent状态workflow里其他节点不感知它内部跑了多少步。第二种是子图用一个已经compile好的LangGraph图作为节点直接add_node(agent_sub, compiled_agent)等于把整个Agent塞进workflow的一环。第三种是Supervisor编排一个supervisor节点负责决定下一步把控制权交给哪个子Agent这是Multi-Agent的标准形态。三种做法对应不同复杂度。简单工具调用用第一种一个节点搞定需要「先规划再执行」的场景用第二种多个专家角色协作用第三种。这里特别提一下agentic rag——很多人问它和普通RAG的区别我的理解是普通RAG是「检索一次、生成一次」agentic rag把检索变成Agent的一个工具Agent根据问题决定检索几次、检索哪几个库、回答不了就换一种问法再检。在低代码平台上agentic rag通常就是「Agent节点检索工具」的组合画布上多配一个工具不用改框架。5.2 Multi-Agent编排在低代码平台上怎么做Multi-Agent的编排模式在画布上最终落到三种结构。Supervisor模式一个中心节点接收任务、分发给多个worker节点、汇总结果层级模式顶层supervisor只管下一层supervisor逐级下放对等模式Agent之间互相传递消息没有中心调度。从低代码平台的实现成本看Supervisor和层级模式最容易做成模板对等模式要处理消息路由和死锁不建议塞进画布做成可视化节点。平台里做Multi-Agent最常用的配置是一个supervisor节点配置它有哪些worker可选、每个worker负责什么worker本身是一个子图或内置Agent节点。运行时supervisor每轮决定调哪个workerworker完成后把结果写回statesupervisor再决定下一步。用户看到的画布只有一层但跑起来是两级图嵌套这对编译和trace都有要求画布上需要能展开子图看到内部否则一报错用户完全不知道是哪一层的问题。5.3 高频坑清单状态合并、检索命中、流式缓冲、死循环、并发串号坑一节点的返回值把整个state覆盖了。现象是画布上两个节点都往context字段写后执行的节点把前一个的结果整个顶掉生成节点只看到最后一份。原因是LangGraph的state默认是普通dict合并两个节点写同一个key没有合并逻辑。解决在state_schema里对这类字段声明reducer比如context用Annotated[list, add]让每次写入变成追加而不是覆盖或者约定每个节点只写自己的独立字段最后用一个聚合节点合并。坑二检索节点返回空结果答案全靠LLM脑补。现象是hit rate在验证集上只有三四成问题换个说法就检索不到。原因多半在切片粒度、embedding模型和query三处模型换了、chunk也改了效果还是玄学最后发现是query里有大量口语表述和库里书面文档相似度天然低。解决检索节点前加query改写节点用LLM把口语问题改写成检索式表达同时做混合检索BM25和向量召回各拿一批用RRF融合注意RRF的k参数默认60融合前两个列表的分数最好先归一化。multivector retriever也是一种解法一个小chunk配一个summary向量召回再取原始chunk适合文档碎片化严重的库。坑三SSE流式输出在前端一次性全量到达打字机效果死活做不出来。现象是后端日志里token一条条出来浏览器却等很久才渲染全部。原因几乎都在中间层缓冲Nginx默认缓冲SSE响应NextJS的Route Handler如果在转发前把整个流读完也会这样。解决FastAPI端直接用StreamingResponse不要先收集到list再返回Nginx里对该路径关掉proxy_bufferingNextJS的Route Handler用ReadableStream逐块转发不要await res.text()。这是血泪经验光调后端不动中间层问题会一直在。坑四Agent循环停不下来或者一到某一步就报RecursionError。现象是LangGraph抛错说超过recursion limit或者画布上同一个节点来回跑几十次。原因是Agent内部工具调用没收敛或者条件边的路由函数永远返回同一个目标。解决编译时显式配置{recursion_limit: 100}给单次运行加步数上限每个循环边配一个计数器字段超过阈值强制走ENDAgent的工具返回格式做一层清洗把非法JSON或空文本提前拦截不然Agent会不停重试同一把工具。坑五多用户并发时对话历史串号A用户的问题带着B用户的上下文。现象是同一个workflow实例在不同会话间共享了历史消息。原因是thread_id没传或用了同一个默认值checkpointer把不同会话存到了同一条thread上。解决每个会话在前端创建时分配唯一thread_id后端从请求头或token里解析不信任前端裸传生产环境把内存checkpointer换成Postgres实现LangGraph有对应的checkpoint包进程重启后thread状态也不丢。这个坑最阴险并发低测不出来一上线压力一大就现原形。6. 上线前的最后一公里性能验证、缓存与可观测性6.1 三个压测数字workflow平台上线前先压出三个数字首token延迟、端到端延迟、可支撑的并发数。首token延迟在RAG场景主要由检索耗时决定目标p95在1.5秒内端到端延迟受生成模型影响大p95按5秒卡线并发数要看LLM供应商配额和中间件一般从50路并发开始压。工具用locust就行脚本分别打/run和/stream两个接口/stream要模拟逐token消费而不是等全部完成否则压出来的数字完全不可信。6.2 缓存与版本回滚跑完压测再上缓存否则缓存会把问题盖住。最值钱的缓存是编译结果同一个workflow_id编译一次LangGraph图就够了后续请求直接复用我用一个简单dict加LRU容量设100个图够用。检索结果缓存要慎重知识库更新后缓存必须失效我见过团队给检索结果加Redis缓存知识库改了半个月用户还在读旧答案。如果要做建议缓存命中判断用embedding相似度而非完全匹配阈值0.95以上直接复用并在响应里带cache_hit字段方便排查。6.3 trace与日志最后是可观测性。LangSmith是省事的选项不想引入外部依赖就在FastAPI中间件里给每个请求生成trace_id在节点工厂里给每个节点记录开始时间、结束时间、token数、输入输出摘要汇总成一条JSON日志。日志字段里有trace_id和workflow_id出问题才能按一次完整请求串起来看。我自己的习惯是每个节点都留一条日志节点多了以后没有trace根本定位不了是哪一步在拖慢或报错。这套workflow低代码平台走到生产这一步最深的教训是低代码不是把复杂度消灭而是把复杂度移到框架里。画布上拖出来的每一根边背后都是一段要维护的图逻辑节点类型越收越紧平台才越稳。希望帮到你。本文还有配套的精品资源点击获取