使用 AIKit 构建的聊天示例(浅色主题)

一个聊天统治所有:我们基于 Gravity UI 构建了 AI 助手库
一个聊天统治所有:我们基于 Gravity UI 构建了 AI 助手库
一篇关于 AIKit 库发布的文章:我们在开发过程中关注的重点、为什么需要它,以及如何在您自己的项目中使用它。
过去一年我们看到 AI 助手迎来爆发,这也影响到了 Yandex Cloud 的界面:要么在技术支持里出现了接入模型的聊天机器人,要么在控制台里出现了用于日常操作的代理。各团队会接入模型、设计对话逻辑、画 UI 设计、搭建聊天界面——而且都是各自为战。
不同团队都基于同一个框架 Gravity UI 来构建界面,但随着变体越来越多,维护统一的用户体验变得困难。同时,大家也越来越频繁地发现,自己在反复花时间解决同样的问题。
为了不再每次都重复造轮子,我们把积累的实践沉淀成统一的方法,并做了一个面向 AI 聊天机器人的工具——@gravity‑ui/aikit。它能在几天内创建一个完整的助手界面,并且还能轻松适配不同的场景。

我叫伊利亚·洛姆捷夫,是 Yandex Cloud Foundation Services 团队的高级开发工程师。在这篇文章里我会讲讲:我们为什么决定打造 AIKit、它的内部结构是什么样、未来的一些计划——以及你可以在自己的项目里试用些什么。
我们如何以及为什么做了 AIKit
过去一年,Yandex Cloud 中带有 AI 助手的服务数量增长了,例如:
-
SourceCraft 中的 Code Assistant Chat——助手帮助开发者编写代码;在 AI 代理模式下还能创建与配置仓库、启动 CI/CD 流程、回答文档问题并自动化任务;也能管理 issues、pull requests,并对代码进行操作:解释、创建与编辑文件。
-
云控制台中的 AI 助手——一个用于管理 Yandex Cloud 资源的助手。核心目标是在隐藏与 API 和工具交互复杂性的前提下,帮助用户快速且安全地配置、变更与管理云基础设施。
生态里出现了十来个聊天界面,每个都有自己的逻辑、自己的消息格式,以及一套 corner case。
我们发现各团队会遇到一组大致相同的任务。大多数场景需要的是:
-
规范地展示用户与助手消息;
-
正确组织回答的流式输出(streaming);
-
展示「助手正在输入」指示;
-
处理诸如连接中断或重试等错误。
这些任务本质相同,但解决路径很多,UX 也因此不同。比如聊天历史的放置与展示方式:既可能是一个像菜单一样打开的独立页面,也可能是在 popup 里显示的聊天列表。
问题逐渐显现:不同聊天中的体验差异很大。有的地方助手会流式输出答案,有的地方则直接显示完整文本。在一个界面里消息会分组,而在另一个界面里则是连续时间线。这破坏了整体 UX——用户在同一生态的不同产品间切换时,对助手的感受却完全不同。

另外还明显感觉到,把模型的新特性逐步推到线上变得越来越难。 要把例如工具能力、多模态,或工具的结构化输出等能力传递给用户, 就需要对齐契约、改造后端,然后再由每个团队分别更新 UI。 在这样的条件下,任何变更都耗时很长,而且难以规模化。
我们希望阻止这种变体不断增长的趋势,并恢复可预期性。 为此需要统一数据模型与工作模式,提供现成的组件与 hooks, 让团队不必从零开始,同时也要保留可定制空间——毕竟每个团队的 场景都不一样。
于是我们就有了单独的库 @gravity‑ui/aikit:它是 Gravity UI 的扩展, 遵循相同的原则,但面向现代 AI 场景:对话、助手、多模态。
AIKit 架构:我们依托了什么
在设计 AIKkit 时,我们参考了 AI SDK 的经验以及若干基础原则。
以 Atomic Design 为核心:整个库从原子(atoms)一路搭到页面(pages)。这样的结构带来清晰的层级关系,便于复用组件,并且在需要时可以在任何层级替换或调整行为。

完全 SDK 无关(SDK‑agnostic):AIKit 不依赖特定的 AI 提供商。你可以使用 OpenAI、Alice AI LLM 或自己的后端——UI 通过 props 接收数据,而状态与请求仍由产品侧负责。
面向复杂场景的两层使用方式:既有开箱即用的组件,也提供包含逻辑的 hook,让你可以完全控制 UI。例如,你可以直接用 PromptInput,或基于 usePromptInput 自行搭建输入框。这样既保持灵活性,又不需要重写底层基础。
可扩展的类型系统。为了保证一致性与类型安全,我们构建了可扩展的数据模型。消息用统一的类型化结构表示:包含用户消息、助手消息,以及若干基础内容类型——文本(text)、模型思考过程(thinking)、工具(tool)。同时也可以通过 MessageRendererRegistry 添加自定义类型。
所有内容都用 TypeScript 进行类型标注,这能帮助更快搭建复杂场景,并在开发阶段避免错误。
// 1. 定义数据类型
type ChartMessageContent = TMessageContent<
'chart',
{
chartData: number[];
chartType: 'bar' | 'line';
}
>;
// 2. 创建展示组件
const ChartRenderer = ({part}: MessageContentComponentProps<ChartMessageContent>) => {
return <div>图表可视化:{part.data.chartType}</div>;
};
// 3. 注册渲染器
const customRegistry = registerMessageRenderer(createMessageRendererRegistry(), 'chart', {
component: ChartRenderer,
});
// 4. 在 AssistantMessage 中使用
<AssistantMessage message={message} messageRendererRegistry={customRegistry} />;
最后,我们通过 CSS 变量实现主题化,加入 i18n(RU/EN),保证可访问性(ARIA、键盘导航),并在 Docker 中使用 Playwright Component Testing 配置了视觉回归测试——库也就具备了生产可用状态。
底层实现
AIKit 的核心是统一的对话模型。要构建它,首先需要理清消息的层级结构。
消息本身是相当多维的实体。来自 LLM 的第一条消息是一个 stream。但在其内部可能包含很多不同的嵌套子消息:本质上是推理、建议、为解决某个问题而发起的 tool 调用。所有这些不同的子消息,实际上是后端发来的同一个消息。但在更简单的 LLM 使用方式中,每个子消息也完全可能是独立的一条消息。
因此我们保留了两种用法:消息可以互相嵌套,也可以是扁平结构——取决于你的需求。
状态管理仍由服务侧掌控。AIKit 不自行存储数据——它从外部接收。团队可以使用 React State、Redux、Zustand、Reatom——任何合适的方案。我们只提供封装典型 UI 逻辑的 hooks,例如:
-
通过
useSmartScroll实现智能滚动; -
通过
useDateFormatter处理日期,例如按 locale 格式化; -
通过
useToolMessage处理 tool 消息; -
以及构建对话所需的其他能力。
此外,AIKit 仍然保持可扩展。你可以接入任何模型、创建自己的内容类型,并完全按自己的任务构建 UI——既可以复用 hooks 中的逻辑,也可以用现成组件作为基础。该架构允许在不破坏共同原则的前提下进行实验。
如何搭建自己的聊天
为了创建第一个聊天,我们将使用预置组件 ChatContainer:
import React, { useState } from 'react';
import { ChatContainer } from 'aikit';
import type { ChatType, MessageType } from 'aikit';
function App() {
const [messages, setMessages] = useState<MessageType[]>([]);
const [chats, setChats] = useState<ChatType[]>([]);
const [activeChat, setActiveChat] = useState<ChatType | null>(null);
const handleSendMessage = async (content: string) => {
// Your message sending logic
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message: content })
});
const data = await response.json();
// Update state
setMessages(prev => [...prev, data]);
};
return (
<ChatContainer
messages={[]}
onSendMessage={() => {}}
welcomeConfig={{
description: '输入消息或选择一个建议来开始对话。',
image: <Icon data={() => {}} size={48}/>,
suggestionTitle: '可以尝试问:',
suggestions: [
{
id: '1',
title: '用简单的方式解释量子计算'
},
{
id: '2',
title: '写一首关于自然的诗'
},
{
id: '3',
title: '帮我调试 JavaScript 代码'
},
{
id: '4',
title: '总结近期 AI 领域的发展'
}
],
title: '欢迎来到 AI 聊天'
}}
/>
);
}
「开箱即用」的效果如下:

增添一点节日气氛:
-
调整初始状态。
为了更细致地配置,我们用独立组件组装聊天:
Header、MessageList、PromptBox。import { Header, MessageList, PromptBox } from 'aikit'; function CustomChat() { return ( <div className="custom-chat"> <Header title="AI Assistant" onNewChat={() => {}} /> <MessageList messages={messages} showTimestamp /> <PromptBox onSend={handleSend} placeholder="随便问点什么..." /> </div> ); } -
使用通过
MessageType导入的不同内置消息类型。-
thinking——展示 AI 的思考过程(用户可以了解助手如何推导出答案)。 -
tool——适合展示交互式的答案块;在我们的例子里是代码块,语法高亮能正确工作,并支持编辑与复制到剪贴板。
也可以添加自定义类型,例如带图片的消息:
type ImageMessage = BaseMessage<ImageMessageData> & { type: 'image' }; const ImageMessageView = ({ message }: { message: ImageMessage }) => ( <div> <img src={message.data.imageUrl} /> {message.data.caption && <p>{message.data.caption}</p>} </div> ); const customTypes: MessageTypeRegistry = { image: { component: ImageMessageView, validator: (msg) => msg.type === 'image' } }; <ChatContainer messages={messages} messageTypeRegistry={customTypes} /> -
-
再通过 CSS 加点样式……
……就能得到一个和“杰德·莫罗兹”(俄罗斯版圣诞老人)一起聊天的界面:)

若要对某些元素进行完全定制,可以使用 hooks —— 也欢迎在文章评论区分享你们的样式方案!
AIKit 如何影响各个服务
在 Yandex Cloud 中使用 AIKit 的效果很快就显现出来了。所有服务中的助手行为都变得一致:一致地流式输出回答、一致地展示错误、一致地对消息分组。UX 变得统一,现在在整个生态中更容易交互,行为也更符合预期、更可预测。
-
UX 语言实现统一——不同产品里的助手聊天现在更像同一生态的一部分。用户看到可预期的行为:相同的 streaming、错误处理和交互模式。
-
聊天 UI 的开发速度显著提升。
-
集中式演进——像 thinking 内容类型或更好的工具处理等新特性只需添加一次,所有团队即可自动获得。
-
该库成为生态中 AI 界面标准化的基础。
接下来
再说说计划。我们确定了几个方向:
-
通过虚拟化提升性能,以支持非常大的聊天历史。
-
随着 AI 代理能力快速发展,扩展基础场景以适配新的能力。
-
增加一些工具函数,简化将主流 AI 模型数据映射到我们聊天数据模型的过程。
另外我们也会持续完善文档与示例。当然还有社区建设——我们希望这个库不仅对公司内部有用,也能帮助到外部开发者。

伊利亚·洛姆捷夫
前端开发工程师