项目文档智能问答系统
系统面向企业项目管理场景,支持 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/retrievePOST /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_idfile_namefile_typesection_titlepage_no/sheet_namechunk_idparent_id-
source_text summary_textkeywordscreate_time
作用:
- 方便过滤,比如只查某个文档
- 方便 citation 展示
- 方便调试召回错误
- 方便做层级检索
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 条,就触发一次摘要压缩。
做法是:
-
取较早的历史消息;
-
用Qwen3-8B生成一段
session_summary; -
把摘要存到
chat_session.summary字段; -
后续请求不再携带全部历史,只携带:
历史摘要 + 最近 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": "这个模块怎么实现的?"
}
后端收到后:
-
根据
sessionId查询最近 N 轮对话; -
用最近对话辅助 query rewrite;
-
执行 BM25 + 向量检索 + RRF + Rerank;
-
调用 LLM 生成答案;
-
把用户问题和 AI 回答都写入
chat_message; -
返回答案和引用来源。
前端展示:
左侧是历史会话列表,右侧是当前聊天窗口。每条 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 -> parsingtask:123 -> embeddingtask:123 -> success
Celery 是什么
Celery 是 Python 里常见的异步任务队列框架。
它通常配合:
- Redis
- RabbitMQ
来做任务分发。
流程
- 上传文件
- 后端生成任务
- 把任务丢给 Celery
- worker 去执行解析、切分、向量化、入库
- 更新任务状态
Celery / 线程池
解决的是:
任务在后台怎么执行
WebSocket
解决的是:
前后端怎么实时通信
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)