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 模式清空旧详情。该方案有两个核心痛点:
- 大表唯一索引成本高:在承载海量节点详情的大表上维护复合唯一索引,写入吞吐和锁竞争压力大;
- 违背运行期只追加的性能目标:运行中频繁 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.id且parentId相同的多条 rows 表示同一个展示节点的多次增量,读取时合并成一个节点;两条 row 都没有parentId时也视为同一个 parent。 mergeSignId已废弃:不再写入、不再读取、不再兼容旧合并语义。旧数据若依赖mergeSignId,展示异常可接受,迁移或回放另行处理。dispatchWorkFlow.responseChatItemId是必填运行参数,dispatch 不生成兜底 ID,也不查询MongoChatItem或MongoChatItemResponse判断是否重复。- 保存对话记录的新运行必须先走
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来自ChatSourceTypeEnum,time默认为当前时间。
保留的索引
当前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),其工作流程为:
- 一个 workflow 请求复用一个 writer;子 workflow、loop、parallel、toolcall 等共享该 writer。
record()接收本次要保存的 nodeResponses,补齐id/parentId、裁剪 dataset quote、计算childResponseCount,转成 flat rows(对应createChatItemResponseRows)。recordWithParent()只给没有parentId的 root child 补外层 parent;已有parentId的响应保持内部层级,避免破坏更细的层级结构。- writer 通过 promise queue(
writeQueue)串行化并发record,保证 Mongo_id顺序接近运行期写入顺序——因为子 workflow、parallel 分支可能并发调用同一个 writer,串行化后才能保证详情展示顺序稳定。 - 默认
batchSize = 5,达到阈值或 close 时 flush。 - flush 只执行
create(rowsWithTime, { ordered: true, session, ...writePrimary }),不做任何 delete/update/replace。 - 普通写入失败重试 3 次(
NODE_RESPONSE_WRITE_RETRY_TIMES = 3);仍失败则写 slim rows(只保留节点身份、名称、类型、父子关系、运行时间和消耗统计等关键字段的瘦身版本);slim 仍失败时丢弃本批详情 rows 并记录日志,不阻断主 workflow。 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):
- 一个 workflow 请求只创建一个 sink,内部复用同一个
WorkflowNodeResponseWriter。 - root workflow、child workflow、Agent、ToolCall、LoopRun、ParallelRun共享该 sink。
- 节点和 Agent adapter 只上交标准 nodeResponse,不直接操作 writer,也不直接发送
flowNodeResponseSSE。 - sink 为缺少 parentId 的响应补调用方显式传入的 parentId(
WorkflowNodeResponseInput.parentId),调用 writer 规范化并写入,再按请求可见性配置发布本次响应。 - writer 仍按
batchSize批量物理写 Mongo;“接收一个、返回一个”指每个逻辑 nodeResponse 都产生独立 SSE 事件,不要求每条 response 单独执行 Mongo create。 - sink不负责
RuntimeNodeResponseSummary、usage、计费、child count 或控制流判断;这些仍由 WorkflowQueue/Agent collector 在各自运行作用域内计算,避免跨作用域重复累计。 - 同
(id, parentId)的多条响应仍是 append-only 增量,sink 不去重、不覆盖、不改变数值字段的增量语义。
输出协议矩阵
- V2
stream=true, detail=true:可见 nodeResponse 逐条发送flowNodeResponse,客户端按(id, parentId)拼树;结束时不再发送完整 nodeResponse 数组。 - V1
stream=true, detail=true:运行期不发送单个 nodeResponse,结束时一次性发送flowResponses。 - V1/V2
stream=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),规则如下:
- 只处理存在
data.id的 rows(无 id 的 row 会被丢弃,无法参与合并)。 - 合并 identity 是
(data.id, data.parentId);parentId不存在时归一为同一个空值(getNodeResponseIdentityKey用\u0000分隔 id 与 parentId)。 - 同 identity 的多条 rows 合并为一个展示节点。
- 数值字段按增量累加,包括
runningTime(保留两位小数)、totalPoints、childResponseCount、tokens(含 input/output/toolCall/embedding/reRank/extension)等。 llmRequestIds去重合并。compressTextAgent、deepSearchResult这类结构化用量字段按现有规则累加。- 普通标量字段以后到的 incoming 为准。
childrenResponses递归按同一规则合并。- 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.id(getNanoid())。这类 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。 - 普通新运行检查 AI
dataId冲突。 - 普通新运行严格创建本轮 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.chatId和preparedRound.responseChatItemId,不能继续使用请求里的原始值。nodeResponseWriteConfig.persistToDb应等于preparedRound.shouldPersistChatRound。
普通新运行顺序
- 解析最终
chatId/responseChatItemId; NO_RECORD_HISTORIES直接返回不持久化结果,不占用生成锁;- 调用
tryStartGenerateChat占用生成锁;已有 generating 时抛ChatErrEnum.chatIsGenerating; - 校验已有 AI chat item 中不存在同
responseChatItemId; - 严格 create 本轮 Human + AI placeholder,二者使用同一个
dataId = responseChatItemId(prepareChatRound使用严格 create 而非 upsert); - 检查或创建失败时写生成状态
error并抛错; - 创建成功后才进入 workflow。
AI dataId 冲突检查口径
MongoChatItem.findOne( { appId, chatId, obj: ChatRoleEnum.AI, dataId: responseChatItemId }, 'dataId' );只检查 AI 的原因:
- Human/AI 同
dataId是新运行的正常结构; - 本轮 nodeResponse rows 归属于 AI
chatItemDataId; - 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,但发请求前应去重; - 删除接口支持 body
contentIds,body 优先;body 为空时兼容 querycontentId。OpenAPI 默认声明 body。
客户端约束
- 普通新运行一轮只生成一个
roundDataId,Human/AI 共用该值; - 交互继续复用上一条 AI
dataId,不是新一轮 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 交互继续:复用上一条 AI
dataId,不创建新 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),仅供参考