系统面向企业项目管理场景,支持 DOCX / PDF / XLSX 多格式文档解析上传,基于 LLM + RAG 做检索问答

技术栈:LangChain、Langfuse、ChromaDB、Agentic RAG、Cohere、BM25、RRF、Qwen3、Python

  • 前端交互层

1.文件上传页

前端提供拖拽上传和文件选择两种方式,支持批量上传项目文档。

上传前会展示文件名、大小、类型,并做基础校验。上传后展示解析状态,例如:

待解析解析中向量化中已入库解析失败

如果后端返回重复文档标识,前端直接提示“文档已存在,已复用知识库索引”。

支持批量上传 DOCX、PDF、XLSX。

  • 校验文件类型、大小、文件名
  • 上传时展示进度条
  • 上传成功后轮询或通过 WebSocket 查看解析状态

状态一般分成异步任务:

  • 待解析
  • 解析中
  • 已入库
  • 失败

解析是耗时任务,所以上传后不会阻塞等待,而是立即返回 task_id,前端通过轮询任务状态接口获取当前阶段和进度。

2. 知识库管理页

前端展示当前用户已上传的文档列表,包括文档名称、上传时间、解析状态、chunk 数量、所属知识库、操作按钮。

用户可以删除文档、重新解析文档、查看解析结果,也可以选择某个知识库作为问答范围。

展示每个文件的元信息,例如:

  • 文件名
  • 文件类型
  • 上传时间
  • 文档页数 / Sheet 数
  • 分块数量
  • 入库状态

同时支持:

  • 删除文档
  • 重建索引
  • 查看解析结果预览
  • 查看某个 chunk 的 metadata

为方便排查召回问题,在知识库管理页提供了 chunk 预览和 metadata 查看,便于定位是解析问题、切分问题还是召回问题。

3. 问答对话页

用户在聊天框中输入问题后,前端将问题、会话 ID、知识库 ID、历史上下文 ID 发送给后端。

如果后端采用 SSE 流式返回,前端会逐字渲染模型输出,形成类似 ChatGPT 的打字效果。

回答完成后,前端展示完整答案、引用来源、相关文档片段和本轮耗时。

核心交互包括:

  • 输入问题
  • 展示回答内容
  • 展示引用来源
  • 支持追问

回答区可以拆成三块:

  • 最终答案
  • 检索命中文档列表
  • 引用片段高亮

4. 引用来源展示

每条回答下方展示引用列表,例如:

来源 1:需求文档,第 3 章,用户权限设计
来源 2:接口文档,第 5 节,任务状态查询接口

用户点击引用后,前端弹出原文片段,或者跳转到文档预览区对应位置。

这部分是项目的一个亮点,因为它解决了大模型回答“无法验证”的问题。

5. 多轮对话管理

前端维护当前会话 ID,每次提问都带上会话标识。

后端根据会话 ID 读取最近几轮对话,用于问题改写和上下文压缩。前端展示完整历史消息,但后端不会把全部历史都传给模型,而是采用滑动窗口或摘要压缩,控制 token 成本。

6. 异常交互处理

如果文档还没解析完成,用户提问时前端提示“知识库仍在构建中,请稍后再试”。

如果检索结果为空,前端展示“未在当前知识库中找到相关内容”。

如果模型生成超时,前端停止 loading,并允许用户重新生成。

如果后端返回低置信度标识,前端会提示“当前回答依据不足,建议补充更多文档”。

使用技术:

  • Vue3
  • Composition API
  • Pinia(状态管理)
  • Vue Router(路由)
  • Axios(接口请求)
  • Element Plus / Ant Design Vue(UI 组件)
  • ECharts(可视化,可选)
  • WebSocket / EventSource(实时更新或流式输出)

前端用 Vue3,配合 Composition API 做组件逻辑组织,路由用 Vue Router,状态管理用 Pinia,请求层用 Axios;知识库状态更新和流式回答接 了WebSocket。

  • 后端 API 层

  • Python
  • FastAPI 或 Flask(更推荐你说 FastAPI,和 AI/RAG 项目更搭)
  • LangChain:串 RAG 流程
  • ChromaDB:向量库
  • BM25:关键词检索
  • Ollama:本地模型推理服务
  • SQLite / MySQL:存知识库文件元信息、任务状态、chunk 元数据
  • Redis:做任务状态缓存或会话缓存(可选)
  • Celery / 后台任务线程池:处理异步解析(可选)

1.文档管理接口

  • POST /kb/upload:上传文件触发解析
  • GET /kb/files:查询知识库文件列表
  • DELETE /kb/file/{id}:删除文件
  • POST /kb/reindex/{id}:重建索引
  • GET /kb/status/{id}:查询处理状态

2. 问答接口

  • POST /chat/ask
    请求体一般包括:
    • question
    • session_id
    • kb_id / knowledge_scope
    • top_k
    • 是否开启 query rewrite
    • 是否开启 citation

返回:

  • answer
  • retrieval_docs
  • citations
  • latency(耗时)

3. 调试接口

  • POST /debug/retrieve
  • POST /debug/rerank
  • 文档处理与入库层

前端上传 PDF、Word、Excel、Markdown、TXT 等项目文档,请求进入后端后,后端先校验文件类型、大小、用户权限和重复文件。

后端会计算文件 MD5 或 SHA-256,作为文档唯一标识。如果数据库中已经存在相同哈希值,说明该文档已解析过,可以直接复用已有知识库索引,避免重复解析和重复向量化。

如果是新文档,后端会在 MySQL 中创建一条文档解析任务记录,状态为 PENDING,然后进入解析流程。

后端使用 unstructured 解析非结构化文档,例如 PDF、Word、Markdown、HTML 等;对于 Excel、CSV 等表格类文件,使用 pandas 读取并转换成结构化文本。

解析完成后,会对文本做清洗处理,包括去除页眉页脚、空行、乱码、重复段落、无意义符号,并保留文档标题、章节层级、页码、表格标题等元数据。

1. DOCX 解析

DOCX 主要抽取:

  • 标题
  • 正文段落
  • 列表
  • 表格

处理逻辑:

  • 保留标题层级,作为 chunk 的结构信息
  • 表格按“表头 + 行内容”转成自然语言
  • 去除空白段、重复换行、页眉页脚噪声

2. PDF 解析

PDF 主要问题是:

  • 段落断裂
  • 页眉页脚干扰
  • 表格跨页
  • 双栏 / 图片说明文字混入正文

处理思路是:

  • 先按版面元素解析
  • 再做文本拼接
  • 去除连续重复页眉页脚
  • 对明显断裂的段落做合并
  • 对表格区域单独抽取

3. XLSX 解析

  • 用 pandas 读取每个 sheet
  • 每个 sheet 单独建逻辑文档单元
  • 首先识别表头
  • 再把每一行转成“字段名:字段值”的文本表示
  • 对空列、合并单元格、备注列做清洗

比如一行:
项目名称=A,负责人=B,状态=进行中,截止时间=2025-01-10

转成:
在Sheet[项目计划]中,项目名称为A,负责人为B,当前状态为进行中,截止时间为2025-01-10。

更适合向量检索。

4. 切分策略

清洗后的文本不会直接整体送入向量模型,而是先进行分块。

我采用的是层级分块策略:优先按照文档标题、章节、段落进行切分;如果段落过长,再按照句子或固定 token 长度进行递归切分。每个 chunk 会保留所属文档 ID、章节标题、页码、段落序号、父级 chunk ID 等元数据。

这样做的好处是,检索时既能命中细粒度文本片段,又能通过父子块关系补充上下文,避免模型只看到孤立句子导致理解不完整。

第一级:按文档结构切

先按:

  • 一级标题
  • 二级标题
  • 表格
  • sheet
  • 段落块

切成语义单元。

第二级:按长度切

对过长语义单元再按 token 长度滑窗切分,比如:

  • chunk_size = 400~800 tokens
  • overlap = 80~150 tokens
第三级:特殊块单独建索引
  • 表格块
  • 摘要块
  • 标题块

3. Multi-representation

一份内容构造多种索引表示:

  • 原文 chunk
  • chunk 摘要
  • 标题 + chunk
  • 表格转写文本
  • 关键词表示

Multi-representation 是为同一语义块生成不同检索视图,例如原始文本视图、摘要视图、标题增强视图和表格自然语言化视图。这样可以提升不同提问方式下的召回率。

  • 数据库

每个 chunk 会调用 Qwen3-Embedding-8B 生成向量表示。向量化完成后,将 chunk 文本、embedding 向量和元数据一起写入 ChromaDB。

ChromaDB 主要负责语义检索,MySQL 负责保存文档任务状态、文件信息、用户信息、会话记录和引用关系。这样可以把向量检索能力和业务数据管理解耦。

  • 检索编排层

1.metadata 

  • doc_id
  • file_name
  • file_type
  • section_title
  • page_no / sheet_name
  • chunk_id
  • parent_id
  • source_text

  • summary_text
  • keywords
  • create_time

作用:

  1. 方便过滤,比如只查某个文档
  2. 方便 citation 展示
  3. 方便调试召回错误
  4. 方便做层级检索

2. 向量化流程

预处理:

  • 去多余空白
  • 统一标点
  • 保留标题前缀
  • 表格块做自然语言化

3. Collection 设计

  • 一个项目一个 collection
  • 或者公共 collection + metadata filter

如果文档规模不大,按知识库统一 collection 更方便;如果企业里有多个项目库,为了隔离和检索效率,按项目或业务域拆 collection。

3. 层级索引

  • 第一层:文档 / 章节级粗召回
  • 第二层:chunk 级细召回

先用标题、摘要或章节级表示做粗筛,
再在候选范围里做 chunk 级向量检索。

2. 检索执行流程

第一路是 BM25 关键词检索,用来解决精确词、接口名、类名、字段名、错误码等关键词匹配问题。

第二路是 ChromaDB 向量检索,用来解决语义相似、同义表达、自然语言模糊查询的问题。

两路检索结果的分数体系不同,不能直接相加,所以我使用 RRF 算法按排名进行融合。RRF 不关心原始分数,只关心文档在各路检索中的排名,可以比较稳定地融合 BM25 和向量检索结果。

融合后会进行去重,避免同一段内容因为同时命中 BM25 和向量检索而重复进入上下文。

路线 A:BM25 检索

对 query rewrite 之后的查询抽关键词,
在 chunk 文本上做关键词召回。

路线 B:向量检索

对查询做 embedding,
到 ChromaDB 中做 topK 语义检索。

路线 C:特殊结构召回

如果问题像“表格里负责人是谁”
会优先查表格块 / sheet 块。

3. 去重

去重一般按两层:

  • chunk_id 去重
  • 文本相似度去重

比如同一个段落可能同时被 BM25 和向量检索命中,
或者因为 overlap 导致两个 chunk 很接近。

4. RRF 融合

采用 RRF 做排序融合,按候选在各检索器里的相对名次合并,能更稳地提升召回质量。

5. BGE Rerank

RRF 融合后的结果只是粗排,仍然可能存在相关性不够精准的问题。(原因:query和chunk分别编码,两边看不到对方,Rerank用的是Cross-Encode,能够使用交叉注意力判断是否能真正回答问题)

所以我会把 Top-N 候选 chunk 送入 BGE-Reranker 进行重排序。Reranker 会同时看用户问题和候选文本,判断两者的真实相关性,然后重新打分。

这里建议这样讲:

  • 初召回 topN,比如 20~30
  • 把 query 和候选 chunk 一起送入 reranker
  • rerank 后保留 topM,比如 5~8
  • 再组装上下文给生成模型

它的作用是:

  • 降低错召回
  • 提高最终上下文纯度
  • 减少 hallucination

  • LLM 问答生成层

用户发起问题后,后端不会直接拿原问题去检索,而是先进入问题理解阶段。

系统会使用本地 Ollama 部署的 Qwen3-32B 对问题进行语义分析,包括:

识别用户意图,比如是事实查询、总结归纳、对比分析、流程解释还是追问上下文。

如果用户的问题比较模糊,例如“这个模块怎么做的”,系统会结合最近几轮会话进行上下文压缩和指代消解,把问题改写成更明确的检索查询。

如果问题包含多个意图,比如“介绍架构并说明优缺点”,Agent 会拆解成多个子任务,分别检索后再汇总回答。

1. 问题转化流程

第一步:意图识别

我在query 前面加了一个 Query Router。普通问题,比如指代消解、关键词扩展、单文档查询,走 Qwen3-8B;如果问题包含多意图、跨文档对比、架构权衡、长历史上下文,才升级到 Qwen3-32B。判断方式一开始用规则,例如问题长度、关键词、历史轮数,后面可以让 8B 输出结构化复杂度评分。这样能把 32B 用在真正需要推理能力的地方,降低延迟和显存成本。

  • 查定义
  • 查时间
  • 查责任人
  • 查状态
  • 查影响分析
  • 多文档对比
第二步:query rewrite

把口语问题改成更适合检索的问题,比如:

  • 补齐主语
  • 去掉噪声词
  • 抽取关键词
  • 改成更标准表达
第三步:结构化拆解

例如把问题转成:

  • topic = 接口变更
  • constraint = 上周
  • target = 验收时间
  • intent = 影响分析
第四步:是否拆子问题

例如:
“这个需求什么时候提的,谁负责,延期原因是什么”
会拆成 3 个子查询。

第五步:每个子任务单独检索

然后后端不是直接拿原问题检索,而是对每个子任务分别走一遍检索链路:

子任务 1:系统整体架构设计
→ BM25 + 向量检索 → RRF → Rerank → Top-K chunks

子任务 2:系统架构的优点
→ BM25 + 向量检索 → RRF → Rerank → Top-K chunks

子任务 3:系统架构的缺点和风险
→ BM25 + 向量检索 → RRF → Rerank → Top-K chunks
第六步: 合并和去重上下文

每个子任务都会召回一些 chunk,后端需要做去重:

按 chunk_id 去重
按相似文本去重
保留 rerank 分数更高的结果
限制总 token 数

最终得到一个结构化上下文:

{
  "architecture_context": [...],
  "advantage_context": [...],
  "risk_context": [...]
}
第七步:让 LLM 分段生成答案

Prompt 里明确要求模型按子任务回答:

请基于以下检索资料回答用户问题。

用户问题:介绍系统架构,并说明优缺点。

你需要按以下结构回答:
1. 系统整体架构
2. 方案优点
3. 潜在缺点/风险
4. 改进建议

要求:
- 只能基于检索上下文回答
- 每个结论尽量关联引用来源
- 如果资料不足,明确说明“不确定”

2. 上下文压缩

 后端采用“两层记忆”:

第一层:短期窗口

每次请求只取最近 5~10 轮完整对话,例如:

最近 5 轮 user/assistant 消息

这部分直接拼进 Prompt,保证当前上下文连贯。

第二层:历史摘要

如果某个 session 的消息数量超过阈值,比如超过 20 条,就触发一次摘要压缩。

做法是:

  1. 取较早的历史消息;

  2. 用Qwen3-8B生成一段 session_summary

  3. 把摘要存到 chat_session.summary 字段;

  4. 后续请求不再携带全部历史,只携带:

历史摘要 + 最近 5 轮完整对话 + 当前问题

这样既保留长期上下文,又不会让 Prompt 无限膨胀。

3.会话管理

后端加两张表:

chat_session:保存一次问答会话。

核心字段:

id
user_id
knowledge_base_id
title
create_time
update_time

chat_message:保存每轮对话。

核心字段:

id
session_id
role        -- user / assistant
content
citations   -- JSON,保存引用 chunkId、文档名、页码
create_time

前端流程:

用户进入知识库问答页时,先创建或选择一个 sessionId。每次提问时,前端请求带上:

{
  "sessionId": "xxx",
  "knowledgeBaseId": "xxx",
  "question": "这个模块怎么实现的?"
}

后端收到后:

  1. 根据 sessionId 查询最近 N 轮对话;

  2. 用最近对话辅助 query rewrite;

  3. 执行 BM25 + 向量检索 + RRF + Rerank;

  4. 调用 LLM 生成答案;

  5. 把用户问题和 AI 回答都写入 chat_message

  6. 返回答案和引用来源。

前端展示:

左侧是历史会话列表,右侧是当前聊天窗口。每条 AI 回答下面展示引用来源,点击可以查看原文片段。

4. Citation 来源追溯

每个 chunk 在入库时都绑定了文档 ID、章节标题、页码、段落位置等元数据。

所以模型生成答案时,后端可以把答案中的关键结论和对应 chunk 绑定起来,返回给前端。

前端展示时,不只是显示答案,还会展示“来源文档”“页码/章节”“引用片段”。用户点击引用后,可以定位到原文位置,增强可信度和可验证性。

5. Langfuse 监控与评估

系统接配置了Langfuse 记录完整调用链路,包括用户问题、改写后的 query、检索结果、Rerank 分数、Prompt、模型输出、token 消耗和耗时。

这样做的价值是方便排查问题。比如用户反馈答案不准时,可以判断是文档解析问题、分块问题、召回问题、重排序问题,还是模型生成问题。


  • 监控评估层

  • 问题排查

  • 看 chunk 预览和 metadata 能定位是解析、切分还是召回问题

第一步:先看命中的 chunk

如果命中的 chunk 里根本没有对应文本,说明是召回问题

第二步:如果知识库里有包含“验收标准”的 chunk,但内容是残缺的

比如只有一句:

验收…

那说明是切分问题,chunk 把句子切断了。

第三步:如果原文 preview 就是乱码或顺序错乱

比如 PDF 把页脚、页码、两栏文字拼到一起
那是解析问题

用途:

1)缓存任务状态

比如文档上传后异步解析:

前端轮询时可以直接查 Redis,速度快。

2)缓存问答会话上下文

多轮对话里,session 的历史摘要可以放 Redis。

3)缓存热点查询结果

相同问题多次被问,可以直接命中缓存,降低 latency。

4)做消息中间件

Celery 常用 Redis 作为 broker。

Redis 在这个项目里更适合做高频访问但不要求永久存储的数据,比如任务状态、会话上下文和热点缓存;正式元数据和文件映射仍然建议放 MySQL。

  • 数据库

  • 1. Redis 

    Redis 是内存型 key-value 数据库,特点是:

  • 读写快
  • 支持过期时间
  • 支持字符串、哈希、列表、集合等结构
  • 常用来做缓存、会话、任务状态、分布式锁
  • task:123 -> parsing
  • task:123 -> embedding
  • task:123 -> success

Celery 是什么

Celery 是 Python 里常见的异步任务队列框架

它通常配合:

  • Redis
  • RabbitMQ

来做任务分发。

流程

  1. 上传文件
  2. 后端生成任务
  3. 把任务丢给 Celery
  4. worker 去执行解析、切分、向量化、入库
  5. 更新任务状态

Celery / 线程池

解决的是:

任务在后台怎么执行

WebSocket

解决的是:

前后端怎么实时通信

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐