FEATURED · 精选文章

AI 树形会话分支管理方案

发布时间 / 2026/8/20 23:47:12
来源 / 创域科博编辑部
栏目 / 资讯中心
AI 树形会话分支管理方案 整体思路前端维护树形节点数组 activePath激活路径上下文主体存储在后端节点原始数据不被修改用于前端渲染完整历史记录摘要 / 浓缩产物为额外附属字段不污染原始节点内容两种触发总结逻辑①分叉节点触发向上总结②线性路径节点数到达阈值触发截断总结拼接给大模型的 prompt 上下文向上回溯到最近的总结锚点节点使用锚点的浓缩上下文 锚点之后节点摘要 / 原文混合 用户当前提问最近 N 条对话保留原文保证对话准确性更早内容做摘要压缩。交互流程前端发起提问 → 携带activePath激活路径 用户新问题 → 后端基于路径回溯组装上下文 → 请求大模型 → 返回回答后端新增对话子节点。逻辑图核心概念定义对话节点 Node树的最小单元每一轮用户提问 AI 回答生成一个节点节点可以拥有多个子节点代表对话分叉。activePath 激活路径节点 ID 数组从根节点到当前正在交互的叶子节点代表用户当前所处对话分支例如[rootId, n1, n3, n7]。总结锚点节点 (SummaryAnchor)打上锚标记的节点该节点保存向上回溯得到的浓缩完整上下文回溯上下文遇到锚点就停止向上遍历避免遍历全部历史。原始内容userContent、aiContent永远不变用于前端历史回放禁止改写。摘要内容附属字段对历史对话压缩总结仅用于 LLM 入参不影响历史展示。线性路径阈值单条无分叉的链路上累计节点达到阈值生成总结锚点最近原文保留条数锚点之后末尾 K 条节点不做摘要直接使用原始对话文本保障语义准确。关键数据结构对话节点结构体后端存储数据库 / 内存原始对话字段只读summary 相关为衍生压缩数据不修改 userContent/aiContentinterface DialogueNode { nodeId: string; // 节点唯一ID parentNodeId: string | null; // 父节点ID根节点为null childrenNodeIds: string[]; // 子节点ID列表多个代表对话分叉 // 原始内容【只读历史渲染使用绝不修改】 userContent: string; // 用户原始提问 aiContent: string; // AI原始返回回答 createTime: number; // 压缩摘要字段【衍生数据仅用于LLM请求】 isSummaryAnchor: boolean; // 是否为总结锚点节点 anchorSummary?: string; // 锚点该节点向上追溯到根的全部对话浓缩总结分叉/线性阈值触发 nodeSummary?: string; // 当前节点单轮对话的简短摘要非锚点节点使用 }会话顶层会话对象interface DialogueSession { sessionId: string; rootNodeId: string; allNodes: Recordstring, DialogueNode; // 总节点字典keynodeId维护全部节点替代数组快速查找 // 后端缓存前端传递的activePath也可以每次直接由前端传入后端不持久化activePath // activePath?: string[]; config: TreeDialogConfig; // 当前会话压缩配置 } // 系统可配置参数 interface TreeDialogConfig { linearThreshold: number; // 线性路径触发总结阈值例如12个节点 keepRawCount: number; // 末尾保留原文节点数量例如3最近3轮不走摘要 }说明使用Recordstring, DialogueNode字典存储全部节点里面是n1:{nodeID:n1}相比数组通过 nodeId 查找、找父、找子节点效率更高前端可以把字典转成数组渲染历史。前端请求入参前端调用对话接口不需要把全部节点传给后端只传递激活路径和用户问题interface ChatRequestDTO { sessionId: string; activePath: string[]; // [rootId, n1, n3, n7]从根到当前节点id数组 userQuestion: string; // 用户最新问题 }后端返回给前端interface ChatResponseDTO { newNodeId: string; // 新建子节点ID aiAnswer: string; }核心业务逻辑流程流程 1接收前端对话请求组装 LLM 上下文输入sessionId、activePath、userQuestion输出组装完成给大模型的 messages 上下文数组根据sessionId拿到会话根据activePath校验路径合法性路径节点必须全部存在向上回溯查找最近的总结锚点节点从 activePath 最后一个节点的父节点开始沿着 parentNodeId 向根节点遍历停止条件找到isSummaryAnchortrue节点 || 到达根节点分割路径anchorNode找到的锚点节点如果没有锚点则 anchorNodenullafterAnchorNodeIdList锚点节点之后一直到当前 activePath 末尾的全部节点 ID 列表处理上下文片段如果存在anchorNode取anchorNode.anchorSummary作为前置全局上下文遍历afterAnchorNodeIdList节点集合判断距离当前路径末尾位置属于最后keepRawCount条直接取原始userContentaiContent其余节点使用nodeSummary单轮摘要将前置全局上下文 遍历得到的片段消息 当前用户新问题组装成 LLM messages 数组调用大模型。关键点原始 node 对象不会被修改只是读取 summary 附属字段。流程 2大模型返回后新建子节点在 activePath 最后一个节点下创建新子节点填充原始 userContent、aiContent将新节点 id 加入父节点的childrenNodeIds存入allNodes触发总结判断逻辑两个条件满足任意就执行总结生成不修改原始字段条件 A【分叉触发总结】父节点在新增子节点之后childrenNodeIds.length 1→ 当前父节点成为分叉节点执行向上浓缩总结生成anchorSummary标记isSummaryAnchortrue条件 B【线性阈值触发总结】当前分支activePath最新节点离上一个锚点或根节点后的节点数量 linearThreshold把该位置节点设置为锚点生成anchorSummary对非锚点的旧节点生成单轮nodeSummary单轮对话简短摘要注意生成 anchorSummary、nodeSummary 是调用大模型做总结生成的结果写入附属字段原始 userContent/aiContent 保持原样用于前端历史渲染。流程 3锚点总结逻辑分叉 / 线性阈值触发只写衍生 summary 字段原始节点只读从待标记为锚点的节点向上回溯直到上一个锚点或根收集该段全部原始对话内容调用 LLM对收集的全部对话做整体浓缩总结提示模型锚点的上下文尽量不要改只总结后面的原始文本与前面的上下文拼接成新的上下文输出anchorSummary大致是上个锚点上下文后续总结设置节点isSummaryAnchor true赋值anchorSummary后续回溯上下文遇到这个节点就直接使用这份总摘要不再继续向上遍历。流程 4单节点摘要逻辑普通历史节点对单条用户 AI 对话做简短摘要输出nodeSummary仅用于上下文组装原始数据不动。边界规则说明分叉节点只要一个节点产生≥2 个子对话就触发锚点总结分叉之后所有子分支回溯都会复用该分叉锚点的anchorSummary分叉之后各自分支继续独立增长。多锚点嵌套树形路径可以存在多个锚点回溯只取离当前节点最近那一个更早锚点直接忽略。原文保留策略锚点之后末尾 N 轮强制原始文本防止摘要丢失最新细节避免大模型幻觉。回退历史分支场景当用户切换 activePath 到历史某旧节点回溯逻辑不变依然向上找最近锚点自动适配旧分支上下文。历史展示前端读取allNodes全部原始userContent、aiContent渲染对话树完全不读取 summary 字段summary 仅后端内部 LLM 调用使用。锚点节点本身也参与对话可以继续往下生成子节点。伪代码实现核心片段回溯查找最近锚点节点/** * 沿着节点向上查找最近的总结锚点 * param nodeId 当前节点id * param allNodes 全部节点字典 * returns {anchorNode: DialogueNode|null, pathAfterAnchor: string[]} */ function findNearestSummaryAnchor(nodeId: string, allNodes: Recordstring, DialogueNode) { let cur: string | null nodeId; const afterAnchor: string[] []; while(cur) { const node allNodes[cur]; if (!node) break; // 找到锚点直接返回afterAnchor不包含锚点自身 if(node.isSummaryAnchor) { return { anchorNode: node, pathAfterAnchor: [...afterAnchor] } } afterAnchor.unshift(cur); cur node.parentNodeId; } // 遍历到根节点依然没有锚点 return { anchorNode: null, pathAfterAnchor: [...afterAnchor] }; }组装 LLM messagesasync function buildLLMMessages( activePath: string[], userNewQuestion: string, session: DialogueSession ) { const { allNodes, config } session; const currentNodeId activePath.at(-1)!; const { anchorNode, pathAfterAnchor } findNearestSummaryAnchor(currentNodeId, allNodes); const messages: Array{role: string, content: string} []; // 加入锚点全局浓缩上下文 if(anchorNode anchorNode.anchorSummary) { messages.push({ role: system, content: 【历史对话总摘要】${anchorNode.anchorSummary} }); } // 处理锚点之后的节点区分摘要/原文 const totalAfter pathAfterAnchor.length; for(let idx 0; idx totalAfter; idx) { const nid pathAfterAnchor[idx]; const node allNodes[nid]; // 判断是否属于末尾需要保留原文的条目 const isKeepRaw (totalAfter - idx) config.keepRawCount; if(isKeepRaw) { messages.push({role:user, content: node.userContent}); messages.push({role:assistant, content: node.aiContent}); } else { // 使用单轮节点摘要 if(node.nodeSummary) { messages.push({role:system, content: 【历史片段摘要】${node.nodeSummary}}) } else { // 兜底如果摘要还未生成降级使用原始文本 messages.push({role:user, content: node.userContent}); messages.push({role:assistant, content: node.aiContent}); } } } // 追加当前用户最新提问 messages.push({role:user, content: userNewQuestion}); return messages; }新增节点后判断是否触发锚点总结/** * 创建完新子节点之后调用 * param parentNodeId 父节点id刚刚新增了子节点 * param session 会话 */ async function tryTriggerSummaryAnchor(parentNodeId: string, session: DialogueSession) { const { allNodes, config } session; const parentNode allNodes[parentNodeId]; // 条件1分叉子节点数量大于1触发锚点 const isFork parentNode.childrenNodeIds.length 1; // 条件2获取当前从根到父节点路径线性节点数达到阈值 const pathToParent getPathFromRoot(parentNodeId, allNodes); const isReachLinearThreshold pathToParent.length config.linearThreshold; if ((isFork || isReachLinearThreshold) !parentNode.isSummaryAnchor) { // 收集从根到该节点全部原始对话调用LLM做整体浓缩 const rawDialogues collectRawDialogue(pathToParent, allNodes); const anchorSummary await callLLMGenerateWholeSummary(rawDialogues); // 仅修改衍生字段原始userContent/aiContent不变 parentNode.isSummaryAnchor true; parentNode.anchorSummary anchorSummary; } // 给普通节点生成单轮摘要 for(const nodeId of pathToParent) { const node allNodes[nodeId]; if(!node.isSummaryAnchor !node.nodeSummary) { node.nodeSummary await callLLMGenerateSingleTurnSummary(node.userContent, node.aiContent); } } }辅助工具函数getPathFromRoot根据节点 id 向上回溯生成完整 path 数组collectRawDialogue收集路径上全部原始对话文本callLLMGenerateWholeSummary、callLLMGenerateSingleTurnSummary调用大模型做摘要生成。前后端分工前端维护会话树维护activePath根到当前交互节点 ID 数组渲染全部历史读取所有节点userContent、aiContent原始字段渲染树形分叉对话 UI用户切换历史分支时更新activePath请求接口仅传递sessionId activePath 用户新问题不把完整对话传给大模型上下文全部后端组装。后端持久化session会话对象存储全部节点根据activePath回溯树节点组装 LLM 上下文新增节点后判断分叉、线性阈值异步生成锚点摘要、单轮节点摘要绝对禁止改写节点原始对话字段summary 全部为附属字段提供接口返回全部节点数据给前端渲染历史。性能与优化点摘要生成可以异步任务处理不要阻塞主对话接口对话时摘要未生成做降级兜底直接读取原始内容锚点越多单次组装上下文遍历节点越少减少 token 消耗数据库存储节点表一条记录存一个DialogueNodesession 表存会话基础信息分叉节点会成为公共锚点多个子分支可以复用同一个anchorSummary减少重复总结配置调参示例linearThreshold12keepRawCount3可根据模型窗口大小动态调整。异常 case切换旧分支 activePath回溯逻辑完全复用自动找到该分支最近锚点摘要生成失败程序降级直接读取原始对话保证对话可用activePath 路径非法后端校验返回错误前端重新同步会话节点。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻