콘텐츠로 이동

예측 상태 업데이트 (Predictive State Updates)

진행 중인 작업을 보여 주세요

보통 툴 호출은 UI 입장에서 보면 한 번에 끝나는 일이에요. 에이전트가 무엇을 쓸지 정하고, 인터페이스는 그 호출이 끝난 뒤에야 결과를 보게 되죠. 큰 문서를 만드는 툴이라면 잠시 멈춰 있다가 모든 내용이 한꺼번에 튀어나오는 식이라 답답합니다. 예측 상태 업데이트(predictive state updates)는 이 대기 시간을 없애 줍니다. 스트리밍 중인 툴 인자를 에이전트 상태의 한 필드로 투영하니, 모델이 인자를 토큰 단위로 만들어 낼 때마다 그 상태 필드가 실시간으로 채워져요. 에이전트가 쓰고 있는 문서가 완성된 뒤가 아니라, 입력되는 그 순간 편집기에 나타납니다.

예측 상태는 커스텀 상태를 가진 Flow(Flow[AgentState])가 전제예요. 스트리밍 툴 인자를 상태 필드로 투영하므로, 단순한 Crew에는 대응하는 방식이 없습니다.

두 패턴 모두 프론트엔드에서 에이전트 상태를 읽지만, 해결하는 문제가 서로 달라요.

패턴 하는 일
예측 상태 (Predictive state) 단방향. 진행 중인 툴 인자를 상태 필드로 스트리밍해서, 호출이 끝나기 생성 중에 UI를 갱신해요.
공유 상태 (Shared State) 양방향. UI가 에이전트의 커밋된 상태를 읽고 쓰기도 해서, 턴이 오갈 때마다 앱과 에이전트를 동기화합니다.

에이전트가 만들어 내는 것을 낙관적으로 미리 보여 주고 싶을 때 예측 상태를 쓰세요. 그 상태를 사용자가 다시 편집해야 한다면 공유 상태가 맞아요.

워크스루

이 가이드는 AG-UI 위에 제공되는 Crew나 Flow, 그리고 연결된 CopilotKit 프론트엔드가 이미 있다고 가정해요. 아직 없다면 프론트엔드 개요부터 시작하세요.

1단계: 커스텀 상태를 가진 Flow 정의하기

예측 상태는 툴 인자를 상태 필드로 투영하므로, Flow가 그 값을 받을 타입 있는 상태 필드가 필요합니다. 스트리밍 대상으로 삼고 싶은 필드를 CopilotKitState 하위 클래스에 추가하세요.

from typing import Optional
from crewai.flow.flow import Flow, start, router, listen
from litellm import acompletion
from ag_ui_crewai.sdk import copilotkit_stream, copilotkit_predict_state, CopilotKitState

WRITE_DOCUMENT_TOOL = {
    "type": "function",
    "function": {
        "name": "write_document",
        "description": "Write the full document in markdown.",
        "parameters": {
            "type": "object",
            "properties": {
                "document": {
                    "type": "string",
                    "description": "The document to write",
                },
            },
        },
    },
}

class AgentState(CopilotKitState):
    document: Optional[str] = None

class DocumentFlow(Flow[AgentState]):
    @start()
    @listen("route_follow_up")
    async def start_flow(self):
        pass

2단계: 상태 필드를 툴 인자에 매핑하기

완성본 스트리밍을 시작하기 전에 copilotkit_predict_state를 호출하세요. 이 호출이 런타임에 "이름 붙은 툴 인자를 이름 붙은 상태 필드로 투영하라"고 알려 줍니다. write_document 호출이 document 인자를 스트리밍하는 동안, document 상태 필드가 실시간으로 갱신되는 거예요.

@router(start_flow)
async def chat(self):
    # `document` 상태 필드를 write_document의 `document` 인자에 매핑합니다.
    # 툴 호출이 스트리밍되는 동안 상태 필드가 실시간으로 갱신됩니다.
    await copilotkit_predict_state({
        "document": {
            "tool_name": "write_document",
            "tool_argument": "document",
        },
    })

    response = await copilotkit_stream(
        await acompletion(
            model="openai/gpt-4o",
            messages=[
                {
                    "role": "system",
                    "content": "Write and edit the document with write_document.",
                },
                *self.state.messages,
            ],
            tools=[*self.state.copilotkit.actions, WRITE_DOCUMENT_TOOL],
            parallel_tool_calls=False,
            stream=True,
        )
    )
    message = response.choices[0].message
    self.state.messages.append(message)

핵심은 copilotkit_predict_state({ "<state_field>": {"tool_name": ..., "tool_argument": ...} })예요. 이 호출이 없으면 프론트엔드는 툴 호출이 끝난 뒤에야 document를 볼 수 있어요. 있으면 에이전트가 아직 생성하는 중에도 부분 인자가 해당 필드로 스트리밍됩니다.

Flow를 제공할 때는 프론트엔드 개요에서 본 것처럼 add_crewai_flow_fastapi_endpoint(...)로 서빙하세요.

3단계: 프론트엔드에서 예측 상태 읽기

프론트엔드에서는 useAgent로 필드를 읽고 상태 변경을 구독하세요. 백엔드가 스트리밍 인자를 document에 투영하고 있으므로, 에이전트가 타이핑할 때마다 이 컴포넌트가 다시 렌더링됩니다.

"use client";
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core/v2";

function DocumentView() {
  const { agent } = useAgent({
    agentId: "document",
    updates: [UseAgentUpdate.OnStateChanged],
  });
  const document = (agent?.state as { document?: string })?.document ?? "";
  return <article>{document}</article>; // 에이전트가 타이핑할 때마다 갱신
}

에이전트가 write_document 호출을 만드는 동안 document 필드가 점진적으로 채워지므로, 편집기가 마지막에 한꺼번에 튀어나오는 대신 실시간으로 갱신됩니다.


더 읽어보기

  • 공유 상태 (Shared State) — 에이전트 상태를 양방향으로 읽고 씁니다.
  • 에이전틱 생성형 UI (Agentic Generative UI) — 변하는 라이브 에이전트 상태를 렌더링합니다.
  • 툴 기반 생성형 UI (Tool-Based Generative UI) — 에이전트 툴 호출을 컴포넌트에 매핑합니다.