2. 项目核心:TinyRobot 要解决什么问题
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. 为什么我最终会从一个组件库开始做 AI 对话
1.1 从“调接口”到“研发提效”:重复造轮子的困局
以前团队接 AI 对话需求,最头疼的从来不是大模型接口,而是那段怎么都绕不开的对话 UI。你以为做个聊天窗口很简单,真做起来就发现:消息列表要维护滚动位置、流式输出要一帧一帧渲染、打字中的三个点要控制节奏、错误状态要设计重试按钮、Markdown 里的代码块要带高亮、长文本要支持复制和折叠、图片附件要能预览……一套完整聊天界面从零写到能上线,少说要三周,多则一个月。而且这些代码一旦散落在业务项目里,下次接新项目就是再写一遍,风格还不统一。
这个问题在单个项目里看不出来,一旦公司里同时有三五个客服系统、智能助手、Agent 后台在迭代,研发成本就成倍放大。更麻烦的是,每个团队各自维护一套聊天界面,最终产的 UI 规范、交互细节、无障碍支持全都不一样,用户反馈也说体验割裂。我一直在找一个真正“企业级”的对话方案,不是那种只能应付简单问答的 demo 组件,而是能把权限、审计、主题、多轮状态、Agent 工具调用这些复杂场景都接住的库。直到看到 OpenTiny 生态里的 TinyRobot,这个 Vue 3 企业级 AI 对话 UI 组件库,我才觉得路走通了。
1.2 为什么绑定 Vue 3 和 OpenTiny 生态
先说结论:选组件库,本质是选生态和维护策略。TinyRobot 挂在 OpenTiny 体系下,意味着它和 TinyVue 这套已开源多年的企业组件库天然同源,设计语言、编码规范、主题变量都是继承过来的,不用再纠结聊天组件和表单组件是不是两个风格。对于一个要服务多个业务线的中后台系统来说,这比随便找一个独立聊天组件要稳得多。
另外我从实践角度非常看重 Vue 3 的组合式 API。对话场景的状态非常碎:消息列表、输入框内容、发送状态、流式缓冲、滚动锁定、中断恢复、会话切换,如果用 options API 强行组织,各个 state 之间互相引用来引用去,超过 500 行基本就乱套。TinyRobot 选择 Vue 3 + TypeScript 组合式 API,把逻辑拆成可复用的 hooks,作为一个组件库,它的可扩展性决定了你后续能不能在它上面长出业务能力。这一点在后面的 Agent 场景里会体现得更明显。
2. TinyRobot 核心设计拆解:一个 AI 对话组件库的内在逻辑
2.1 组合式 API 架构与消息模型设计
TinyRobot 内部把整个对话看成一个“消息数组 + 派发器”模型,这个概念非常重要。所谓消息数组,就是所有聊天气泡、Markdown、工具卡片、状态提示,都统一抽象成 Message 对象,结构上类似:
json复制{
"id": "msg_1032",
"role": "assistant",
"status": "streaming",
"content": "正在查询订单状态...",
"type": "markdown",
"meta": {
"toolCall": "query_order",
"elapsed": 1200
}
}
这套模型的好处是,组件库本身并不关心业务里是什么类型的数据,它只需要根据 role 决定左右位置、根据 status 决定渲染样式、根据 type 决定用什么块去渲染。我理解 TinyRobot 特意保留了 meta 这个自由扩展字段,就是为了接 Agent 工具调用信息、流式 token 统计、业务埋点这类数据时,不需要改组件库源码,直接在业务层填充对象即可。
在这个基础上,所有的操作都变成对消息数组的追加、更新、删除和替换。这种单向数据流思路配合 Vue 3 的响应式系统,在并发场景下特别有用。比如同时有两条异步消息在返回,一条是用户问题对应的解析状态,一条是工具调用结果回传,最终展示层都能以消息 id 为准精准更新,不会出现内容串位。
2.2 流式渲染与滚动定位:体验的关键战场
对话 UI 里最影响体验的是流式输出。如果等大模型把整段答案全生成完再一次展示,用户会以为系统坏了,只好狂点“发送”,实际体验可以说是灾难。TinyRobot 在这块的处理逻辑值得借鉴:它并不是简单地把一个 textarea 的值直接绑到组件里,而是内部维护了一个流缓冲队列,通过事件驱动的方式,把服务端推送的增量文本按 order 顺序合并进目标消息。
具体到代码层面,如果你在业务侧对接流式接口,常用的方式是 fetch + ReadableStream:
typescript复制const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: currentMessages })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
chatRef.value.updateMessage('assistant', { append: text });
}
这里的 updateMessage 是 TinyRobot 暴露的关键方法,它会执行 diff 更新而不是整条消息重渲染,之所以这么设计,是因为整条替换会导致光标跳变和滚动闪烁,用户体验很差。我实际项目里踩过这个坑,所以提醒大家:做对话流式更新时,千万不要每次把整条 content 重新赋值,一定要走增量追加,这比组件库本身的设计还要优先级高。
滚动定位方面,TinyRobot 引入了“跟随模式”和“自由模式”的切换机制。默认情况下,如果用户没有手动滚动,组件会持续保持在底部,实时展示新内容。一旦用户向上翻阅历史消息,组件会暂停自动跟随,等用户主动点击“回到底部”按钮再恢复。实现这个动作在业务侧看很简单,但能在组件层面内置,对开发来说省了不少判断逻辑。
2.3 Agent 场景的扩展点:不只是聊天的聊天组件
Agent 应用和普通客服系统最大的区别在于,回答不再是单纯的文本流,而是伴随“思考过程”“工具调用”“执行结果”“人工确认”等多类型节点。纯 Markdown 展示撑不住这种结构。TinyRobot 在这个方向上做得比较聪明,它把消息的消息类型 type 扩展成可插拔的技术方案。
组件库内置了 text、markdown、card、table、image 等常用消息块,同时允许自定义块注入。Agent 场景里常见的一个做法是,把一次工具调用包装成一张“任务卡片”,卡片里包含调用参数、执行状态、耗时、返回摘要,用户可以点开折叠查看详细步骤。这种卡片样式在 TinyRobot 里不需要改组件库,只需要传入:
vue复制<template>
<tiny-chat :messages="messages">
<template #message-tool-call="{ message }">
<TaskCard :data="message.meta" />
</template>
</tiny-chat>
</template>
这其实利用了 Vue 3 的 slot 机制,把类型订阅和插槽渲染绑定在一起。支持这种插槽设计,等于把整个对话界面彻底解耦了,未来不管 Agent 的形态怎么迭代,组件库都能应付。这也是我判断一个对话组件能不能长期使用的重要标准:它允不允许你在不 fork 源码的情况下扩展消息类型。
3. 快速上手:用 TinyRobot 搭出一个能用的智能助手
3.1 环境准备与安装
TinyRobot 基于 Vue 3,所以前提是项目本身已经是 Vue 3 环境。如果你正在用 Vue 2,想迁移过来会麻烦很多,我的建议是直接评估重构成本。另外因为组件库大量使用了组合式 API 和模板类型推导,Node 版本尽量保持在 18 以上,太低会导致依赖安装和编译报错。
安装非常简单:
bash复制npm install @opentiny/tinyrobot
# 或者
pnpm add @opentiny/tinyrobot
如果你项目本身已经用了 @opentiny/tinyvue,那主题变量天然兼容,主题切换几乎零成本。如果没装过 TinyVue 也没关系,TinyRobot 会依赖一套基础样式,但不会强制要求你全量引入整个 TinyVue。
3.2 核心代码:几十行启动一个对话窗口
基础用法非常直观:
vue复制<script setup lang="ts">
import { ref } from 'vue';
import { TinyChat, useChat } from '@opentiny/tinyrobot';
const { messages, sendMessage, loading } = useChat({
api: '/api/chat/stream',
// 是否在发送前自动清空输入框
autoClearInput: true,
});
const handleSend = (text: string) => {
sendMessage(text);
};
</script>
<template>
<div style="height: 600px; border: 1px solid #e0e0e0">
<tiny-chat
v-model:messages="messages"
:loading="loading"
title="智能助手"
placeholder="请输入你的问题..."
@send="handleSend"
/>
</div>
</template>
如果不希望组件内部去请求后端,而是完全自己处理 HTTP 流,可以把 api 参数去掉,改成监听 send 事件,然后用上面提到的 updateMessage 手动塞回响应:
vue复制<script setup lang="ts">
import { ref } from 'vue';
import { TinyChat } from '@opentiny/tinyrobot';
const chatRef = ref();
const messages = ref([]);
const handleSend = async (text: string) => {
// 用户消息会由组件自动插入
const res = await fetch('/custom/chat/stream', {
method: 'POST',
body: JSON.stringify({ prompt: text }),
headers: { 'Content-Type': 'application/json' }
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const delta = decoder.decode(value, { stream: true });
chatRef.value.updateMessage('assistant', { append: delta });
}
};
</script>
两种模式建议项目初期都试一下。如果后端是标准 SSE 协议且稳定,推荐用内置 api 模式,省代码;如果后端格式不够规范、或者需要做复杂的鉴权签名,用手动模式更灵活。我个人偏向后端契约不稳定的情况走到手动模式,代码多一点,但故障排查时非常清晰。
3.3 快速上手阶段的关键参数说明
TinyRobot 对外暴露的参数不少,但真正初期需要关注的就那么几个。我把常用配置整理成了下面的表,你可以直接对照着手:
| 参数 | 类型 | 默认值 | 作用 | 建议 |
|---|---|---|---|---|
| messages | Message[] | [] | 消息数组,控制整个对话内容 | 使用 ref 管理,避免直接替换数组 |
| api | string | - | 内部流式请求地址 | 接非标准协议时留空,手动控制 |
| loading | boolean | false | 控制输入框禁用和转圈状态 | 与发送状态关联 |
| title | string | 智能助手 | 顶部标题,支持插槽替换 | 客服系统里可显示坐席名称 |
| placeholder | string | - | 输入框占位提示 | 按场景写,例如“描述您遇到的问题” |
| autoClearInput | boolean | true | 发送后是否清空输入框 | 一般保持默认 |
| scrollMode | string | auto | auto 为跟随底部,manual 为自由滚动 | 涉及大量历史记录时建议手动触发 |
这里面 scrollMode 容易被忽略。如果你在做一个知识库问答系统,用户经常需要翻看之前答案,然后复制关键内容,默认 auto 模式会一直在用户翻页时把它拽回底部,所以适当切换到 manual 或者增加“回到底部”悬浮按钮,是很重要的产品决策。
3.4 首次接入时最容易忽略的样式层级问题
我见过不少同学第一次接入 TinyRobot,部署完发现组件样式被自己的全局 CSS 覆盖,或者组件宽度撑不开。这种问题通常和 scoped 样式、CSS 优先级有关。TinyRobot 的根节点一般带一个 .tiny-robot 等类名前缀,建议业务侧不要用全局通配符选择器去覆盖内部元素,尤其是 .chat-header、.message-item 这类内部类。规范做法是给外层容器单独设置类名,再进行内容样式覆盖。
另外如果组件高度没有限制,聊天区默认会撑高父容器,所以务必在外层设置高度,比如 height: calc(100vh - 180px),否则页面滚动条和内部滚动条会打架,体验非常别扭。这个细节不算是 bug,但不注意的话真的会排查很久。
4. 进阶实战:从智能助手到客服系统与 Agent 应用
4.1 客服系统:多轮会话与会话切换如何落地
客服系统比单助手复杂在“会话是多个的”。用户可能开了三个会话,分别咨询售前、售后和技术支持。TinyRobot 本身是一个对话窗口组件,但我们可以通过 v-model 绑定 messages 数组,在切换会话时直接替换 messages 引用的数据源即可。
实战里我会做一个会话列表,每一行存一条 session 记录,包含 sessionId、title、lastMessage、unread 等字段。点击会话列表项时,调用 chatRef.value.setMessages(session.messages) 或者直接重新赋值绑定值。这里的关键点是,每个会话的滚动位置要单独记住,否则切换回来时用户会看到一片空白或定位在最后一条。经验做法是给每个会话维护一个 scrollTop 字段,在切换时调用组件的滚动 API 恢复。
客服系统还有一个高频需求是“转人工”。由于 TinyRobot 是纯前端组件,它不能直接强制接入人工,但可以通过消息类型定制实现一个“转人工卡片”,卡片上放按钮,用户点击后触发业务接口,把会话状态改为人工接待,同时 via 插槽展示一个在线坐席的信息卡片。这种方式既保留了组件库的通用性,又把业务语义融进去了。
4.2 Agent 应用:把工具调用状态呈现出来
Agent 应用和普通客服系统最大的不同在于,它的回复过程往往是多步骤的。比如用户问“帮我查一下 4 月份的订单金额”。Agent 内部可能先调用意图识别工具,再调订单查询工具,最后汇总成表格。这个过程如果只靠流式文本,用户根本不知道系统在干嘛,会不断重复提问,体验极差。
TinyRobot 在对接 Agent 场景时,我的设计思路是:把 Agent 的每一次工具调用封装成一条单独的消息 type,叫 tool-call,内容是调用参数和执行状态,展示成一张可折叠卡片。当工具执行完成后,利用 updateMessage 更新卡片状态,比如把 status 从 running 改成 success,并附带执行耗时和结果摘要。最终大模型生成的答案再作为一条 markdown 消息追加到消息列表尾部。
这里说一下为什么这种 UI 设计和最终答案分开更合理。如果只把最终答案展示给用户,中间过程的信息全部丢失,用户会很疑惑“系统到底怎么算出来的”。如果只展示中间过程而不给最终答案,又显得非常冗长。所以把它们拆成两条不同 type 的消息,既保留了过程的可解释性,又让重点落在答案上。这种模式在 Agent 越来越复杂的趋势下几乎是必须的。
4.3 主题定制与企业级视觉规范
企业级组件库最重要的能力之一是品牌定制。TinyRobot 基于 CSS 变量做主题,这意味着你可以不改源码,只通过覆盖变量来调整整套视觉。常见的变量包括主色、气泡背景色、字体、圆角、间距等。
css复制:root {
--tr-primary-color: #4b6bff;
--tr-user-bubble-bg: #eef3ff;
--tr-assistant-bubble-bg: #ffffff;
--tr-border-radius: 12px;
--tr-font-size: 14px;
}
这套 CSS 变量机制在搭建企业内部系统时非常省心。比如给甲方的项目,把主色改成品牌蓝,登录背景配上企业 logo,一下就贴近了客户的心理预期。暗黑模式则可以通过在 html.dark 下重新覆盖变量实现,不需要逐组件适配。
注意一点,组件库内部的 CSS 变量命名在不同版本可能会调整,项目接进去后建议把自定义主题变量收敛到一个独立 theme.css 文件里,方便升级依赖时统一处理。我第一次升级时因为直接改了 node_modules 里的样式变量,结果一个小版本更新导致全部覆盖丢失,白白排除了半天问题。以后一律通过业务侧变量覆盖,避免直接改库内文件。
4.4 历史会话持久化与埋点审计
企业级应用里,历史聊天记录不能只是在内存中存着。无论组件库怎么设计,最后落地聊天记录都要走服务端持久化。我的方案是:每次 messages 数组有变更时,通过 watch 监听并做节流,把变更记录发送到后端;进入会话时,后端按分页倒序返回历史记录,前端再调 setMessages 初始化。
消息埋点在合规场景下尤其重要。像客服系统的质检、Agent 过程的审计,以及后续算法团队的 case 复盘,都需要知道每一轮输入输出、耗时、成功率。这些数据建议不要在组件库内部写死,而是通过消息的 meta 字段携带,增加一个监听器统一收集。TinyRobot 的 message 对象支持自定义 meta 字段,实际上等于提供了一个天然的埋点载体,用好了后面接数据平台能省一半工作量。
5. 常见问题与排查技巧实录
5.1 流式输出中断,界面卡在 chunk 半截
最常见的线上问题是流式请求中途断开。用户看到回答到一半就停了,加载动画也没了,整个对话窗口处于一种“半死不活”的状态。这种问题大多是后端网关超时或者网络抖动导致 SSE 连接被切断。前端的排查思路是,先区分是连接被服务端主动关闭还是客户端网络异常,可以通过监听 onerror 事件记录异常码。
解决方式上,首先在 UI 上要明确提供“重新生成”或“继续生成”的入口。其次,前后端要建立重试协议:前端收到流中断信号时,携带 lastMessageId 重新发起请求,让后端基于已有文段续生成,而不是从零开始。这里要特别注意,不要在 fetch 的 reader 读取外层直接套一个无脑 try/catch 然后静默吞掉错误,一定要先判断已消费文本的长度,再做追加或重试的决策。
5.2 长对话后页面卡死或输入卡顿
聊天组件用久了,messages 数组会越来越大。一旦超过几百条消息,每次响应式更新都会引起性能问题。尤其是把整条 messages 用 v-model 双向绑定,更新一个小字段会触发大范围 diff。这时候要做的是把列表性能优化放到首位。
TinyRobot 在内部已经做了虚拟滚动,也就是只渲染可视区域的 DOM 节点,不在视口内的消息会被回收。但即便如此,业务侧把无关数据塞进 message 对象也会加重更新开销,比如把 2MB 的原始响应 JSON 塞进 meta 字段。建议只保存需要的字段,大头数据通过消息 id 关联到外部 Map 存储。另外一个技巧是,对于超过一定长度的历史记录,仅保留摘要文本,详情内容通过点击“加载更多”或单独请求查看。
5.3 自定义 slot 不生效,卡片渲染不出来
这种问题基本都指向类型匹配错误。TinyRobot 的插槽命名是和消息 type 关联的,比如 #message-tool-call 对应的必须是一条 type 为 tool-call 的消息。如果 type 写成了 toolCall 或者自定义类型名没注册,slot 自然捕不到。遇到时先用 console 打印每条消息的完整对象,确认 type 字段是否匹配,同时查看组件是否对该类型做了内置渲染兜底。
第二个坑是 Vue 的 slot 作用域。自定义卡片需要的 message 数据,在模板里取不到,多半是因为 slot 的传递方式被忽略。正确的写法是把 message 对象作为 slot props 传下去,而不是在 slot 内部直接访问外层变量。这块建议参考组件库文档给的插槽示例,不要凭经验猜。
5.4 中文文本长按选择穿透问题
聊天消息里用户经常要复制答案,但 Markdown 渲染出来的代码块或表格,长按选择时会发现选中不了,或者选中后拖拽时触发气泡拖拽。这个问题的根源是消息容器上可能监听了点击或者移动事件。排查时先看是否是组件自带的 draggable 能力被意外开启。如果项目本身没有拖拽需求,可以显式关闭相关配置项,给 message-content 设置 user-select: text,确保文本选择优先级高于其他操作。
这类问题往往不会在验收文档里写,只有真实用过一段时间才会遇到。我一般会在组件接入的第一周,专门让测试同学做长文本复制、代码块选择、混合图文消息的回归测试,把体验细节提前收口。
5.5 对接多个大模型服务时如何保持状态同步
现在很多项目的后端不只接一个大模型,而是根据用户问题的类型路由到不同模型。这就导致某些模型返回的是一个标准 Markdown,另一个模型返回的是带思考链的流式 JSON。TinyRobot 本身不关心模型是谁,只关心消息数组里 type 和 content。所以我在这种场景下的做法是,在后端统一把模型的输出转换成标准的消息事件协议,比如 SSE 事件分三种:
json复制{ "event": "token", "data": "增量文本" }
{ "event": "tool_call", "data": { "name": "search_order", "status": "running" } }
{ "event": "done", "data": { "messageId": "msg_1032", "elapsed": 3200 } }
前端根据 event 类型,分别调用 updateMessage 或直接 push 一条 tool-call 消息。这样做的好处是,TinyRobot 前端的逻辑完全不用跟着模型品牌走,以后接再多的模型,界面层都是稳定的。这个角度上,TinyRobot 的职责更像一个“对话视图层框架”,而不是某个 AI 服务的封装器。
6. 想继续深入学习,我建议你关注的几个方向
如果你准备在正式项目里全面使用 TinyRobot,除了官方文档里那些 API 用例之外,我建议你再花点时间去研究三个方向:第一是它的虚拟列表实现原理,因为对话列表的滚动位置维护是未来最影响体验的点;第二是它的插槽和消息类型注册机制,这是你能否在业务里灵活扩展的关键;第三是它的主题变量体系,理解了变量如何继承和覆盖,你在多个项目里做品牌定制时就会非常顺手。
另外,当你把它接入客服系统,和 TinyVue 的表单、审批、流程组件结合使用时,你会发现整个 OpenTiny 生态对一个中后台系统来说几乎是无缝覆盖的。这些都是我实际用下来觉得值得深入研究的地方。
