实战!【一个企业知识库的逐步搭建】持续更新ing

前言

博主在一个医疗制造公司做IT,有一天,一个同事给我说,坏了,最近一批原材料又出问题了。我问她,为什么会出现这种问题,好像也不是第一次。她回答说,最近做了一个变更,但是负责的团队没有识别到这个物料的风险,所以忽略了这个物料的变更。我那时就在想,现在AI的发展,早就可以在这块领域提前发现风险,或者退一步来说,可以帮助发现风险。于是,我就产生了这个想法,做一个企业级的知识库协作平台,实现以下目标:

1、第一阶段完成知识库的搭建,支持上传文档到知识库,通过对话的方式完成对应知识的搜索与呈现

2、进一步扩充知识域,通过各个信息系统的集成,将物料主数据、BOM、技术图纸、注册文件、项目文档、生产文件、库存等信息全部集成起来,通过数据抽取和建模到知识库,形成一个围绕产品级别的知识库,不仅实现知识检索,同时可以基于规则设置、知识理解发现风险、预知风险或者提供建议

3、第三阶段我想更进一步,在左右的资料完善的情况下,可以完成智能写作/修改文档

1、第一版,使用WeKnora搭建一个初始版本

1.1 初试

我一开始使用的fastgpt来做向量搜索,但是每次搜索的内容确实差强人意。主要原因我分析是没有语义理解,而且,向量相似度的值每次还要去调整。后面刚好腾讯发布了WeKnora,我马上勾起了兴趣。

我的思路就变成了,通过WeKnora来搭建比较完整的企业知识库平台底座。具备"知识导入 + 检索问答 + Agent 扩展 + 多模型/多索引后端 + 前端管理界面"这一整套能力。

1.2 项目现在的主要框架

整体是前后端分离,核心分成 4 层:

前端:Vue 3 + Vite + Pinia + Vue Router,负责知识库管理、会话聊天、Agent、组织/设置等页面,入口在 main.ts 和 index.ts。

后端主服务:Go + Gin,负责 API、业务编排、权限、多租户、模型管理、知识库管理、会话问答,启动入口在 main.go。

文档解析服务:独立的 Python gRPC 服务 docreader,负责 PDF/Word/图片/URL/Markdown 等解析、OCR、多模态切块,入口在 main.py

基础设施:Postgres、Redis、DocReader、Qdrant、MinIO、Neo4j、Jaeger 等通过 docker-compose.yml 组装起来。

后端内部又是比较标准的分层:

router:注册 API 路由

handler:接 HTTP 请求

service:业务主流程

repository:数据库/检索引擎访问

container:依赖注入装配

这一层的总装配在 container.go。

1.3 现在项目的主流程

最核心其实就两条:导入流程、问答流程。

1.3.1 知识导入流程

用户从前端上传文件、导入 URL,或者手工录入 Markdown。

后端在 knowledge.go 里先创建知识记录、保存原文件、做去重和配额校验。

然后把"文档处理任务"丢给 Asynq 异步队列,队列注册在 task.go。

异步任务 ProcessDocument 再调用 docreader 去解析文档、切 chunk、抽图片/OCR/结构信息。

解析后的 chunk 会落库,并调用 embedding 模型向量化,再写入检索引擎;如果开了图谱,也会写入图相关数据。

最终形成"原始文件 + chunk + 向量索引 + 可选图谱索引"的可检索知识。

这一条就是典型的"导入 -> 解析 -> 切块 -> 向量化 -> 建索引"。

1.3.2 检索问答流程

前端发起会话请求,后端走 session.go。

普通知识问答入口是 KnowledgeQA,Agent 模式入口是 AgentQA。

普通问答不是一坨代码直接写死的,而是走一个插件化 chat pipeline,在 chat_pipline.go。

pipeline 里会按阶段执行:

查询改写 rewrite

历史加载 load_history

检索 search

重排 rerank

合并上下文 merge

组装消息 into_chat_message

调大模型生成回答 chat_completion / chat_completion_stream

检索本身支持"知识库检索 + Web Search 并行",这部分在 search.go。

查询改写会结合历史对话,把"它/这个/上一个"这类省略信息补全,在 rewrite.go。

最后通过 SSE 流式把思考过程、工具调用、最终答案回给前端,session handler 里这块做得比较完整,在 handler/session 下面。

1.4 现在已经实现的主要功能

当前已具备这些能力:

  • 多租户、用户、认证、组织/共享空间
  • 知识库管理:创建、复制、删除、标签、共享
  • 文档知识导入:文件、URL、手工 Markdown FAQ
  • 知识库:FAQ 条目导入、相似问、批量管理
  • 检索问答:知识搜索、知识问答、流式输出 Agent 模式:工具调用、MCP 集成、Web Search、技能系统
  • 模型管理:聊天模型、Embedding、Rerank 模型统一管理
  • 多种检索后端:Postgres/pgvector、Elasticsearch、Qdrant,代码上是可插拔的
  • 图谱/实体关系相关能力:已经有实体抽取、关系抽取、GraphRAG 的铺垫
  • 异步任务:文档处理、FAQ 导入、摘要/问题生成、知识库复制等

1.5 项目大致原理讲解

本质上还是一个增强版 RAG 平台,只是做得比普通 RAG 更完整:

文档理解:先通过 docreader 把复杂文档转成结构化 chunk

语义索引:用 embedding 模型把 chunk 向量化

混合检索:关键词检索 + 向量检索 + 可选图谱检索

重排过滤:用 rerank 提升召回精度

上下文拼装:把最相关片段组织成 prompt

大模型回答:让 LLM 基于检索结果作答

Agent 扩展:当不是纯问答,而是需要多步推理/调用工具时,切到 Agent 路径

1.6 演示效果

1)知识库模型配置:

2)知识库分块设置:

3)智能体搭建

4)问答与知识库检索


2 遇到一个有意思的问题

2.1 问题描述

上面大家可以看到,切块那里,都是按照指定符号或者分隔符来切割的,而实际我们的word是这样的:

和这样的:

所以,

1)如果用之前的章节去切块,首先我们的表格肯定是乱的,因为表格的首行首列一般都是标题,而表格的名称也一般是在表格前或者表格尾部位置,如果直接使用文字分隔符来切换,势必无法去理解表格的内容。

2)我们的word都是有章节的,而且不同章节之间的内容也是不同的,如果切块把不同章节内容放到一起,被我们检索到,用户会看到很多不需要的答案

2.2 我们来看看weknora初始的文档解析是怎么做的

2.2.1 总流程

上传文件 -> 创建 knowledge 记录 -> 异步任务 ProcessDocument -> 调 docreader.ReadFromFile -> docreader 选择 Word 解析器 -> 提取纯文本/图片 -> 按知识库 ChunkingConfig 切块 -> 后端把 chunk 落库 -> 调 embedding -> 写入检索引擎索引

2.2.2 入口:Word 文件什么时候开始被解析

用户上传 Word 后,后端先在 knowledge.go 的 CreateKnowledgeFromFile 里做几件事:

校验文件类型、重名/去重、配额

保存原始文件

创建一条 knowledge 记录,状态先是 pending

投递异步任务给 Asynq

真正开始解析是在同文件里的 ProcessDocument。这里会把文件内容读出来,然后调用 docReaderClient.ReadFromFile(...),并把当前知识库的切块配置一起传过去:

cpp 复制代码
ChunkSize
ChunkOverlap
Separators
EnableMultimodal

也就是这里决定了"一个 Word 切多大、重叠多少、优先按什么分隔符切"。这些参数来自知识库自己的 knowledgebase.go 里的 ChunkingConfig。

2.2.3 docreader 怎么识别 Word 并解析

docreader 的统一入口在 parser.py

它按扩展名选解析器:

cpp 复制代码
docx -> Docx2Parser
doc -> DocParser

这里对 .docx 很关键的一点是:它不是只走一种解析方式,而是用 docx2_parser.py 定义的 FirstParser 链:

先用 MarkitdownParser

失败后再回退到 DocxParser

对应逻辑在 chain_parser.py。

所以 .docx 的策略其实是:

先试更通用的 markitdown 转文本/markdown

如果结果无效,再用项目自己写的 DocxParser

2.2.4 Word 文本到底是怎么抽出来的

.docx 的深度解析在 docx_parser.py。

它的主逻辑是:

读取 docx 二进制

用 Docx 处理器解析段落、表格、图片

把所有 section 的文本拼成一个大文本 text

同时收集图片信息,形成 document.images

如果复杂解析失败,还会 fallback 到 _parse_using_simple_method:

用 python-docx 直接遍历 paragraphs

再遍历 tables

把段落和表格文本拼起来

所以对一个 Word 文档来说,docreader 先做的是"抽取结构化文本内容",不是直接边读边索引。

.doc 则在 doc_parser.py 里,优先尝试:

转成 docx 再解析

不行就用 antiword

再不行用 textract

2.2.5 切块是在什么时候发生的

切块不是在 Go 后端做的,而是在 docreader 的 BaseParser.parse() 里统一做,代码在 base_parser.py。

流程是:

parse_into_text(content) 先把 Word 转成一个 Document

如果 document.chunks 为空,就进入统一切块逻辑

创建 TextSplitter(chunk_size, chunk_overlap, separators)

调 split_text(document.content)

得到 (start, end, text) 列表

转成标准 Chunk(seq, content, start, end)

这里用的切块器是 splitter.py

2.2.6 TextSplitter 是怎么切的

这个切块器不是简单 substring,而是"递归分隔 + overlap 合并":

先按 separators 递归切

优先用大的分隔符,比如 \n\n、\n、。

如果某一段还是太长,再继续往下切

最后实在不行,退化到按字符切

核心逻辑:

_split():递归拆成不超过 chunk_size 的小片段

_merge():把这些小片段重新拼成 chunk,同时保留 chunk_overlap

_split_protected() / _join():尽量保护某些内容不要被切坏,比如:

Markdown 图片

链接

表格

数学公式

代码块

所以它的思路更像:

"先按语义边界拆散,再按 chunk size 重新拼装成最终块"。

2.2.7 TextSplitter 是怎么切的Word 文件切完块之后,后端怎么处理

docreader 返回的 chunk 会回到 Go 后端 ProcessDocument,然后调用 processChunks(...),代码也在 knowledge.go。

这里做的事情是:

先删掉旧的 chunks 和旧索引,避免重复数据

把 proto.Chunk 转成数据库里的 types.Chunk

文本 chunk 直接生成一条记录

如果 chunk 里还有图片,会额外拆出:

ImageOCR chunk

ImageCaption chunk

给文本 chunk 建前后关系 PreChunkID / NextChunkID

生成 indexInfoList

indexInfoList 的核心字段是:

cpp 复制代码
Content
SourceID
ChunkID
KnowledgeID
KnowledgeBaseID

也就是说,建索引的最小单位就是 chunk。

2.2.8 TextSplitter 是怎么切的建索引具体怎么做

建索引发生在 processChunks 末尾:

先 chunkService.CreateChunks(...) 把 chunk 落库

再 retrieveEngine.BatchIndex(ctx, embeddingModel, indexInfoList)

检索引擎是组合式的,实现在:

composite.go

keywords_vector_hybrid_indexer.go

BatchIndex 的逻辑是:

取每个 chunk 的 Content

调 embedding 模型批量向量化

把 SourceID -> embedding 组织起来

写入具体的索引后端

具体后端可以是:

cpp 复制代码
PostgreSQL / pgvector
Elasticsearch
Qdrant

所以"建索引"本质上就是:

"把 chunk 内容转 embedding,并把 chunk 元数据 + 向量一起写进检索后端"。

2.2.9 TextSplitter 是怎么切的如果是一个 Word 文件,最终会形成什么数据

以一个普通 .docx 为例,最终通常会生成三层数据:

原始文件记录 knowledge

多条文本块 chunk

对应 chunk 的索引项 indexInfo -> vector index

如果开启多模态,还会再多:

OCR chunk

图片 caption chunk

图片元信息

2.3 思路

2.3.1 初始思路

我觉得现在切块策略整体还是不能符合我的最终需求的,所以下一步我就在想:

现有 docx_parser -> base_parser -> splitter 链路中,哪里最适合注入章节化切块

怎样识别:

标题

条款编号

表格

目录

变更记录

附录

2.3.2 方案

整体思考:

最适合注入"章节化切块"的位置,不是在后端 knowledge.go,而是在 docreader 这一侧,优先放在 docx_parser -> base_parser 之间,具体说是:

文本结构识别放在 docx_parser.py

章节化切块编排放在 base_parser.py

splitter.py 保留为"兜底的二级切块器"

核心原则是:先做"结构识别和分段",再做"长度控制"。不要一上来就按通用 separator 切。

一、注入点

新增一层"结构化块构建器",位置在 BaseParser.parse() 里、调用 TextSplitter 之前。

现状大致是:

cpp 复制代码
docx_parser.parse_into_text() 输出 Document(content, images)
base_parser.parse() 直接 TextSplitter.split_text(document.content)

改成未来的设计链路:

cpp 复制代码
docx_parser.parse_into_text() 输出 Document(content, images, structural_blocks?)

base_parser.parse() 先判断是否有 structural_blocks

如果有,就先按结构块生成 chunk

单个结构块过大时,再调用 TextSplitter 做二次拆分

如果没有结构块,再走现有通用切块逻辑
这样有几个好处:

docx_parser 最了解 Word 原始结构,适合识别标题、表格、目录、附录

base_parser 适合做统一 chunk 编排,避免把章节逻辑写死在某一个 parser

splitter 继续负责超长块切分,不需要彻底推翻

所以职责建议是:

docx_parser:识别"这是什么块"

base_parser:决定"这些块怎么组 chunk"

splitter:解决"块太大怎么再切"

二、怎样识别这些结构

一开始先不追求 100% 语义模型识别,第一版用"样式 + 正则 + 局部规则"组合就够了。

标题

识别信号:

Word 段落样式名包含 Heading, 标题, Heading 1/2/3

字号更大、加粗、独立成段、上下空行明显

文本短且不以句号结尾

匹配编号标题:

1

1.1

1.1.1

第一章

一、

(一)

1)

输出:

cpp 复制代码
block_type=heading
heading_level
heading_text
section_path
条款编号
识别信号:
段首正则:
^\d+(\.\d+){0,5}
^第[一二三四五六七八九十百]+[章节条款]
^[一二三四五六七八九十]+、
^([一二三四五六七八九十0-9]+)
^\([0-9a-zA-Z]+\)
Word 的编号列表属性
建议和标题区分:

标题通常短、独立
条款正文通常"编号 + 一整句/一整段"
可拆成:
clause_title
clause_body

输出:

cpp 复制代码
block_type=clause
clause_no
section_path

表格

识别信号:

docx_parser 已经能拿到 tables,这里最适合直接结构化输出

每个表格作为一个独立 block,不建议先拍平成普通文本再让 splitter 切

建议表格保留三种表示:

原始二维结构 table_cells

行拼接文本 table_text

可索引摘要 table_summary_text

输出:

cpp 复制代码
block_type=table
table_title
table_index
table_headers
row_count
col_count
目录

识别信号:

标题为 目录

连续多行出现"标题 + 页码"模式

Word TOC 样式

行尾大量页码、点线、制表符

处理:

默认识别但不作为主要检索正文

可存储,但索引降权或默认不索引

输出:

cpp 复制代码
block_type=toc
is_indexable=false
变更记录
识别信号:
标题包含:
修订记录
变更记录
版本记录
Revision History
Change Log

紧随其后常见表格列:

版本号

日期

修订内容

修订人

审批人

处理建议:

识别为独立 section

允许单独索引,后续对"版本差异、法规变更、文件追溯"很有价值

输出:

cpp 复制代码
block_type=change_log
version
effective_date
附录
识别信号:
附录
附录A/B/C
Appendix A
Annex I/II/III

常伴随大段表格、术语、模板、样例

处理:

识别 section 边界

section_type=appendix

后续检索时可单独过滤

输出:

cpp 复制代码
block_type=appendix_heading 或正文仍按 paragraph/clause/table
section_category=appendix
三、新增的中间抽象

在 docreader 内部新增两层抽象,而不是直接把所有逻辑塞进 Chunk:

StructuralBlock

表示文档中的结构块,例如:

cpp 复制代码
heading
clause
paragraph
table
toc
change_log
appendix

字段:

cpp 复制代码
id
block_type
text
start
end
page_no
heading_level
heading_text
clause_no
section_path
section_title
section_type
is_indexable
table_data
images
ChunkPlan

表示"这个结构块最终如何生成 chunk"

比如:

标题单独成块

条款标题和后续一段合并

大表格单独块

超长正文再二次 splitter

这样后面可以逐步迭代策略,而不动最终存储结构。

四、chunk 建议增加哪些 metadata

建议优先把 metadata 加在 chunk 上,而不是一开始改索引表结构。

最值得加的字段:

cpp 复制代码
chunk_type

text
heading
clause
table
toc
change_log
appendix
image_ocr
image_caption
section_path

例:1 > 1.2 > 1.2.3
section_level

标题层级
section_title

当前所属标题名
section_category

main_body
appendix
toc
change_log
clause_no

例:3.2.1
block_role

title
body
table
note
list_item
table_title

table_index

table_headers

table_row_count

table_col_count

version_tag

对变更记录/制度文件很有用

cpp 复制代码
is_indexable

是否参与向量/关键词索引

cpp 复制代码
importance

可作为后续 rerank 或召回加权依据

cpp 复制代码
source_style

原 Word 样式名,便于调试和回溯

五、如何兼容现有索引结构

项目现在的索引主键逻辑是:

cpp 复制代码
Content
SourceID
ChunkID
KnowledgeID
KnowledgeBaseID

所以后续兼容方案分两层:

存储层兼容

不破坏现有 Chunk 主表和 IndexInfo 主流程。

做法:

chunk 仍然照常生成

新 metadata 先放到 Chunk 扩展字段里

索引仍按 IndexInfo.Content 建,不改核心接口

检索层渐进增强

在不改底层索引接口的前提下,先做两件事:

关键词/向量内容增强

IndexInfo.Content 不只放正文

可拼入轻量结构前缀,例如:

标题路径

条款号

表格标题

例:

章节: 3.2 灭菌流程\n条款: 3.2.1\n内容: ...

检索后过滤/加权

在数据库 chunk 记录中读 metadata

对 toc 降权

对 heading/clause/table/change_log 提权

对 appendix 可按场景决定是否参与

2.3.3 总之一句话:

第一版完全不改向量库 schema,只改"chunk metadata + 送去 embedding 的文本拼装方式 + 检索后排序逻辑"。

3、动手(下次继续)


相关推荐
前端小趴菜~时倾2 小时前
自我提升-python爬虫学习:day05-函数与面向对象编程
爬虫·python·学习
Thomas.Sir2 小时前
第五章:Python3 之 条件、循环和其他语句
python
空空潍2 小时前
Spring AI 实战教程(一)入门示例
java·后端·spring·ai
VIP_CQCRE2 小时前
Windsurf与Flux MCP:在编码时便利的AI图像生成
ai
凌云之程2 小时前
避坑宝典:PyTorch最简安装路径(含CUDA + VSCode + 中文手册)
pytorch·python·conda·安装
WHS-_-20222 小时前
LDM代码学习日记
ide·python·pycharm
凌盛羽2 小时前
使用python绘图分析电池充电曲线
开发语言·python·stm32·单片机·fpga开发·51单片机
CoderJia程序员甲3 小时前
GitHub 热榜项目 - 日榜(2026-03-27)
人工智能·ai·大模型·github·ai教程
__zRainy__3 小时前
npx skills核心功能速查及技能开发指南
ai·node.js