Documentation Index Fetch the complete documentation index at: https://nvd-54.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
与 AI 智能体的对话很少是线性的。你可能想要重新措辞一个问题、重新生成一个你不喜欢的响应,或者在不丢失之前工作的情况下探索完全不同的对话路径。分支对话将版本控制语义引入你的聊天 UI。每次编辑都会创建一个新分支,你可以在分支之间自由导航。
什么是分支对话?
分支对话将对话视为树而不是列表。每条消息是一个节点,编辑消息或重新生成响应会从该点创建一个分叉 。原始路径作为兄弟分支被保留,因此用户可以在不同的对话轨迹之间来回切换。
关键功能:
编辑任意用户消息: 重写之前的提示词并从该点重新运行智能体
重新生成任意 AI 响应: 请求智能体为相同的输入生成不同的回答
导航分支: 使用每条消息的分支控件在不同版本的对话之间切换
设置带历史记录的 useStream
要启用分支功能,传递 fetchStateHistory: true,以便 useStream 获取分支操作所需的检查点元数据。
导入你的智能体并将 typeof myAgent 作为类型参数传递给 useStream,以获得对状态值的类型安全访问:
import type { myAgent } from "./agent" ;
import { useStream } from "@langchain/react" ;
const AGENT_URL = "http://localhost:2024" ;
export function Chat () {
const stream = useStream < typeof myAgent > ( {
apiUrl : AGENT_URL ,
assistantId : "branching_chat" ,
fetchStateHistory : true ,
} ) ;
return (
< div >
{ stream . messages . map ( ( msg ) => {
const metadata = stream . getMessagesMetadata (msg) ;
return (
< Message
key = { msg . id }
message = { msg }
metadata = { metadata }
onEdit = {( text ) => handleEdit (stream , msg , metadata , text) }
onRegenerate = {() => handleRegenerate (stream , metadata) }
onBranchSwitch = {( id ) => stream . setBranch (id) }
/>
) ;
} ) }
</ div >
) ;
}
理解消息元数据
getMessagesMetadata(msg) 函数返回每条消息的分支信息:
interface MessageMetadata {
branch : string ;
branchOptions : string [] ;
firstSeenState : {
parent_checkpoint : Checkpoint | null ;
};
}
属性 描述 branch此特定消息版本的分支 ID branchOptions此消息位置所有可用分支 ID 的数组 firstSeenState.parent_checkpoint此消息之前的检查点。用作编辑和重新生成的分叉点
当消息只有一个版本时,branchOptions 包含单个条目。编辑或重新生成后,新的分支 ID 被添加到 branchOptions 中,你可以在它们之间导航。
编辑消息
要编辑用户消息并创建新分支:
从消息的元数据获取 parent_checkpoint
使用该检查点提交编辑后的消息
智能体从该点重新运行,创建新分支
function handleEdit (
stream : ReturnType < typeof useStream > ,
originalMsg : HumanMessage ,
metadata : MessageMetadata ,
newText : string
) {
const checkpoint = metadata . firstSeenState ?. parent_checkpoint ;
if ( ! checkpoint) return ;
stream . submit (
{
messages : [ { ... originalMsg , content : newText } ] ,
},
{ checkpoint }
) ;
}
编辑后:
消息的 branchOptions 增加一个新条目
视图自动切换到新分支
智能体使用更新的消息从分叉点重新运行
原始版本被保留,可通过分支切换器访问
重新生成响应
要在不更改输入的情况下重新生成 AI 响应:
从 AI 消息的元数据获取 parent_checkpoint
使用 undefined 输入和父检查点提交
智能体生成新的响应,创建新分支
function handleRegenerate (
stream : ReturnType < typeof useStream > ,
metadata : MessageMetadata
) {
const checkpoint = metadata . firstSeenState ?. parent_checkpoint ;
if ( ! checkpoint) return ;
stream . submit ( undefined , { checkpoint } ) ;
}
每次重新生成都会为该位置的 AI 消息创建新分支。用户然后可以使用分支切换器比较不同的响应。
重新生成对非确定性智能体很有用。由于 LLM 输出随温度变化,重新生成相同的提示词通常会产生有意义的不同响应。
构建分支切换器
当消息有多个分支时,显示一个紧凑的内联控件,包含当前版本索引和导航箭头:
function BranchSwitcher ({
metadata ,
onSwitch ,
} : {
metadata : MessageMetadata ;
onSwitch : ( branchId : string ) => void ;
}) {
const { branch , branchOptions } = metadata ;
if (branchOptions . length <= 1 ) return null ;
const currentIndex = branchOptions . indexOf (branch) ;
const hasPrev = currentIndex > 0 ;
const hasNext = currentIndex < branchOptions . length - 1 ;
return (
< div className = "inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600" >
< button
disabled = { ! hasPrev }
onClick = {() => onSwitch (branchOptions[currentIndex - 1 ]) }
className = "hover:text-gray-900 disabled:opacity-30"
aria-label = "上一个版本"
>
◀
</ button >
< span className = "min-w-[3ch] text-center" >
{ currentIndex + 1 } / { branchOptions . length }
</ span >
< button
disabled = { ! hasNext }
onClick = {() => onSwitch (branchOptions[currentIndex + 1 ]) }
className = "hover:text-gray-900 disabled:opacity-30"
aria-label = "下一个版本"
>
▶
</ button >
</ div >
) ;
}
当用户点击分支箭头时,调用 stream.setBranch(branchId) 将对话视图切换到该分支。这是即时的,因为所有分支数据已通过 fetchStateHistory: true 加载。
切换分支不仅影响目标消息,还影响所有后续消息。如果你切换到消息 3 的不同版本,消息 4、5、6 等也会更新以反映该版本之后的对话。
分支在底层的工作原理
LangGraph 将每次状态转换持久化为一个检查点 。当你使用 checkpoint 参数提交时,后端从该点分叉而不是追加到当前对话。结果是树形结构:
用户:"What is React?"
└─ AI:"React is a JavaScript library..."(分支 A)
└─ AI:"React is a UI framework..."(分支 B,重新生成)
用户:"Tell me about hooks"(分支 A)
└─ AI:"Hooks are functions..."
用户:"Tell me about JSX"(从分支 A 编辑)
└─ AI:"JSX is a syntax extension..."
每个分支是对话树中的独立路径。切换分支会更新显示的消息但不会删除任何数据。所有分支持久化在检查点存储中。
完整消息组件
这是一个完整的组件,结合了消息显示、编辑、重新生成和分支切换:
function MessageWithBranching ({
message ,
metadata ,
stream ,
} : {
message : BaseMessage ;
metadata : MessageMetadata ;
stream : ReturnType < typeof useStream > ;
}) {
const [ isEditing , setIsEditing ] = useState ( false ) ;
const [ editText , setEditText ] = useState (message . content as string ) ;
const isHuman = message . _getType () === "human" ;
const isAI = message . _getType () === "ai" ;
const hasBranches = metadata . branchOptions . length > 1 ;
return (
< div className = "group relative py-2" >
{ isEditing ? (
< EditForm
text = { editText }
onChange = { setEditText }
onSave = {() => {
handleEdit (stream , message as HumanMessage , metadata , editText) ;
setIsEditing ( false ) ;
}}
onCancel = {() => {
setEditText (message . content as string ) ;
setIsEditing ( false ) ;
}}
/>
) : (
<>
< div className = { isHuman ? "text-right" : "text-left" } >
< div
className = {
isHuman
? "inline-block rounded-lg bg-blue-600 px-4 py-2 text-white"
: "inline-block rounded-lg bg-gray-100 px-4 py-2"
}
>
{ message . content as string }
</ div >
</ div >
< div className = "mt-1 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100" >
{ isHuman && (
< button
className = "text-xs text-gray-400 hover:text-gray-700"
onClick = {() => setIsEditing ( true ) }
>
编辑
</ button >
) }
{ isAI && (
< button
className = "text-xs text-gray-400 hover:text-gray-700"
onClick = {() =>
handleRegenerate (stream , metadata)
}
>
重新生成
</ button >
) }
{ hasBranches && (
< BranchSwitcher
metadata = { metadata }
onSwitch = {( id ) => stream . setBranch (id) }
/>
) }
</ div >
</>
) }
</ div >
) ;
}
function EditForm ({
text ,
onChange ,
onSave ,
onCancel ,
} : {
text : string ;
onChange : ( text : string ) => void ;
onSave : () => void ;
onCancel : () => void ;
}) {
return (
< div className = "space-y-2" >
< textarea
className = "w-full rounded-lg border p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
value = { text }
onChange = {( e ) => onChange (e . target . value) }
rows = { 3 }
/>
< div className = "flex gap-2" >
< button
className = "rounded bg-blue-600 px-4 py-1.5 text-sm text-white hover:bg-blue-700"
onClick = { onSave }
>
保存并重新运行
</ button >
< button
className = "rounded border px-4 py-1.5 text-sm hover:bg-gray-50"
onClick = { onCancel }
>
取消
</ button >
</ div >
</ div >
) ;
}
与乐观更新结合
将分支与乐观更新结合,获得无缝的编辑体验。当用户保存编辑时,在服务器响应之前乐观地显示更新后的消息:
function handleEditOptimistic (
stream : ReturnType < typeof useStream > ,
originalMsg : HumanMessage ,
metadata : MessageMetadata ,
newText : string
) {
const checkpoint = metadata . firstSeenState ?. parent_checkpoint ;
if ( ! checkpoint) return ;
const updatedMsg = { ... originalMsg , content : newText };
stream . submit (
{ messages : [updatedMsg] },
{
checkpoint ,
optimisticValues : ( prev ) => {
if ( ! prev ?. messages) return { messages : [updatedMsg] };
const idx = prev . messages . findIndex ( ( m ) => m . id === originalMsg . id) ;
if (idx === - 1 ) return prev ;
return {
... prev ,
messages : [ ... prev . messages . slice ( 0 , idx) , updatedMsg] ,
};
},
}
) ;
}
添加键盘导航
为高级用户添加键盘快捷键来导航分支:
useEffect ( () => {
function handleKeyDown ( e : KeyboardEvent ) {
if ( ! focusedMessageMetadata) return ;
const { branch , branchOptions } = focusedMessageMetadata ;
const idx = branchOptions . indexOf (branch) ;
if (e . altKey && e . key === "ArrowLeft" && idx > 0 ) {
stream . setBranch (branchOptions[idx - 1 ]) ;
}
if (e . altKey && e . key === "ArrowRight" && idx < branchOptions . length - 1 ) {
stream . setBranch (branchOptions[idx + 1 ]) ;
}
}
window . addEventListener ( "keydown" , handleKeyDown) ;
return () => window . removeEventListener ( "keydown" , handleKeyDown) ;
}, [focusedMessageMetadata , stream]) ;
Alt + ← / Alt + → 是分支导航的自然映射,因为它反映了浏览器的前进/后退导航。
最佳实践
始终启用 fetchStateHistory :没有它,getMessagesMetadata 无法返回分支信息。
仅在有多个分支时显示分支切换器 :1/1 指示器增加了无价值的杂乱信息。
悬停时显示分支控件 :分支导航箭头和编辑按钮应在悬停时出现以保持 UI 整洁。
保持分支切换器紧凑 :它与消息控件内联,不应主导 UI。
保持滚动位置 :切换分支时,尝试将视口锚定到已更改的消息。
指示活跃分支 :使用微妙的视觉提示(如彩色点或分支标签)让用户知道他们正在查看哪个分支。
流式处理时禁用控件 :在智能体正在流式传输响应时不允许编辑或重新生成。在启用这些操作前检查 stream.isLoading。
取消时保留编辑文本 :如果用户开始编辑然后取消,将文本区重置为原始消息内容。
测试深层分支树 :频繁编辑和重新生成的用户可能会创建许多分支。确保分支切换器和数据处理保持高性能。
将这些文档连接 到 Claude、VSCode 等工具,通过 MCP 获取实时答案。