实现一个兼容多输出类型的 Agent Chat

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

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

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

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

文章目录

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

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-chat-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-chatdeepseek-reasoner 模型切换;
  • API Key 存于 localStoragedeepseek_api_key),支持设置 / 修改 / 清除;
  • 前端直连 DeepSeek SSE(https://api.deepseek.com/chat/completions);
  • 会话历史持久化到 localStorageai-chat-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 数据流(一次用户提问)

  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目录结构与模块职责

bash 复制代码
src/
├── main.tsx                 # 入口:Arco CSS + KaTeX + 全局样式
├── app/App.tsx              # BrowserRouter 三路由
├── pages/
│   ├── ModeSelect/          # 首页
│   ├── MockChat/            # 模拟模式(destroyEnginesForAgent)
│   └── RealChat/            # 真实模式(API Key Modal)
├── components/
│   ├── chat/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

arduino 复制代码
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/chat/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 --- 引擎重置策略

scss 复制代码
// 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 核心逻辑

typescript 复制代码
// 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/chat/ 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 流式渲染稳定性

scss 复制代码
// 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 容错

arduino 复制代码
// 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 流式切分

go 复制代码
// src/mock/createMockTransport.ts
/** 普通正文按小段切;代码围栏 / md-echarts 整帧,避免半截 JSON */
function splitStreamableText(text: string): string[] {
  const pattern = /(```[\s\S]*?```|<md-echarts>[\s\S]*?</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 工厂

javascript 复制代码
// 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 引擎回收(核心代码)

scss 复制代码
// 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 本地运行

bash 复制代码
git clone <your-repo>
cd ai-chat
npm install
npm run dev
# 浏览器打开 http://localhost:5173

16.2 构建部署

arduino 复制代码
npm run build    # 产物在 dist/
npm run preview  # 本地预览生产构建

16.3 关键文件索引

文件 说明
src/app/App.tsx 路由定义
src/components/chat/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 设计文档
相关推荐
Rocky Ding*1 小时前
【三年面试五年模拟】2026-09-16 字节跳动 Agent 秋招一面全解析:从 Harness、记忆与并发到算法题的系统化解析
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·ai agent
AI创界者1 小时前
Flux 2 本地无限制独立部署指南:文生图/图生图/多图参考架构解析与开箱即用整合包
人工智能·aigc
小小程序猴11 小时前
GEO的技术实现视角:可引用性(Citability)如何工程化
人工智能
鲜于言悠9051 小时前
android进程是怎么来的
人工智能
Είναι η κοπέλα1 小时前
小目标检测实战:YOLO26 的四板斧与避坑清单
人工智能
陈童学哦1 小时前
Codex配置瘦身实践:给Agent提示词做一次深度大扫除
人工智能
shark-chili2 小时前
关于AI辅助编程的认知
数据库·人工智能·redis·macos·缓存
机核研创社2 小时前
一次性内裤无人生产线详解:从裁片到成品 12–35 秒一件的整线自动化架构
人工智能·自动化
有Li2 小时前
【AI答疑】MR数据预处理步骤
人工智能·笔记·算法·语言模型·医学生