예측 상태 업데이트 (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) — 에이전트 툴 호출을 컴포넌트에 매핑합니다.