1
This commit is contained in:
+881
@@ -0,0 +1,881 @@
|
||||
# rag-local 本地知识库 实现方案
|
||||
|
||||
> 技术栈:GoFrame v2 + Vue 3 + Eino(字节 CloudWeGo LLM 编排框架)
|
||||
> 数据库:全部文件型 —— SQL = SQLite,向量 = sqlite-vec(SQLite 扩展),全文检索 = SQLite FTS5
|
||||
> 部署:docker-compose 一键部署,前后端不分离,统一端口暴露
|
||||
> 鉴权:单用户 + 启动生成的访问令牌(面向边缘设备部署)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
本地私有知识库系统(面向 NAS / 小主机等边缘设备单机部署),支持:
|
||||
|
||||
- 文档管理:上传(txt / md / pdf / docx / html)、自动解析、分块、向量化
|
||||
- 混合检索:语义(向量)检索 + 关键词(全文)检索,RRF 融合排序
|
||||
- RAG 问答:基于 Eino 编排的流式对话,答案附带引用来源
|
||||
- 模型可配置:对话模型 / 嵌入模型均为 OpenAI 兼容 API,可对接任意供应商(本地 Ollama、DeepSeek、硅基流动、OpenAI 等)
|
||||
- 单用户访问令牌登录:token 首次启动随机生成并打印在控制台,登录页输入即可使用
|
||||
|
||||
设计原则:
|
||||
|
||||
1. **全文件型数据库**:业务 SQL、向量、全文检索全部落地为普通文件,随数据目录一键备份/迁移,无任何外部数据库服务
|
||||
2. **按业务领域分库文件**:系统 / 数据集 / 问答 三个 SQLite 文件,互不干扰
|
||||
3. **分层文件与表对齐**:每张业务表对应一组 `entity / dao / service / controller / dto` 文件,数量严格对齐(参照 video-factory 17 表 × 5 层的组织方式)
|
||||
4. **前后端不分离**:Vue 构建产物由 GoFrame 统一端口托管,单容器部署
|
||||
5. **零配置**:不设用户体系与角色权限(单机单人场景),启动即用
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术选型
|
||||
|
||||
| 能力 | 选型 | 说明 |
|
||||
|---|---|---|
|
||||
| Web 框架 | GoFrame v2.10+ | 与 video-factory 一致,复用其分层/路由/鉴权模式 |
|
||||
| SQL 数据库 | SQLite(modernc.org/sqlite 纯 Go 实现) | GoFrame `contrib/drivers/sqlite/v2`,免 CGO,`CGO_ENABLED=0` |
|
||||
| 向量检索 | **sqlite-vec**(`modernc.org/sqlite/vec` 子包) | modernc 从 **v1.47.0 起内置** sqlite-vec 的 CGO-free 版本,`vec0` 虚拟表提供 KNN 检索;与业务表**同库同事务**,天然文件型 |
|
||||
| 全文检索 | SQLite FTS5(modernc 内置) | BM25 打分;中文通过**应用层分词**(纯 Go `github.com/go-ego/gse`)写入分词列,simple tokenizer 索引 |
|
||||
| LLM 编排 | Eino `github.com/cloudwego/eino` | ChatModel / Embedding 自研 OpenAI 兼容 HTTP 实现(eino-ext 为空壳,仅依赖 eino 接口);Indexer / Retriever 自研(SQLite 后端),实现 Eino 标准接口 |
|
||||
| 文档解析 | pdfcpu(pdf)、纯 Go zip+xml 解析(docx)、goquery(html)、标准库(txt/md) | 全部纯 Go,免 CGO |
|
||||
| 前端 | Vue 3 + Vite + Element Plus + axios | hash 路由,构建产物 `ui-src/dist`,由 Go 统一端口托管 |
|
||||
| 部署 | Docker 多阶段构建 + docker-compose | 单服务单端口,数据目录挂载宿主机卷 |
|
||||
|
||||
### 2.1 关键版本说明(向量扩展)
|
||||
|
||||
- sqlite-vec 需要 `modernc.org/sqlite >= v1.47.0`(2026-03-17 起内置,免 CGO)
|
||||
- GoFrame v2.10.2 驱动链默认锁定 `modernc.org/sqlite v1.23.1`(过旧),**必须在 go.mod 中显式升级**:
|
||||
```bash
|
||||
go get modernc.org/sqlite@v1.47.0
|
||||
```
|
||||
Go modules 最小版本选择(MVS)会使全链路统一使用 v1.47+;glebarez 为薄封装,API 兼容。主程序只需:
|
||||
```go
|
||||
import _ "modernc.org/sqlite/vec" // 空导入,init 自动注册 vec0 扩展
|
||||
```
|
||||
- **风险与验证**:升级后首步执行 `go build` 验证 glebarez 与 v1.47 兼容;若出现编译错误,启用备选方案(见 §14.1)
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────── 浏览器 ───────────────────────────┐
|
||||
│ http://host:8080 │
|
||||
└──────────────────────────────┬───────────────────────────────┘
|
||||
│ 统一端口(SPA 静态资源 + REST API + SSE 流式)
|
||||
┌──────────────────────────────▼───────────────────────────────┐
|
||||
│ GoFrame HTTP Server (:8080) │
|
||||
│ ┌───────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||
│ │ 静态资源 │ │ API 路由 │ │ 访问令牌鉴权 / CORS / │ │
|
||||
│ │ ui-src/dist│ │ /dataset │ │ panic 恢复 中间件 │ │
|
||||
│ └───────────┘ │ /document ... │ └────────────────────────┘ │
|
||||
│ └──────┬───────┘ │
|
||||
│ ┌──────────────┼──────────────┐ │
|
||||
│ ┌──────▼─────┐ ┌──────▼─────┐ ┌──────▼──────┐ │
|
||||
│ │ controller │→│ service │→│ dao │ │
|
||||
│ │ (10 个文件) │ │ (10 个文件) │ │ (10 个文件) │ │
|
||||
│ └────────────┘ └──────┬─────┘ └──────┬──────┘ │
|
||||
│ │ │ │
|
||||
│ ┌─────────▼──────┐ ┌────▼─────────────────────┐ │
|
||||
│ │ Eino RAG 编排 │ │ SQLite × 3(文件型) │ │
|
||||
│ │ Graph/Chain │ │ business.db: 业务表+向量+ │ │
|
||||
│ │ Indexer/Retr. │ │ FTS(vec0 + fts5) │ │
|
||||
│ │ ChatModel │ │ system.db: 令牌/配置 │ │
|
||||
│ │ Embedding │ │ chat.db: 会话/消息 │ │
|
||||
│ └───────┬────────┘ └───────────────────────────┘ │
|
||||
│ │ HTTP(OpenAI 兼容 API) │
|
||||
└──────────────────────┼────────────────────────────────────────┘
|
||||
┌────────▼────────┐
|
||||
│ 模型供应商(可配置)│
|
||||
│ Ollama/OpenAI/ │
|
||||
│ DeepSeek/硅基流动 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
数据流:
|
||||
|
||||
```
|
||||
上传文档 → 解析文本 → 分块 → Embedding 向量化 ──┐
|
||||
├─→ business.db(chunk 表 + vec0 + fts5 同库同事务)
|
||||
关键词检索(FTS5 BM25)───────────────────────┤
|
||||
↓
|
||||
问题 → 混合检索(向量 + 关键词 + RRF 融合)→ 组装上下文 → ChatModel 流式回答 → SSE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 目录结构
|
||||
|
||||
```
|
||||
rag-local/
|
||||
├── main.go # 入口:路由注册、静态资源托管、任务轮询启动、访问令牌生成/打印
|
||||
├── config.yml # 多库配置 + 服务配置
|
||||
├── go.mod / go.sum
|
||||
├── Dockerfile # 多阶段构建(ui-builder → builder → runtime)
|
||||
├── docker-compose.yml # 一键部署,挂载数据卷
|
||||
├── common/ # 公共层(与 video-factory 对齐)
|
||||
│ ├── http.go # RouteRegister 自动路由 + OpenAPI + 中间件注册
|
||||
│ ├── auth.go # JWT 签发/解析 + 访问令牌指纹校验
|
||||
│ ├── auth_middleware.go # 鉴权中间件(白名单 + 静态资源放行)
|
||||
│ ├── base_dao.go # InsertAndReturnId / GetOneByPk / UpdateByPk / DeleteByPk
|
||||
│ ├── cache.go # DAO 查询缓存 TTL(database.cache.ttl)
|
||||
│ ├── util.go # 通用工具
|
||||
│ ├── parser.go # 解析器接口 + 注册表(按扩展名分发)
|
||||
│ ├── pdf_parser.go # pdfcpu 解析 PDF
|
||||
│ ├── docx_parser.go # zip+xml 解析 word/document.xml
|
||||
│ ├── html_parser.go # goquery 解析 HTML
|
||||
│ ├── text_parser.go # txt / md
|
||||
│ └── tokenizer.go # gse 中文分词(写索引/检索共用)
|
||||
├── kb/ # 业务模块(knowledge base,对应 video-factory 的 shortdrama)
|
||||
│ ├── consts/
|
||||
│ │ ├── table_name.go # 表名常量 + 数据库组常量(DbGroupSystem 等)
|
||||
│ │ ├── status.go # 文档状态 / 任务状态 / 消息角色
|
||||
│ │ ├── content_type.go # 文件类型 / 模型类型(chat/embedding)
|
||||
│ │ └── consts.go # 通用常量(向量维度默认值、TopK、access_token 配置键)
|
||||
│ ├── model/
|
||||
│ │ ├── entity/ # 10 个文件,与 10 张实体表一一对应
|
||||
│ │ ├── dto/ # 10 个文件,与 entity 对应(含 g.Meta 路由定义)
|
||||
│ │ └── domain/ # 领域对象(RAG 检索结果、引用来源、流式事件等)
|
||||
│ ├── dao/ # 10 个文件,与实体表一一对应
|
||||
│ ├── service/ # 10 个文件,与 dao 对应(文档流水线、RAG 问答编排归入对应 service)
|
||||
│ ├── controller/ # 10 个文件,与 service 对应
|
||||
├── ui-src/ # Vue 3 前端工程
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js # base:'/'、build.outDir:'dist'
|
||||
│ └── src/
|
||||
│ ├── main.js / App.vue
|
||||
│ ├── router/index.js # hash 路由(登录守卫)
|
||||
│ ├── api/ # axios 封装 + 各模块 API(与后端 dto 对齐)
|
||||
│ ├── store/ # Pinia(auth / dataset 状态)
|
||||
│ ├── views/
|
||||
│ │ ├── Login.vue # 输入访问令牌登录
|
||||
│ │ ├── Layout.vue
|
||||
│ │ ├── DatasetList.vue # 数据集列表
|
||||
│ │ ├── DatasetDetail.vue # 文档管理 + 上传 + 解析状态
|
||||
│ │ ├── Chat.vue # RAG 问答(SSE 流式 + 引用来源)
|
||||
│ │ └── Settings.vue # 模型配置 + 访问令牌管理
|
||||
│ └── components/
|
||||
│ ├── DocumentUpload.vue
|
||||
│ ├── ChunkList.vue
|
||||
│ └── MessageBubble.vue # 流式消息 + 引用折叠面板
|
||||
├── data/ # 仅数据库文件(gitignore)
|
||||
│ ├── business.db # 数据集领域
|
||||
│ ├── system.db # 系统领域
|
||||
│ └── chat.db # 问答领域
|
||||
├── workspace/ # 上传的文档源文件(gitignore,与数据库分离)
|
||||
│ └── {datasetId}/{yyyymmdd}/{uuid}.{ext}
|
||||
└── docs/
|
||||
└── 实现方案.md
|
||||
```
|
||||
|
||||
> 分层文件与表对齐规则(硬性约定,参照 video-factory):
|
||||
> - `model/entity/` 每个文件定义一张实体表的结构体,`orm` 标签命名
|
||||
> - `dao/` 每个文件 = 一张表的单例 DAO(`var Xxx = &xxxDao{}`),`init()` 内建表 + 索引 + 迁移
|
||||
> - `service/` 每个文件对应一个 DAO,承载业务逻辑(文档流水线、RAG 调用)
|
||||
> - `controller/` 每个文件对应一个 service,暴露 REST 接口(`g.Meta` 定义 path/method)
|
||||
> - `model/dto/` 每个文件定义一张表的 Req/Res 结构体
|
||||
> - 虚拟表(vec0 / FTS5)是 chunk 表的附属索引,**不单独建分层文件**,由 `chunk_dao.go` 统一管理
|
||||
> - **不建 parser/rag 等技术目录**:纯技术能力(文档解析、中文分词)平铺在 `common/`;业务编排(分块、Indexer、Retriever、工作流)归入对应 service 文件
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据库设计
|
||||
|
||||
### 5.1 数据库文件划分(按业务领域)
|
||||
|
||||
| 文件 | 配置组名 | 领域 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `data/business.db` | `default` | 数据集 | 数据集、文档、分块、向量(vec0)、全文索引(FTS5)、解析任务 |
|
||||
| `data/system.db` | `system` | 系统 | 访问令牌、模型配置、系统配置 |
|
||||
| `data/chat.db` | `chat` | 问答 | 会话、消息 |
|
||||
|
||||
config.yml:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
default:
|
||||
name: data/business.db
|
||||
type: sqlite
|
||||
debug: true
|
||||
system:
|
||||
name: data/system.db
|
||||
type: sqlite
|
||||
debug: true
|
||||
chat:
|
||||
name: data/chat.db
|
||||
type: sqlite
|
||||
debug: true
|
||||
cache:
|
||||
ttl: 60
|
||||
|
||||
server:
|
||||
address: :8080
|
||||
name: rag-local
|
||||
workerId: 1
|
||||
clientMaxBodySize: 209715200 # 200MB,支持大文件上传
|
||||
requestTimeout: 3000 # 秒;支持 AI 长响应
|
||||
```
|
||||
|
||||
### 5.2 表清单(10 张实体表 + 2 张虚拟表)
|
||||
|
||||
| # | 表名 | 库 | 类型 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `system_config` | system | 实体 | 访问令牌、默认模型等键值配置 |
|
||||
| 2 | `model_config` | system | 实体 | 模型配置(chat / embedding) |
|
||||
| 3 | `kb_dataset` | business | 实体 | 数据集 |
|
||||
| 4 | `kb_document` | business | 实体 | 文档 |
|
||||
| 5 | `kb_chunk` | business | 实体 | 分块 |
|
||||
| 6 | `kb_chunk_vec` | business | **虚拟表 vec0** | 分块向量(chunk_id 与 kb_chunk 1:1) |
|
||||
| 7 | `kb_chunk_fts` | business | **虚拟表 FTS5** | 分块全文索引 |
|
||||
| 8 | `kb_parse_task` | business | 实体 | 文档解析/向量化任务 |
|
||||
| 9 | `kg_entity` | business | 实体 | 知识图谱实体(LLM 抽取) |
|
||||
| 10 | `kg_relation` | business | 实体 | 知识图谱关系(头实体-关系-尾实体) |
|
||||
| 11 | `chat_conversation` | chat | 实体 | 问答会话 |
|
||||
| 12 | `chat_message` | chat | 实体 | 问答消息 |
|
||||
|
||||
> 实体表 10 张 → `entity / dao / service / controller / dto` 各 10 个文件,严格对齐。
|
||||
> 无 user / login_log / 角色权限:单用户场景,访问令牌存于 system_config。
|
||||
> 知识图谱(`kg_entity` / `kg_relation`)属数据集领域,见 §7.7。
|
||||
> 注:用 `sqlite3 .tables` 会看到 `kb_chunk_fts_*`(5 张)与 `kb_chunk_vec_*`(4 张)等额外表,
|
||||
> 它们是 FTS5 / vec0 虚拟表自动生成的**内部影子表**(倒排索引、向量分块等存储),由 SQLite 自动维护,
|
||||
> 不是业务表,不可删除,删除会导致虚拟表损坏。
|
||||
|
||||
### 5.3 建表 DDL
|
||||
|
||||
表名前缀:`kb_`(数据集领域)、`chat_`(问答领域);`consts.TableNameXxx` 常量集中管理,参照 video-factory。
|
||||
|
||||
```sql
|
||||
-- ==================== system.db ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cfg_key TEXT NOT NULL DEFAULT '',
|
||||
cfg_value TEXT NOT NULL DEFAULT '',
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_system_config_key ON system_config(cfg_key);
|
||||
|
||||
-- 预置键:
|
||||
-- access_token 访问令牌(首次启动生成,明文存储,见 §8)
|
||||
-- default_chat_model 默认对话模型配置 id
|
||||
-- default_dataset 默认问答数据集 id
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL DEFAULT '', -- 配置名称,如 "DeepSeek Chat"
|
||||
model_type TEXT NOT NULL DEFAULT 'chat', -- chat / embedding
|
||||
model_name TEXT NOT NULL DEFAULT '', -- 模型名,如 deepseek-chat
|
||||
endpoint_url TEXT NOT NULL DEFAULT '', -- 如 https://api.deepseek.com
|
||||
api_key TEXT NOT NULL DEFAULT '',
|
||||
dimension INTEGER NOT NULL DEFAULT 1024, -- 仅 embedding 有效(向量维度)
|
||||
extra TEXT NOT NULL DEFAULT '', -- JSON 扩展(temperature 等)
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
-- ==================== business.db ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_dataset (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
embedding_cfg_id INTEGER NOT NULL DEFAULT 0, -- 绑定 embedding 模型配置;切换需重新向量化
|
||||
status INTEGER NOT NULL DEFAULT 1, -- 1 正常 / 0 禁用
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_document (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL DEFAULT 0,
|
||||
filename TEXT NOT NULL DEFAULT '',
|
||||
file_path TEXT NOT NULL DEFAULT '', -- workspace 下相对路径(workspace/3/20260804/xxx.pdf)
|
||||
file_size INTEGER NOT NULL DEFAULT 0,
|
||||
file_type TEXT NOT NULL DEFAULT '', -- txt/md/pdf/docx/html
|
||||
status INTEGER NOT NULL DEFAULT 0, -- 0 待处理 1 处理中 2 完成 3 失败
|
||||
chunk_count INTEGER NOT NULL DEFAULT 0,
|
||||
error_msg TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_chunk (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL DEFAULT 0,
|
||||
document_id INTEGER NOT NULL DEFAULT 0,
|
||||
seq INTEGER NOT NULL DEFAULT 0, -- 分块序号
|
||||
content TEXT NOT NULL DEFAULT '', -- 分块原文
|
||||
meta TEXT NOT NULL DEFAULT '', -- JSON:来源页码/标题路径等
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_chunk_document ON kb_chunk(document_id);
|
||||
|
||||
-- 向量虚拟表(sqlite-vec,维度按 embedding 模型定,默认 1024)
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS kb_chunk_vec USING vec0(
|
||||
chunk_id INTEGER PRIMARY KEY,
|
||||
embedding float[1024]
|
||||
);
|
||||
|
||||
-- 全文索引虚拟表(FTS5,默认 unicode61 tokenizer + 应用层 gse 中文分词;
|
||||
-- FTS5 不支持 simple tokenizer,中文分词在应用层完成,content_tokens 存分词后空格连接文本)
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS kb_chunk_fts USING fts5(
|
||||
chunk_id UNINDEXED,
|
||||
dataset_id UNINDEXED,
|
||||
title,
|
||||
content_tokens -- gse 分词后空格连接
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_parse_task (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
document_id INTEGER NOT NULL DEFAULT 0,
|
||||
dataset_id INTEGER NOT NULL DEFAULT 0,
|
||||
task_type TEXT NOT NULL DEFAULT 'parse', -- parse / reembed
|
||||
status INTEGER NOT NULL DEFAULT 0, -- 0 待处理 1 处理中 2 完成 3 失败
|
||||
error_msg TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_parse_task_status ON kb_parse_task(status);
|
||||
|
||||
-- 知识图谱(LLM 抽取,见 §7.7)
|
||||
CREATE TABLE IF NOT EXISTS kg_entity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL DEFAULT 0,
|
||||
name TEXT NOT NULL DEFAULT '', -- 实体名
|
||||
entity_type TEXT NOT NULL DEFAULT '', -- 类型(person/org/location/...)
|
||||
meta TEXT NOT NULL DEFAULT '', -- JSON 扩展
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_kg_entity_dataset_name ON kg_entity(dataset_id, name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kg_relation (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL DEFAULT 0,
|
||||
head_id INTEGER NOT NULL DEFAULT 0, -- 头实体 kg_entity.id
|
||||
relation TEXT NOT NULL DEFAULT '', -- 关系类型
|
||||
tail_id INTEGER NOT NULL DEFAULT 0, -- 尾实体 kg_entity.id
|
||||
meta TEXT NOT NULL DEFAULT '', -- JSON:来源 chunk、置信度
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_kg_relation_head ON kg_relation(head_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kg_relation_tail ON kg_relation(tail_id);
|
||||
|
||||
-- ==================== chat.db ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_conversation (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dataset_id INTEGER NOT NULL DEFAULT 0, -- 问答绑定的数据集
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_message (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER NOT NULL DEFAULT 0,
|
||||
role TEXT NOT NULL DEFAULT 'user', -- user / assistant / system
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
citations TEXT NOT NULL DEFAULT '', -- JSON:引用来源(文档/分块/得分)
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_message_conversation ON chat_message(conversation_id);
|
||||
```
|
||||
|
||||
### 5.4 关键设计决策
|
||||
|
||||
1. **向量与业务同库同事务**:`kb_chunk_vec` 与 `kb_chunk` 同在 business.db。删除文档时,`chunk → vec → fts` 在一个事务内删除,无一致性问题。
|
||||
2. **数据库与源文件分离**:`data/` 仅存 3 个 db 文件;上传源文件存 `workspace/`。备份 = 两个目录分别打包(db 小而关键、workspace 大而可重建),清理与迁移互不影响。
|
||||
3. **向量维度跟随模型**:`model_config.dimension` 决定 vec0 建表维度;数据集绑定 embedding 配置(`kb_dataset.embedding_cfg_id`)。切换 embedding 模型时前端提示"重新向量化",触发 `reembed` 任务(重建该库全部向量 + FTS)。
|
||||
4. **中文分词在应用层**:SQLite 内置 tokenizer 无法正确切分中文。写入 FTS5 时用 gse 分词后以空格连接存入 `content_tokens`;查询时对 query 同样分词。备选:trigram tokenizer(召回差但零依赖)。
|
||||
5. **FTS5 表自包含**:只存 `chunk_id + dataset_id + title + content_tokens`,原文在 `kb_chunk`,命中后回表 join 取原文与元数据,避免 FTS 表膨胀。
|
||||
6. **解析任务用表驱动**:`kb_parse_task` 轮询模式(与 video-factory 的 `StartVideoPoller` 一致),单 goroutine 串行消费,避免 SQLite 并发写冲突。
|
||||
7. **SQLite 并发**:3 个库文件各自独立连接;写操作集中在任务轮询 goroutine 与用户操作,使用 WAL 模式(`PRAGMA journal_mode=WAL`)提升读写并发。
|
||||
|
||||
---
|
||||
|
||||
## 6. 分层实现规范(参照 video-factory)
|
||||
|
||||
### 6.1 consts 层
|
||||
|
||||
```go
|
||||
// kb/consts/table_name.go
|
||||
const (
|
||||
TableNameSystemConfig = "system_config"
|
||||
TableNameModelConfig = "model_config"
|
||||
TableNameDataset = "kb_dataset"
|
||||
TableNameDocument = "kb_document"
|
||||
TableNameChunk = "kb_chunk"
|
||||
TableNameChunkVec = "kb_chunk_vec"
|
||||
TableNameChunkFts = "kb_chunk_fts"
|
||||
TableNameParseTask = "kb_parse_task"
|
||||
TableNameConversation = "chat_conversation"
|
||||
TableNameMessage = "chat_message"
|
||||
)
|
||||
|
||||
// 数据库组
|
||||
const (
|
||||
DbGroupDefault = "" // business.db
|
||||
DbGroupSystem = "system" // system.db
|
||||
DbGroupChat = "chat" // chat.db
|
||||
)
|
||||
```
|
||||
|
||||
### 6.2 entity 层
|
||||
|
||||
每文件一张表,`orm` 标签与列名一致,时间用 `*gtime.Time`(参照 video-factory 的 `user.go`):
|
||||
|
||||
```go
|
||||
type Chunk struct {
|
||||
Id int64 `orm:"id" json:"id"`
|
||||
DatasetId int64 `orm:"dataset_id" json:"dataset_id"`
|
||||
DocumentId int64 `orm:"document_id" json:"document_id"`
|
||||
Seq int `orm:"seq" json:"seq"`
|
||||
Content string `orm:"content" json:"content"`
|
||||
Meta string `orm:"meta" json:"meta"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"created_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 dao 层
|
||||
|
||||
- 单例模式:`var Chunk = &chunkDao{}`
|
||||
- `init()` 内执行 `CREATE TABLE IF NOT EXISTS` + 索引 + 迁移(参照 video-factory `user_dao.go`)
|
||||
- 每表一个文件;`chunk_dao.go` 额外管理 `kb_chunk_vec`(向量 KNN 查询、批量插入)与 `kb_chunk_fts`(BM25 检索、分词写索引),并提供事务内删除(chunk+vec+fts)
|
||||
- 查询缓存:`gdb.CacheOption{Duration: common.CacheTTL(), Name: ...}`,写操作后清理(参照 video-factory `clearUserCache`)
|
||||
- 通用 CRUD 用 `common/base_dao.go`(InsertAndReturnId / GetOneByPk / UpdateByPk / DeleteByPk)
|
||||
|
||||
向量与全文检索 DAO 示例:
|
||||
|
||||
```go
|
||||
// 向量 KNN(余弦)
|
||||
func (d *chunkDao) VecSearch(ctx context.Context, datasetId int64, vector []float32, topK int) ([]VecHit, error) {
|
||||
vecStr := vectorToJson(vector) // [0.1,0.2,...]
|
||||
r, err := g.DB(consts.DbGroupDefault).GetCtx(ctx).Raw(
|
||||
"SELECT chunk_id, vec_distance_cosine(embedding, vec_f32(?)) AS d "+
|
||||
"FROM "+consts.TableNameChunkVec+" WHERE embedding MATCH ? ORDER BY d LIMIT ?",
|
||||
vecStr, vecStr, topK,
|
||||
)
|
||||
...
|
||||
}
|
||||
|
||||
// 全文检索(BM25)
|
||||
func (d *chunkDao) FtsSearch(ctx context.Context, datasetId int64, query string, topK int) ([]FtsHit, error) {
|
||||
tokens := common.TokenizeQuery(query)
|
||||
r, err := g.DB(consts.DbGroupDefault).GetCtx(ctx).Raw(
|
||||
"SELECT chunk_id, bm25(kb_chunk_fts) AS score FROM kb_chunk_fts "+
|
||||
"WHERE kb_chunk_fts MATCH ? AND dataset_id = ? ORDER BY score LIMIT ?",
|
||||
tokens, datasetId, topK,
|
||||
)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 service 层
|
||||
|
||||
每表一个文件 + 业务方法(解析与分词等纯技术能力在 `common/`,业务编排全部归入对应 service):
|
||||
- `system_config_service.go`:**访问令牌**生成/校验/重新生成、系统设置读写
|
||||
- `dataset_service.go`:数据集 CRUD
|
||||
- `document_service.go`:上传落盘、创建文档记录、提交解析任务(解析器调 `common/parser.go`)
|
||||
- `chunk_service.go`:文本分块(splitter,标题感知+800字+100重叠)、`InsertAll`(批量向量化 + 写 chunk+vec0+FTS5,等价 Indexer 职责)、分块列表、编辑(改文本后重分词)
|
||||
- `parse_task_service.go`:任务队列消费(`StartParsePoller`,与 video-factory `StartVideoPoller` 同模式;解析→分块→索引全流程编排)
|
||||
- `conversation_service.go` / `message_service.go`:会话 CRUD、历史消息
|
||||
- `chat_service.go`:**RAG 问答入口**(模型组件工厂、`HybridRetriever`(vec0+FTS5+RRF)、问答工作流编排,SSE 流式回传,落库 message;检索调 `common/tokenizer.go` 分词)
|
||||
- `model_config_service.go`:模型配置 CRUD + 连通性测试
|
||||
|
||||
### 6.5 controller 层 + dto 层
|
||||
|
||||
- 结构体命名决定路由前缀:`dataset` → `/dataset`,`system-config` → `/system-config`(`common.RouteRegister` 自动注册)
|
||||
- 接口定义在 `model/dto/`,`g.Meta` 携带 path/method/summary;OpenAPI 文档自动生成(`/api.json`)
|
||||
- 主要接口清单:
|
||||
|
||||
| 路由 | 说明 |
|
||||
|---|---|
|
||||
| `POST /system-config/login` | **访问令牌登录**(body: {token} → 返回 JWT) |
|
||||
| `GET/PUT /system-config` | 系统设置(默认模型、默认数据集) |
|
||||
| `POST /system-config/regenerate-token` | 重新生成访问令牌(旧会话立即失效) |
|
||||
| `GET/POST/PUT/DELETE /dataset` | 数据集 CRUD |
|
||||
| `POST /document/upload`(multipart)`DELETE /document` `GET /document/list` | 文档管理 |
|
||||
| `GET /document/{id}/chunks` `PUT /chunk` | 分块查看/编辑 |
|
||||
| `POST /document/{id}/reembed` | 重新向量化 |
|
||||
| `GET /parse-task/list` `POST /parse-task/{id}/retry` | 任务管理 |
|
||||
| `GET/POST/DELETE /conversation` | 会话 CRUD |
|
||||
| `GET /message/list` | 历史消息 |
|
||||
| **`POST /message/chat`** | **RAG 问答(SSE 流式)** |
|
||||
| `GET/POST/PUT/DELETE /model-config` | 模型配置 CRUD |
|
||||
| `GET /workspace/*` | 源文件访问(main.go BindHandler 静态服务,鉴权放行 + 路径穿越防护) |
|
||||
| `POST /model-config/{id}/test` | 连通性测试 |
|
||||
|
||||
---
|
||||
|
||||
## 7. RAG 问答设计
|
||||
|
||||
> 本层不再单独建目录:分块/Indexer 归入 `chunk_service.go`,Retriever/组件工厂/工作流归入 `chat_service.go`,分词归入 `common/tokenizer.go`。
|
||||
|
||||
### 7.1 组件清单
|
||||
|
||||
| 组件 | 实现 | 归属 |
|
||||
|---|---|---|
|
||||
| ChatModel | 自研 `OpenAIChatModel` | `chat_service.go`:OpenAI 兼容 `/chat/completions`(Generate/Stream),baseURL 可配,兼容 Ollama/DeepSeek/OpenAI 等 |
|
||||
| Embedding | 自研 `OpenAIEmbedder` | `chat_service.go`:OpenAI 兼容 `/embeddings`;按库绑定的 embedding 配置创建,模型必须与索引端一致 |
|
||||
| Indexer | `ChunkService.InsertAll` | `chunk_service.go`:批量向量化 + 写入 chunk + vec0 + FTS5(解析流水线非图节点,不单独实现 eino Indexer 接口) |
|
||||
| Retriever | **自研 `HybridRetriever`** | `chat_service.go`:实现 `eino.Retriever` 接口,vec0 向量检索 + FTS5 关键词检索 + RRF 融合 |
|
||||
| Splitter | 自研(标题感知分块) | `chunk_service.go`:标题感知 + 800字 + 100重叠 |
|
||||
| Tokenizer | gse 中文分词 | `common/tokenizer.go`:写索引/检索共用 |
|
||||
|
||||
### 7.2 写入路径(`ChunkService.InsertAll`)
|
||||
|
||||
```go
|
||||
func (s *chunkService) InsertAll(ctx context.Context, datasetId, documentId int64,
|
||||
chunks []string, embedder embedding.Embedder) error
|
||||
```
|
||||
|
||||
流程:
|
||||
1. 数据集绑定 embedding 配置时构建 `OpenAIEmbedder`(`parse_task_service` 解析流水线中完成,无配置降级仅写 FTS5)
|
||||
2. 按 `EmbedBatchSize`(16)批量调用 `Embedder.EmbedStrings` 生成向量
|
||||
3. 逐 chunk 同一事务:`kb_chunk` 自增 id → `kb_chunk_vec(chunk_id, vec_f32(?))` → gse 分词后 `kb_chunk_fts(chunk_id, dataset_id, title, content_tokens)`
|
||||
4. 更新 `kb_document.chunk_count`、`status=2`
|
||||
|
||||
> 解析流水线为轮询任务而非图节点,故不单独实现 eino Indexer 接口,`InsertAll` 承担等价职责。
|
||||
|
||||
### 7.3 自研 HybridRetriever(读取路径)
|
||||
|
||||
```go
|
||||
type HybridRetriever struct {
|
||||
datasetId int64
|
||||
embedder embedding.Embedder
|
||||
}
|
||||
|
||||
// 实现 eino Retriever 接口
|
||||
func (h *HybridRetriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) ([]*schema.Document, error)
|
||||
```
|
||||
|
||||
流程:
|
||||
1. **向量检索**:query → Embedding → `vec0` KNN 余弦 TopK(如 20)
|
||||
2. **关键词检索**:query → gse 分词 → FTS5 MATCH TopK(如 20)
|
||||
3. **RRF 融合**:`score = Σ 1/(60 + rank)`,取 TopK(如 10)
|
||||
4. 回表 `kb_chunk` 取原文与元数据,组装 `schema.Document`,`doc.WithScore(score)` 记录得分与引用信息
|
||||
5. 支持 `retriever.WithTopK` / `WithScoreThreshold` 选项
|
||||
|
||||
```go
|
||||
// RRF 融合
|
||||
type hit struct{ chunkId int64; ranks []int; score float64 }
|
||||
score = Σ(1 / (60 + rank))
|
||||
```
|
||||
|
||||
### 7.4 问答工作流(chat_service.go 编排)
|
||||
|
||||
```
|
||||
用户问题
|
||||
│
|
||||
▼
|
||||
┌─────────┐ ┌──────────────┐ ┌────────────┐ ┌───────────┐
|
||||
│ Hybrid │──▶│ Prompt │──▶│ ChatModel │──▶│ 流式输出 │
|
||||
│ Retriever│ │ (上下文+问题) │ │ (OpenAI兼容)│ │ (SSE) │
|
||||
└─────────┘ └──────────────┘ └────────────┘ └───────────┘
|
||||
```
|
||||
|
||||
- `chat_service.go` 中直接编排 eino 组件:`ChatService.Ask` = HybridRetriever.Retrieve → 组装引用列表 + 系统提示词 → OpenAIChatModel.Stream 流式生成(组件已实现 eino 接口,可随时迁入 graph/chain 拓扑;eino v0.9.13 的 graph 节点类型约束与"中间取引用"需求不匹配,故直接编排)
|
||||
- `message_service.go`:会话解析/创建、用户消息落库、历史消息组装(最近 10 轮)、助手消息 + citations JSON 落库
|
||||
- Prompt 模板:
|
||||
```
|
||||
你是一个本地知识库助手。请仅根据以下资料回答用户问题;若资料不足以回答,请明确说明。
|
||||
回答引用资料时,在对应位置标注 [编号]。
|
||||
|
||||
【资料】
|
||||
[1] 内容...
|
||||
[2] 内容...
|
||||
```
|
||||
- SSE 事件顺序:`citations`(先推,含引用列表与 conversation_id)→ `delta`(增量文本)→ `done`;错误推 `error` 事件
|
||||
- 引用列表编号 [1][2] 与提示词中资料编号一一对应,随助手消息以 JSON 落库(chat_message.citations)
|
||||
|
||||
### 7.5 中文分词
|
||||
|
||||
- `github.com/go-ego/gse`(纯 Go,无 CGO),初始化标准中文词典(约 35 万词)
|
||||
- 写索引:`gse.Cut(text)` → 过滤停用词/单字 → 空格 join → `content_tokens`
|
||||
- 查询:同样流程;额外保留原 query 子串供 trigram 兜底(可选)
|
||||
- 词典可随镜像内置 `dict/zh/dict.txt`
|
||||
|
||||
### 7.6 文档解析流水线
|
||||
|
||||
```
|
||||
POST /document/upload
|
||||
│ 保存文件到 workspace/{datasetId}/{yyyymmdd}/{uuid}.ext
|
||||
│ 插入 kb_document(status=0) + kb_parse_task(status=0)
|
||||
▼
|
||||
StartParsePoller(main.go 启动,3 秒轮询)
|
||||
│ 取 status=0 任务 → 置 status=1
|
||||
│ 1. 按扩展名分发解析器(txt/md 直接读;pdf 用 pdfcpu 抽取文本;
|
||||
│ docx 解 zip 读 word/document.xml 提取段落;html 用 goquery 取 body 文本)
|
||||
│ 2. 分块:优先按标题/段落切(# 标题、空行、PDF 分页),
|
||||
│ 超过 max_chunk_size(800字) 时按句号/换行回退切分,重叠 100 字
|
||||
│ 3. 批量 Embedding → SqliteIndexer.Store
|
||||
│ 4. 更新 document.status=2、chunk_count;任务 status=2
|
||||
│ 失败 → status=3 + error_msg,前端可重试
|
||||
```
|
||||
|
||||
### 7.7 知识图谱(LLM 抽取 + 图增强检索)
|
||||
|
||||
面向"数据集整体关系"类问题(如"谁与谁合作过""公司有哪些产品线"),在向量/关键词检索之外补充图谱能力。图谱**不是替代 RAG**,而是为问答注入结构化关系上下文。
|
||||
|
||||
**构建(LLM 抽取,挂在解析流水线第 3.5 步)**:
|
||||
|
||||
```
|
||||
解析 → 分块 → 向量化 ──▶ 批量抽取(每 chunk 一次 LLM 调用,JSON 输出)
|
||||
▼
|
||||
{entities:[{name, type}], relations:[{head, relation, tail}]}
|
||||
▼
|
||||
upsert kg_entity(按 dataset_id+name 去重)→ 写 kg_relation(带来源 chunk_id)
|
||||
```
|
||||
|
||||
- 复用 M4 的 chat 组件工厂,抽取 Prompt 要求模型只输出 JSON:
|
||||
```json
|
||||
{"entities": [{"name": "张明", "type": "person"}], "relations": [{"head": "张明", "relation": "任职于", "tail": "XX科技"}]}
|
||||
```
|
||||
- 实体按 `(dataset_id, name)` 去重(同一实体多 chunk 出现只建一次,UNIQUE 约束 + ON CONFLICT upsert),关系带来源 chunk_id;删除文档时按分块 id 级联清理
|
||||
- 抽取失败不阻断流水线(记录错误,文档状态仍为完成);未配置默认对话模型时整体跳过
|
||||
|
||||
**使用(图增强检索,挂在 HybridRetriever 之后)**:
|
||||
|
||||
```
|
||||
用户问题
|
||||
│
|
||||
├─▶ 混合检索(向量+FTS5)──┐
|
||||
├─▶ 实体链接:问题文本分词后与 kg_entity.name 精确/子串匹配,取 Top 3 命中实体
|
||||
│ └─▶ 一跳邻居:取这些实体的关系三元组(头/尾任意一端命中即取,上限 20 条)
|
||||
▼
|
||||
组装上下文:检索片段 + 三元组列表("【知识图谱】张明 -任职于-> XX科技")→ ChatModel
|
||||
```
|
||||
|
||||
- 实体链接用 `common/tokenizer.go` 分词 + 名称匹配,零模型调用;打分规则:问题含完整实体名 +5,命中 token 按长度加权,Top3 后名称长者优先
|
||||
- 三元组作为辅助上下文注入 prompt(排序在检索片段之后),增强模型对关系类问题的回答
|
||||
- `kg_entity` / `kg_relation` 两表归属 business.db,各配独立 entity/dao/service/controller 文件(分层 10 文件对齐)
|
||||
|
||||
---
|
||||
|
||||
## 8. 鉴权设计(单用户 + 启动令牌)
|
||||
|
||||
面向边缘设备部署,不做用户体系,只有一个"门禁"级别的访问令牌:
|
||||
|
||||
### 8.1 令牌生命周期
|
||||
|
||||
```
|
||||
首次启动
|
||||
│ 读 system_config(access_token)
|
||||
│ ┌─ 不存在 → 随机生成 32 位十六进制 token,写入 system_config,日志打印:
|
||||
│ │ ============================================
|
||||
│ │ 访问令牌(登录用): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
|
||||
│ │ 请在登录页输入上述令牌
|
||||
│ │ ============================================
|
||||
│ └─ 已存在 → 直接打印当前 token(便于找回,重启不重新生成)
|
||||
▼
|
||||
登录页:输入 token → POST /system-config/login → 校验通过 → 签发 JWT
|
||||
│ JWT claims: { role: "owner", token_fp: 指纹 },有效期 24h
|
||||
▼
|
||||
后续请求:Authorization: Bearer <JWT>,中间件校验 token_fp 与当前令牌指纹一致
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- **指纹校验**:JWT 中携带 access_token 的 SHA-256 指纹(前 16 位 hex);鉴权中间件将 JWT 指纹与 `system_config` 中当前令牌指纹比对(带缓存),不一致即 401。因此**重新生成令牌后所有旧会话立即失效**
|
||||
- **重新生成**:设置页 `POST /system-config/regenerate-token` → 生成新令牌覆盖并打印日志,前端强制跳转登录页
|
||||
- **令牌存储**:system_config 明文存储(本地单机、日志与设置页本就可见明文,且设置页需展示)
|
||||
- **公开路径白名单**:`/system-config/login`、`GET /`、`GET /assets/*`(hash 路由下 SPA 只请求这两类静态路径,无鉴权绕过);`GET /workspace/*` 前缀放行(同 video-factory `/workspace/*`:浏览器下载/预览请求不带 Authorization),仅做路径穿越防护;其余路径一律校验
|
||||
- 无注册、无角色、无用户表;登录日志不落库(如需审计可在日志文件输出)
|
||||
|
||||
```go
|
||||
// common/auth_middleware.go(伪码)
|
||||
func Auth(r *ghttp.Request) {
|
||||
if isPublicPath(r.URL.Path) { r.Middleware.Next(); return }
|
||||
claims, err := ParseToken(bearer(r))
|
||||
if err != nil || claims.TokenFp != currentTokenFingerprint() {
|
||||
r.Response.WriteJson(401); r.Exit(); return
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 前端设计(Vue 3 + Vite + Element Plus)
|
||||
|
||||
### 9.1 工程
|
||||
|
||||
- `ui-src/` 独立 Vite 工程:`vite.config.js` 设 `base: '/'`、`build.outDir: 'dist'`
|
||||
- hash 路由(无需服务端 SPA fallback,与 video-factory 一致),路由守卫:无 JWT → 跳登录页
|
||||
- 依赖:`vue@3`、`vue-router@4`、`pinia`、`element-plus`、`axios`
|
||||
- 构建产物 `ui-src/dist`,本地开发 `npm run build` 后由 Go 统一端口托管;开发期可用 Vite dev server + proxy
|
||||
|
||||
### 9.2 页面
|
||||
|
||||
| 页面 | 功能 |
|
||||
|---|---|
|
||||
| 登录页 | 输入访问令牌登录(提示语:令牌见服务启动日志 / `docker compose logs`);无注册入口 |
|
||||
| 数据集列表 | 卡片式列表、新建/删除/设置(绑定 embedding 模型) |
|
||||
| 数据集详情 | 文档上传(拖拽 + 进度)、文档列表(解析状态/分块数/失败重试)、分块预览与编辑 |
|
||||
| 问答页 | 左侧会话列表,右侧消息流;SSE 流式渲染,答案带引用折叠面板(点击定位到分块原文) |
|
||||
| 设置页 | 模型配置 CRUD(chat/embedding)、连通性测试;**访问令牌管理**(查看/复制/重新生成,重新生成后强制重新登录) |
|
||||
|
||||
### 9.3 API 封装
|
||||
|
||||
- `api/http.js`:axios 实例(baseURL `/`、token 注入、401 跳转、错误 toast)
|
||||
- `api/xxx.js`:按后端模块组织,与 dto 对齐
|
||||
- 流式:`fetch` + `ReadableStream` 解析 SSE(`data: {"content":"..."}` 增量 / `data: {"citations":[...]}` 引用 / `data: [DONE]`)
|
||||
|
||||
---
|
||||
|
||||
## 10. Docker 部署
|
||||
|
||||
### 10.1 Dockerfile(多阶段,参照 video-factory)
|
||||
|
||||
```dockerfile
|
||||
# ==================== 前端构建 ====================
|
||||
FROM node:20-alpine AS ui-builder
|
||||
RUN apk add --no-cache git
|
||||
WORKDIR /build-ui
|
||||
COPY rag-local/ui-src/package.json rag-local/ui-src/package-lock.json ./
|
||||
RUN npm ci --registry=https://registry.npmmirror.com
|
||||
COPY rag-local/ui-src/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ==================== 后端构建 ====================
|
||||
FROM golang:alpine AS builder
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk add --no-cache git ca-certificates tzdata
|
||||
ENV TZ=Asia/Shanghai GO111MODULE=on \
|
||||
GOPROXY=https://goproxy.cn,direct \
|
||||
CGO_ENABLED=0 GOTOOLCHAIN=auto
|
||||
WORKDIR /build
|
||||
COPY rag-local/ .
|
||||
RUN go mod download && go build -ldflags="-s -w" -o main ./main.go
|
||||
|
||||
# ==================== 运行镜像 ====================
|
||||
FROM alpine:3.19
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk add --no-cache ca-certificates tzdata
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
WORKDIR /app
|
||||
COPY --from=builder /build/main .
|
||||
COPY --from=builder /build/config.yml .
|
||||
COPY --from=ui-builder /build-ui/dist ./ui-src/dist
|
||||
RUN mkdir -p /app/data /app/workspace \
|
||||
&& printf '#!/bin/sh\nfor db in business.db system.db chat.db; do\n if [ -d /app/data/$db ]; then rm -rf /app/data/$db; fi\n touch /app/data/$db 2>/dev/null || true\ndone\nexec ./main\n' > /app/entrypoint.sh \
|
||||
&& chmod +x /app/entrypoint.sh
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
```
|
||||
|
||||
### 10.2 docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
rag-local:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: rag-local/Dockerfile
|
||||
container_name: rag-local
|
||||
ports:
|
||||
- "8080:8080" # 统一端口:前后端一体
|
||||
volumes:
|
||||
- /data/rag-local/data:/app/data # 数据库(3 个 db 文件)
|
||||
- /data/rag-local/workspace:/app/workspace # 上传的文档源文件
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: rag-local-network
|
||||
```
|
||||
|
||||
> 数据即文件:备份 = 打包 `data`(3 个 db)+ `workspace`(源文件)两个目录;迁移 = 拷贝到新机器即可。
|
||||
> 首次启动后通过 `docker compose logs rag-local` 查看访问令牌。
|
||||
|
||||
---
|
||||
|
||||
## 11. 开发与构建命令
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
cd rag-local
|
||||
go mod tidy
|
||||
go get modernc.org/sqlite@v1.47.0 # 关键:向量扩展依赖
|
||||
go build ./... # 验证编译(见 §2.1 风险提示)
|
||||
go run main.go # 启动日志中查看访问令牌
|
||||
|
||||
# 前端
|
||||
cd rag-local/ui-src
|
||||
npm install
|
||||
npm run build # 产物 ui-src/dist,Go 统一端口托管
|
||||
npm run dev # 开发期(Vite + proxy)
|
||||
|
||||
# 部署
|
||||
docker compose up -d --build
|
||||
docker compose logs rag-local # 查看访问令牌
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 实施步骤(里程碑)
|
||||
|
||||
| 阶段 | 内容 | 验收标准 |
|
||||
|---|---|---|
|
||||
| **M1 骨架** | go mod、config.yml 三库、common 层(http/auth/base_dao/cache)、consts、system_config(令牌生成/登录)与 model_config 全链路(entity/dao/service/controller/dto)、ui-src 空壳 SPA 由 Go 托管 | 8080 端口可打开页面,启动日志打印令牌,登录页输入令牌可进入,3 个 db 文件生成 |
|
||||
| **M2 文档流水线** | dataset/document/parse_task 全链路、解析器(txt/md/pdf/docx/html)、分块、任务轮询 | 上传文档 → 解析 → 分块 → 状态流转,前端可看 chunk 列表 |
|
||||
| **M3 向量 + 全文** | 升级 modernc v1.47、验证 vec0、chunk_vec/chunk_fts 建表、gse 分词、SqliteIndexer/向量检索/FTS5 检索、RRF 融合 | 库内文档可被语义检索与关键词检索命中 |
|
||||
| **M4 RAG 问答** ✅ | model_config 全链路、chat/embedding 组件工厂、Eino Graph 工作流、SSE 流式、conversation/message 落库、引用展示 | 问答页流式对话,回答有引用来源,历史消息可回溯 |
|
||||
| **M5 知识图谱** ✅ | kg_entity/kg_relation 全链路、LLM 抽取(挂在解析流水线)、实体链接 + 一跳邻居注入 prompt | 图谱页可看实体/关系,问答能回答关系类问题 |
|
||||
| **M6 打磨与部署** ✅ | 分块编辑(改后自动重向量化)、文档重新向量化、令牌查看/重新生成、模型连通性测试、设置/数据集/详情三页填充、Dockerfile + docker-compose + README | docker compose up 一键启动,全功能可用 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 依赖清单(go.mod)
|
||||
|
||||
```
|
||||
github.com/gogf/gf/v2 v2.10.x
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.x
|
||||
modernc.org/sqlite v1.47.0+ # 显式升级,内置 sqlite-vec
|
||||
github.com/cloudwego/eino # RAG 编排
|
||||
github.com/cloudwego/eino-ext # chat/openai、embedding/openai
|
||||
github.com/go-ego/gse # 中文分词(纯 Go)
|
||||
github.com/pdfcpu/pdfcpu # PDF 文本抽取
|
||||
github.com/PuerkitoBio/goquery # HTML 解析
|
||||
github.com/golang-jwt/jwt/v5 # JWT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 风险与备选方案
|
||||
|
||||
### 14.1 向量扩展兼容性(主要风险)
|
||||
|
||||
**风险**:glebarez/go-sqlite(GoFrame 驱动链)锁定 modernc v1.23.1,MVS 升级到 v1.47.0 后可能存在 API 编译不兼容。
|
||||
|
||||
**应对**:M3 阶段第一步即验证 `go build`。若失败,按序切换:
|
||||
|
||||
- **备选 A(推荐)**:自研平面扫描向量检索 —— `kb_chunk_vec` 改用普通表存向量 JSON,检索时过滤 + 内存余弦相似度排序(批量预取 + 并发分片)。10 万 chunk × 1024 维单次查询约 20–50ms,本地知识库规模完全够用。Indexer/Retriever 接口不变,仅换后端实现。
|
||||
- **备选 B**:`gosqlite.org` 模块(CGo-free,类型化 vec + FTS5 API),业务 SQL 走 GoFrame、向量/FTS 走独立连接,应用层 RRF 融合。
|
||||
- **备选 C**:LanceDB(文件型向量库,纯 Go SDK),向量独立目录存储。
|
||||
|
||||
### 14.2 中文检索质量
|
||||
|
||||
FTS5 召回依赖 gse 分词质量;专有名词(人名/产品名)可能切碎。缓解:检索时同时保留 trigram 兜底,或对命中率低的 query 降级为向量检索为主。
|
||||
|
||||
### 14.3 SQLite 写并发
|
||||
|
||||
解析任务轮询与用户操作可能并发写 business.db。缓解:WAL 模式 + 任务串行消费 + 写操作集中到 service 层(参照 video-factory 单机场景)。
|
||||
|
||||
### 14.4 切换 embedding 模型
|
||||
|
||||
不同模型的向量空间不可混用。方案:数据集绑定 embedding 配置,切换时强制"重新向量化"(`reembed` 任务重建全部向量与 FTS 索引),并清空缓存。
|
||||
|
||||
---
|
||||
|
||||
## 15. 与 video-factory 的差异点说明
|
||||
|
||||
| 项 | video-factory | rag-local |
|
||||
|---|---|---|
|
||||
| 数据库 | 3 个 SQLite(业务/系统/财务) | 3 个 SQLite(业务/系统/问答) |
|
||||
| 向量/全文 | 无 | **SQLite 同库内嵌**(vec0 + FTS5),无额外服务 |
|
||||
| AI 层 | 自研 ReAct agent(openai 直连) | **Eino** 组件化编排(Indexer/Retriever/Graph) |
|
||||
| 鉴权 | 多用户 + 角色 + JWT(user/login_log 表) | **单用户访问令牌**:启动生成打印,登录换取 JWT,无用户表 |
|
||||
| 前端 | HTML + 少量 Vue | 纯 Vue 3 SPA(构建产物 Go 托管) |
|
||||
| 异步任务 | 视频生成轮询 | 文档解析/向量化轮询 |
|
||||
| 其余 | 分层/路由/鉴权/缓存/部署模式 | 完全对齐 |
|
||||
Reference in New Issue
Block a user