Skip to main content

LLM UI Generation Protocol

Goal: Design and organize a method for AI UI generation in an LLM chatbot.
문제 : 단순히 LLM과 텍스트 (마크다운)로 이야기 나누는것은 지루하고, 사용자와의 인터렉션에서 한계가 있다.
솔루션 : Chat bot과 이야기를 나누는 중간에 UI를 보여주면 사용자의 행동을 더 이끌어 낼 수 있다.

무엇이 가능한가?

  • LLM의 React Component 호출
  • 예) 차트 생성, 카드 UI, 테이블 UI, 반응형 UI를 포함한.

한계

  • A-Z 모든 UI를 LLM이 만드는것은 아니다.
  • 개발자가 재사용 가능한 UI 컴포넌트를 미리 만들어야 한다.
  • 미리 만든 컴포넌트 안에서 몇개의 문구, 설정값 정도 변경 가능하다.
    • 예)

📌 해당 툴에서 가능한 UI Scope를 사전에 정의 해야 한다.

1, Google Ads 캠페인 성과를 표로 정리하고 비효율 캠페인을 알려줘 - 가능

  • 출력 가능한 범위 밖 데이터 컬럼 요청 - 불가능

2, 최근 30일 광고 성과 추이를 차트로 보여줘

  • 최근 7일 구글 광고 성과 추이를 차트로 보여줘 - 가능
  • 최근 7일 캠페인 수익 차트로 보여줘 - 가능
  • 최근 30일 캠페인 광고비 및 수익 차트로 보여줘 - 가능
  • 최근 900일 광고 성과 추이를 차트로 보여줘 - 불가능

3, 지금 바로 적용할 수 있는 광고 최적화 제안을 간단히 보여줘 - 가능

4, 전환수 확대를 위해 지금 실행할 핵심 액션을 보여줘

  • 클릭수 확대를 위해 지금 실행할 핵심 액션을 보여줘 - 가능
  • 리뷰 수 확대를 위해 지금 실행할 핵심 액션을 보여줘 - 불가능

📌 가드레일

  • 입력 가드레일 : zod shcema에서 실패하는 경우, 이를 통한 피드백 루푸 생성
  • 출력 가드레일 : tool call 결과 함수가 실패하는 경우 적절한 애러 메시지와 그리고 데이터 스콥에 대한 피드백 출력

Core Concepts

  • 1, Streaming Protocol: LLM output is streamed because it is generated by an autoregressive Transformer model. The UI can display results immediately using SSE.

  • 2, Tool Calling : An LLM can decide which functions to invoke through Chain-of-Thought (CoT), and we can capture this decision using formatted output.

    • It can perform tasks such as mathematical calculations, local operations, and API calls (including MCP).
  • 3, UI Generation with Formated Output : The result of a specific tool call can produce a structured response in JSON format. By matching this response with a UI component renderer, UI generation becomes possible.

Terms

  • Provider: A service that provides AI models, such as OpenAI or Anthropic.
  • Model: A specific LLM identified by a model ID, such as gpt-codex-5.3.

End-to-End 시퀀스 요약

  1. 사용자 입력 -> useChat.sendMessage
  2. /api/chat 호출
  3. Prompt 본문 구성
  4. API에서 모델/툴/Reasoning 정책 결정 후 스트림 시작
  5. 텍스트/툴/reasoning/data 이벤트가 SSE로 전달
  6. 클라이언트:
    • 메시지 파트(text/reasoning/tool) 렌더링
    • data 이벤트는 DataStreamHandler가 Artifact 상태 갱신
  7. 스트림 종료 시 메시지/문서 버전 DB 저장 및 히스토리 갱신

Background

Tool Calling

https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling

About Tools

  • actions that an LLM can invoke.
  • A tool consists of three properties:
    • 1, description: An optional description of the tool that can influence when the tool is picked.
    • 2, inputSchema: A Zod schema or a JSON schema that defines the input required for the tool to run. The schema is consumed by the LLM, and also used to validate the LLM tool calls.
    • 3, execute: An optional async function that is called with the arguments from the tool call.

Types of Tools

  • Custom Tools : Full Custom defined by above 1,2,3 properties
  • Provider-Defined Tools : Model provider preset tool, developer implements excute function parts
  • Provider-Executed Tools : fully excuted by Model provider and theirs clouds.

fyi,

  • Tools concepts include mcp calling
  • AI SDK not support yet skills, skills include serveral sequential tool calling with

Streaming Protocol

1, Simple text reponse, text can be markdown format

[
{
"type": "text",
"text": "What is the weather in San Francisco?"
}
]

2, Custom Protocol Type

  • We can extend types, and those types can represent actionable items on the client.
[
// 채팅방 이름 변경하기
{
"type": "data-chat-title",
"data": "Weather in San Francisco"
},
{
"type": "step-start"
},
// tool 실행 및 승인 요청 대기
{
"type": "tool-getWeather",
"toolCallId": "ym21m2PNK5x7lSpp",
"state": "approval-requested",
"input": {
"city": "San Francisco"
},
"approval": {
"id": "aitxt-zI840Zb9AxdspaAoC3Aq6pyJ"
}
}
]
const state: "input-streaming" | "input-available" | "approval-requested" | "approval-responded" | "output-available" | "output-error" | "output-denied"  

- input-streaming : 툴 입력 인자(JSON)를 모델이 아직 스트리밍 중인 상태
- input-available : 툴 입력이 완성되어 실행 가능한 상태(아직 승인/실행 전)
- approval-requested : 툴 실행 전에 사용자 승인 대기 상태(Allow/Deny 필요)
- approval-responded : 사용자가 승인/거부 응답을 보낸 상태(재개 처리 전/)
- output-available : 툴 실행이 완료되어 결과(output)가 준비된 상태
- output-error : 툴 실행 중 오류가 발생한 상태
- output-denied : 사용자 거부 등으로 툴 실행이 차단된 상태

2-1, Tool Call Result

[
{
"type": "step-start"
},
{
"type": "tool-getWeather",
"toolCallId": "mAVSBBLo0edCkRRl",
"state": "output-available",
"input": {
"city": "San Francisco"
},
"approval": {
"id": "aitxt-OTJ2NKZVadOD1MVsjnTbuIQ9",
"approved": true
},
"output": {
"latitude": 37.763283,
"longitude": -122.41286,
"generationtime_ms": 0.062465667724609375,
"utc_offset_seconds": -28800,
"timezone": "America/Los_Angeles",
"timezone_abbreviation": "GMT-8",
"elevation": 18,
"current_units": {
"time": "iso8601",
"interval": "seconds",
"temperature_2m": "°C"
},
"current": {
"time": "2026-03-07T20:30",
"interval": 900,
"temperature_2m": 17.6
},
"hourly_units": {
"time": "iso8601",
"temperature_2m": "°C"
},
"hourly": {
"time": [
"2026-03-07T00:00",...
"2026-03-13T22:00",
"2026-03-13T23:00"
],
"temperature_2m": [
13.5,...
13.4
]
},
"daily_units": {
"time": "iso8601",
"sunrise": "iso8601",
"sunset": "iso8601"
},
"daily": {
"time": [
"2026-03-07",...
"2026-03-13"
],
"sunrise": [
"2026-03-07T06:32",...
"2026-03-13T06:23"
],
"sunset": [
"2026-03-07T18:09",...
"2026-03-13T18:14"
]
},
"cityName": "San Francisco"
}
},
{
"type": "data-chat-title",
"data": "Weather in San Francisco"
},
{
"type": "step-start"
},
{
"type": "text",
"text": "The weather in San Francisco is currently 17.6°C.",
"state": "done"
}
]

Other Examples

📌 시나리오

1, [Table UI]

  • 캠페인 성과를 표로 정리하고 비효율 캠페인을 알려줘

2, [Chart UI ]

  • 2-1, 광고 성과 그래프 보여줘.
  • 2-2, 최근 30일 광고 성과 추이를 차트로 보여줘
  • 2-3, 최근 7일 광고 성과 추이를 차트로 보여줘
  • 2-4, Campaign 지출과 Revenue 매출 그래프만 보여줘

3, [Nudge Type, Action Items - Data Mutate ]

  • 3-1, 지금 바로 적용할 수 있는 광고 최적화 제안을 간단히 보여줘

4, [Card Type, Action Items - Data Mutate ]

  • 4-1, 전환수 확대를 위해 지금 실행할 핵심 액션을 보여줘
  • 4-2, 클릭수 확대를 위해 지금 실행할 핵심 액션을 보여줘

📌 지금 바로 적용할 수 있는 광고 최적화 제안을 간단히 보여줘

data: {"type":"start","messageId":"4f64e0c9-fb1a-43a0-95be-3c9305b64c4f"}

data: {"type":"data-chat-title","data":"광고 최적화 제안"}

data: {"type":"start-step"}

data: {"type":"tool-input-start","toolCallId":"dHeUtSL2n1ILlU3O","toolName":"generateAdsDemo"}

data: {"type":"tool-input-delta","toolCallId":"dHeUtSL2n1ILlU3O","inputTextDelta":"{\"demoType\":\"nudge\",\"goal\":\"성과 개선\"}"}

data: {"type":"tool-input-available","toolCallId":"dHeUtSL2n1ILlU3O","toolName":"generateAdsDemo","input":{"campaignContext":"US eCommerce","dateRange":{"from":"2026-02-07","to":"2026-03-08","label":"최근 30일"},"goal":"성과 개선","datePreset":"30d","chartPanels":["trend","campaign","metric","device"],"budget":{"daily":1200,"currency":"USD"},"demoType":"nudge","metrics":["cpa","roas","ctr"],"channels":["search","display","pmax"],"campaignStatus":["learning","limited","active"],"recommendations":["브랜드 캠페인 예산 10% 증액","모바일 광고그룹 입찰가 -8% 조정","CTR 낮은 소재 2개 교체"],"ctaVariant":"budget-redistribution"}}

data: {"type":"tool-output-available","toolCallId":"dHeUtSL2n1ILlU3O","output":{"demoType":"nudge","title":"즉시 실행 가능한 최적화 넛지","summary":"작은 액션 중심으로 우선순위 제안을 제공합니다.","campaignContext":"US eCommerce","goal":"성과 개선","dateRange":{"from":"2026-02-07","to":"2026-03-08","label":"최근 30일"},"nudges":[{"id":"nudge-1","title":"브랜드 캠페인 예산 10% 증액","impact":"high","difficulty":"easy","expectedLift":"전환 +4% 예상","reason":"최근 7일 대비 CPA 3% 상승, CTR 1% 하락","status":"pending"},{"id":"nudge-2","title":"모바일 광고그룹 입찰가 -8% 조정","impact":"medium","difficulty":"easy","expectedLift":"전환 +6% 예상","reason":"최근 7일 대비 CPA 4% 상승, CTR 1.5% 하락","status":"pending"},{"id":"nudge-3","title":"CTR 낮은 소재 2개 교체","impact":"medium","difficulty":"medium","expectedLift":"전환 +8% 예상","reason":"최근 7일 대비 CPA 5% 상승, CTR 2% 하락","status":"pending"}]}}

data: {"type":"finish-step"}
data: {"type":"start-step"}
data: {"type":"finish-step"}
data: {"type":"finish","finishReason":"stop"}

data: [DONE]

메세지 구조 및 Reasoning에 대한 처리

1, Message 테이블 및 parts 필드

CREATE TABLE IF NOT EXISTS "Message_v2" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"chatId" uuid NOT NULL,
"role" varchar NOT NULL,
"parts" json NOT NULL,
"attachments" json NOT NULL,
"createdAt" timestamp NOT NULL
);
--> s
  • Message라는 사전에 정의 된 타입이 존재한다.
  • parts는 {"type":"data-chat-title","data":"광고 최적화 제안"} 처럼 구성된 엘리먼트가 배열로 들어간다.

2, Type은 확장 가능하다.

  - text { type: "text"; text: string; state?: "streaming" | "done" }
- reasoning { type: "reasoning"; text: string; state?: "streaming" | "done" }
- file { type: "file"; mediaType: string; url: string; filename?: string }
- step-start { type: "step-start" }
- tool-* (이 프로젝트는 4)
- tool-getWeather
- tool-createDocument
- tool-updateDocument
- tool-requestSuggestions
공통 상태:
- input-streaming
- input-available
- approval-requested
- approval-responded
- output-available
- output-error
- output-denied
- data-* (CustomUIDataTypes 기반)
- data-textDelta
- data-imageDelta
- data-sheetDelta
- data-codeDelta
- data-suggestion
- data-appendMessage
- data-id
- data-title
- data-kind
- data-clear
- data-finish
- data-chat-title

3, 추론 상태의 업데이트

  • 사용자의 Tool call 에서 state가 있다.
  • 승인 대기 -> 먼저 UI를 보여준다.
  • 사용자의 승인이 되면 다시 Message를 보내서
// 1, 승인 대기 -> 먼저 UI를 보여준다.  
{
"type": "tool-getWeather",
"toolCallId": "ym21m2PNK5x7lSpp",
"state": "approval-requested", // 승인 대기
"input": {
"city": "San Francisco"
},
"approval": {
"id": "aitxt-zI840Zb9AxdspaAoC3Aq6pyJ"
}
}
// 2, Allow 클릭하면 메시지 parts에 approval-responded 상태가 반영되어 재전송
- "state": "approval-responded", // 승인

// 3, 그 이후 출력
---
{
"type": "tool-getWeather",
"toolCallId": "mAVSBBLo0edCkRRl",
"state": "output-available", // 승인 후 출력 생성 완료
"input": {
"city": "San Francisco"
},
"approval": {
"id": "aitxt-OTJ2NKZVadOD1MVsjnTbuIQ9",
"approved": true
},
"output": {
"latitude": 37.763283,
"longitude": -122.41286,
"generationtime_ms": 0.062465667724609375,
"utc_offset_seconds": -28800,
"timezone": "America/Los_Angeles",
"cityName": "San Francisco"
}
}