一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

构建支持多种输出形态的 Agent Chat

时间:2026-09-18 15:34:01 编辑:袖梨 来源:一聚教程网

普通聊天界面只处理文本并不困难,但当 Agent 开始持续输出思考过程、图表、表单、流程图和文档时,消息协议、流式解析与组件渲染就会相互牵连。要让这些内容稳定落到同一套界面中,需要重新划分传输层、会话引擎和消息块的职责,并处理持久化、异常状态及响应式布局。

Agent Chat Lab 技术分享:从架构设计到踩坑实践

体验地址,请先体验,功能有需求再看下面得文章。

支持表格、表单、思考链、报表、文档、Mermaid、LaTeX 公式、代码等多种输出

项目定为→ 功能全景 → 五层 SDK 架构 → 自定义组件体系 → Mock / DeepSeek 双模式 → 会话持久化 → 移动端适配 → 难点复盘

文章目录

  1. 项目地图:读完后你能带走什么
  2. 项目是什么:为什么要做 Agent Chat Lab
  3. 功能全景:模拟交互 vs 真实调用
  4. 技术栈与工程化选型
  5. 整体架构:页面 / 服务 / SDK 三层
  6. 目录结构与模块职责
  7. SDK 五层设计:Engine + Transport + Block
  8. 页面层:ModeSelect / MockChat / RealChat / ChatPage
  9. 会话管理:草稿、晋升、localStorage 持久化
  10. 自定义组件封装清单
  11. 富渲染体系:Md* 组件与 Block 消息块
  12. Mock 数据池:Presets + Transport 流式模拟
  13. 真实 DeepSeek 接入:API Key + SSE
  14. 移动端布局适配
  15. 难点与踩坑:8 个真实问题复盘
  16. 扩展指南:如何接入自己的后端
  17. 快速开始 & 附录

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 流式 + 历史会话持久化。

ui-home.png

mock3.png

mock1.png

1.1 三条路由

路径页面说明
/ModeSelect首页,选择 Mock 或 Real 模式
/mockMockChat离线模拟,内置 9 个示例问题
/realRealChatDeepSeek 真实 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@tdeepseek-reasoner 模型切换;
  • API Key 存于 localStoragedeepseek_api_key),支持设置 / 修改 / 清除;
  • 前端直连 DeepSeek SSE(https://api.deepseek.com/ch@t/completions);
  • 会话历史持久化到 localStorageai-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.6UI 渲染,StrictMode 下引擎生命周期管理
构建Vite 5Dev / Build,路径别名 @ → src/
路由react-router-dom 7三页面 SPA
UI 库Arco Design 2.66Modal、Select、Button、Form 等
样式SCSS + Tailwind 3组件 SCSS + 首页/工具类 Tailwind
SSE@microsoft/fetch-event-sourcePOST SSE,支持自定义 Header(API Key)
Markdownreact-markdown + remark/rehype 插件GFM、数学公式、原始 HTML
图表ECharts 5.6柱状/折线/饼图/雷达/表格
流程图Mermaid 11审批流、架构图等
状态use-sync-external-store + zustandEngine 快照订阅 / 局部状态
工具json5 + jsonrepair流式半截 JSON 容错解析

4整体架构:页面 / 服务 / SDK 三层

项目采用薄页面 + 厚 SDK + 独立组件库的分层思路。页面只负责「选模式、配 Transport、传 Props」; 所有对话逻辑收敛在 ChatPage + useChatSessions + SDK 的 useAgentChat 中。

4.1 数据流(一次用户提问)

  1. 用户在 Sender 输入 → ChatPage.handleSend
  2. rememberDraftQuery 记录首问 → 真实模式立即晋升草稿为历史会话;
  3. useAgentChat.onRequestAgenticEngine.submit(userInput)
  4. Engine FSM 进入 sending → streaming
  5. ChatTransport.connect() 建立本轮 SSE(Mock 或 DeepSeek);
  6. 每帧 onmessage({ data })agentProcessChunk 更新 message.blocks[]
  7. AgentMessageListBlockRenderer → ThinkBlock / AnswerBlock / …;
  8. 流结束 → FSM doneuseChatSessions 持久化 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 类型组件展示内容
thinkThinkBlock → ThinkChain思考链、工具步骤、工作流
answerAnswerBlock → MdContentMarkdown 正文(代码/图表/表单…)
clarifyClarifyCard澄清问答(单选/多选)
tasklistTaskListBlock任务清单
documentDocumentBlock生成文件/文档产物
errorErrorBlock错误信息

Transport 与协议分离:换 DeepSeek → OpenAI 只需换 Transport 工厂;换事件格式只需换 processChunk。两层独立替换,互不影响。

7页面层:ModeSelect / MockChat / RealChat / ChatPage

7.1 ChatPage — 统一聊天壳

src/components/ch@t/ChatPage.tsx 是所有模式的唯一 UI 入口,Props 驱动差异:

PropMockReal
mode'mock''real'
transportcreateMockTransport()createLiveDeepSeekTransport()
showPresetstrue(9 个示例问题)false
sessionMenuEnabledfalsetrue(重命名/删除)
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晋升时机标题来源
Mockdraft-new用户提问 + 助手有 server 输出后首条用户问题(截断 24 字)
Realreal-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 聊天框架

组件路径职责
ChatPagecomponents/ch@t/Header + Sidebar + Messages + Sender 统一壳
Conversationscomponents/Conversations/分组会话列表、新建/重命名/删除
Sendercomponents/Sender/富输入框:发送/停止/文件/语音/深度思考
Welcomecomponents/Welcome/空态欢迎页
Promptscomponents/Prompts/预设问题 Chip(wrap 换行 / 分页)
Bubble / ChatItemcomponents/Bubble/ ChatItem/用户/助手消息气泡容器
Actionscomponents/Actions/复制、TTS、点攒/踩、刷新、删除

9.2 思考链(Think*)

组件职责
ThinkChain思考链主容器,树形步骤渲染
ThinkWorkflow工作流工具步骤
ThinkKnowBase知识库检索步骤
ThinkTamper人工干预步骤
ThinkModel模型思考步骤
ThinkDispatcher步骤类型分发器

9.3 富 Markdown(Md*)

组件自定义标签 / 能力
MdContent主 Markdown 渲染器(GFM + 数学 + 自定义 Tag)
MdCodeHighlighter语法高亮代码块
MdECharts<md-echarts> JSON → 柱状/折线/饼/雷达/表格
MdMermaidMermaid 流程图 + 导出
MdForm<md-form> JSON 驱动动态表单
MdTable增强表格
MdLatexLaTeX 公式
MdImg自定义图片标签
MdSup引用角标 + Popover
MdDataTableSelect可选数据表格

9.4 SDK 消息层

组件路径职责
AgentMessageListsdk/message/components/messages[] → Bubble + BlockRenderer
BlockRenderersdk/message/blocks/按 block.type 分发到注册组件
ClarifyCardsdk/message/blocks/ClarifyCard/交互式澄清卡片
DocumentBlocksdk/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 个真实问题复盘

#问题根因解决方案
1Mock 页首次进入预设问题/发送无效React StrictMode 双挂载 dispose 了 Engine,useMemo 缓存了已销毁实例useResolveEngine 每帧从 Registry 重新解析,disposed 则 ensureEngine
2进入 Mock 页仍显示上次会话Engine 按 agentID:sessionId 缓存,路由切换未清理MockChat mount/unmount 时 destroyEnginesForAgent
3ECharts 流式时闪一下又空白ReactMarkdown 每次 content 变化 recreate components,ECharts 被 disposememo(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 importBlockRenderer 内显式 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.connectWebSocket / 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.tslocalStorage 持久化
src/mock/createMockTransport.tsMock SSE 模拟
src/services/createDeepSeekService.tsDeepSeek 接入
src/sdk/hooks/useAgentChat/index.tsSDK 主 Hook
src/sdk/engine/registry.tsEngine 生命周期
src/sdk/protocol/processChunk.tsChunk → Block 解析
src/components/MdContent/富 Markdown 渲染
src/sdk/overview/summary.mdSDK 设计文档

热门栏目