从流式消息到多会话:TinyRobot Kit 如何组织 AI 对话数据层
一个最简单的 AI 对话页面并不难:维护消息数组,调用模型接口,再把结果渲染出来即可。
真正进入产品开发后,复杂度很快会从“调一次接口”扩散到整个页面:流式内容需要持续合并,请求需要能够停止,多轮上下文要正确组织,不同会话要隔离运行状态,页面刷新后还要恢复历史消息。如果这些逻辑都直接堆在 Vue 组件里,请求、消息和 UI 状态会越来越难拆开。
@opentiny/tiny-robot-kit(下文简称 Kit)提供的正是一层面向 AI 对话的数据处理能力。它不负责决定页面长什么样,而是把消息生命周期、会话运行时、后端响应适配、持久化和插件扩展组织成几块可以独立组合的能力。
本文不会只按 API 顺序介绍 useMessage 和 useConversation。我们会从一个可运行的 Vue 3 Demo 出发,先看 Kit 如何划分职责,再逐步验证几件更接近真实产品的问题:
- 单个会话如何处理流式回复和请求取消;
- 为什么替换后端接口不需要重写消息状态管理;
- 多个会话如何拥有彼此独立的消息 Engine;
- 切换会话时,仍在生成的后台会话为什么不会被一起销毁;
- 会话和消息如何通过 Storage Strategy 持久化;
- Reasoning、自动续写和 Tool Calling 如何进入同一条消息处理链路。
本文最终 Demo 演示如下,源码地址:https://github.com/opentiny/tiny-robot/tree/demo/kit-article-260828

AI 对话的数据层,究竟在管理什么
把聊天页面抽象成一个 messages 数组,只能覆盖“已经拿到结果以后怎么显示”的部分。真正需要长期维护的是一套运行时状态:当前请求处于什么阶段、正在更新哪条助手消息、某个会话是否仍在后台生成、什么时候保存消息、收到模型的特殊字段后应该触发什么行为。
Kit 将这些职责拆成五个边界:
| 能力 | 在 Kit 中的角色 | 解决的问题 |
|---|---|---|
useMessage |
单会话消息 Engine | 消息、请求状态、流式合并、取消请求 |
useConversation |
多会话管理层 | 会话列表、Engine 创建与切换、后台运行状态 |
responseProvider |
响应适配边界 | 将业务后端或模型服务转换成统一响应流 |
| Storage Strategy | 持久化边界 | 会话元数据和历史消息的保存、恢复 |
| Plugins | 消息处理扩展 | Reasoning、自动续写、Tool Calling 等 |
它们之间的关系可以概括为:
这里最重要的不是多了几个 Composable,而是职责边界变清楚了:UI 只消费响应式状态,Message Engine 管理一次会话的运行过程,Conversation 管理多个 Engine,Provider 隔离后端协议,Storage 隔离持久化实现。
后面的 Demo 都围绕这几个边界展开。
环境准备
本文基于 @opentiny/tiny-robot-kit 0.5.1,面向熟悉 Vue 3 基础用法的前端开发者。
准备环境:
- Node.js:22+
- pnpm:10+
使用 Vite 创建 Vue 3 + TypeScript 工程:
pnpm create vite tiny-robot-kit-article-demo --template vue-ts
cd tiny-robot-kit-article-demo
pnpm install
安装 TinyRobot 组件与 Kit:
pnpm add @opentiny/tiny-robot@0.5.1 \
@opentiny/tiny-robot-kit@0.5.1 \
@opentiny/tiny-robot-svgs@0.5.1
在入口文件中引入组件库样式:
import '@opentiny/tiny-robot/dist/style.css'
为了把注意力放在数据层,Demo 的 UI 只使用 TrBubbleList、TrSender 和 TrHistory 这几个组件。
第一步:让 useMessage 接管单会话请求生命周期
单会话是最小运行单元。useMessage 对外提供消息列表、请求状态、发送和取消能力,组件不需要自己维护“当前正在生成哪条消息”这样的中间状态:
const { messages, isProcessing, sendMessage, abortRequest } = useMessage({
responseProvider: mockResponseProvider,
})
这里真正决定数据从哪里来的,是 responseProvider。
先创建 src/mockResponseProvider.ts,用异步生成器模拟模型逐字符返回:
import type { ChatCompletion, ResponseProvider } from '@opentiny/tiny-robot-kit'
const characters = [...'这是模拟的流式回复。Kit 会持续合并消息,页面只需要绑定状态和组件。']
export const mockResponseProvider: ResponseProvider = async function* (_, signal) {
for (const [index, content] of characters.entries()) {
await new Promise((resolve) => setTimeout(resolve, 40))
if (signal.aborted) return
yield {
id: 'mock-response',
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: 'mock',
system_fingerprint: null,
choices: [
{
index: 0,
message: undefined,
delta: { role: index === 0 ? 'assistant' : undefined, content },
finish_reason: index === characters.length - 1 ? 'stop' : null,
logprobs: null,
},
],
} satisfies ChatCompletion
}
}
这里有两个细节值得注意。
第一,Provider 不直接修改 Vue 状态,而是持续产出 ChatCompletion chunk;Message Engine 负责把增量内容合并到当前助手消息中。
第二,Provider 接收 Kit 传入的 AbortSignal。请求取消不需要组件和 Provider 之间再维护一套额外状态,只要 Provider 在合适的位置响应同一个 signal 即可。
在 App.vue 中,页面只绑定 Kit 暴露出来的状态:
<script setup lang="ts">
import { TrBubbleList, TrSender, type BubbleRoleConfig } from '@opentiny/tiny-robot'
import { useMessage } from '@opentiny/tiny-robot-kit'
import { ref } from 'vue'
import { mockResponseProvider } from './mockResponseProvider'
const input = ref('')
const { messages, isProcessing, sendMessage, abortRequest } = useMessage({
responseProvider: mockResponseProvider,
})
const roles: Record<string, BubbleRoleConfig> = {
assistant: { placement: 'start' },
user: { placement: 'end' },
}
function submit(content: string) {
if (!content.trim()) return
void sendMessage(content)
input.value = ''
}
</script>
<template>
<main class="chat-shell">
<header>
<span>TinyRobot Kit</span>
<small>Mock 流式响应</small>
</header>
<TrBubbleList class="messages" :messages="messages" :role-configs="roles" auto-scroll />
<TrSender
v-model="input"
:loading="isProcessing"
:placeholder="isProcessing ? '正在生成…' : '输入问题体验流式回复'"
@submit="submit"
@cancel="abortRequest"
/>
</main>
</template>
为了让 Demo 可以直接运行,再补一组最小布局样式:
<style scoped>
.chat-shell {
display: grid;
grid-template-rows: auto 1fr auto;
width: min(880px, calc(100% - 32px));
height: min(680px, calc(100vh - 64px));
margin: 32px auto;
padding: 24px;
background: white;
border-radius: 16px;
box-shadow: 0 16px 48px #23395d14;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
font-weight: 700;
}
header small {
color: #667085;
font-weight: 400;
}
.messages {
min-height: 0;
overflow: auto;
padding: 16px 0;
}
</style>
默认模板中的 src/style.css 可以简化为:
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #f5f7fa;
}
从页面视角看,状态只是在不断变化;从数据层视角看,实际发生的是一条完整的请求链路:
运行:
pnpm dev
发送消息后,可以看到回复内容持续写入同一条助手消息;生成过程中点击停止或等待请求结束,输入区随即恢复。

Response Provider:把后端协议留在数据层边界之外
前面的 Mock Demo 有一个容易被忽略的价值:页面并不知道响应来自 Mock。
ResponseProvider 的职责很窄。它接收当前请求体和 AbortSignal,然后返回 Kit 可以消费的单次结果或异步数据流:
export type ResponseProvider<T = ChatCompletion> = (
requestBody: MessageRequestBody,
abortSignal: AbortSignal,
) => AsyncStreamableResult<T>
因此,Mock、企业内部 AI 网关、OpenAI Compatible API 或其他 SSE 服务,都可以放在这一层适配。只要 Provider 最终产出的数据符合 Message Engine 的消费格式,上层的:
messages;isProcessing;abortRequest;- 多会话管理;
- UI 绑定;
都不需要因为后端变化而重写。
这也是后面从 Mock 切到真实 SSE 时,只替换 Provider 就能继续使用同一套会话逻辑的原因。
第二步:用 useConversation 组织多个独立 Engine
单会话只需要一个 Message Engine。加入历史会话后,需要进一步解决三个问题:
- 每个会话的消息和请求状态如何隔离;
- 切换历史会话时,哪个 Engine 应该进入当前视图;
- 离开当前会话以后,仍在生成的请求是否继续保留。
useConversation 就位于这一层。它为会话管理对应的 useMessage Engine,并通过 activeConversation 暴露当前会话:
const {
conversations,
activeConversation,
activeConversationId,
createConversation,
switchConversation,
sendMessage,
abortActiveRequest,
} = useConversation({
useMessageOptions: { responseProvider: mockResponseProvider },
})
const messages = computed(() => activeConversation.value?.engine.messages.value ?? [])
const isProcessing = computed(() => activeConversation.value?.engine.isProcessing.value ?? false)
这里的 conversations 主要保存会话元数据;真正运行消息请求的是每个会话自己的 Engine。
用长回复验证会话隔离
为了让后台运行状态更容易观察,把 Mock Provider 改成同时支持普通回复和长回复:
const defaultReply = '这是模拟的流式回复。Kit 会持续合并消息,页面只需要绑定状态和组件。'
const longReply =
'这是用于演示多会话隔离的长回复。当前会话会持续接收字符,切换到其他会话后仍会保持自己的消息和请求状态。'
.repeat(20)
.slice(0, 300)
export const mockResponseProvider: ResponseProvider = async function* ({ messages }, signal) {
const prompt = messages.at(-1)?.content
const reply = typeof prompt === 'string' && prompt.startsWith('/long') ? longReply : defaultReply
const characters = [...reply]
for (const [index, content] of characters.entries()) {
await new Promise((resolve) => setTimeout(resolve, 40))
if (signal.aborted) return
yield {
id: 'mock-response',
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: 'mock',
system_fingerprint: null,
choices: [
{
index: 0,
message: undefined,
delta: { role: index === 0 ? 'assistant' : undefined, content },
finish_reason: index === characters.length - 1 ? 'stop' : null,
logprobs: null,
},
],
} satisfies ChatCompletion
}
}
新对话采用“首条消息创建会话”的方式:点击“新对话”只清空当前选择,真正发送第一条消息时再创建会话。
function submit(content: string) {
if (!content.trim()) return
if (!activeConversation.value) {
createConversation({ title: content.slice(0, 16) })
}
sendMessage(content)
input.value = ''
}
function startConversation() {
activeConversationId.value = null
input.value = ''
}
function openConversation(item: HistoryItem) {
if (item.id) void switchConversation(item.id)
}
页面加入 TrHistory:
<template>
<main class="app-shell">
<aside class="history-panel">
<div class="history-header">
<strong>历史会话</strong>
<button class="new-chat" type="button" @click="startConversation">新对话</button>
</div>
<TrHistory
class="history-list"
:data="conversations as HistoryItem[]"
:selected="activeConversationId ?? undefined"
:menu-items="[]"
@item-click="openConversation"
/>
</aside>
<header>
<span>TinyRobot Kit</span>
<small>多会话</small>
</header>
<TrBubbleList class="messages" :messages="messages" :role-configs="roles" auto-scroll />
<TrSender
class="sender"
v-model="input"
:loading="isProcessing"
:placeholder="isProcessing ? '正在生成…' : '输入问题体验流式回复'"
@submit="submit"
@cancel="abortActiveRequest"
/>
</main>
</template>
多会话版 App.vue 需要补充以下 import:
import { TrBubbleList, TrHistory, TrSender, type BubbleRoleConfig, type HistoryItem } from '@opentiny/tiny-robot'
import { useConversation } from '@opentiny/tiny-robot-kit'
import { computed, ref } from 'vue'
import { mockResponseProvider } from './mockResponseProvider'
布局调整为左侧会话列表、右侧聊天区:
<style scoped>
.app-shell {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr) auto;
width: min(1040px, calc(100% - 32px));
height: min(680px, calc(100vh - 64px));
margin: 32px auto;
overflow: hidden;
background: white;
border-radius: 16px;
box-shadow: 0 16px 48px #23395d14;
}
.history-panel {
grid-row: 1 / -1;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
padding: 20px 16px;
background: #f7f8fa;
border-right: 1px solid #e4e7ec;
}
.history-header,
.app-shell > header {
display: flex;
justify-content: space-between;
align-items: center;
}
.history-header {
padding: 0 8px 16px;
}
.new-chat {
padding: 6px 10px;
color: white;
font: inherit;
background: #1476ff;
border: 0;
border-radius: 6px;
cursor: pointer;
}
.history-list {
min-height: 0;
overflow: auto;
}
.app-shell > header {
grid-column: 2;
padding: 24px 24px 0;
font-weight: 700;
}
.app-shell > header small {
color: #667085;
font-weight: 400;
}
.messages {
grid-column: 2;
min-height: 0;
overflow: auto;
padding: 16px 24px;
}
.sender {
grid-column: 2;
margin: 0 24px 24px;
}
@media (max-width: 700px) {
.app-shell {
grid-template-columns: 1fr;
grid-template-rows: 180px auto minmax(0, 1fr) auto;
}
.history-panel {
grid-row: auto;
border-right: 0;
border-bottom: 1px solid #e4e7ec;
}
.app-shell > header,
.messages,
.sender {
grid-column: 1;
}
}
</style>
切换的是当前视图,不是把后台请求一起销毁
这一点比“能显示历史会话列表”更重要。
useConversation 内部会缓存当前需要工作的 Message Engine。切换会话时,它会确保目标会话的 Engine 已经存在,然后清理不再处理请求的非当前 Engine;仍处于 isProcessing 状态的 Engine 会继续保留。
因此可以出现这样的运行状态:
这意味着“切换会话”与“取消会话请求”是两件不同的事。切换本身不会取消后台请求;显式调用 abortActiveRequest、删除会话或清空会话等操作,才会中止对应的运行任务。
实际验证时,可以在会话 A 输入:
/long 演示多会话隔离
随后新建会话 B 并发送普通问题。B 回复结束后切回 A,可以看到 A 的长回复仍然在自己的 Engine 中继续更新。

第三步:用 Storage Strategy 让运行时跨刷新恢复
多会话解决的是运行时隔离,页面刷新以后还需要恢复会话和消息。
Kit 没有把持久化写死在 useConversation 内部,而是通过 Storage Strategy 暴露保存和加载边界。当前版本在没有显式传入 storage 时,会默认创建 LocalStorage 策略。
为了让 Demo 的持久化行为写得更明确,这里仍然显式传入默认策略:
const {
conversations,
activeConversation,
activeConversationId,
createConversation,
switchConversation,
updateConversationTitle,
deleteConversation,
sendMessage,
abortActiveRequest,
} = useConversation({
useMessageOptions: { responseProvider: mockResponseProvider },
storage: localStorageStrategyFactory(),
autoSaveMessages: true,
autoSaveThrottle: 500,
})
这里需要区分两个概念:
- LocalStorage Strategy 负责提供会话和消息的保存、加载能力;
autoSaveMessages决定消息发生变化时是否自动保存,默认值是false;autoSaveThrottle控制流式输出期间自动保存的节流间隔,避免每个 chunk 都触发一次持久化写入。
会话列表会从 Storage 中恢复;某个历史会话真正被切换到前台时,useConversation 再为它创建或恢复 Message Engine,并加载对应消息。这使“历史数据”和“当前正在工作的 Engine”不必始终一一常驻内存。
重命名和删除仍然走同一个会话边界
加入会话操作:
function renameConversation(title: string, item: HistoryItem) {
if (item.id) updateConversationTitle(item.id, title)
}
function handleHistoryAction(action: HistoryMenuItem, item: HistoryItem) {
if (action.id === 'delete' && item.id) {
void deleteConversation(item.id)
}
}
TrHistory 绑定对应事件:
<TrHistory
class="history-list"
:data="conversations as HistoryItem[]"
:selected="activeConversationId ?? undefined"
@item-click="openConversation"
@item-title-change="renameConversation"
@item-action="handleHistoryAction"
/>
删除会话时,Kit 会先中止该会话仍在运行的请求,再移除对应 Engine 和持久化数据。重命名则会同步更新会话元数据。
完成后可以验证:
- 创建多个会话并分别发送消息;
- 重命名其中一个会话;
- 刷新页面;
- 切换到历史会话,消息能够重新加载;
- 删除会话后刷新,已删除的数据不会再次出现。

历史消息很多时换成 IndexedDB
Storage Strategy 的价值在于,上层会话逻辑不需要关心实际存在哪里。
当历史消息量超过 LocalStorage 更适合承担的范围时,只需要替换策略:
storage: indexedDBStorageStrategyFactory({
dbName: 'tiny-robot-chat',
dbVersion: 1,
})
useConversation、Message Engine 和页面组件都不需要跟着重写。
第四步:替换 Provider,接入真实 SSE
到这里,消息和会话逻辑仍然运行在 Mock Provider 上。因为 Provider 与 Message Engine 解耦,接入真实接口只需要替换数据源。
Kit 提供了 sseStreamToGenerator,可以把标准 SSE Response 转换成异步生成器,让真实接口和前面的 Mock 使用相同的消费模型。
创建 src/sseResponseProvider.ts:
import { sseStreamToGenerator, type ResponseProvider } from '@opentiny/tiny-robot-kit'
export const sseResponseProvider: ResponseProvider = async (requestBody, signal) => {
const response = await fetch(`${import.meta.env.VITE_LLM_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${import.meta.env.VITE_LLM_API_KEY}`,
},
body: JSON.stringify({
...requestBody,
model: import.meta.env.VITE_LLM_MODEL,
stream: true,
}),
signal,
})
if (!response.ok) {
throw new Error(`模型请求失败:${response.status}`)
}
return sseStreamToGenerator(response, { signal })
}
这段 Provider 做了三件事:发起请求、检查 HTTP 状态、把 SSE 转换为 Kit 可消费的数据流。消息合并、请求状态和会话逻辑仍由原来的数据层处理。
在项目根目录创建 .env.local:
VITE_LLM_BASE_URL=https://your-llm-endpoint.example.com
VITE_LLM_API_KEY=your_api_key
VITE_LLM_MODEL=your_model_name
如果使用 DeepSeek、企业内部 AI 网关或其他兼容 Chat Completions 流式格式的服务,只需要把这里的地址、鉴权方式和模型名换成实际配置。
以
VITE_开头的变量会进入浏览器构建产物。这里为了缩短 Demo 链路才从浏览器直连模型服务;生产环境应把 API Key 放在服务端或 API 网关中。
Provider 切换只需要改 useConversation 的这一处配置:
- responseProvider: mockResponseProvider
+ responseProvider: sseResponseProvider
重新启动开发服务器后,页面层不需要其他改动。

为什么响应里出现 reasoning_content 后,页面能显示思考状态
这不是 SSE 层额外写了一套逻辑。
Message Engine 默认注册了 thinkingPlugin。当响应 chunk 中包含 reasoning_content 时,插件会更新当前助手消息的 state.thinking 和 state.open;思考内容结束后,再把对应状态收起。
也就是说,Provider 负责“把数据送进来”,Plugin 负责“看到某类数据后怎么扩展消息行为”,两者仍然是分开的。
从聊天到 Agent:插件机制把能力挂到同一条消息链路
Message Engine 除了处理普通文本,还提供插件生命周期。当前版本默认注册 thinkingPlugin 和 lengthPlugin,业务还可以按需加入 toolPlugin 等插件。
插件不是另起一套状态管理,而是参与同一条消息处理链路:请求前可以修改请求体,收到 chunk 时可以更新当前消息,请求结束后还可以决定是否追加消息并继续下一次请求。
Reasoning:thinkingPlugin
如果模型返回 reasoning_content,默认 thinkingPlugin 会维护思考状态。业务不需要这项行为时,可以用同名插件覆盖默认配置并禁用:
plugins: [thinkingPlugin({ disabled: true })]
自动续写:lengthPlugin
当模型返回 finish_reason: 'length' 时,默认 lengthPlugin 会追加一条续写消息并继续请求。
需要自定义续写指令时:
plugins: [
lengthPlugin({
continueContent: '请继续上一段回答。',
}),
]
这里的“继续请求”仍然发生在同一个 Message Engine 中,因此消息、请求状态和取消逻辑不需要再实现一遍。
Tool Calling:toolPlugin
toolPlugin 把模型工具调用接入消息生命周期。getTools 向模型提供可用工具,callTool 执行业务函数;工具结果形成 tool 消息后,Message Engine 可以继续下一轮模型请求。
plugins: [
toolPlugin({
getTools: () => [
{
type: 'function',
function: {
name: 'get_weather',
description: '查询城市天气',
parameters: {
type: 'object',
properties: {
city: { type: 'string' },
},
required: ['city'],
},
},
},
],
callTool: async (toolCall) => {
const { city } = JSON.parse(toolCall.function?.arguments || '{}')
return `${city}:晴,25℃`
},
}),
]
在真实业务中,callTool 可以进一步连接 MCP、企业 API、检索服务或其他领域能力。
一张图看懂完整运行链路
经过前面的几个步骤,Demo 已经从“一个输入框调用一个接口”演变成完整的对话运行时:
这套结构的价值并不是“少写几个 ref”,而是把变化最频繁的几部分拆开了:
- UI 可以替换,不影响消息运行时;
- 模型或业务后端可以替换,只需要适配 Provider;
- LocalStorage 可以换成 IndexedDB,不影响会话 API;
- 一个会话的请求状态不会和另一个会话混在一起;
- Reasoning、自动续写和工具调用通过插件进入同一条消息链路。
对于只有一个简单问答框的页面,可以直接使用 useMessage;当产品开始出现历史会话、后台生成和持久化需求,再把管理层提升到 useConversation。两者使用的是同一套 Message Engine 能力,不需要重做消息模型。
常见问题
页面能运行,但没有 TinyRobot 样式
确认入口文件已经引入组件库样式:
import '@opentiny/tiny-robot/dist/style.css'
接入真实模型后发送消息没有回复
先看浏览器 Network 面板:
- 请求是否成功返回;
- 响应是否为预期的流式格式;
- Provider 产出的 chunk 是否包含 Kit 可以消费的
choices数据; .env.local中的接口地址、鉴权和模型配置是否正确。
修改 Vite 环境变量后,需要重新启动开发服务器。
刷新后会话还在,但消息丢失
会话列表能够保存,不代表消息已经开启自动保存。确认:
autoSaveMessages: true
同时检查刷新前后使用的是不是同一套 Storage Strategy 和存储配置。
流式回复期间写入 LocalStorage 太频繁
通过 autoSaveThrottle 控制自动保存频率:
autoSaveThrottle: 500
具体值应根据消息长度、写入成本和业务对恢复实时性的要求调整。
点击停止后,网络请求仍然继续
abortRequest 会触发 Kit 管理的取消信号,但自定义 Provider 也必须把同一个 AbortSignal 继续传给实际网络请求和流解析:
fetch(url, { signal })
return sseStreamToGenerator(response, { signal })
切换到其他会话后,原会话为什么还在生成
这是 useConversation 的运行时设计:切换当前会话不会自动取消仍在 processing 的其他 Engine。
如果业务希望离开会话时立即停止生成,需要在自己的交互逻辑里显式调用对应的取消行为;删除会话时,Kit 会主动中止该会话仍在运行的请求。
什么时候适合把 Kit 放进项目
如果应用只有一次性的模型调用,没有流式消息、历史会话和复杂状态,直接调用接口通常已经足够。
当页面开始出现下面这些需求时,独立的数据层会越来越有价值:
- 同一条助手消息需要持续接收流式 chunk;
- 用户需要随时停止生成;
- 多个会话拥有独立的消息和请求状态;
- 会话切换后仍要保留后台运行任务;
- 历史消息需要跨页面刷新恢复;
- 后端可能在 Mock、企业网关和不同模型服务之间切换;
- Reasoning、自动续写、Tool Calling 等能力需要进入同一套消息生命周期。
TinyRobot Kit 的定位不是替代业务后端,也不是把 UI 和模型调用绑成一个黑盒,而是在两者之间提供稳定的数据运行层。对于 AI 对话类前端,这一层往往也是业务复杂度最先开始积累的地方。
关于 OpenTiny NEXT
OpenTiny NEXT 是一套企业智能前端开发解决方案,以生成式 UI 和 WebMCP 两大核心技术为基础,对现有传统的 TinyVue 组件库、TinyEngine 低代码引擎等产品进行智能化升级,构建出面向 Agent 应用的前端 NEXT-SDKs、AI Extension、TinyRobot 智能组件库、GenUI 等新产品,实现 AI 理解用户意图自主完成任务,加速企业应用的智能化改造。
欢迎加入 OpenTiny 开源社区。
OpenTiny 官网:opentiny.design
TinyRobot 代码仓库:github.com/opentiny/tiny-robot(欢迎 star ⭐)
如果你也想要共建,可以进入代码仓库,找到 good first issue 标签,一起参与开源贡献~如果你有任何问题,欢迎在评论区留言交流!
更多推荐




所有评论(0)