Notex:一个开源 NotebookLM 替代方案的实现

cover

起源

01-起源

Google 的 NotebookLM 上线后,不少人体验了它的文档问答和内容生成功能。它很好用,但有个问题:数据需要上传到 Google 的服务器。对于一些敏感的内部文档,这不太合适。

于是就想做一个开源版本:数据存在本地,想用什么模型就自己配置。一个元旦假期,基本功能就跑起来了。

项目叫 Notex,代码在 GitHub 上,今天聊聊它的实现。

本项目受open-notebook项目的启发。但是open-notebook缺少信息图、思维导图、幻灯片等高级功能。本项目弥补了这些功能
信息图采用最新的nano banana pro实现
幻灯片采用 宝玉的 《预订本年度最有价值提示词 —— 生成既有质感,又能随意修改文字的完美 PPT》 的方式,生成真正意义上的PPT

功能概览

02-功能概览

Notex 是一个隐私优先的开源知识管理工具,基于 RAG(检索增强生成)技术。它支持多种文档格式,通过 AI 帮助你理解、总结和可视化文档内容。

核心功能

功能分类 具体能力
文档管理 支持 PDF、DOCX、PPTX、Markdown、TXT、HTML 等多种格式的文档上传和解析
AI 对话 基于文档内容的智能问答,回答会标注来源引用,避免 AI 幻觉
内容转换 摘要、FAQ、学习指南、大纲、时间线、术语表、测验、播客脚本等 9 种预设类型
视觉生成 思维导图(Mermaid.js)、信息图(Gemini Nano Banana Pro)、幻灯片/PPT
模型支持 OpenAI(含兼容 API)、Ollama 本地模型、Google Gemini

特色功能详解

1. 幻灯片(PPT)生成

采用两阶段生成流程:

  • 阶段一:使用 Gemini Flash 生成 PPT 大纲,包含:

    • 全局风格指南(字体、色彩、设计美学)
    • 每页的叙事目标、关键内容、视觉元素、布局说明
  • 阶段二:使用 Gemini Pro Image 为每页幻灯片生成配图

最终输出图文并茂的完整 PPT,而非简单的文字大纲。

2. 信息图生成

通过 Prompt Engineering 将文本内容"翻译"为结构化的视觉描述,再调用 Gemini Nano Banana Pro 生成手绘风格的信息图。

适用于:数据可视化、流程说明、概念解释等场景。

3. 思维导图生成

自动将文档结构提炼为 Mermaid.js mindmap 格式,支持:

  • 中心主题、主要分支、细节节点的层级展示
  • 自动缩放和交互式导航
  • 导出为 SVG/PNG

4. 智能问答(RAG)

支持基于文档内容的自然语言问答:

  • 中文/英文自适应分词和检索
  • 回答会标注来源,可追溯原文
  • 支持多轮对话历史

整体架构

03-整体架构

技术栈选型很简单:

1
2
3
后端:Go 1.25 + Gin + SQLite
前端:原生 HTML/CSS/JavaScript
AI:LangChainGo + OpenAI/Ollama + Gemini

为什么不选 Python?因为 Go 编译完就是一个二进制文件,部署起来省事。而且 Go 的并发性能好,后续如果需要处理大量文档也不会成为瓶颈。

目录结构也很清晰:

1
2
3
4
5
6
7
8
9
notex/
├── main.go # 入口
├── backend/
│ ├── agent.go # AI 调用逻辑
│ ├── server.go # HTTP 服务
│ ├── vector.go # 文档索引
│ ├── store.go # 数据持久化
│ └── nanobanana.go # Gemini 图片生成
└── frontend/ # 单页应用(嵌入二进制)

前端通过 //go:embed 编译进二进制,一个文件就能跑起来。

核心模块

1. 文档索引(vector.go)

04-核心模块-向量

RAG 的第一步是把文档切成小块,建立索引。

这里有个问题:中文和英文的分词方式不一样。英文按空格分词就行,中文需要按字符切。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 检测 CJK 字符比例
cjkCount := 0
for _, r := range runes {
if r >= 0x4E00 && r <= 0x9FFF { // CJK Unified Ideographs
cjkCount++
}
}
cjkRatio := float64(cjkCount) / float64(len(runes))
if cjkRatio > 0.3 {
// 中文按字符切分
for i := 0; i < len(runes); i += (chunkSize - chunkOverlap) {
// ...
}
} else {
// 英文按单词切分
words := strings.Fields(text)
// ...
}

检索部分目前用的是简单的关键词匹配,没有用向量 Embedding。原因?对于中小规模的文档(几万字以内),关键词匹配够用了,而且快。如果后续文档量大,可以接入向量数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 多层评分策略
score := 0.0
// 1. 子串匹配(权重高)
if strings.Contains(content, queryLower) {
score += 10.0
}
// 2. 字符匹配率
matchCount := 0
for _, r := range queryRunes {
if strings.ContainsRune(content, r) {
matchCount++
}
}
if matchCount > 0 {
charMatchRatio := float64(matchCount) / float64(len(queryRunes))
score += charMatchRatio * 5.0
}
// 3. 单词匹配(针对英文)
queryWords := strings.Fields(queryLower)
for _, word := range queryWords {
if len(word) > 2 && strings.Contains(content, word) {
score += 2.0
}
}

2. AI 调用(agent.go)

05-核心模块-AI

这块是核心,负责和 LLM 打交道。

使用 LangChainGo 作为抽象层,支持 OpenAI 和 Ollama:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func createLLM(cfg Config) (llms.Model, error) {
if cfg.IsOllama() {
return ollamallm.New(
ollamallm.WithModel(cfg.OllamaModel),
ollamallm.WithServerURL(cfg.OllamaBaseURL),
)
}
opts := []openai.Option{
openai.WithToken(cfg.OpenAIAPIKey),
openai.WithModel(cfg.OpenAIModel),
}
if cfg.OpenAIBaseURL != "" {
opts = append(opts, openai.WithBaseURL(cfg.OpenAIBaseURL))
}
return openai.New(opts...)
}

Prompt Engineering 是关键。不同的转换类型需要不同的 prompt 模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case "mindmap":
return `你是一位资深的信息架构师和知识管理专家。
请将【文本内容】提炼并转换为 Mermaid.js 的 mindmap 格式。
# 样式规范:
1. 中心主题:root((内容))
2. 主要分支:(内容)
3. 细节节点:[内容]
# 严格规范:
- 严禁使用 graph, LR, --> 等字符
- 节点内容 10 字以内
- 只输出代码块,不解释
来源:{sources}
`

注意这里的 prompt 设计:明确的输出格式要求,加上反面示例("严禁"),能有效减少 LLM 输出格式不稳定的问题。

3. PPT 生成

06-核心模块-PPT

这是比较复杂的 feature。需要两个步骤:

第一步:生成大纲

用 Gemini Flash 生成 PPT 大纲,每页包含:

  • 叙事目标(这张幻灯片要讲什么)
  • 关键内容(标题、要点)
  • 视觉元素(需要什么图)
  • 布局(怎么排版)

第二步:解析和生成图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (a *Agent) ParsePPTSlides(content string) []Slide {
// 1. 提取风格指南
styleStart := strings.Index(content, "<STYLE_INSTRUCTIONS>")
styleEnd := strings.Index(content, "</STYLE_INSTRUCTIONS>")
// 2. 按幻灯片分割(支持多种标记)
re := regexp.MustCompile(`(?m)^(?:\s*#{1,6}\s*)?(?:Slide|幻灯片)\s*\d+`)
indices := re.FindAllStringIndex(content, -1)
// 3. 验证每张幻灯片是否包含必需字段
if strings.Contains(lower, "叙事目标") || strings.Contains(lower, "关键内容") {
slides = append(slides, Slide{Style: style, Content: slideContent})
}
}

然后为每张幻灯片调用 Gemini Pro Image 生成图片:

1
2
3
4
5
for i, slide := range slides {
prompt := fmt.Sprintf("Style: %s\n\nSlide Content: %s", slides[0].Style, slide.Content)
imagePath, err := s.agent.GenerateImage(ctx, "gemini-3-pro-image-preview", prompt)
slideURLs = append(slideURLs, "/uploads/"+filepath.Base(imagePath))
}

4. 信息图生成

07-核心模块-信息图

信息图的核心是让 LLM 把文本内容"翻译"成视觉描述,然后把描述作为 prompt 喂给图像模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
case "infograph":
return `# Role
你是一位世界顶级的数据可视化设计师和信息图专家。
# Task
阅读所附文本,设计一张信息图。不要进行总结,
而是描述这张图应该长什么样。你的输出将被直接用作图像生成的 prompt。
# Design Guidelines
1. 核心信息提炼:找出最重要的 3-5 个数据点
2. 视觉隐喻:用形象的比喻(如网络安全用"盾牌和锁")
3. 布局结构:明确定义图的结构
4. 文本限制:极简,只保留标题和关键数据
5. 风格:插画或手绘感
# Output Format
Start with "Infographic illustration created in a soft, hand-drawn digital art style..."
[描述整体布局和背景风格]
[详细描述主要视觉元素]
...
`

这个 prompt 设计的要点是:明确告诉 LLM 它的输出会被用作另一个 prompt,这样 LLM 就会更注意输出的结构化和可用性。

5. 思维导图

08-核心模块-思维导图

思维导图的实现最简单,核心是生成 Mermaid.js 的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
case "mindmap":
return `将文本转换为 Mermaid.js mindmap 格式。
# 样式规范:
root((中心主题)) # 圆圈
(主要分支) # 圆角矩形
[细节节点] # 矩形
# 严格规范:
- 仅限 mindmap 语法
- 节点内容 10 字以内
- 严禁包含引号
`

前端用 Mermaid.js 自动渲染:

1
2
import mermaid from 'https://s4.zstatic.net/ajax/libs/mermaid/11.4.0/mermaid.min.js';
mermaid.initialize({ startOnLoad: true });

数据流

09-数据流

整个系统的数据流如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用户上传文档
markitdown 转换为 Markdown(如果需要)
文本分块并索引
存储到 SQLite
用户选择转换类型(PPT/信息图/思维导图)
根据类型构建 prompt
调用 LLM 生成内容
如果需要图片,调用 Gemini 生成
保存为 Note
前端渲染展示

部署和使用

10-部署和使用

本地运行

1
2
3
4
5
6
7
8
9
10
11
12
git clone https://github.com/smallnest/notex.git
cd notex
go mod tidy
# 使用 OpenAI
export OPENAI_API_KEY=your_key
go run . -server
# 或使用 Ollama(本地模型)
export OLLAMA_BASE_URL=http://localhost:11434
export OLLAMA_MODEL=qwen2.5:7b
go run . -server

Docker 部署

1
docker-compose up

环境变量

变量 说明 默认值
OPENAI_API_KEY OpenAI API 密钥 -
OPENAI_BASE_URL 自定义 API 地址 -
OPENAI_MODEL 模型名称 gpt-4o-mini
OLLAMA_BASE_URL Ollama 地址 -
OLLAMA_MODEL Ollama 模型 -
GOOGLE_API_KEY Gemini API(PPT/信息图) -
SERVER_PORT 服务端口 8080

扩展性

代码结构支持几种扩展方式:

1. 添加新的转换类型

agent.gogetTransformationPrompt 添加新 case:

1
2
case "new_type":
return `你的 prompt 模板...`

2. 支持新的 LLM

LangChainGo 支持多种模型,只需在 createLLM 添加新的分支。

3. 接入向量数据库

VectorStore 目前是内存实现,可以替换成 Qdrant、Milvus 等。

4. 添加新的文档格式

vector.goneedsMarkitdown 添加新的文件扩展名。

代码量和学习价值

项目核心代码大约 2000 行左右,适合学习:

  1. RAG 的完整实现:从文档处理到检索到生成
  2. Prompt Engineering:如何设计稳定的 prompt
  3. Go Web 开发:Gin 框架、SQLite、并发处理
  4. 前后端分离:单页应用 + REST API
  5. LLM 应用架构:如何组织一个 AI 应用的代码

如果你想学习如何构建 AI 应用,或者想找一个 NotebookLM 的替代方案,这个项目可以作为一个起点。

总结

Notex 不是什么高大上的项目,代码也写得朴素。但它解决了实际问题:让你在本地使用 AI 处理文档,数据不外泄。

核心功能就几个:

  • 文档问答(RAG)
  • 多种内容转换(摘要、FAQ、大纲等)
  • 视觉内容生成(PPT、信息图、思维导图)

技术选型务实地选择了 Go 和原生 JS,没有过多的依赖。部署简单,维护成本低。

如果你感兴趣,可以看看代码,提提 PR,或者直接 fork 改成你自己的工具。


项目地址: https://github.com/smallnest/notex

欢迎 Star、Issue、PR。