多会话 + 流式输出怎么落地?TinyRobot Kit 对话数据层深度解析

0 阅读16分钟

其实实现一个基础的 AI 对话页面,通常会从维护消息数组、调用模型接口和渲染结果开始。

但真正进入产品开发后,复杂度很快会从“调一次接口”扩散到整个页面:流式内容需要持续合并,请求需要能够停止,多轮上下文要正确组织,不同会话要隔离运行状态,页面刷新后还要恢复历史消息。如果这些逻辑都直接堆在 Vue 组件里,请求、消息和 UI 状态会越来越难拆开。

@opentiny/tiny-robot-kit(下文简称 Kit)是 TinyRobot 的 AI 对话数据层工具包。它不负责决定页面长什么样,而是把消息生命周期、会话运行时、后端响应适配、持久化和插件扩展组织成几块可以独立组合的能力。

本文不会只按 API 顺序介绍 useMessage 和 useConversation。我们会从一个可运行的 Vue 3 Demo 出发,先看 Kit 如何划分职责,再逐步验证几件更接近真实产品的问题:

  • 单个会话如何处理流式回复和请求取消;
  • 后端接口变化时,如何复用已有的消息状态管理;
  • 多个会话如何拥有彼此独立的消息运行时;
  • 切换会话时,仍在生成的后台会话为什么不会被自动取消;
  • 会话和消息如何通过存储策略持久化;
  • 推理内容、自动续写和工具调用如何进入同一条消息处理链路。

本文最终 Demo 演示如下,源码地址:github.com/opentiny/ti…

1.gif

AI 对话的数据层,究竟在管理什么

把聊天页面抽象成一个 messages 数组,只能覆盖“已经拿到结果以后怎么显示”的部分。真正需要长期维护的是一套运行时状态:当前请求处于什么阶段、正在更新哪条助手消息、某个会话是否仍在后台生成、什么时候保存消息、收到模型的特殊字段后应该触发什么行为。

Kit 将这些职责拆成五个边界:

能力在 Kit 中的角色解决的问题
useMessage单会话消息引擎消息、请求状态、流式合并、取消请求
useConversation多会话管理层会话列表、消息引擎创建与切换、后台运行状态
responseProvider响应适配边界将业务后端或模型服务适配成单次结果或异步数据流
存储策略持久化边界会话元数据和历史消息的保存、恢复
插件消息处理扩展推理内容、自动续写、工具调用等

它们之间的关系可以概括为:

flowchart LR
  UI[聊天 UI] --> Conversation[useConversation]
  UI --> Message[useMessage]

  Conversation --> EngineA[Message Engine A]
  Conversation --> EngineB[Message Engine B]
  EngineA --> Provider[responseProvider]
  EngineB --> Provider
  Message --> Provider

  Provider --> Backend[业务后端 / 模型服务]
  Conversation <--> Storage[Storage Strategy]
  Plugins[Plugins] -.挂载到消息处理链路.-> EngineA
  Plugins -.挂载到消息处理链路.-> EngineB

本文把 useMessage 管理的一套单会话消息运行时简称为消息引擎(Message Engine)。它维护消息、请求状态、流式合并和取消操作。useConversation 负责管理多个消息引擎,responseProvider 隔离后端协议,存储策略隔离持久化实现,UI 只消费响应式状态。

后面的 Demo 都围绕这几个边界展开。

环境准备

本文基于 @opentiny/tiny-robot-kit 0.5.1,面向熟悉 Vue 3 基础用法的前端开发者。

本文 Demo 使用以下开发环境:

  • 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;消息引擎负责把增量内容合并到当前助手消息中。

第二,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>

页面布局与本文关注的 Kit 消息处理逻辑相互独立。单会话示例的组件布局样式见 src/App.vue 的 <style scoped>,全局基础样式见 src/style.css。

从页面视角看,状态只是在不断变化;从数据层视角看,实际发生的是一条完整的请求链路:

sequenceDiagram
  participant UI as Vue UI
  participant Engine as useMessage / Message Engine
  participant Provider as responseProvider

  UI->>Engine: sendMessage(content)
  Engine->>Provider: requestBody + AbortSignal
  Provider-->>Engine: chunk 1
  Engine-->>UI: 更新 messages / isProcessing
  Provider-->>Engine: chunk 2...n
  Engine-->>UI: 持续更新当前助手消息
  UI->>Engine: abortRequest()
  Engine-->>Provider: AbortSignal

运行:

pnpm dev

发送消息后,可以看到回复内容持续写入同一条助手消息;生成过程中点击停止或等待请求结束,输入区随即恢复。

4.gif

关键边界:Response Provider 隔离后端协议

在进入多会话管理之前,先单独看一下前面使用的 responseProvider。它是消息引擎与业务后端之间的适配边界,前面的页面不需要知道响应来自 Mock 还是真实服务。

ResponseProvider 的职责很窄。它接收当前请求体和 AbortSignal,然后返回 Kit 可以消费的单次结果或异步数据流:

export type ResponseProvider<T = ChatCompletion> = (
  requestBody: MessageRequestBody,
  abortSignal: AbortSignal,
) => AsyncStreamableResult<T>

因此,Mock、企业内部 AI 网关、OpenAI 兼容 API 或其他 SSE 服务,都可以在这一层完成适配。只要 Provider 最终产出的数据符合消息引擎的消费格式,上层的:

  • messages;
  • isProcessing;
  • abortRequest;
  • 多会话管理;
  • UI 绑定;

都可以继续复用。

这也是后面从 Mock 切到真实 SSE 时,通常只需调整 Provider 适配层,就能继续使用同一套会话逻辑的原因。

第二步:用 useConversation 组织多个独立消息引擎

一个会话对应一个消息引擎。加入历史会话后,需要进一步解决三个问题:

  1. 每个会话的消息和请求状态如何隔离;
  2. 切换历史会话时,哪个消息引擎应该进入当前视图;
  3. 离开当前会话以后,仍在生成的请求是否继续保留。

useConversation 就位于这一层。它为每个会话创建并管理独立的消息引擎,并通过 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 主要保存会话元数据;真正运行消息请求的是每个会话自己的消息引擎。

用长回复验证会话隔离

为了让后台运行状态更容易观察,把 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'

多会话示例采用左侧会话列表、右侧聊天区的布局,布局样式见 src/App.vue 的 <style scoped>。

切换的是当前视图,不是把后台请求一起销毁

这一点比“能显示历史会话列表”更重要。

useConversation 内部会缓存当前需要工作的消息引擎。切换会话时,它会确保目标会话的消息引擎已经存在,然后清理其他会话中已经完成请求的消息引擎;仍处于 isProcessing 状态的消息引擎会继续保留。

因此可以出现这样的运行状态:

flowchart LR
  ConversationA[会话 A] --> EngineA[Engine A\nprocessing]
  ConversationB[会话 B\n当前视图] --> EngineB[Engine B\nprocessing]
  ConversationC[会话 C] --> StorageC[仅保留持久化数据]

  EngineA -.后台继续生成.-> Provider
  EngineB --> Provider

这意味着“切换会话”与“取消会话请求”是两件不同的事。切换本身不会取消后台请求;显式调用 abortActiveRequest、删除会话或清空会话等操作,才会中止对应的运行任务。

实际验证时,可以在会话 A 输入:

/long 演示多会话隔离

随后新建会话 B 并发送普通问题。B 回复结束后切回 A,可以看到 A 的长回复仍然在自己的消息引擎中继续更新。

6.gif

第三步:用存储策略让运行时跨刷新恢复

多会话解决的是运行时隔离,页面刷新以后还需要恢复会话和消息。

Kit 没有把持久化写死在 useConversation 内部,而是通过存储策略(Storage Strategy)暴露保存和加载边界。当前版本在没有显式传入 storage 时,会默认创建 LocalStorage 策略。

以下示例显式传入默认策略,便于直接查看持久化配置:

const {
  conversations,
  activeConversation,
  activeConversationId,
  createConversation,
  switchConversation,
  updateConversationTitle,
  deleteConversation,
  sendMessage,
  abortActiveRequest,
} = useConversation({
  useMessageOptions: { responseProvider: mockResponseProvider },
  storage: localStorageStrategyFactory(),
  autoSaveMessages: true,
  autoSaveThrottle: 500,
})

这里需要区分三个概念:

  • LocalStorage 策略负责提供会话和消息的保存、加载能力;
  • autoSaveMessages 决定消息发生变化时是否自动保存,默认值是 false;
  • autoSaveThrottle 控制流式输出期间自动保存的节流间隔,避免每个 chunk 都触发一次持久化写入。

会话列表会从存储策略中恢复;某个历史会话真正被切换到前台时,useConversation 再为它创建或恢复消息引擎,并加载对应消息。因此,历史会话数据和当前正在工作的消息引擎不必全部同时常驻内存。

LocalStorage 和 IndexedDB 适合 Demo 或非敏感数据。生产环境如涉及用户隐私或企业内部信息,建议通过自定义存储策略接入服务端存储,并结合访问控制、加密、数据保留期限和清理机制。

重命名和删除仍然走同一个会话边界

加入会话操作:

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 会先中止该会话仍在运行的请求,再移除对应的消息引擎和持久化数据。重命名则会同步更新会话元数据。

完成后可以验证:

  1. 创建多个会话并分别发送消息;
  2. 重命名其中一个会话;
  3. 刷新页面;
  4. 切换到历史会话,消息能够重新加载;
  5. 删除会话后刷新,已删除的数据不会再次出现。

7.gif

历史消息很多时换成 IndexedDB

存储策略的价值在于,上层会话逻辑不需要关心数据实际存储在哪里。

当历史消息量超过 LocalStorage 更适合承担的范围时,可以替换为 IndexedDB 策略:

storage: indexedDBStorageStrategyFactory({
  dbName: 'tiny-robot-chat',
  dbVersion: 1,
})

替换存储策略时,useConversation、消息引擎和页面组件的调用方式保持不变。

第四步:替换 Provider,接入真实 SSE

到这里,消息和会话逻辑仍然运行在 Mock Provider 上。因为 Provider 与消息引擎解耦,接入真实接口时可以在 Provider 层替换数据源。

Kit 提供了 sseStreamToGenerator,可以把服务器发送事件(Server-Sent Events,SSE)的 Response 转换成异步生成器,让真实接口和前面的 Mock 使用相同的消费模型。

**安全提示:**下面的浏览器直连方式只用于缩短本地 Demo 链路。以 VITE_ 开头的变量会进入浏览器构建产物,不能用于保管生产环境 API Key。生产环境应由服务端或 API 网关持有模型凭据,浏览器只调用受控的业务接口。

创建 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 流式格式的服务,可以在本地 Demo 中把地址、鉴权方式和模型名换成实际配置。

Provider 切换通常只需调整 useConversation 的这一处配置:

- responseProvider: mockResponseProvider
+ responseProvider: sseResponseProvider

重新启动开发服务器后,页面层可以继续复用原有的消息与会话绑定。

8.gif

为什么响应里出现 reasoning_content 后,页面能显示推理内容

这不是 SSE 层额外写了一套逻辑。

消息引擎默认注册了 thinkingPlugin。当响应 chunk 中包含 reasoning_content 时,插件会更新当前助手消息的 state.thinking 和 state.open;推理内容结束后,再把对应状态收起。

也就是说,Provider 负责“把数据送进来”,Plugin 负责“看到某类数据后怎么扩展消息行为”,两者仍然是分开的。

从聊天到智能体:插件机制把能力挂到同一条消息链路

消息引擎除了处理普通文本,还提供插件生命周期。当前版本默认注册 thinkingPlugin 和 lengthPlugin,业务还可以按需加入 toolPlugin 等插件。

插件不是另起一套状态管理,而是参与同一条消息处理链路:请求前可以修改请求体,收到 chunk 时可以更新当前消息,请求结束后还可以决定是否追加消息并继续下一次请求。

推理内容:thinkingPlugin

如果模型返回 reasoning_content,默认 thinkingPlugin 会维护推理内容的展示状态。业务不需要这项行为时,可以用同名插件覆盖默认配置并禁用:

plugins: [thinkingPlugin({ disabled: true })]

自动续写:lengthPlugin

当模型返回 finish_reason: 'length' 时,默认 lengthPlugin 会追加一条续写消息并继续请求。

需要自定义续写指令时:

plugins: [
  lengthPlugin({
    continueContent: '请继续上一段回答。',
  }),
]

这里的“继续请求”仍然发生在同一个消息引擎中,因此可以继续复用已有的消息、请求状态和取消逻辑。

工具调用:toolPlugin

toolPlugin 把模型工具调用接入消息生命周期。getTools 向模型提供可用工具,callTool 执行业务函数;工具结果形成 tool 消息后,消息引擎可以继续下一轮模型请求。

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 已经覆盖对话运行时的核心链路:

%%{init: {&#34;flowchart&#34;: {&#34;nodeSpacing&#34;: 22, &#34;rankSpacing&#34;: 30, &#34;curve&#34;: &#34;linear&#34;}}}%%
flowchart TB
  Sender[TrSender<br/>用户输入] --> Conversation[useConversation]

  Conversation -->|管理| Pool[Message Engine 运行池<br/>当前会话 / 后台会话]
  Conversation <-->|持久化| Storage[Storage Strategy<br/>LocalStorage / IndexedDB]

  Pool --> Runtime[Engine 独立运行<br/>Plugins + responseProvider]
  Runtime <-->|请求 / 流式响应| Backend[模型服务<br/>业务 AI 网关]

  Runtime --> View[响应式 messages / request state<br/>由 TrBubbleList/TrSender 消费]

这套结构把变化较频繁的几部分拆开了:

  • UI 调整时可以继续复用消息运行时;
  • 模型或业务后端变化时,通常只需适配 Provider;
  • LocalStorage 可以换成 IndexedDB,上层会话 API 的调用方式保持不变;
  • 一个会话的请求状态不会和另一个会话混在一起;
  • 推理内容、自动续写和工具调用通过插件进入同一条消息链路。

对于只有一个基础问答框的页面,可以直接使用 useMessage;当产品开始出现历史会话、后台生成和持久化需求,再使用 useConversation 管理多个消息引擎。两者复用同一套消息模型和运行能力。

常见问题

页面能运行,但没有 TinyRobot 样式

确认入口文件已经引入组件库样式:

import '@opentiny/tiny-robot/dist/style.css'

接入真实模型后发送消息没有回复

先看浏览器 Network 面板:

  • 请求是否成功返回;
  • 响应是否为预期的流式格式;
  • Provider 产出的 chunk 是否包含 Kit 可以消费的 choices 数据;
  • .env.local 中的接口地址、鉴权和模型配置是否正确。

修改 Vite 环境变量后,需要重新启动开发服务器。

刷新后会话还在,但消息丢失

会话列表能够保存,不代表消息已经开启自动保存。确认:

autoSaveMessages: true

同时检查刷新前后使用的是不是同一套存储策略和存储配置。

流式回复期间写入 LocalStorage 太频繁

通过 autoSaveThrottle 控制自动保存频率:

autoSaveThrottle: 500

具体值应根据消息长度、写入成本和业务对恢复实时性的要求调整。

点击停止后,网络请求仍然继续

abortRequest 会触发 Kit 管理的取消信号,但自定义 Provider 也必须把同一个 AbortSignal 继续传给实际网络请求和流解析:

fetch(url, { signal })
return sseStreamToGenerator(response, { signal })

切换到其他会话后,原会话为什么还在生成

这是 useConversation 的运行时设计:切换当前会话不会自动取消 isProcessing 仍为 true 的其他消息引擎。

如果业务希望离开会话时立即停止生成,需要在自己的交互逻辑里显式调用对应的取消行为;删除会话时,Kit 会主动中止该会话仍在运行的请求。

什么时候适合把 Kit 放进项目

如果应用只有一次性的模型调用,没有流式消息、历史会话和复杂状态,直接调用接口通常已经足够。

当页面开始出现下面这些需求时,独立的数据层会越来越有价值:

  • 同一条助手消息需要持续接收流式 chunk;
  • 用户需要随时停止生成;
  • 多个会话拥有独立的消息和请求状态;
  • 会话切换后仍要保留后台运行任务;
  • 历史消息需要跨页面刷新恢复;
  • 后端可能在 Mock、企业网关和不同模型服务之间切换;
  • 推理内容、自动续写、工具调用等能力需要进入同一套消息生命周期。

TinyRobot Kit 的定位不是替代业务后端,也不是把 UI 和模型调用绑成一个黑盒,而是在两者之间提供稳定的数据运行层。对于 AI 对话类前端,这一层往往也是业务复杂度最先开始积累的地方。

关于 OpenTiny

OpenTiny 是面向企业级场景的前端 AI 应用开发平台。平台依托生成式 UI与WebMCP两大核心底层技术,打造适配 Agent 智能应用生态的 NEXT-SDKs、AI 扩展工具、TinyRobot 对话 AI 组件库、GenUI SDK生成式 UI 、WebAgent 智能代理等系列产品,能够依托 AI 解析用户需求意图,自动驱动任务执行,助力企业现有业务系统快速完成智能化升级。

同时 OpenTiny 沉淀成熟通用前端基建能力:提供跨端、跨框架、跨版本的 TinyVue 组件库,配套基于 Angular+TypeScript 技术栈的 TinyNG 组件库;内置可高度自定义拓展的低代码引擎 TinyEngine,搭配 TinyPro 中后台业务模板、TinyCLI 命令行工具等配套工具链,全方位赋能开发者高效搭建各类 Web 端企业应用。

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design
GenUI SDK 代码仓库:github.com/opentiny/ge… (欢迎star ⭐)

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~如果你有任何问题,欢迎在评论区留言交流!