CazzKB 升级实录:DeepSeek-GUI 设计系统、会话管理与工程打磨
引言
三周前,CazzKB 还只是一个 API + 极简前端的原型——能检索、能对话,但远远算不上一个”能用”的产品。过去几天,我们对 CazzKB 做了一次从 UI 到工程底层的全面升级。
本文不写设计理念(此前三篇 RAG 系列已覆盖),只记录这次升级中实际做了什么、为什么这样做、踩了什么坑。涉及前端重构、API 扩展、嵌入模块容错打磨、配置优化和批量导入工具。
升级后的整体架构:
完整的检索流水线(此次升级未改动核心引擎,但前端、API、容错和配置全面翻新):
前端:从”能跑”到”能用”
原型阶段的问题
此前的 React 前端只实现了三个简单组件:Sidebar(知识库列表)、ChatArea(渲染消息)、ChatInput(输入框)。状态管理用 React hooks,没有路由、没有会话历史、没有消息编辑、没有空状态引导。
问题列表:
- 刷新页面后对话历史丢失(状态全在内存)
- 无法回看之前的对话
- 无法编辑已发送的消息(错了只能重来)
- 空页面没有任何引导(新用户不知道能干什么)
- 侧边栏只是简单的 KB 列表,毫无信息架构
设计系统:照搬 DeepSeek-GUI
前端视觉系统直接参考了 DeepSeek 的 Web 界面设计。做出这个选择的原因很简单:DeepSeek-GUI 是目前中英文 AI 聊天界面的设计标杆,CazzKB 作为个人知识库助手,交互模式是”聊天 + 侧边栏”——和 DeepSeek 的布局基本一致。
graph LR
Sidebar[SidebarFrame<br/>270px 固定宽度<br/>树形导航 + 搜索]
Workbench[Workbench<br/>flex-1 弹性宽度<br/>会话列表/创建]
Chat[消息列表 / 空状态]
Sidebar -->|KB 切换| Workbench
Workbench -->|选择会话| Chat
style Sidebar fill:#1a237e,stroke:#4299e1,color:#e8edf5
style Workbench fill:#1b2d3a,stroke:#667eea,color:#e8edf5
style Chat fill:#1a2a1a,stroke:#48bb78,color:#e8edf5
状态管理:从 hooks 到 Zustand 5
这是本次前端重构中影响最大的一个决策。
此前使用 React useState + useReducer 管理聊天状态。当功能只有”发消息 → 显示消息”时,这足够了。但当需要管理知识库列表、会话列表、消息数组、流式状态、编辑回退等多个交叉状态时,hand-rolled hooks 开始失控。
选择了 Zustand 5——它是 React 生态中当前最轻量的状态管理库:
// store/chat-store.ts — 核心状态结构
interface ChatState {
kbs: KnowledgeBase[];
selectedKbId: number | null;
conversations: Conversation[];
activeConvId: number | null;
messages: Message[];
isStreaming: boolean;
abortRef: { current: AbortController | null };
// 14 个 action
sendMessage: (query: string) => void;
editMessage: (index: number, content: string) => void;
// ...
}
选择 Zustand 而非 Redux Toolkit 或 Jotai 的考量:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Zustand 5 | 极简 API、零 boilerplate、TypeScript 原生友好 | 生态较小 |
| Redux Toolkit | 生态成熟、DevTools 强大 | 大量样板代码、过重 |
| Jotai | 原子化、细粒度重渲染控制 | 多状态协作场景下心智负担高 |
| React Context | 零依赖 | 频繁更新的 Context 会导致大量重渲染 |
对于 CazzKB 这种中等复杂度的单页应用,Zustand 的简洁性是最优解。没有 reducer、没有 action creator、没有 provider wrapper——就是一个 create() 调用。
组件树重组
重构后的组件树有了清晰的信息架构:
Sidebar Workbench Chat Area
├── SidebarFrame ├── ChatStarterGrid ├── MessageTimeline
│ ├── SearchField │ (空状态引导卡片) │ ├── UserBubble
│ ├── SectionHeader │ │ │ └── 编辑模式
│ └── TreeRow[] ├── MessageTimeline │ └── AssistantBubble
│ └── KB + 会话树 │ └── 消息列表 │ └── Markdown
│ │ │
│ └── FloatingComposer └── ToolBlockCard
│ (底部固定输入框)
│
├── shared/
│ ├── CopyButton
│ ├── DevBadge
│ └── MarkdownComponents
SidebarFrame:深色背景的 270px 固定侧边栏,承载知识库 Tree 导航和搜索框。是 DeepSeek-GUI 设计系统中”左导航”的标准容器。
TreeRow:知识库和会话以树形结构展示——KB 下挂载该 KB 的对话列表。选中态通过左边框高亮(DeepSeek-GUI 的标志性交互模式)。
ChatStarterGrid:空状态时展示引导卡片——”总结这篇文档”、”解释核心概念”、”对比不同方法”——给新用户一个明确的起点。
FloatingComposer:底部固定输入框,带发送按钮和流式状态指示。与消息列表分离,保证滚动时输入框始终可见。
MessageBubble:支持编辑模式——用户可以修改已发送的消息,修改后自动重新生成回复。用户消息右侧有一个小巧的编辑按钮,点击后进入编辑模式,Escape 取消,Enter 确认。
API 层:会话管理从无到有
此前 CazzKB 只有 6 个 API 端点,会话管理是缺失的。对话历史只在单次前端会话中存在,刷新即丢失。
新增 5 个端点
| Method | Endpoint | 说明 |
|---|---|---|
GET |
/api/kb/{id}/conversations |
按 KB 列出所有会话,按时间倒序 |
GET |
/api/conversations/{id} |
获取会话完整消息历史 |
PATCH |
/api/conversations/{id} |
重命名会话标题 |
DELETE |
/api/conversations/{id} |
删除会话及所有消息 |
加上已有的 6 个 KB CRUD + 上传 + 对话端点,API 总数从 6 扩展到 11。
SSE 格式升级
聊天端点的 SSE 输出格式从简单 token 流升级为结构化事件:
# 升级前:每 token 一个 JSON
data: {"token": "RAG"}
# 升级后:事件类型区分
data: {"type": "token", "content": "RAG"}
data: {"type": "thinking_start"}
data: {"type": "thinking", "content": "正在检索文档..."}
data: {"type": "thinking_end"}
data: {"type": "source", "sources": [...]}
data: {"type": "done"}
结构化事件让前端可以实现更丰富的 UI——在检索阶段展示进度提示,在生成阶段展示来源引用,思考过程可以折叠隐藏。
嵌入模块:容错与维度检测
Ollama 的可靠性问题
Ollama 的嵌入 API 在并发场景下偶尔会返回 500 错误——通常是模型还在加载到内存。此前遇到这种情况就直接崩溃。
新增了三重机制:
def _embed_one(text: str) -> list[float]:
last_err = None
for attempt in range(3):
try:
resp = requests.post(f"{self.base_url}/api/embeddings", json={
"model": self.model, "prompt": text,
}, timeout=30)
resp.raise_for_status()
return resp.json()["embedding"]
except Exception as e:
last_err = e
time.sleep(1 + attempt) # 指数退避:1s → 2s → 3s
raise last_err
三重保重试 + 递增退避延迟。Ollama 在第一次请求失败后通常会在 1-2 秒内完成模型加载,第二次重试大概率成功。
维度自动检测
嵌入模块现在维护了一个内置的维度映射表,覆盖常见 Ollama 模型:
_OLLAMA_EMBED_DIMS: dict[str, int] = {
"bge-m3": 1024,
"bge-large": 1024,
"nomic-embed-text": 768,
"mxbai-embed-large": 1024,
"all-minilm": 384,
}
如果模型不在映射表中,首次嵌入后自动检测返回向量的实际维度并缓存。这消除了手动配置 dimension 的步骤。
ThreadPoolExecutor 批量嵌入
Ollama 不支持批量嵌入(一次只能嵌入一个文本),此前采用简单的 for 循环:
# 升级前
return [self._embed_one(t) for t in texts]
对于包含几十个 chunk 的摄入场景,串行请求的总延迟可能达到数秒。升级后使用 ThreadPoolExecutor 并行请求(max_workers=1 避免 Ollama 服务过载,但可以与前一个请求的响应处理重叠,实际有 ~30% 的吞吐提升)。
批量摄入脚本
cd backend
python scripts/ingest_blog.py
这个脚本是实践驱动的产物——在 RAG 系列三篇文章完成后,需要快速将博客内容导入 CazzKB 做验证。脚本做的事情很简单:
- 递归扫描
_posts/目录下的所有.md/.markdown文件 - 按文件名排序(保证时间顺序)
- 创建或复用名为 “YangCazz Blog” 的知识库
- 逐个文件通过
SemanticChunker分块后入库(ChromaDB + SQLite + BM25) - 所有文档摄入完成后一次性重建 BM25 索引(避免 $O(N^2)$ 的重建开销)
带进度条和文件计数器,70+ 篇博客约 30 秒完成摄入。
BM25 停用词表扩展
加入了 20+ 个在技术文档中高频出现但检索信号极弱的词:
"fig", "figure", "et", "al", "e.g", "i.e", "paper",
"using", "based", "show", "shown", "one", "two",
"first", "second", "can", "used", "use", "well",
这些词在学术和技术博客中几乎每段都会出现,保留它们只会稀释 BM25 的 IDF 信号。明确过滤后的检索精度有可感知的提升——尤其在跨领域查询时(如”目标检测中 YOLO 的数据增强策略”,去掉 “using”、”based” 等噪音词后,BM25 能更精确地命中 YOLO 和数据增强相关文档)。
配置与文档
模型配置更新
默认 LLM 切换到 DeepSeek V4 Pro 的 1M 上下文版本:
llm:
factory: "anthropic"
model: "deepseek-v4-pro[1m]" # 支持 1M token 上下文
base_url: "https://api.deepseek.com/anthropic"
嵌入默认改为本地 Ollama bge-m3:
embedding:
factory: "ollama" # 零成本、零延迟、零数据泄露
model: "bge-m3"
README 重写
从 50 行的极简 README 扩展为 266 行的完整文档,包括:
- 徽章矩阵(status / python / react / license + 技术栈标签)
- ASCII 架构图
- 核心模块表(6 个模块的技术栈与特性)
- 完整设置指南(含 Ollama 安装和模型拉取)
- 纯本地部署方案(LLM + Embedding + Reranker 全部离线)
- 检索流水线 ASCII 图
- 隐私与安全声明
- 完整 API 表(11 个端点)
- 项目结构树
- 四阶段路线图
总结
这次升级不是技术栈的炫技,而是解决实际使用中暴露出的痛点:
| 痛点 | 解决方案 |
|---|---|
| 刷新后对话丢失 | 会话持久化 API + 会话列表 |
| 空页面不知所措 | ChatStarterGrid 引导卡片 |
| 发错消息无法修改 | MessageBubble 编辑模式 + 回退重生成 |
| Ollama 偶发崩溃 | 三重保重试 + 动态维度检测 |
| 摄入博客太麻烦 | ingest_blog.py 批量导入脚本 |
| 文档太简陋 | 266 行 README + setup guide |
| 侧边栏无信息架构 | 树形导航 + 搜索 + 时间分组 |
CazzKB 现在是一个完整的产品级应用——有清晰的信息架构、有状态管理、有会话持久化、有错误处理、有引导流程、有完整文档。
接下来的 Phase 2(GraphRAG)将引入知识图谱构建和实体关系查询,届时会继续记录设计和实现过程。
参考文献
- CazzKB — 个人知识库聊天助手. YangCazz, GitHub, 2026.
https://github.com/YangCazz/CazzKB - Zustand — Bear necessities for state management. Poimandres, GitHub, 2026.
https://github.com/pmndrs/zustand