#完整示例:对话应用
一个对话应用的接入示例,组件代码放在一个文件中、使用公开接口:左边会话列表,中间聊天区,右边是任务进度和常用提问;附件、赞踩、@ 提及都开着;页面自己加了公告、知识库选择、业务卡片、轮次计数和右侧栏。代码从上到下每一段对应文档里的一页。示例的 demo-* 类名使用演示页样式,复制到自己的项目时需补充页面布局和卡片样式。
full-app.tsx
import '@volcengine/chatkit/styles.css';
import {
Chatkit,
type ChatkitDataPartRenderer,
type ChatkitMentionSource,
ChatkitProvider,
type ChatkitUploader,
SessionList,
useChatSessions,
useComposer,
useCurrentRun,
useCurrentSession,
} from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import { useState } from 'react';
import { localUpload } from '../backends/local-upload';
import { useDemoSessions } from '../sessions';
// 适配器:见「接入后端」。整个应用一个实例,换后端只改这一段。
// transformOutput 把后端的输出变成回答里的业务数据:这里在搜索完成后带上一张参考资料卡片。
const adapter = createMockAdapter({
transformOutput: (step, context) => {
const parts = (() => {
if (step.kind !== 'tool' || step.name !== 'search') return undefined;
const { results = [] } = step.result as { results?: { title: string }[] };
return [{ id: 'sources', type: 'data' as const, kind: 'acme.sources', data: results.map(item => item.title) }];
})();
return parts && context.messageId
? [
...context.defaultChanges,
...parts.map(part => ({ type: 'set-part' as const, messageId: context.messageId!, part })),
]
: undefined;
},
});
// 附件:见「内置功能 › 附件」。上传由页面实现,这里用本地地址代替。
const upload: ChatkitUploader = localUpload;
// @ 提及:见「内置功能 › @ 与 /」。
const mention: ChatkitMentionSource = {
option: [
{ label: '数据分析助手', value: 'data-agent' },
{ label: '代码审查助手', value: 'review-agent' },
],
};
// 业务卡片:见「定制 › 业务卡片」。按数据类型登记组件。
const Sources: ChatkitDataPartRenderer = ({ data }) => (
<div className="demo-card">
<header>参考资料</header>
{(data as string[]).map(title => (
<div key={title}>· {title}</div>
))}
</div>
);
const KNOWLEDGE_BASES = ['产品手册', '常见问题'];
const TEMPLATES = ['总结这个会话的结论', '把上面的回答改写成邮件', '列出还没解决的问题'];
// 页面顶栏:当前会话的标题和这次打开后完成的轮数。公开 hooks 在 ChatkitProvider 下面都能用。
function AppHeader({ rounds }: { rounds: number }) {
const { session } = useCurrentSession();
return (
<header className="demo-app-header">
<strong>{session?.title ?? '新会话'}</strong>
<span className="demo-muted">已完成 {rounds} 轮</span>
</header>
);
}
// 页面右侧:这一轮的状态,和几条常用提问。点一条把它填进输入框。
function AppRail() {
const run = useCurrentRun();
const composer = useComposer();
return (
<aside className="demo-app-rail">
<section className="demo-rail-card">
<h4>任务进度</h4>
<div className="demo-rail-row" data-status={run?.status ?? 'idle'}>
<i />
{run ? (run.finishedAt ? '这一轮已结束' : '正在处理') : '还没有开始'}
</div>
{run?.workItems?.total ? (
<div className="demo-rail-row" data-status={run.workItems.active ? 'running' : 'completed'}>
<i />
步骤 {run.workItems.total - run.workItems.active} / {run.workItems.total}
</div>
) : null}
</section>
<section className="demo-rail-card">
<h4>常用提问</h4>
{TEMPLATES.map(template => (
<button
key={template}
type="button"
className="demo-rail-row"
onClick={() => {
composer.setText(template);
composer.focus();
}}
>
<span>{template}</span>
</button>
))}
</section>
</aside>
);
}
export default function FullApp() {
// 会话层:见「指南 › 两种用法」。会话列表和聊天区用同一个返回值。
const { chat: sessions, listProps } = useDemoSessions({ adapter });
const [ratings, setRatings] = useState<Record<string, 'like' | 'dislike' | undefined>>({});
const [starred, setStarred] = useState<string[]>([]);
const [knowledgeBase, setKnowledgeBase] = useState<string>();
const [rounds, setRounds] = useState(0);
const pickNext = () =>
setKnowledgeBase(current => KNOWLEDGE_BASES[current ? KNOWLEDGE_BASES.indexOf(current) + 1 : 0]);
return (
// ChatkitProvider 把页面包起来,顶栏和右侧栏才能用公开 hooks;props 写在它上面,<Chatkit /> 从它取。
<ChatkitProvider
sessions={sessions}
// 设置:见「参考 › 配置参考」。只填值。
settings={{
welcome: '有什么可以帮忙的?',
placeholder: '问点什么,输入 @ 提及助手',
recommendations: ['帮我查一下接入步骤', '写一份本周周报', '模拟一次失败'],
}}
// 内置功能:见「内置功能 › 总览」。附件、赞踩、提及给了函数或候选项才显示;
// 重新生成、编辑后重发来自后端,不用配置。
features={{
attachments: { upload },
feedback: {
ratings,
onRate: (rating, message) => setRatings(current => ({ ...current, [message.id]: rating })),
},
mentions: { mention },
}}
// 插槽:见「定制 › 插槽」。按位置放页面自己的内容。
slots={{
'session.top': <div className="demo-line demo-line--notice">公告:周五 18:00 起停机维护一小时</div>,
'composer.toolbarStart': (
<button type="button" className="demo-chip" aria-pressed={Boolean(knowledgeBase)} onClick={pickNext}>
知识库
</button>
),
'composer.bodyTop': knowledgeBase ? <span className="demo-badge">参考:{knowledgeBase}</span> : null,
'composer.bottom': <div className="demo-legal">内容由人工智能模型生成,请注意甄别</div>,
}}
// 渲染器:见「定制 › 业务卡片」「工具展示」「失败卡片与系统提示」。发一句带「失败」的话看失败卡片。
renderers={{
parts: { 'acme.sources': Sources },
tools: { search: { runningLabel: '正在查资料', successLabel: '已查到资料' } },
errors: {
MODEL_OVERLOADED: ({ formattedContent }) => (
<div className="demo-card">
<header>现在用的人有点多</header>
<span className="demo-muted">{formattedContent}</span>
</div>
),
},
}}
// 发送前处理:见「定制 › 发送前处理 onSend」。把选中的知识库带给后端。
onSend={input => ({ ...input, metadata: { ...input.metadata, knowledgeBase } })}
// 事件:见「可观测 › 事件」。每一轮结束时收到 run_settled。
onEvent={event => {
if (event.name === 'run_settled') setRounds(count => count + 1);
}}
>
<div className="demo-app">
{/* 会话列表:见「内置功能 › 会话列表」。itemActions 在每一行的菜单里,itemBadge 在标题后。 */}
<SessionList
{...listProps}
header={<div className="demo-brand">Acme 助手</div>}
footer={<div className="demo-user">Chloe</div>}
itemBadge={({ session }) => (starred.includes(session.id) ? <span>★</span> : null)}
itemActions={({ session }) => (
<button type="button" onClick={() => setStarred(current => [...current, session.id])}>
收藏
</button>
)}
/>
<div className="demo-app-main">
<AppHeader rounds={rounds} />
<div className="demo-app-body">
<div className="demo-main">
<Chatkit />
</div>
<AppRail />
</div>
</div>
</div>
</ChatkitProvider>
);
}点推荐问题,或者切换左侧的会话新窗口打开
#代码里的每一段
| 代码里的部分 | 做了什么 | 详见 |
|---|---|---|
createMockAdapter | 对接后端;这里用不连接后端、按脚本回放的示例适配器 | 接入后端、Mock 适配器 |
useChatSessions | 一个适配器得到会话列表和每个会话的运行时 | 两种用法 |
SessionList | 会话列表,每一行的菜单里加了「收藏」 | 会话列表 |
settings | 欢迎语、推荐问题、占位文字 | 欢迎语与推荐问题 |
features.attachments | 附件,上传由页面实现 | 附件 |
features.feedback | 赞踩,选中态由页面保存 | 赞踩 |
features.mentions | @ 提及的候选项 | @ 与 / |
slots | 消息区上方的公告、工具栏的知识库按钮、输入框下方的提示 | 插槽 |
renderers.parts | 参考资料卡片 | 业务卡片 |
renderers.tools | 搜索工具换了文字 | 工具展示 |
renderers.errors | 模型繁忙时的失败卡片 | 失败卡片与系统提示 |
onSend | 在发送输入的 metadata 中记录选中的知识库 | 发送前处理 |
onEvent | 累计当前页面收到的 run_settled 事件 | 事件 |
ChatkitProvider | 把页面包起来,顶栏和右侧栏才能用公开 hooks | Hooks |
AppHeader、AppRail | 页面自己的顶栏和右侧栏:会话标题、这一轮的状态、常用提问 | Hooks |
没有出现在代码里、但界面上有的功能(重新生成、编辑后重发、划词追问)默认开启:重新生成、编辑后重发还需适配器具备对应能力,划词追问不依赖这两项能力,见内置功能总览。
示例的轮次计数累加所有已打开会话的 run_settled,包含失败和停止,不等于当前会话的成功轮数。上传使用本地对象 URL,赞踩、收藏和知识库选择保存在页面状态中;生产接入需补充上传服务、持久化和统计筛选。
#给一个客服 Agent 做对话页面:七步
以「已经有一个客服 Agent 的后端,要给它做对话页面」为例:
| 步骤 | 做什么 | 谁来做 | 对应上面的代码 |
|---|---|---|---|
| 1 | 在业务前端的工程里安装 @volcengine/chatkit | 业务前端 | import |
| 2 | 按后端的协议选适配器,为要用的每项能力写一个发请求的函数 | 业务前端 | createMockAdapter 换成真实适配器 |
| 3 | 在业务自己的服务端加一个代理:校验登录态,带上密钥,转发给 Agent 后端 | 业务后端 | 能力函数里的地址指向它 |
| 4 | 填 settings,在 features 里给上传、赞踩这些函数 | 业务前端 | settings、features |
| 5 | 用 slots 放业务入口(工单号、知识库),用 renderers 显示业务卡片 | 业务前端 | slots、renderers |
| 6 | 用 createTelemetry 把事件接到业务自己的埋点,把 onError 接到错误监控 | 业务前端 | createTelemetry、onError |
| 7 | 用 --chatkit-* 变量对齐品牌色 | 业务前端 | --chatkit-* 样式变量(需自行添加) |
怎么部署:chatkit 是一个前端的包,和业务前端一起构建、一起发布,没有需要单独部署的服务。第 3 步的代理是业务服务端里的一个接口,不是 chatkit 的组成部分。
上线前的检查项见上线清单。
#换成真实后端
可复用页面结构和多数配置。上传需换成真实服务,transformOutput 要按目标协议取业务数据,onSend 中的 metadata 也要确认如何传给后端。OpenAI 兼容接口与 ADK 的完整写法,以及各后端上会显示哪些功能,见完整示例:接入真实后端。