最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
构建支持多种输出形态的 Agent Chat
时间:2026-09-18 15:34:01 编辑:袖梨 来源:一聚教程网
普通聊天界面只处理文本并不困难,但当 Agent 开始持续输出思考过程、图表、表单、流程图和文档时,消息协议、流式解析与组件渲染就会相互牵连。要让这些内容稳定落到同一套界面中,需要重新划分传输层、会话引擎和消息块的职责,并处理持久化、异常状态及响应式布局。
Agent Chat Lab 技术分享:从架构设计到踩坑实践
体验地址,请先体验,功能有需求再看下面得文章。
支持表格、表单、思考链、报表、文档、Mermaid、LaTeX 公式、代码等多种输出
项目定为→ 功能全景 → 五层 SDK 架构 → 自定义组件体系 → Mock / DeepSeek 双模式 → 会话持久化 → 移动端适配 → 难点复盘
文章目录
- 项目地图:读完后你能带走什么
- 项目是什么:为什么要做 Agent Chat Lab
- 功能全景:模拟交互 vs 真实调用
- 技术栈与工程化选型
- 整体架构:页面 / 服务 / SDK 三层
- 目录结构与模块职责
- SDK 五层设计:Engine + Transport + Block
- 页面层:ModeSelect / MockChat / RealChat / ChatPage
- 会话管理:草稿、晋升、localStorage 持久化
- 自定义组件封装清单
- 富渲染体系:Md* 组件与 Block 消息块
- Mock 数据池:Presets + Transport 流式模拟
- 真实 DeepSeek 接入:API Key + SSE
- 移动端布局适配
- 难点与踩坑:8 个真实问题复盘
- 扩展指南:如何接入自己的后端
- 快速开始 & 附录
0项目地图:读完后你能带走什么
| 模块 | 内容 | 学习产出 |
|---|---|---|
| 产品层 | 双模式演示平台:离线 Mock 富渲染 + 真实 DeepSeek SSE 对话 | 理解 Agent UI 产品形态 |
| 架构层 | Pages → Services → SDK 分层;Transport 与协议解耦 | 能设计可扩展的 Chat SDK |
| 渲染层 | 20+ 自定义组件:思考链、图表、表单、澄清卡… | 知道流式 Markdown 怎么拆帧渲染 |
| 工程层 | StrictMode 引擎回收、ECharts 流式 remount、会话 ID 对齐 | 避开 AI Chat 常见坑 |
| 交付层 | PC + 移动端响应式、localStorage 历史、API Key 管理 | 可直接对外 Demo 或二次开发 |
一句话总结: Agent Chat Lab 不只是一个 ChatGPT 壳,而是一套可演示、可扩展、可踩坑学习的 Agent 对话前端实验室—— 上层是产品 Demo,底层是自研 SDK,中间层是大量富渲染组件的工程化封装。
1项目是什么:为什么要做 Agent Chat Lab
Agent Chat Lab(包名 agent-ch@t-lab)是一个 Agent 对话能力演示平台。它解决的核心问题不是「能不能调大模型」,而是:
- 如何展示 Agent 的完整输出形态——不只是纯文本,还有思考链、工具调用、任务列表、澄清交互、文档产物;
- 如何在流式场景下稳定渲染富内容——代码块、ECharts 图表、Mermaid 流程图、动态表单、LaTeX 公式;
- 如何在没有后端的情况下 Demo——Mock Transport 模拟 SSE 流式推送;
- 如何接入真实模型——DeepSeek API Key + SSE 流式 + 历史会话持久化。



1.1 三条路由
| 路径 | 页面 | 说明 |
|---|---|---|
/ | ModeSelect | 首页,选择 Mock 或 Real 模式 |
/mock | MockChat | 离线模拟,内置 9 个示例问题 |
/real | RealChat | DeepSeek 真实 SSE 对话 |
2功能全景:模拟交互 vs 真实调用
2.1 模拟 AI 交互(/mock)
- 内置 9 个精选 Preset 问题:纯文本、代码、表格、ECharts、Mermaid、表单、思考链、澄清卡等;
- 无需 API Key,完全离线,通过 Mock Transport 模拟 SSE 流式推送;
- 预置 2 条历史会话(SDK 分层演示、富文本半截回答),进入即可体验侧栏切换;
- 进入页面时自动重置引擎,保证每次 Demo 状态干净。
2.2 真实 DeepSeek 调用(/real)
- 支持
deepseek-ch@t与deepseek-reasoner模型切换; - API Key 存于 localStorage(
deepseek_api_key),支持设置 / 修改 / 清除; - 前端直连 DeepSeek SSE(
https://api.deepseek.com/ch@t/completions); - 会话历史持久化到
localStorage(ai-ch@t-real-sessions); - 首条问题自动成为会话标题,支持重命名 / 删除。
2.3 共享 Chat 能力(ChatPage)
- 左侧 Conversations 会话列表 + 新建会话;
- 中间 Welcome + Prompts 空态引导(Mock 模式);
- AgentMessageList 消息流渲染(用户 Bubble + 助手 Block);
- Sender 输入框:Enter 发送、流式中 Stop;
- 自动滚底:用户上滑阅读时暂停跟随,回到底部恢复。
图 2 · 聊天页:会话侧栏 + 思考链 + 富渲染 Answer + Sender 输入区
3技术栈与工程化选型
| 类别 | 选型 | 用途 |
|---|---|---|
| 框架 | React 18 + TypeScript 5.6 | UI 渲染,StrictMode 下引擎生命周期管理 |
| 构建 | Vite 5 | Dev / Build,路径别名 @ → src/ |
| 路由 | react-router-dom 7 | 三页面 SPA |
| UI 库 | Arco Design 2.66 | Modal、Select、Button、Form 等 |
| 样式 | SCSS + Tailwind 3 | 组件 SCSS + 首页/工具类 Tailwind |
| SSE | @microsoft/fetch-event-source | POST SSE,支持自定义 Header(API Key) |
| Markdown | react-markdown + remark/rehype 插件 | GFM、数学公式、原始 HTML |
| 图表 | ECharts 5.6 | 柱状/折线/饼图/雷达/表格 |
| 流程图 | Mermaid 11 | 审批流、架构图等 |
| 状态 | use-sync-external-store + zustand | Engine 快照订阅 / 局部状态 |
| 工具 | json5 + jsonrepair | 流式半截 JSON 容错解析 |
4整体架构:页面 / 服务 / SDK 三层
项目采用薄页面 + 厚 SDK + 独立组件库的分层思路。页面只负责「选模式、配 Transport、传 Props」; 所有对话逻辑收敛在 ChatPage + useChatSessions + SDK 的 useAgentChat 中。
4.1 数据流(一次用户提问)
- 用户在
Sender输入 →ChatPage.handleSend; rememberDraftQuery记录首问 → 真实模式立即晋升草稿为历史会话;useAgentChat.onRequest→AgenticEngine.submit(userInput);- Engine FSM 进入
sending → streaming; ChatTransport.connect()建立本轮 SSE(Mock 或 DeepSeek);- 每帧
onmessage({ data })→agentProcessChunk更新message.blocks[]; AgentMessageList→BlockRenderer→ ThinkBlock / AnswerBlock / …;- 流结束 → FSM
done→useChatSessions持久化 messages。
5目录结构与模块职责
src/
├── main.tsx # 入口:Arco CSS + KaTeX + 全局样式
├── app/App.tsx # BrowserRouter 三路由
├── pages/
│ ├── ModeSelect/ # 首页
│ ├── MockChat/ # 模拟模式(destroyEnginesForAgent)
│ └── RealChat/ # 真实模式(API Key Modal)
├── components/
│ ├── ch@t/ChatPage.tsx # ★ 统一聊天壳
│ ├── Sender/ # 输入框
│ ├── Conversations/ # 会话侧栏
│ ├── Welcome/ Prompts/ # 空态 + 预设问题
│ ├── Think* / ThinkChain/ # 思考链 UI
│ └── Md* # 富 Markdown 渲染组件族
├── services/
│ ├── sessionStore.ts # RealSessionStore + 草稿 ID 常量
│ ├── useChatSessions.ts # 会话切换 / 晋升 / 持久化 Hook
│ └── createDeepSeekService.ts
├── mock/
│ ├── createMockTransport.ts
│ ├── presets.ts # 9 个示例问题
│ └── dataPool/ # 模板 + 素材 + materialize
└── sdk/ # ★ Agent Chat SDK
├── hooks/useAgentChat/
├── engine/ # AgenticEngine + registry + FSM
├── transport/ # SSE / OpenAI / DeepSeek / Anthropic
├── protocol/ # processChunk + 事件类型
└── message/ # Block 类型 + AgentMessageList
6SDK 五层设计:Engine + Transport + Block
SDK 是整个项目的技术内核,设计理念见 src/sdk/overview/summary.md:
UI(Sender / AgentMessageList / Block)
↑
useAgentChat(onRequest / abort / setMessages / dispatch)
↑
AgenticEngine(Turn、队列、重连、FSM)
↑
processChunk / resolveTurnStatus(chunk → AgentChatMessage)
↑
ChatTransport(每轮 Turn 一次 connect)
6.1 AgenticEngine + Registry
每个 agentID + sessionId 对应一个 Engine 实例,通过模块级 Registry 管理生命周期:
acquireEngine/releaseEngineInstance— refCount 控制;destroyEngine/destroyEnginesForAgent— 强制销毁;rebindRegistryKey— 会话 ID 变更时迁移 Engine;tryGC— refCount=0 且非 busy 时自动回收。
6.2 会话 FSM(有限状态机)
| 状态 | 含义 |
|---|---|
idle | 空闲,可接受新输入 |
sending | 已提交,等待首帧 |
streaming | 流式接收中 |
done | 本轮完成 |
stopped | 用户主动 Stop |
error | 传输或解析错误 |
6.3 Block 消息模型
助手消息不以纯 content 字符串为主,而是 blocks 数组:
| Block 类型 | 组件 | 展示内容 |
|---|---|---|
think | ThinkBlock → ThinkChain | 思考链、工具步骤、工作流 |
answer | AnswerBlock → MdContent | Markdown 正文(代码/图表/表单…) |
clarify | ClarifyCard | 澄清问答(单选/多选) |
tasklist | TaskListBlock | 任务清单 |
document | DocumentBlock | 生成文件/文档产物 |
error | ErrorBlock | 错误信息 |
✅ Transport 与协议分离:换 DeepSeek → OpenAI 只需换 Transport 工厂;换事件格式只需换 processChunk。两层独立替换,互不影响。
7页面层:ModeSelect / MockChat / RealChat / ChatPage
7.1 ChatPage — 统一聊天壳
src/components/ch@t/ChatPage.tsx 是所有模式的唯一 UI 入口,Props 驱动差异:
| Prop | Mock | Real |
|---|---|---|
mode | 'mock' | 'real' |
transport | createMockTransport() | createLiveDeepSeekTransport() |
showPresets | true(9 个示例问题) | false |
sessionMenuEnabled | false | true(重命名/删除) |
headerExtra | — | 修改 Key / 清除 Key 按钮 |
7.2 MockChat — 引擎重置策略
// src/pages/MockChat/index.tsx
useEffect(() => {
destroyEnginesForAgent(MOCK_AGENT_ID);
return () => destroyEnginesForAgent(MOCK_AGENT_ID);
}, []);
return <ChatPage key={mountKey} mode="mock" ... />;
每次进入 Mock 页销毁该 Agent 下所有 Engine,避免 StrictMode 或路由复用导致会话绑定错乱。
7.3 RealChat — API Key 生命周期
- 首次进入弹出 Modal 要求输入 Key,取消则返回首页;
- Key 写入 localStorage,并迁移旧版 sessionStorage 数据;
- 顶栏提供「修改 Key」「清除 Key」,清除后重新弹出 Modal。
8会话管理:草稿、晋升、localStorage 持久化
8.1 草稿(Draft)机制
每个模式维护一个草稿会话,不在侧栏历史中出现,直到被「晋升」:
| 模式 | 草稿 ID | 晋升时机 | 标题来源 |
|---|---|---|---|
| Mock | draft-new | 用户提问 + 助手有 server 输出后 | 首条用户问题(截断 24 字) |
| Real | real-draft-new | 发送首条消息时立即晋升 | 首条用户问题 |
8.2 RealSessionStore 核心逻辑
// src/services/sessionStore.ts
export const REAL_DEFAULT_DRAFT_ID = 'real-draft-new';
// 草稿不写入 localStorage,晋升后才 persist
private persist() {
const sessions = this.historyOrder
.map(id => this.sessions.get(id))
.filter(s => s && !s.isDraft);
localStorage.setItem(this.storageKey, JSON.stringify({ order, sessions }));
}
// 原地晋升:保持同一 sessionId,避免流式中途 rebind
promoteDraftInPlace(draftId, title): boolean { ... }
⚠️ 踩坑记录:早期 RealChat 与 useChatSessions 各自 new 了一个 RealSessionStore, 导致草稿 ID 不一致,消息 save 到了「另一个草稿」上,侧栏历史永远不出现。修复方案:统一使用常量 REAL_DEFAULT_DRAFT_ID。
8.3 useChatSessions Hook
src/services/useChatSessions.ts 编排以下职责:
- 切换会话时 abort 进行中的流,保存当前 messages;
- messages 变化时 auto-save;
- 删除会话时
destroyEngine(agentId, id, true); - 重命名 / 删除弹 Arco Modal.confirm;
- hydration 时若用户已开始输入则跳过 setMessages(防覆盖)。
9自定义组件封装清单
项目在 src/components/ 下封装了完整的 Agent Chat UI 组件库(公开导出见 components/index.ts), 按职责分为以下几族:
9.1 聊天框架
| 组件 | 路径 | 职责 |
|---|---|---|
| ChatPage | components/ch@t/ | Header + Sidebar + Messages + Sender 统一壳 |
| Conversations | components/Conversations/ | 分组会话列表、新建/重命名/删除 |
| Sender | components/Sender/ | 富输入框:发送/停止/文件/语音/深度思考 |
| Welcome | components/Welcome/ | 空态欢迎页 |
| Prompts | components/Prompts/ | 预设问题 Chip(wrap 换行 / 分页) |
| Bubble / ChatItem | components/Bubble/ ChatItem/ | 用户/助手消息气泡容器 |
| Actions | components/Actions/ | 复制、TTS、点攒/踩、刷新、删除 |
9.2 思考链(Think*)
| 组件 | 职责 |
|---|---|
| ThinkChain | 思考链主容器,树形步骤渲染 |
| ThinkWorkflow | 工作流工具步骤 |
| ThinkKnowBase | 知识库检索步骤 |
| ThinkTamper | 人工干预步骤 |
| ThinkModel | 模型思考步骤 |
| ThinkDispatcher | 步骤类型分发器 |
9.3 富 Markdown(Md*)
| 组件 | 自定义标签 / 能力 |
|---|---|
| MdContent | 主 Markdown 渲染器(GFM + 数学 + 自定义 Tag) |
| MdCodeHighlighter | 语法高亮代码块 |
| MdECharts | <md-echarts> JSON → 柱状/折线/饼/雷达/表格 |
| MdMermaid | Mermaid 流程图 + 导出 |
| MdForm | <md-form> JSON 驱动动态表单 |
| MdTable | 增强表格 |
| MdLatex | LaTeX 公式 |
| MdImg | 自定义图片标签 |
| MdSup | 引用角标 + Popover |
| MdDataTableSelect | 可选数据表格 |
9.4 SDK 消息层
| 组件 | 路径 | 职责 |
|---|---|---|
| AgentMessageList | sdk/message/components/ | messages[] → Bubble + BlockRenderer |
| BlockRenderer | sdk/message/blocks/ | 按 block.type 分发到注册组件 |
| ClarifyCard | sdk/message/blocks/ClarifyCard/ | 交互式澄清卡片 |
| DocumentBlock | sdk/message/blocks/DocumentBlock/ | 文件产物展示 |
10富渲染体系:Md* 组件与 Block 消息块
10.1 流式 Markdown 的核心约束
并非所有内容都适合「逐字流式」。项目在 Mock Transport 和文档中明确约定:
| 内容类型 | 流式策略 | 原因 |
|---|---|---|
| 普通文本 | 按 10~24 字切分 | 模拟打字效果 |
代码块 ``` | 整帧推送 | 避免半截代码高亮错乱 |
<md-echarts> | 整帧推送 | JSON 不完整无法 parse |
| 表格 / 表单 / Mermaid | 整帧推送 | 结构型内容需完整 DOM |
10.2 ECharts 流式渲染稳定性
// src/components/MdContent/components/renderConfig.tsx
/** 流式更新时保持组件引用稳定,避免 ReactMarkdown remount 导致 ECharts 被 dispose */
const MdEChartsBlock = memo(({ children, ...props }) => {
const rawText = toRawText(children);
const config = useMemo(() => parseJson(rawText), [rawText]);
return <MdECharts {...config} />;
});
配合 STABLE_MARKDOWN_COMPONENTS(稳定组件引用 + Context), 避免 ReactMarkdown 在 content 变化时 remount 子组件,导致图表「闪一下又消失」。
10.3 半截 JSON 容错
// parseJson: JSON.parse → JSON5.parse → jsonrepair → 再次 parse
// 流式过程中即使 JSON 未完整,也能尽量渲染已有字段
11Mock 数据池:Presets + Transport 流式模拟
11.1 九个 Preset 问题
| ID | 示例问题 | 展示能力 |
|---|---|---|
| plain | 介绍一下你能展示哪些内容 | 纯文本 |
| code | 写一段 JS 数组去重代码 | 代码高亮 |
| table | 生成三行三列成绩表格 | Markdown 表格 |
| echarts | 画一个成本对比柱状图 | ECharts 柱状图 |
| mermaid | 画一个审批流程图 | Mermaid 流程图 |
| form | 帮我填一份信息表单 | 动态表单 |
| think | 展示你的思考过程 | 思考链 + 工具 |
| clarify | 需要进一步澄清的问题 | 澄清卡交互 |
11.2 Mock Transport 流式切分
// src/mock/createMockTransport.ts
/** 普通正文按小段切;代码围栏 / md-echarts 整帧,避免半截 JSON */
function splitStreamableText(text: string): string[] {
const pattern = /(```[sS]*?```|<md-echarts>[sS]*?</md-echarts>)/g;
// 匹配到的结构块整帧推送,其余按 splitPlainText 切字
}
11.3 数据池三层
- presets.ts — 问题 → templateId + seed 映射;
- turnTemplates.ts — 13 套回合模板(plain / think-answer / full-kit / clarify…);
- snippets.ts + materialize.ts — 素材片段组装为 DemoStreamEvent[]。
12真实 DeepSeek 接入:API Key + SSE
12.1 Transport 工厂
// src/services/createDeepSeekService.ts
export function createLiveDeepSeekTransport(options) {
return createDeepSeekTransport({
apiKey: () => options.apiKey(),
model: () => options.model(),
baseURL: 'https://api.deepseek.com',
});
}
12.2 思考模式
选择 deepseek-reasoner 时,handleSend 附加 enableDeepThink: true, Transport 映射 delta.reasoning_content → SDK Think 事件,Answer 映射 delta.content。
12.3 安全说明
⚠️ 本项目为前端直连 Demo,API Key 存在用户浏览器 localStorage 中。 生产环境应通过后端代理转发,Key 不可暴露在前端。
13移动端布局适配
PC 端采用「固定侧栏 + 主区域」经典布局;移动端(≤768px)改为:
- 侧栏隐藏,通过 Header 左侧 ☰ 按钮打开抽屉式会话列表;
- 半透明遮罩层,点击关闭;
- 选择会话 / 新建会话后自动关闭抽屉;
- Header 精简:隐藏品牌名,保留模式 Badge;
- 模型选择与 Key 按钮缩小;
- 使用
100dvh+safe-area-inset-bottom适配 iPhone。
14难点与踩坑:8 个真实问题复盘
| # | 问题 | 根因 | 解决方案 |
|---|---|---|---|
| 1 | Mock 页首次进入预设问题/发送无效 | React StrictMode 双挂载 dispose 了 Engine,useMemo 缓存了已销毁实例 | useResolveEngine 每帧从 Registry 重新解析,disposed 则 ensureEngine |
| 2 | 进入 Mock 页仍显示上次会话 | Engine 按 agentID:sessionId 缓存,路由切换未清理 | MockChat mount/unmount 时 destroyEnginesForAgent |
| 3 | ECharts 流式时闪一下又空白 | ReactMarkdown 每次 content 变化 recreate components,ECharts 被 dispose | memo(MdEChartsBlock) + STABLE_MARKDOWN_COMPONENTS |
| 4 | 真实对话不写入历史 | 两个 RealSessionStore 实例,草稿 ID 不一致 | 统一 REAL_DEFAULT_DRAFT_ID,首问立即 promoteDraftInPlace |
| 5 | 流式半截 JSON 图表报错 | JSON.parse 对不完整字符串抛错 | parseJson 链:JSON → JSON5 → jsonrepair |
| 6 | 切换会话时覆盖用户输入 | hydration setMessages 与用户输入竞态 | switchingRef + 检测 userAlreadyStarted 跳过加载 |
| 7 | 流式中途 promote 导致 rebind | 晋升时更换 sessionId 会中断 Engine 绑定 | promoteDraftInPlace 保持同一 ID 原地晋升 |
| 8 | 生产构建 Block 组件未注册 | Tree-shaking 移除了 side-effect import | BlockRenderer 内显式 registerBuiltinBlocks() |
14.1 StrictMode 引擎回收(核心代码)
// src/sdk/engine/hooks/useAgenticEngine.ts
// 每帧从 registry 解析,避免 StrictMode dispose 后 useMemo 仍缓存已销毁实例
let engine = contextEngine ?? ensureEngine(agentID, sessionId, optionsRef.current);
if (!contextEngine && engine.disposed) {
engine = ensureEngine(agentID, sessionId, optionsRef.current);
}
15扩展指南:如何接入自己的后端
SDK 支持由外到内逐层替换,多数业务只需改 1~2 层:
| 层级 | 改什么 | 适用场景 |
|---|---|---|
| ① 完全自定义 Transport | 实现 ChatTransport.connect | WebSocket / gRPC / 本地 Mock |
| ② request 配置 | url / headers / getBody | 同协议,换接口地址和鉴权 |
| ③ mapData | 改写 SSE data 字符串 | 后端字段名略有差异 |
| ④ processChunk | 自定义 chunk → message 映射 | 事件模型与默认 Agent 协议不同 |
| ⑤ Block 注册 | registerBlock({ type, component }) | 新增自定义消息块类型 |
| ⑥ Md* 扩展 | 在 renderConfig 注册新 Tag | 新增富渲染组件(如地图、3D) |
? 推荐路径:后端已符合 Agent SSE 协议 → 只用 useAgentChat({ request: { url, getBody } }) + AgentMessageList,零 Transport 代码。
16快速开始 & 附录
16.1 本地运行
git clone <your-repo>
cd ai-ch@t
npm install
npm run dev
# 浏览器打开 http://localhost:5173
16.2 构建部署
npm run build # 产物在 dist/
npm run preview # 本地预览生产构建
16.3 关键文件索引
| 文件 | 说明 |
|---|---|
src/app/App.tsx | 路由定义 |
src/components/ch@t/ChatPage.tsx | 统一聊天壳 |
src/services/useChatSessions.ts | 会话 Hook |
src/services/sessionStore.ts | localStorage 持久化 |
src/mock/createMockTransport.ts | Mock SSE 模拟 |
src/services/createDeepSeekService.ts | DeepSeek 接入 |
src/sdk/hooks/useAgentChat/index.ts | SDK 主 Hook |
src/sdk/engine/registry.ts | Engine 生命周期 |
src/sdk/protocol/processChunk.ts | Chunk → Block 解析 |
src/components/MdContent/ | 富 Markdown 渲染 |
src/sdk/overview/summary.md | SDK 设计文档 |
相关文章
- 从脚本到平台:搭建可配置的 AI Agent 工厂 09-18
- 用 AI 规划十一行程:快速生成可共享的旅行网页 09-18
- chat.asp聊天程序的编写方法 09-18
- 构建支持多种输出形态的 Agent Chat 09-18
- ASP基础入门第六篇(ASP内建对象Request) 09-18
- ASP基础入门第四篇(脚本变量、函数、过程和条件语句) 09-18