news 2026/9/10 15:00:12

FastGPT Workflow 节点响应持久化改造:Append-Only 存储与交互恢复 NodeResponse ID 设计解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FastGPT Workflow 节点响应持久化改造:Append-Only 存储与交互恢复 NodeResponse ID 设计解析

FastGPT Workflow 节点响应持久化改造:Append-Only 存储与交互恢复 NodeResponse ID 设计解析

【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT

FastGPT 的 workflow 运行详情通过chat_item_responses集合平铺持久化,每条 row 的data即一个节点响应(nodeResponse)。本文以仓库内权威设计文档(node-response-append-only-interactive-id.md)为主线,结合 nodeResponseStorage.ts、nodeResponseSink.ts、mergeNode.ts 等实现源码,系统讲解 append-only 数据模型、读取时的增量合并算法、交互恢复场景下 nodeResponse ID 的复用规则,以及运行前preChatRound的职责边界。读完你将掌握 FastGPT 节点详情从“运行期可更新”迁移到“只追加 + 读取时折叠”的完整设计思路,以及交互恢复如何避免展示节点重复。

背景:从“可更新存储”到“只追加存储”的演进

workflow 运行详情通过chat_item_responses平铺保存。每条 row 的data是一个 nodeResponse,包含三个关键身份字段:

  • data.id:展示节点 ID,标识一个节点响应实例;
  • data.parentId:父展示节点 ID,读取时用于还原childrenResponses树形结构;
  • chatItemDataId:所属 AI chat item 的dataId,即本轮响应消息 ID。

早期设计依赖{ appId, chatId, chatItemDataId, 'data.id' }唯一索引,并在运行期先删除同data.id的旧 row 再写入新 row,或用 replace 模式清空旧详情。该方案有两个核心痛点:

  1. 大表唯一索引成本高:在承载海量节点详情的大表上维护复合唯一索引,写入吞吐和锁竞争压力大;
  2. 违背运行期只追加的性能目标:运行中频繁 delete/update 增加写放大,且并行 retry 时“删除旧 rows 再写入”的时序很难保证一致性。

因此当前权威方案将 nodeResponse 表调整为append-only:workflow 运行过程中只createrows,不更新、不删除。重复展示节点不再依赖数据库去重,而是通过读取时按(data.id, data.parentId)fold(折叠合并)来还原最终形态。该设计文档合并并替代了历史文档node-response-stream-persistence.md(其中的data.idunique 索引、运行期 delete 后 create、replace/append 模式、parallel retry 删除旧 rows 等描述已过时)以及旧版 append-only 讨论稿。

核心结论速览

设计文档沉淀的结论如下:

  • chat_item_responses运行期只追加 rows;对话删除、应用删除、过期清理等外部清理流程可以批量删除。
  • data.id不再是数据库唯一键,只表示前端展示节点身份。
  • 同一个data.idparentId相同的多条 rows 表示同一个展示节点的多次增量,读取时合并成一个节点;两条 row 都没有parentId时也视为同一个 parent。
  • mergeSignId已废弃:不再写入、不再读取、不再兼容旧合并语义。旧数据若依赖mergeSignId,展示异常可接受,迁移或回放另行处理。
  • dispatchWorkFlow.responseChatItemId是必填运行参数,dispatch 不生成兜底 ID,也不查询MongoChatItemMongoChatItemResponse判断是否重复。
  • 保存对话记录的新运行必须先走preChatRound,由业务入口完成最终chatId/responseChatItemId解析、生成锁、AI dataId 冲突检查和 Human/AI placeholder 预创建。
  • 普通新运行中 Human 和 AI 使用同一个roundDataId = responseChatItemId。Human/AI 同 dataId 是预期行为;同一个obj下重复 dataId 才是不合法语义。本轮运行前只阻塞 AI dataId 冲突。

数据模型与索引设计

row 结构

chat_item_responses的核心字段定义如下(对应 chatItemResponseSchema.ts):

type ChatItemResponseSchema = { teamId: ObjectId; appId: ObjectId; chatId: string; chatItemDataId: string; data: ChatHistoryItemResType; time: Date; };

在真实 Schema 中,appId字段注释说明了其历史物理字段名语义为sourceId(App 场景才是真实 appId),sourceType来自ChatSourceTypeEnumtime默认为当前时间。

保留的索引

当前chat_item_responses保留两个索引:

ChatItemResponseSchema.index({ appId: 1, chatId: 1, chatItemDataId: 1, _id: 1 }); ChatItemResponseSchema.index({ teamId: 1, time: -1 });

索引用途:

  • { appId, chatId, chatItemDataId, _id }:按 AI chat item 拉取完整 nodeResponse rows,并按_id: 1保持写入顺序(源码中复合索引包含_id,避免详情读取时额外排序);
  • { teamId, time: -1 }:过期清理或团队维度清理。

源码中还额外定义了一个{ sourceType, appId, chatId, chatItemDataId, _id }索引(带 TODO 注释,暂未全面检查操作故未加 sourceType 索引的完整方案),说明数据访问正在向 sourceType 维度演进。

明确不再创建的索引

ChatItemResponseSchema.index( { appId: 1, chatId: 1, chatItemDataId: 1, 'data.id': 1 }, { unique: true } );

chat_items当前保留普通索引:

ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }); ChatItemSchema.index({ appId: 1, chatId: 1, deleteTime: 1 }); ChatItemSchema.index({ appId: 1, chatId: 1, _id: -1 }); ChatItemSchema.index({ appId: 1, chatId: 1, obj: 1, _id: -1 });

{ appId, chatId, dataId }不能改成 unique,因为普通新运行中 Human 和 AI 会共享同一个dataId(同一轮消息的 Human/AI 记录同 ID)。如果后续 AI dataId 冲突检查需要优化,可以补普通索引{ appId, chatId, dataId, obj },但不加 unique

写入路径:WorkflowNodeResponseWriter

写入封装在WorkflowNodeResponseWriter(nodeResponseStorage.ts),其工作流程为:

  1. 一个 workflow 请求复用一个 writer;子 workflow、loop、parallel、toolcall 等共享该 writer。
  2. record()接收本次要保存的 nodeResponses,补齐id/parentId、裁剪 dataset quote、计算childResponseCount,转成 flat rows(对应createChatItemResponseRows)。
  3. recordWithParent()只给没有parentId的 root child 补外层 parent;已有parentId的响应保持内部层级,避免破坏更细的层级结构。
  4. writer 通过 promise queue(writeQueue)串行化并发record,保证 Mongo_id顺序接近运行期写入顺序——因为子 workflow、parallel 分支可能并发调用同一个 writer,串行化后才能保证详情展示顺序稳定。
  5. 默认batchSize = 5,达到阈值或 close 时 flush。
  6. flush 只执行create(rowsWithTime, { ordered: true, session, ...writePrimary }),不做任何 delete/update/replace。
  7. 普通写入失败重试 3 次NODE_RESPONSE_WRITE_RETRY_TIMES = 3);仍失败则写 slim rows(只保留节点身份、名称、类型、父子关系、运行时间和消耗统计等关键字段的瘦身版本);slim 仍失败时丢弃本批详情 rows 并记录日志,不阻断主 workflow
  8. saveChat需要的引用(citeCollectionIds)、错误数和根节点积分由 writer 在运行期维护 summary(summaryContributionsMap,按id + parentId覆盖,避免 retry/完成态重复累计);详情 rows 写库失败不影响这些摘要

值得注意的是,写入前不做 JSON/BSON 体积预估,BSON 大小、不可序列化字段等问题统一交给 Mongo 写入校验,失败后进入 retry/slim fallback,避免正常路径额外 CPU 与临时内存开销。flush 后会立即释放 buffer,降低运行期内存占用。

另外,数据集搜索节点(datasetSearchNode)的quoteList在入库前会被瘦身(slimQuoteListForStorage),只保留id/chunkIndex/datasetId/collectionId/sourceId/sourceName/score等引用关联、来源和分数元信息,移除 q/a 完整文本——因为完整 quote 体积很大,且详情展示只需要来源元信息,瘦身可降低单条 row 过大导致 Mongo 写失败的概率。

实时发布路径:WorkflowNodeResponseSink

NodeResponse 的持久化和实时发布统一由请求级WorkflowNodeResponseSink协调(nodeResponseSink.ts):

  1. 一个 workflow 请求只创建一个 sink,内部复用同一个WorkflowNodeResponseWriter
  2. root workflow、child workflow、Agent、ToolCall、LoopRun、ParallelRun共享该 sink
  3. 节点和 Agent adapter 只上交标准 nodeResponse,不直接操作 writer,也不直接发送flowNodeResponseSSE。
  4. sink 为缺少 parentId 的响应补调用方显式传入的 parentId(WorkflowNodeResponseInput.parentId),调用 writer 规范化并写入,再按请求可见性配置发布本次响应。
  5. writer 仍按batchSize批量物理写 Mongo;“接收一个、返回一个”指每个逻辑 nodeResponse 都产生独立 SSE 事件,不要求每条 response 单独执行 Mongo create。
  6. sink不负责RuntimeNodeResponseSummary、usage、计费、child count 或控制流判断;这些仍由 WorkflowQueue/Agent collector 在各自运行作用域内计算,避免跨作用域重复累计。
  7. (id, parentId)的多条响应仍是 append-only 增量,sink 不去重、不覆盖、不改变数值字段的增量语义。

输出协议矩阵

  • V2stream=true, detail=true:可见 nodeResponse 逐条发送flowNodeResponse,客户端按(id, parentId)拼树;结束时不再发送完整 nodeResponse 数组
  • V1stream=true, detail=true:运行期不发送单个 nodeResponse,结束时一次性发送flowResponses
  • V1/V2stream=false, detail=true:结束时在 JSONresponseData中一次性返回。
  • V2 Share 流式:完整 nodeResponse 逐条写库,对外先按 public node/field 规则过滤,再逐条发送;为保持pushResult2Remote原有回调契约,运行期间仍保留最终详情数组。

Share 可见性分层处理

Share 可见性必须分层处理,不能只依赖一个字段过滤函数:

  • responseAllData=false:sink 只发布 public node 类型和字段,并保留客户端拼树需要的id/parentId
  • Share workflow 内部始终保留回答中的引用 ID,writer 始终接收完整 nodeResponse;普通 API 保持retainDatasetCite原有语义。datasetquoteList入库时继续移除 q/a,只保留引用关联、来源和分数等元信息。
  • showCite:控制公开 nodeResponse 是否包含quoteList;关闭时 SSE 与非流式 JSON 都不返回quoteList,但不改写 SSE 回答文本,也不改变持久化数据。客户端没有 quoteList 时不展示引用;之后重新开启配置并刷新 Share,可以根据已保存的引用 ID 和 quote 元信息恢复展示。
  • showRunningStatus:控制flowNodeStatus/toolCall/toolParams/toolResponse等过程事件,不直接禁止引用展示依赖的 publicflowNodeResponse
  • showSkillReferences:继续由 Agent 输出链路控制,并受showRunningStatus约束。
  • showWholeResponse/showFullText/canDownloadSource:继续由前端能力和详情/引用/文件接口鉴权,sink 不替代这些权限检查。
  • 明确隐藏内部 workflow 的系统插件继续既不写入 child rows,也不发布 child 事件,只保留外层工具节点响应。

pushResult2Remote不属于本次 SSE 改造范围,继续使用运行期finalResponseData调用/shareAuth/finish,不增加延迟读库或回调协议变化。

运行期明确删除的行为

  • 不按data.iddelete 旧 rows;
  • 不做updateOne + upsert
  • 不做 replace 模式;
  • 不在持久化 buffer 中按data.id去重;
  • 不依赖data.idunique 索引;
  • 不为 retry 预生成 row_id做幂等;极低概率重复 create 产生的冗余 rows 由读取 fold 吸收。

persistToDb = false 的场景

persistToDb = false的 writer 不写 Mongo,只保留 summary 和可选内存详情(retainInMemory),适用于 debug、eval、临时运行等不保存历史的入口。这类入口仍必须给 dispatch 传随机responseChatItemId,只是该 ID 不参与数据库查重。

读取与合并:按 (data.id, parentId) 折叠增量

读取时先按 chat item 拉 rows:

MongoChatItemResponse.find( { appId, chatId, chatItemDataId }, { data: 1 } ).sort({ _id: 1 });

然后composeNodeResponseDetail()调用mergeNodeResponseDataByIdAndParent()做 fold(实现见 mergeNode.ts),规则如下:

  1. 只处理存在data.id的 rows(无 id 的 row 会被丢弃,无法参与合并)。
  2. 合并 identity 是(data.id, data.parentId)parentId不存在时归一为同一个空值(getNodeResponseIdentityKey\u0000分隔 id 与 parentId)。
  3. 同 identity 的多条 rows 合并为一个展示节点。
  4. 数值字段按增量累加,包括runningTime(保留两位小数)、totalPointschildResponseCount、tokens(含 input/output/toolCall/embedding/reRank/extension)等。
  5. llmRequestIds去重合并。
  6. compressTextAgentdeepSearchResult这类结构化用量字段按现有规则累加。
  7. 普通标量字段以后到的 incoming 为准
  8. childrenResponses递归按同一规则合并。
  9. child row 早于 parent row 到达时先作为临时 root,parent 到达后回收挂到childrenResponses(对应appendNodeResponseByParent的 orphan 回收逻辑)。

批量读取时使用mergeNodeResponseListByParent一次性挂树算法:先按id + parentId合并同层增量,再按 parentId 挂到childrenResponses,避免每条 row 递归扫描已构建的整棵树,在 loop/parallel 产生大量 rows 时把复杂度从接近 O(n²) 降到以线性扫描为主。

历史兼容边界

  • 新数据统一使用childrenResponses
  • pluginDetail/toolDetail/loopDetail/parallelDetail/loopRunDetail只作为历史 detail 字段读取和递归统计来源(getChildrenResponses会把这些旧字段与childrenResponses一并收集),不再作为新链路的通用写入结构;
  • chat_items.responseData已废弃。读取时如果独立表没有 rows,才回退旧内联详情(getChatItemResponseData的 fallback 逻辑),避免历史数据被空结果覆盖;
  • childTotalPoints不再对外保留(mergeNodeResponseDataByIdAndParent最后会stripChildTotalPoints),子节点积分展示由客户端基于childrenResponses现场计算。

NodeResponse ID 语义与交互恢复

普通节点:随机 ID

普通节点首次运行时生成随机data.idgetNanoid())。这类 ID 不需要可预测,也不需要数据库唯一约束。

交互恢复:复用暂停前 ID

交互恢复时需要复用暂停前记录的 nodeResponse ID,避免同一个展示节点在恢复后拆成两个节点:

const nodeResponseId = lastInteractive?.nodeResponseId && lastInteractive.entryNodeIds?.includes(node.nodeId) ? lastInteractive.nodeResponseId : getNanoid();

WorkflowInteractiveResponseType增加通用字段(定义于 interactive/type.ts):

nodeResponseId?: string;

该字段与entryNodeIds平级,表示触发本次暂停的当前 workflow 节点对应的 nodeResponsedata.id。同一时间只允许一个暂停模式,因此一个字符串即可表示当前恢复入口。

嵌套交互

嵌套交互继续沿用childrenResponse:每一层 interactive 都可以携带自己的nodeResponseId。例如 ToolCall 包装的子 workflow 暂停时:

{ type: 'toolChildrenInteractive', entryNodeIds: ['toolCallNodeId'], nodeResponseId: 'tool-call-node-response-id', params: { childrenResponse: { type: 'userInput', entryNodeIds: ['formNodeId'], nodeResponseId: 'form-node-response-id' }, toolParams: { toolCallId: 'call_xxx' } } }

恢复时:

  • ToolCall 节点复用toolChildrenInteractive.nodeResponseId
  • 子 workflow 复用childrenResponse.nodeResponseId
  • 新增 rows 继续写到同一条 AI chat item 的chatItemDataId下;
  • 读取时父 ToolCall 和子节点都按(data.id, parentId)合并,页面只展示一个 ToolCall 节点,用量和运行时间按增量累加。

LoopRun 恢复:iteration wrapper 的 ID 派生

LoopRun 的 iteration wrapper 是虚拟展示节点,ID 由 loopRun 父 nodeResponse ID 派生:

id = `${loopRunNodeResponseId}:iter:${iteration}`;

这样同一个 loop 节点在不同父作用域下运行不会因为node.nodeId + iteration冲突。交互恢复时,只要 loopRun 父节点复用interactive.nodeResponseId,同一轮 iteration wrapper 也会自然复用同一个data.id

LoopRun 暂停时会写一次当前 iteration wrapper,作为暂停前 child nodeResponses 的 parent,并把pendingIterationSummary存到 interactive params。恢复后同一个 wrapper ID 再写本次 resume 的增量统计。由于读取会累加数值字段,恢复后的 wrapper 必须只写本次 resume 片段的增量值,不能写暂停前后合并后的累计值——这是防止数值双算的关键约束(文档在“后续关注”中明确要求 LoopRun、ToolCall 等恢复场景必须持续保证写入的是本次运行片段增量)。

运行前 preChatRound:业务入口的职责边界

保存历史的新运行进入 workflow 前只调用preChatRound(实现见 prepare.ts)。它负责:

  • 解析最终chatId。空chatId自动生成随机 chatId(getNanoid(24));NO_RECORD_HISTORIES(即NO_RECORD_CHAT_ID = 'NO_RECORD_HISTORIES')表示不保存历史。
  • 解析最终responseChatItemId。请求未传时生成随机 ID。
  • 判断是否持久化 chat items 和 nodeResponse rows。
  • 持久化运行占用MongoChat.chatGenerateStatus = generating
  • 普通新运行检查 AIdataId冲突。
  • 普通新运行严格创建本轮 Human + AI placeholder。
  • 交互继续复用上一条 AI 的dataId,不创建新的 Human/AI placeholder。
  • 失败时如果已经占用生成状态,立刻置为error

返回值:

type PreChatRoundResult = { chatId: string; responseChatItemId: string; shouldPersistChatRound: boolean; shouldFinalizePreparedRound: boolean; };

持久化判断统一为:

const finalChatId = chatId === NO_RECORD_CHAT_ID ? chatId : chatId || getNanoid(24); const shouldPersistChatRound = finalChatId !== NO_RECORD_CHAT_ID;

入口后续必须使用preparedRound.chatIdpreparedRound.responseChatItemId,不能继续使用请求里的原始值。nodeResponseWriteConfig.persistToDb应等于preparedRound.shouldPersistChatRound

普通新运行顺序

  1. 解析最终chatId/responseChatItemId
  2. NO_RECORD_HISTORIES直接返回不持久化结果,不占用生成锁;
  3. 调用tryStartGenerateChat占用生成锁;已有 generating 时抛ChatErrEnum.chatIsGenerating
  4. 校验已有 AI chat item 中不存在同responseChatItemId
  5. 严格 create 本轮 Human + AI placeholder,二者使用同一个dataId = responseChatItemIdprepareChatRound使用严格 create 而非 upsert);
  6. 检查或创建失败时写生成状态error并抛错;
  7. 创建成功后才进入 workflow。

AI dataId 冲突检查口径

MongoChatItem.findOne( { appId, chatId, obj: ChatRoleEnum.AI, dataId: responseChatItemId }, 'dataId' );

只检查 AI 的原因:

  • Human/AI 同dataId是新运行的正常结构;
  • 本轮 nodeResponse rows 归属于 AIchatItemDataId
  • Human 历史重复不影响 nodeResponse append-only 的安全性,可以离线审计,不作为运行前阻塞条件。

preChatRound保持在业务入口,不下沉到dispatchWorkFlow。dispatch 被 debug、skill debug、MCP、outLink、定时触发等入口复用,不应该感知source/sourceName/shareId/outLinkUid/userContent等 chat 保存字段。此外,源码中stripUserContentFileUrls会清理用户消息里的文件临时 URL,只保留 file key 参与持久化,避免历史记录保存过期访问地址。

删除与清理

运行期 writer 不删除 nodeResponse rows。外部删除规则:

  • 删除整条对话或批量日志时,可以按chatId删除MongoChatItemResponse
  • 局部消息删除继续保持MongoChatItem软删除语义;
  • 新数据 Human/AI 同dataId,删除一轮消息时前端可以继续收集 Human 和 AI 的 dataId,但发请求前应去重
  • 删除接口支持 bodycontentIds,body 优先;body 为空时兼容 querycontentId。OpenAPI 默认声明 body。

客户端约束

  • 普通新运行一轮只生成一个roundDataId,Human/AI 共用该值;
  • 交互继续复用上一条 AIdataId,不是新一轮 Human/AI;
  • React list key不能只用dataId,因为 Human/AI 可能相同;应包含obj_id/id
  • 前端按dataId更新 AI 记录时需要带 AI 语义,避免命中同 ID Human;
  • nodeResponse SSE 合并和详情弹窗都应使用(id, parentId)合并语义,不再依赖mergeSignId

测试要求与回归保障

仓库为本次改造配套了完整的测试覆盖,核心测试文件包括 nodeResponseStorage.test.ts、nodeResponseSink.test.ts、index.persistence.test.ts 与 mergeNode.test.ts。

preChatRound相关:

  • 普通新运行成功:创建MongoChat、Human、AI placeholder;Human/AI 同dataId = responseChatItemId
  • 初始responseChatItemId命中已有 AI:直接抛错,不进入 workflow,不随机兜底;
  • 初始responseChatItemId只命中 Human:不按重复 ID 报错;
  • 生成锁冲突:抛ChatErrEnum.chatIsGenerating,不创建 placeholder;
  • placeholder 创建失败或重复校验失败:生成状态置为error
  • chatId:自动生成随机 chatId 并保存记录;
  • NO_RECORD_HISTORIES:不写 chat、不写 chat item、不占用生成锁,仍返回 dispatch 可用的随机responseChatItemId
  • 非 query 交互继续:复用上一条 AIdataId,不创建新 placeholder;找不到上一条 AI 时抛错;
  • interactive query:按新一轮创建 Human/AI placeholder;
  • finalizeChatRound能在 Human/AI 同 dataId 时按obj更新两条记录。

nodeResponse append-only 相关:

  • writer 写入只调用 create,不执行运行期 delete/update/replace;
  • buffer 中同data.id多条 rows 全部写入,不预去重;
  • retry 保留 3 次普通重试和 slim fallback,不依赖预生成_id
  • 读取按_id顺序 fold,同(data.id, parentId)合并为一个展示节点;
  • 相同data.id但不同parentId不合并;
  • parentId都不存在时视为同 parent 并合并;
  • 数值字段按增量累加,标量以后到为准,llmRequestIds去重;
  • child 先于 parent 到达时最终能挂回 parent;
  • mergeSignId不参与合并。

交互恢复相关:

  • 暂停时interactive.nodeResponseId写入当前节点data.id
  • 交互继续时恢复入口复用interactive.nodeResponseId,页面只展示一个节点;
  • ToolCall 子 workflow 暂停后继续:父 ToolCall 和子 workflow 分别复用对应层级nodeResponseId
  • LoopRun 暂停后继续:父 loopRun 复用interactive.nodeResponseId,iteration wrapper 使用${loopRunNodeResponseId}:iter:${iteration},恢复后只写本次片段增量,避免数值双算。

索引回归:

  • ChatItemResponseSchema不声明{ appId, chatId, chatItemDataId, 'data.id' }unique 索引;
  • 保留{ appId, chatId, chatItemDataId, _id }读取索引;
  • ChatItemSchema.index({ appId, chatId, dataId })保持普通索引,不改 unique;
  • 如新增{ appId, chatId, dataId, obj },也只能是普通索引。

后续关注与实施边界

设计文档同时记录了需要持续关注的运维与演进事项:

  • append-only 会增加 rows 数量,需要依赖对话删除、应用删除和过期清理控制表规模;
  • 历史mergeSignId数据不迁移,异常展示风险已接受;
  • 如果线上 AI dataId 冲突检查成为热点,再评估普通索引{ appId, chatId, dataId, obj }
  • LoopRun、ToolCall 等恢复场景必须持续保证写入的是本次运行片段增量,而不是累计值。

本次实施 TODO 清单(均已勾选完成)表明改造范围是:新增请求级WorkflowNodeResponseSink统一 writer 与 V2 SSE 发布;WorkflowQueue 的 root/child runtime 通过 sink 逐条发布 nodeResponse;Agent collector 移除 writer 依赖改为上交 sink;LoopRun/ParallelRun 虚拟任务节点改走 sink;系统插件内部 workflow 使用禁用 sink 的作用域保持隐藏语义;V1、非流式 JSON、Share public 过滤和引用/文件权限保持兼容;最终全量测试中全仓并发出现 4 个 20 秒超时,相关文件单独复跑全部通过。

总结

FastGPT 的 nodeResponse 持久化从“唯一索引 + 运行期更新”演进为“append-only 写入 + 读取时按 (data.id, parentId) 折叠”,本质上是把“写时去重”的复杂度转移到了“读时合并”,从而换取运行期稳定、低成本的只追加写入。配合请求级WorkflowNodeResponseSink统一持久化与 SSE 发布、preChatRound在业务入口完成 chat 语义校验与 placeholder 预创建、交互恢复时复用nodeResponseId保证展示节点唯一,这套设计同时解决了大表写入性能、并行运行写入顺序、Share 可见性分层以及暂停/恢复场景的展示一致性问题。对于需要深入理解 FastGPT workflow 运行链路或设计类似对话式 AI 工作流引擎持久化方案的开发者,建议进一步阅读 nodeResponseStorage.ts、nodeResponseSink.ts、mergeNode.ts 以及 prepare.ts 中对应的测试用例。

【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 14:59:59

Kafka事务与消费者隔离级别配置实战解析

1. 事故现场还原:当Kafka事务遇上非read_committed消费者那天凌晨三点,监控系统突然狂发告警——订单系统的库存扣减出现严重不一致。查询日志发现生产者明明成功提交了事务消息,但消费者端却丢失了30%的关键数据。这种诡异现象就像见鬼了一样…

作者头像 李华
网站建设 2026/9/10 14:58:54

XC7Z020-2CLG400I芯片解析与Zynq-7000开发实践

1. XC7Z020-2CLG400I芯片深度解析:Zynq-7000系列FPGA的工业级实践作为Xilinx(现属AMD)Zynq-7000系列中的明星型号,XC7Z020-2CLG400I以其独特的ARMFPGA架构在工业控制、边缘计算等领域持续发热。这款采用28nm工艺的SoC芯片&#xf…

作者头像 李华
网站建设 2026/9/10 14:57:55

数学可视化实用指南:awesome-math 资源地图

数学可视化实用指南:awesome-math 资源地图 【免费下载链接】awesome-math A curated list of awesome mathematics resources 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-math 公式越推越晕,图形看了一堆却抓不住重点&#xff1…

作者头像 李华
网站建设 2026/9/10 14:57:33

PLC技术解析:工业自动化核心与实战应用

1. PLC技术全景解析:从工业控制核心到现代自动化实践在工业自动化领域,可编程逻辑控制器(PLC)已经持续主导了半个多世纪。作为现代制造业的"神经中枢",这种专为工业环境设计的计算机控制系统,以其…

作者头像 李华
网站建设 2026/9/10 14:57:25

书匠策AI:论文写作的“全链路合伙人”,而非“代笔枪手”

官网:www.shujiangce.com | 微信 公众号 :书匠策AI 开篇:一个被误解的赛道 提到AI论文工具,很多人脑子里蹦出的第一个词是“代写”。这个刻板印象让整个赛道蒙上了一层灰色滤镜——仿佛AI与学术写作的结合,天然…

作者头像 李华
网站建设 2026/9/10 14:55:57

三维可视化拖拽式开发技术与数字孪生应用

1. 三维可视化技术演进趋势2026年,三维可视化领域正在经历一场前所未有的技术变革。作为一名长期从事可视化开发的工程师,我亲眼见证了从早期需要手写WebGL代码到如今拖拽式开发的演进历程。这种变革不仅仅是工具层面的改进,更是整个行业开发…

作者头像 李华