브랜칭 채팅
브랜칭 채팅 (Branching chat)
체크포인트에서 포크(fork)하여 메시지를 편집하고 응답을 재생성해요.
AI 에이전트와의 대화는 거의 선형적이지 않아요. 질문을 다시 표현하고 싶거나, 마음에 들지 않는 응답을 재생성하고 싶거나, 체크포인트 기록을 잃지 않고 다른 대화 경로를 탐색하고 싶을 수 있어요. 브랜칭 채팅은 LangGraph 체크포인트를 포크 지점으로 사용해요: 모든 편집이나 재생성은 선택한 메시지의 부모 체크포인트에서 새 실행을 제출해요.
브랜칭 채팅이란? (What is branching chat?)
브랜칭 채팅은 대화를 평평한 목록이 아닌 체크포인트가 있는 타임라인으로 취급해요. 각 메시지에는 해당 메시지가 생성되기 전의 체크포인트를 가리키는 메타데이터가 있어요. 메시지를 편집하거나 응답을 재생성하면 그 체크포인트에서 새 실행이 제출돼요.
핵심 기능:
- 모든 사용자 메시지 편집: 이전 프롬프트를 다시 쓰고 그 지점에서 에이전트를 재실행
- 모든 AI 응답 재생성: 동일 입력에 대해 다른 답변을 생성하도록 에이전트 요청
- 기록 검사: 브랜치 타임라인이 필요할 때 LangGraph 클라이언트로 체크포인트 로드
스트림 메타데이터 설정 (Set up stream metadata)
메시지에는 루트 스트림을 사용하고, 각 메시지를 렌더링하는 컴포넌트에서 메시지별 체크포인트 메타데이터를 읽으세요. 메타데이터에는 포크할 부모 체크포인트 ID가 포함돼요.
React:
import { useStream } from "@langchain/react";
const AGENT_URL = "http://localhost:2024";
export function Chat() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
return (
<div>
{stream.messages.map((msg) => (
<MessageWithForkControls key={msg.id} stream={stream} message={msg} />
))}
</div>
);
}
Vue:
<script setup lang="ts">
import { useStream } from "@langchain/vue";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
</script>
<template>
<div>
<MessageWithForkControls
v-for="msg in stream.messages.value"
:key="msg.id"
:stream="stream"
:message="msg"
/>
</div>
</template>
Svelte:
<script lang="ts">
import { useStream } from "@langchain/svelte";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "simple_agent",
});
</script>
<div>
{#each stream.messages as msg (msg.id)}
<Message message={msg} {stream} />
{/each}
</div>
메시지 메타데이터 이해 (Understand message metadata)
useMessageMetadata(stream, messageId) 헬퍼는 한 메시지에 대한 MessageMetadata를 반환해요. 각 메시지를 렌더링하는 컴포넌트에서 사용해 메타데이터가 그 메시지 ID에 범위가 지정되도록 하세요:
import type { BaseMessage } from "langchain";
import { useState } from "react";
import { useMessageMetadata, useStream } from "@langchain/react";
function Chat() {
return stream.messages.map((message) => (
<MessageWithForkControls
key={message.id}
stream={stream}
message={message}
/>
));
}
function MessageWithForkControls({
stream,
message,
}: {
stream: ReturnType<typeof useStream>;
message: BaseMessage;
}) {
const metadata = useMessageMetadata(stream, message.id);
const checkpointId = metadata?.parentCheckpointId;
const [editedText, setEditedText] = useState(message.text);
return (
<form
onSubmit={(event) => {
event.preventDefault();
if (!checkpointId) return;
stream.submit(
{ messages: [{ type: "human", content: editedText }] },
{ forkFrom: { checkpointId } }
);
}}
>
<textarea
value={editedText}
onChange={(event) => setEditedText(event.target.value)}
/>
<button disabled={!checkpointId || editedText === message.text}>
Submit edited branch
</button>
</form>
);
}
parentCheckpointId는 메시지 바로 직전의 체크포인트예요. 편집·재생성의 포크 지점으로 사용해요.
메시지 편집 (Edit a message)
사용자 메시지를 편집하고 대화를 포크하려면:
- 메시지의 메타데이터에서
parentCheckpointId를 가져옵니다 forkFrom: { checkpointId }로 편집된 메시지를 제출합니다- 에이전트가 그 지점에서 재실행됩니다
function handleEdit(
stream: ReturnType<typeof useStream>,
originalMsg: HumanMessage,
metadata: MessageMetadata | undefined,
newText: string
) {
if (!metadata?.parentCheckpointId) return;
stream.submit(
{
messages: [{ type: "human", content: newText }],
},
{ forkFrom: { checkpointId: metadata.parentCheckpointId } }
);
}
편집 후:
- 에이전트가 업데이트된 메시지로 포크 지점에서 재실행됩니다
- 원래 경로는 스레드 기록에 계속 남아 있습니다
응답 재생성 (Regenerate a response)
입력을 바꾸지 않고 AI 응답을 재생성하려면:
- AI 메시지의 메타데이터에서
parent_checkpoint를 가져옵니다 - 빈 입력과
forkFrom: { checkpointId }로 제출합니다 - 에이전트가 그 지점에서 새로운 응답을 생성합니다
function handleRegenerate(
stream,
metadata: MessageMetadata | undefined
) {
if (!metadata?.parentCheckpointId) return;
stream.submit(undefined, {
forkFrom: { checkpointId: metadata.parentCheckpointId },
});
}
각 재생성은 그 위치의 AI 메시지에 대해 새 경로를 만들어요.
브랜칭이 내부적으로 어떻게 작동하나 (How branching works under the hood)
LangGraph는 모든 상태 전환을 체크포인트로 지속해요. forkFrom으로 제출하면 백엔드는 현재 대화에 추가하는 대신 그 지점에서 새 실행 경로를 시작해요. 결과는 트리 구조예요:
User: "What is React?"
└─ AI: "React is a JavaScript library..." (branch A)
└─ AI: "React is a UI framework..." (branch B, regenerated)
User: "Tell me about hooks" (branch A)
└─ AI: "Hooks are functions..."
User: "Tell me about JSX" (edited from branch A)
└─ AI: "JSX is a syntax extension..."
각 경로는 체크포인트 스토어에 영구화돼요. 체크포인트를 가로지르는 별도의 타임라인 뷰를 만들려면 stream.client.threads.getHistory(threadId)를 사용하세요.
모범 사례 (Best practices)
- 메시지 근처에서 메타데이터 읽기: 메시지 컨트롤을 렌더링하는 컴포넌트에서
useMessageMetadata를 호출하세요. - 호버 시 포크 컨트롤 표시: 편집·재생성 버튼은 호버 시 나타나 UI를 깨끗하게 유지하세요.
- 필요할 때 기록 새로고침: 타임라인을 렌더링하거나 포크가 안정된 후에만
client.threads.getHistory()를 호출하세요. - 스트리밍 중 컨트롤 비활성화: 에이전트가 응답을 적극적으로 스트리밍하는 동안 편집·재생성을 허용하지 마세요. 이 작업을 활성화하기 전에
stream.isLoading을 확인하세요. - 취소 시 편집 텍스트 보존: 사용자가 편집을 시작했다가 취소하면 textarea를 원래 메시지 내용으로 재설정하세요.
- 깊은 체크포인트 트리로 테스트: 자주 편집·재생성하는 사용자는 많은 경로를 만들 수 있어요. 타임라인 렌더링이 성능을 유지하는지 확인하세요.
출처: 문서
본문
이 페이지는 프론트엔드의 브랜칭 채팅 패턴을 다뤄요. LangGraph 체크포인트를 포크 지점으로 사용해 사용자 메시지를 편집하고 AI 응답을 재생성하는 방법을 설명해요. useStream·useMessageMetadata 헬퍼를 React/Vue/Svelte 등에서 사용하고, stream.submit(..., { forkFrom: { checkpointId } })으로 새 실행 경로(포크)를 만들어요. 메시지 메타데이터의 parentCheckpointId가 포크 지점이며, 체크포인트 기록과 모범 사례를 포함해요.