团队知识系统Knowledge-hub技术方案
Knowledge Hub 技术方案
企业 LLM 知识索引平台 — 完整技术设计与踩坑记录
版本: v1.1 | 更新日期: 2026-07-29
1. 项目概述
1.1 背景与目标
搭建企业内部 LLM 知识索引平台,持续采集、处理、索引公司内部知识数据,为 AI Agent、企业问答助手、代码助手等应用提供统一知识检索能力。
1.2 数据源
| 数据源 | 版本 | 说明 |
|---|---|---|
| Confluence | Server 6.7.1 | 私有化部署,SSO(Google 登录)认证 |
| GitLab | - | 10 个 Java 代码仓库,统一使用 xxxxx 分支 |
1.3 部署环境
| 项目 | 规格 |
|---|---|
| 操作系统 | Windows 10 |
| CPU | Intel i7-10700(8 核 16 线程) |
| 内存 | 16GB |
| 磁盘 | 500GB |
| 已有基础设施 | MySQL 8.0、Redis |
1.4 设计原则
- CPU 优先:不依赖 GPU,所有计算(Embedding、tree-sitter 解析)均在 CPU 完成
- 稳定性优先:不追求高速处理,优先保证稳定
- 断点恢复:所有任务以 document 为单位记录状态,支持中断后继续
- 增量更新:所有数据同步支持增量模式(git diff / version 对比)
- 内存可控:避免一次性加载大量数据,batch 处理
- 所有耗时任务异步化:通过 Redis 队列解耦
- 统一进程:API + Worker 一体化部署,简化运维
2. 技术选型与决策
2.1 向量数据库:ChromaDB
选型对比:
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| Qdrant | 功能强大,支持分布式 | 需要独立部署服务端,资源占用高 | ❌ 过重 |
| Milvus Lite | 轻量嵌入式 | Windows 支持不完善 | ❌ 兼容性风险 |
| ChromaDB | 嵌入式、零部署、Python 原生、支持持久化和 metadata 过滤 | 不适合超大规模 | ✅ 最适合 |
选择理由:
- 嵌入式模式,无需额外服务进程
- 原生 Python API,集成简单
- 支持持久化到磁盘,重启不丢数据
- 支持 metadata filter,满足来源过滤需求
- 对于万级文档规模完全够用
2.2 Embedding 模型:bge-base-zh-v1.5
选型对比:
| 模型 | 参数量 | 维度 | 中文效果 | CPU 推理 |
|---|---|---|---|---|
| BGE-M3 | 560M | 1024 | 优秀 | 较慢(~500ms/条) |
| Qwen Embedding | 1.5B | 1536 | 优秀 | 很慢,16GB 内存紧张 |
| bge-base-zh-v1.5 | 102M | 768 | 优秀 | 快速(~50ms/条) |
选择理由:
- 参数仅 102M,占用约 400MB 内存,适合 16GB 机器
- 768 维向量,在中文语义理解上表现出色
- CPU 推理速度完全可接受(batch=16 约 0.8s)
- sentence-transformers 库开箱即用
2.3 代码解析:tree-sitter
选型对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 正则匹配 | 简单 | 无法处理复杂嵌套、泛型等 |
| JavaParser | 准确的 Java AST | 需要 JVM 运行时,Python 集成困难 |
| tree-sitter | 增量解析、多语言支持、纯 C 实现 Python 绑定 | 语法树较底层 |
选择理由:
- 纯 Python(C 扩展),不需要 JVM
- 支持 Java Class / Method / Interface 级别切分
- 可同时提取 package、import、extends、implements 等符号信息
- 性能极佳,解析 2000+ Java 文件只需秒级
2.4 任务队列:Redis List
不使用 Celery 的原因:
- Celery 引入的依赖和配置过重
- 本系统任务类型简单(sync / embed 两种)
- Redis List + 手动消费完全满足需求
- 更容易实现自定义的断点恢复和优雅停机
2.5 LLM 集成:公司自建思考型模型
| 配置项 | 值 |
|---|---|
| API URL | https://xxxx.com/v1/chat/completions |
| 模型 | Qwen3.5(配置可切换) |
| 接口协议 | OpenAI Compatible |
| 限频 | 3 秒/次(RateLimiter 异步限频器) |
| 特殊行为 | content 字段可能为 null,答案在 reasoning_content 中 |
兼容的思考型模型行为:
- Qwen3.5:答案在 reasoning_content 字段,content 可能为 null
- Qwen 系列:可能在 content 中混入思考过程(<think> 标签或中文思考标记)
- 统一处理:LLMClient.chat() 优先取 content,为空回退到 reasoning_content;Chat API 额外做 _clean_thinking_content() 后处理
3. 系统架构设计
3.1 整体架构
┌───────────────────────────────────────────────────────────────────────────┐
│ Knowledge Hub 统一进程 │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ FastAPI │ │ Sync Worker │ │ Embed Worker │ │ MCP Server │ │
│ │ (uvicorn) │ │ (异步协程) │ │ (异步协程) │ │ (FastMCP/SSE) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬────────┘ │
│ │ │ │ │ │
│ ┌──────┴────────────────┴────────────────┴────────────────┴──────┐ │
│ │ asyncio Event Loop │ │
│ └──────┬─────────────────┬──────────────────┬────────────────────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ │
│ │ APScheduler │ │ Health Check │ │ Stop File │ │
│ │ (定时任务) │ │ (10min 轮询) │ │ (停机监控) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ MySQL │ │ Redis │ │ ChromaDB │
│ (元数据) │ │ (队列) │ │ (向量) │
└─────────┘ └─────────┘ └──────────┘
↑ ↑
└────────────────┬───────────────────┘
│
┌─────┴──────┐
│ AI IDE │
│ (Cursor │
│ via MCP) │
└────────────┘
3.2 统一进程模型
所有服务在一个 Python 进程中运行(app.server.py),包括:
| 组件 | 运行方式 | 职责 |
|---|---|---|
| FastAPI + uvicorn | asyncio Task | API 服务、Chat UI、Console |
| MCP Server (FastMCP) | 挂载到 FastAPI /mcp |
为 Cursor 等 AI IDE 提供 MCP 工具 |
| Sync Worker | asyncio Task | 从 Redis 队列消费同步任务 |
| Embed Worker | asyncio Task | 从 Redis 队列消费向量化任务 |
| APScheduler | asyncio 调度器 | 定时触发全量/增量同步、健康检查 |
| Stop File Watcher | asyncio Task | 轮询检查停机文件 |
为什么统一进程?
- 避免多进程协调的复杂性
- Windows 上 signal 的行为不如 Linux,统一管理更简单
- 共享 Embedding 模型和 ChromaDB 实例,避免内存重复加载
- 简化部署:一个 start.bat 脚本搞定一切
备选启动方式:
- app/run_workers.py:仅启动 Sync Worker + Embed Worker + 调度器(不含 API 服务),适合需要分离 API 和 Worker 的场景
- app/main.py:仅启动 FastAPI 应用(含 MCP Server),通过 uvicorn app.main:create_app --factory 运行
4. 数据库设计
4.1 ER 关系
knowledge_source (数据源)
1 ← N knowledge_document (文档)
1 ← N knowledge_chunk (切片)
1 ← N code_symbol (代码符号)
1 ← N sync_task (同步任务)
4.2 表说明
| 表名 | 用途 | 关键字段 |
|---|---|---|
knowledge_source |
数据源配置(Confluence/GitLab) | source_type, config (JSON), status |
knowledge_document |
文档元数据,以 (source_id, external_id) 唯一标识 |
content_hash, version, status |
knowledge_chunk |
文档切片文本,带 FULLTEXT ngram 索引 |
content (MEDIUMTEXT), vector_id |
sync_task |
同步任务状态跟踪 | task_type, status, progress |
code_symbol |
Java 类符号信息(用于依赖图谱) | fqn, extends_fqn, implements_fqn, imports |
term_mapping |
术语映射(中英文对照,可选) | term_zh, term_en |
4.3 关键设计决策
knowledge_chunk.content 使用 MEDIUMTEXT:
- 初始使用 TEXT(64KB 限制)
- 部分 SQL 文件和大型 Java 类超过 64KB
- 踩坑后改为 MEDIUMTEXT(16MB 限制)
knowledge_chunk 上的 FULLTEXT 索引使用 ngram 解析器:
- MySQL 默认的 FULLTEXT 分词器不支持中文
- ngram 解析器可以正确索引中文内容
- 用于混合检索中的关键词匹配路径
5. 核心模块详解
5.1 配置管理
文件: app/config.py
使用 Pydantic Settings,自动从 .env 文件读取配置。
关键设计:
@model_validator(mode="before")
def _empty_strings_to_defaults(cls, values):
"""将 .env 中留空的数值/布尔字段移除,让 Pydantic 使用字段默认值"""
踩坑: .env 中 PROXY_PORT=(空字符串)会导致 Pydantic 尝试将空字符串转为 int 从而报 ValidationError。通过 model_validator 在解析前将空字符串字段移除,使其回退到默认值。
路径处理:
- GITLAB_CLONE_DIR 和 CHROMA_PERSIST_DIR 声明为 str 而非 Path
- 通过 @property 方法(gitlab_clone_path、chroma_persist_path)返回解析后的绝对路径
- 相对路径以项目根目录为基准
5.2 数据源连接器
5.2.1 Confluence 连接器
文件: app/connector/confluence.py、app/connector/confluence_sso.py
认证方式: Cookie + SSO 自动续签
Confluence 6.7.1 不支持 API Token,只能使用 Cookie 认证。Cookie 来源于企业 SSO(Google 登录)。
SSO 自动续签流程:
Confluence API 请求
↓
_request() 发起请求
↓
_is_auth_failure() 检测响应
├── 401/403 → 认证失败
├── 404 (content API) → 可能是匿名用户看不到受限 Space
├── 200 + text/html → 被重定向到登录页
└── 正常响应 → 返回
↓ (认证失败)
_try_sso_refresh()
↓
Playwright 无头浏览器
↓
Google SSO 登录(邮箱 → 密码 → 登录)
↓
抓取 Confluence Cookie
↓
更新 .env 文件 + 重建 HTTP 客户端
↓
重试原始请求
踩坑 #1 — test_connection 不检测 Anonymous:
- /rest/api/user/current 对未登录用户也返回 200,displayName 为 Anonymous
- 修复:在 test_connection() 中主动检测 Anonymous 并触发 SSO 续签
踩坑 #2 — Confluence 对匿名用户返回 404 而非 401:
- 当 Cookie 失效时,匿名用户请求受限 Space 的内容 API 返回 404(非 401/403)
- 修复:_request() 增加 treat_404_as_auth 参数,fetch_documents() 和 get_all_spaces() 传入 True
踩坑 #3 — Playwright 选择器失效:
- Google 登录页的 HTML 结构变化,input[type="email"] 选择器找不到元素
- 修复:使用多选择器 input#identifierId, input[name="identifier"], input[type="email"]
踩坑 #4 — 代理配置:
- Confluence 部署在内网,开发机需要 HTTP 代理才能访问
- httpx.AsyncClient 和 Playwright 均需配置代理
5.2.2 GitLab 连接器
文件: app/connector/gitlab.py
同步策略: git clone + git pull(而非 API 逐文件获取)
选择理由:
- Git 天然支持版本管理
- git diff 可以精确识别变更文件
- 增量更新极其高效
- 适合大型代码仓库
文件过滤规则:
- 包含:.java, .sql, .xml, .yaml, .yml, .md, .py, .js, .ts, .properties, .json
- 忽略目录:target, node_modules, log, logs, build, dist, .git, .idea, .vscode, __pycache__
踩坑 #1 — 路径解析问题:
- GITLAB_CLONE_DIR 配置为相对路径 data/repos
- Worker 运行时工作目录不确定,导致 仓库目录不存在 错误
- 修复:在 config.py 中使用 @property 确保返回绝对路径
踩坑 #2 — 分支名错误:
- 初始配置所有仓库使用 master 分支
- 实际项目使用 xxxxx 分支
- 修复:更新数据库中所有仓库的分支配置,重新 clone
5.3 文档解析与切片
5.3.1 Confluence 文档解析
流程:
XHTML 正文 → BeautifulSoup 预处理 → markdownify 转 Markdown → 标题级切分
踩坑 — markdownify 参数冲突:
- markdownify 同时接收 HTML 字符串和 soup 参数时行为异常
- 修复:先用 BeautifulSoup 解析,再将 soup 对象传给 markdownify
5.3.2 Java 代码切分
两级策略:
1. tree-sitter 优先:按 Class/Method 级别精确切分
2. 正则回退:tree-sitter 失败时,使用正则按空行和方法签名切分
切片参数:
| 参数 | 值 | 说明 |
|------|------|------|
| CHUNK_TARGET_SIZE | 384 | 目标切片字符数 |
| CHUNK_MAX_SIZE | 512 | 最大切片字符数 |
| CHUNK_MIN_SIZE | 32 | 最小切片字符数 |
| CHUNK_OVERLAP | 64 | 切片重叠字符数 |
踩坑 — 大文件处理:
- 部分 SQL 文件超过 100KB,超出 TEXT 列限制
- chunk_code_generic 增强:先按空行分割,如果仍然过大则按换行分割
5.3.3 Java 符号分析
文件: app/parser/java_analyzer.py
使用 tree-sitter 对 Java 源码进行静态分析,提取:
| 信息 | 说明 |
|---|---|
package_name |
包名 |
imports |
所有 import 语句的全限定名 |
fqn |
类的全限定名(package.ClassName) |
extends_fqn |
继承的父类 |
implements_fqn |
实现的接口列表 |
field_types |
成员变量引用的类型 |
method_param_types |
方法参数/返回值类型 |
annotations |
类级注解 |
踩坑 — 从 chunk 提取符号不完整:
- 初始方案:在 sync_worker 的切片过程中从每个 chunk 提取符号
- 问题:chunk 只包含部分代码,缺少 package、import 等上下文
- 修复:改为从 git clone 目录读取完整的原始 Java 文件进行分析
5.4 向量化与存储
5.4.1 Embedding 服务
文件: app/embedding/embedding_service.py
模型: BAAI/bge-base-zh-v1.5
| 配置 | 值 |
|---|---|
| 设备 | CPU |
| 批大小 | 16 |
| 最大长度 | 512 tokens |
| 输出维度 | 768 |
| 内存占用 | ~400MB |
踩坑 — HuggingFace 模型下载失败:
- hf-mirror.com(国内镜像)不可达
- 修复:设置环境变量 HF_ENDPOINT=https://huggingface.co
5.4.2 ChromaDB 存储
文件: app/vector/chroma_store.py
- 嵌入式模式,持久化到
data/vectors/ - 集合名:
knowledge_chunks - 支持 metadata 过滤(source_type、repo、space 等)
踩坑 — ChromaDB HNSW 索引损坏:
- Worker 非正常退出(如进程被杀)导致 HNSW 索引文件损坏
- 重启后出现 Access Violation / Segfault
- 修复:删除 data/vectors/ 目录,触发全量重新向量化
- 预防:实现优雅停机机制,确保 chroma.close() 被调用
5.5 检索引擎
5.5.1 查询增强
文件: app/retrieval/query_enhancer.py
使用公司 LLM(Qwen3.5)对用户查询进行扩展:
原始查询:"贷款审批流程"
↓ LLM 生成变体
变体 1:"loan approval process"
变体 2:"借款审核工作流"
变体 3:"放款审批步骤"
限频: 3 秒/次(公司 LLM API 限制)
踩坑 — Qwen3.5 响应结构特殊:
- content 字段经常为 null
- 实际答案在 reasoning_content 字段中(包含大量思考过程)
- 修复:解析时优先使用 content,为 null 则回退到 reasoning_content,再过滤思考标签
踩坑 — LLM 返回不相关变体:
- LLM 的 reasoning_content 中包含大量思考过程文本
- 直接解析会将思考内容误认为查询变体
- 修复:严格解析 LLM 输出,只提取 JSON 数组中的变体
5.5.2 混合检索 (Hybrid Retrieval)
文件: app/retrieval/hybrid_retriever.py
三阶段流程:
用户查询
↓
┌─────────────────┬─────────────────┐
│ 向量检索路径 │ 关键词检索路径 │
│ │ │
│ query embedding │ 中文关键词提取 │
│ ↓ │ ↓ │
│ ChromaDB 近邻 │ MySQL FULLTEXT │
│ (余弦相似度) │ (ngram 匹配) │
└────────┬────────┴────────┬────────┘
↓ ↓
Reciprocal Rank Fusion (RRF)
↓
语义重排 (Semantic Rerank)
↓
Top-K 结果
RRF 融合公式:
# 向量路径:RRF 分数 × 原始相似度分数加权
weighted_score = (1 / (60 + rank + 1)) × raw_similarity_score
# 关键词路径:标准 RRF 分数
keyword_score = 1 / (60 + rank + 1)
# 合并:同一 chunk 的各路径分数累加
score(d) = Σ weighted_score_i(d)
向量路径的 RRF 会乘以原始相似度分数,让高相关度的结果即使排名稍低也能获得合理分数。
多查询向量检索:
- 原始查询权重 1.0,LLM 生成的变体查询权重 0.8
- 同一 chunk 被多个变体命中时取最高分
- 低于 SEARCH_SIMILARITY_THRESHOLD(默认 0.5)的结果直接过滤
语义重排:
- 对 RRF 融合后的 Top 结果
- 优先从 ChromaDB 获取已存储的文档向量(一次 IO)
- 若 ChromaDB 取不到向量,降级为对候选内容做 batch encode
- 使用 query embedding 与 doc embedding 的余弦相似度重新排序
- 综合 RRF 分数(0.4)和语义分数(0.6)
- 仅关键词匹配的结果(无向量分数)降权至 0.3 × rrf_norm
踩坑 — 空类排名靠前:
- 一个空的 Java 类(只有 class 声明)在搜索结果中排名第一
- 原因:向量相似度高但内容无意义;RRF 未充分过滤
- 修复:
1. 增加向量检索的相似度阈值过滤
2. 改进中文关键词提取算法
3. 修复语义重排中的 numpy 计算问题
5.6 代码智能分析
5.6.1 代码符号表
表: code_symbol
从 Java 源码中提取类级别的结构化信息,存储到数据库,作为代码依赖图谱的基础。
提取流程:
Git 仓库原始 Java 文件
↓
tree-sitter AST 解析
↓
提取: package, imports, class, extends, implements,
field_types, method_param_types, annotations
↓
写入 code_symbol 表
5.6.2 代码依赖图谱引擎
文件: app/retrieval/code_graph.py
核心类: CodeGraphEngine
BFS 依赖追踪算法:
输入: 入口类名(如 InsDisbursementStatusMachine)
↓
1. 从 code_symbol 表查找入口类
2. BFS 队列初始化: [(入口类, depth=0)]
3. 循环:
a. 弹出队首类
b. 提取依赖: extends + implements + imports + field_types + method_param_types
c. 过滤: 只保留项目内的类(排除 java.*, spring.*, lombok.* 等)
d. 从原始文件读取完整源码
e. 将未访问的依赖类加入队列 (depth+1)
4. 直到: 队列为空 || 达到 max_depth || 达到 max_nodes
↓
输出: CodeGraphResult (节点列表 + 关系信息 + 源码)
跨模块依赖追踪:
- 由于 10 个仓库的所有 Java 类符号都存储在同一张 code_symbol 表中
- BFS 可以自然地跨仓库追踪依赖(如 asetloan-installment 中的类引用 asetloan-common 中的类)
5.6.3 智能意图识别
两阶段识别用户查询中的代码类名:
Stage 1 — 正则匹配(快速路径):
# 直接从查询中提取类名
"分析 InsDisbursementStatusMachine 的工作流程"
→ ["InsDisbursementStatusMachine"]
Stage 2 — LLM 辅助识别(业务语言):
用户: "我想知道订单打款状态机的工作流程"
↓
1. 快速判断: _query_needs_code_analysis() 检测业务关键词 → 需要代码分析
2. 业务关键词映射: "打款" → ["Disbursement", "Capital", "Fund", "Payment"]
"状态机" → ["StatusMachine", "StateMachine", "Status", "State"]
3. 仓库名识别: _extract_repo_name() 检测 "asetloan-xxx" 模式
4. 候选符号搜索: 逐关键词单独查 code_symbol 表 (LIKE '%Disbursement%')
→ 在 Python 中合并,按命中关键词数排序(目标仓库的符号优先)
5. 构造 LLM Prompt(含历史对话上下文 + 仓库提示):
"以下是候选 Java 类列表,用户想了解'订单打款状态机',
请选出最相关的入口类..."
6. LLM 返回: ["InsDisbursementStatusMachine"]
→ 如果 LLM 失败,回退到 _fallback_select_classes() 基于后缀权重选择入口类
↓
7. 触发 CodeGraphEngine 追踪依赖
8. 将依赖图谱 + 源码作为上下文发送给 LLM 分析
结合对话历史:
- smart_detect_code_classes() 接受最近 10 条对话历史
- 历史消息会被格式化并加入 LLM 提示词中
- 帮助 LLM 理解上下文关联(如用户追问某个之前提到的类)
踩坑 — LLM 候选类排序不佳:
- MySQL LIKE 查询使用 OR 连接多个条件时,默认排序不可控
- 修复:对每个关键词单独查询,在 Python 中合并并按命中关键词数排序
- 增强:支持从查询中提取仓库名(如 asetloan-installment),优先搜索该仓库的符号
踩坑 — Qwen3.5 JSON 解析困难:
- LLM 返回的 JSON 类名数组常常嵌在 reasoning_content 的大段文本中
- 修复:增强 _parse_class_names_from_llm() 函数
- 尝试所有 [...] 结构并选最佳(按有效类名数排序)
- 回退到正则搜索反引号和引号中的类名
- 增加 _fallback_select_classes() 回退策略:LLM 失败时基于后缀权重(如 Machine=10、Service=8)自动选择入口类
5.7 Chat API 与前端
5.7.1 OpenAI 兼容 Chat API
文件: app/api/chat.py
端点:
- POST /v1/chat/completions — Chat 对话
- GET /v1/models — 返回模型列表(OpenAI 兼容,供 NextChat 等前端发现模型)
CORS 配置:
- 允许所有来源(allow_origins=["*"]),支持 NextChat 等外部前端跨域访问
完整处理流程:
用户消息
↓
1. 意图识别(代码类检测)
├── Stage 1: 正则匹配类名
└── Stage 2: LLM 辅助识别(含历史上下文)
↓
2. 构建上下文
├── 检测到代码类 → CodeGraphEngine 追踪依赖 → 代码图谱上下文
└── 普通查询 → Hybrid Retrieval 混合检索 → 知识库上下文
↓
3. 构造 LLM Prompt
├── 系统提示词 + 检索结果 + 用户问题
└── 代码分析专用提示词(如有代码图谱)
↓
4. 调用公司 LLM (Qwen3.5)
├── 流式 (stream=true) → SSE 推送
└── 非流式 → 直接返回
↓
5. 响应后处理
├── 过滤 <think>...</think> 思考内容
└── 追加参考来源链接
关键配置:
| 参数 | 值 | 原因 |
|------|------|------|
| max_tokens | 32192 | 思考型模型的思考过程消耗大量 token |
| httpx timeout | 180s | 长推理可能需要 2-3 分钟 |
思考型模型兼容:
- Chat API 内置 _clean_thinking_content() 函数
- 检测 Qwen/Deepseek 等模型在 content 中输出的推理过程
- 通过答案边界标记("根据知识库"、"综上所述"等)提取最终答案
- 支持 <think> 标签、--- 分隔线等多种格式的思考内容清理
踩坑 — 回答被截断:
- 初始 max_tokens=2048,思考型模型的 thinking 过程会消耗大量 token
- 实际回答内容被截断
- 修复:提升到 32192,同时增加超时到 180s
踩坑 — 流式响应缺少参考信息:
- 流式 SSE 推送时,参考链接应该在回答结束后追加
- 修复:在流式响应的最后一个 chunk 后追加包含参考链接的额外 chunk
5.7.2 内置 Chat UI
文件: frontend/index.html
纯 HTML/CSS/JS 实现,零依赖,通过 FastAPI 直接 serve。
特性:
- ChatGPT 风格深色主题界面
- 多会话管理(新建/切换/删除)
- 历史会话本地存储(localStorage)
- Markdown 渲染(代码高亮、表格、列表)
- 流式响应实时展示
- 响应式布局支持移动端
踩坑 — emptyState DOM 元素丢失:
- renderMessages() 使用 innerHTML 更新聊天容器
- 导致 emptyState DOM 元素被销毁
- 修复:更新前先将 emptyState 移出容器,更新后再放回
踩坑 — 流式传输中切换/删除会话:
- 流式传输过程中切换或删除会话导致状态混乱
- 修复:添加 isStreaming 标志位,阻止传输中的会话操作
5.7.3 管理控制台
前端: frontend/console.html
功能:
- 系统状态总览(文档数、向量数、待处理数等)
- 数据源 CRUD(添加/编辑/删除 GitLab/Confluence)
- 数据源健康状态展示(✅ 正常 / ❌ 异常 / ⏳ 未检查)
- 增量更新开关(启用/停用自动同步)
- 手动触发同步任务
- 手动触发健康检查
- 最近任务状态监控(30 秒自动刷新)
5.7.4 REST API 全景
管理控制台的功能由以下后端 API 模块支撑:
| 模块文件 | 路径前缀 | 主要端点 |
|---|---|---|
app/api/search.py |
/api |
POST /api/search — 统一知识检索 |
app/api/source.py |
/api |
GET/POST/PUT/DELETE /api/sources — 数据源 CRUD |
app/api/task.py |
/api |
POST /api/tasks/trigger、GET /api/tasks — 任务管理 |
app/api/admin.py |
/api |
GET /api/admin/status — 系统状态 |
GET /api/admin/health — 服务健康检查 |
||
GET/POST /api/admin/health/sources — 数据源健康状态 |
||
POST /api/admin/code-graph — 代码依赖图谱查询 |
||
GET /api/admin/symbols/stats — 代码符号统计 |
||
POST /api/admin/symbols/rebuild — 重建代码符号索引 |
||
app/api/chat.py |
— | POST /v1/chat/completions、GET /v1/models — OpenAI 兼容 |
5.8 MCP Server(AI IDE 集成)
文件: app/mcp/server.py
端点: GET /mcp/sse(SSE 传输协议)
作用: 将知识库的检索、代码图谱、RAG 对话能力封装为 MCP (Model Context Protocol) 工具,供 Cursor 等 AI IDE 直接调用。通过 FastMCP 创建,挂载到 FastAPI 应用的 /mcp 路径。
Cursor 配置示例:
{
"mcpServers": {
"knowledge-hub": {
"url": "http://10.0.136.158:8080/mcp/sse"
}
}
}
提供的 MCP 工具:
| 工具名 | 用途 | 关键参数 |
|---|---|---|
knowledge_search |
知识库混合检索(向量+全文+RRF+重排) | query, top_k, use_enhancement |
code_graph_trace |
Java 类依赖图谱追踪 | class_name, max_depth, max_nodes |
knowledge_chat |
基于知识库的 RAG 对话(自动检索+代码分析+LLM 回答) | question, chat_history |
system_status |
系统状态概览(数据源/文档/向量/队列等统计) | 无 |
knowledge_chat 完整流程:
用户问题
↓
1. 代码类检测(两阶段:正则快速 → LLM 智能识别)
2. 代码图谱追踪(如有代码类,BFS 追踪依赖链)
3. 知识检索(HybridRetriever 混合检索)
4. 构造 LLM 消息
├── 有代码图谱 → 综合模式提示词(文档+代码双视角)
└── 无代码图谱 → 普通知识库提示词
5. 调用 LLM 生成回答
6. 附加参考来源链接
↓
返回完整答案
设计要点:
- 使用全局单例延迟初始化 _retriever、_llm、_code_graph,避免重复创建资源
- knowledge_chat 的 max_tokens 设为 32192,适配思考型模型
- 挂载时禁用 DNS 重绑定保护(enable_dns_rebinding_protection=False),允许非 localhost 的客户端连接
- 如果 mcp 包未安装,MCP Server 挂载失败不影响其他功能
5.9 任务调度与 Worker
5.9.1 调度器
文件: app/scheduler/scheduler.py
| 任务 | 触发方式 | 说明 |
|---|---|---|
daily_sync |
CronTrigger(hour=2, minute=0) | 每日 02:00 全量同步 |
incremental_sync |
CronTrigger(hour="*/4", minute=30) | 每 4 小时增量同步 |
health_check |
IntervalTrigger(minutes=10) | 每 10 分钟数据源健康检查 |
所有定时任务仅对 status='active' 的数据源触发。
5.9.2 Sync Worker
文件: app/worker/sync_worker.py
处理流程:
Redis 队列 (sync)
↓ dequeue
获取 sync_task + knowledge_source
↓
根据 source_type 选择 Connector
├── Confluence: test_connection → fetch_documents → fetch_document_detail → parse → chunk
└── GitLab: clone_or_pull → walk files → parse → chunk
↓
对每个文档:
1. 检查 content_hash 是否变化
2. 删除旧 chunks
3. 解析 + 切片 → 写入 knowledge_chunk
4. 提取 Java 符号 → 写入 code_symbol
5. commit 当前文档
6. 推送 embed 任务到 Redis 队列
↓
更新 sync_task 状态
踩坑 — Sync 与 Embed 竞态条件:
- Sync Worker 批量写入 chunks 但尚未 commit
- Embed Worker 已从队列取到 embed 任务,但查不到 chunks
- 修复:在每个文档处理完 chunk 后立即 session.commit(),然后再 enqueue embed 任务
踩坑 — 空内容文档卡在 pending 状态:
- 一些 .properties 文件内容为空,chunk_count = 0
- Embed Worker 反复补偿但无法处理
- 修复:如果 chunk_count = 0,直接标记为 indexed
踩坑 — SQLAlchemy 事务回滚传播:
- 单个文档处理异常未 rollback,导致后续文档的 session.add() 报错:
This Session's transaction has been rolled back due to a previous exception
- 修复:在异常处理中加 await session.rollback()
5.9.3 Embed Worker
文件: app/worker/embed_worker.py
处理流程:
Redis 队列 (embed)
↓ dequeue
获取 knowledge_document
↓
查询关联的 knowledge_chunk
↓
批量 Embedding (batch_size=16)
↓
写入 ChromaDB (upsert)
↓
更新 chunk.vector_id
↓
更新 document.status = 'indexed'
补偿机制:
- 队列为空但仍有 pending 文档时,启动补偿处理
- 将 pending 文档重新 enqueue 到 embed 队列
进度日志:
Embedding 进度: [12/2976] 0.4% | 当前文档: ApplicationConfig.java
5.10 健康检查
文件: app/health/source_checker.py
功能:
- 启动时立即检查所有数据源连接状态
- 每 10 分钟定时轮询
- 支持手动触发(API POST /api/admin/health/check)
检查策略:
| 数据源类型 | 检查方式 | 并发模式 |
|-----------|---------|---------|
| Confluence | test_connection()(含 Anonymous 检测和 SSO 续签) | 串行(共享 Cookie,避免并发续签冲突) |
| GitLab | test_connection()(git ls-remote) | 并行(限制并发 5 个) |
结果缓存:
- 检查结果缓存在内存 dict 中
- API GET /api/admin/health/sources 直接返回缓存
- Console 前端展示健康状态 badge
6. 数据流与处理管线
6.1 完整数据流
┌────────────────────────────────────────────────────────────────────────────┐
│ 数据采集 │
│ │
│ Confluence ──REST API──→ XHTML 正文 │
│ GitLab ──git clone──→ 源码文件 │
│ ↓ │
│ 文档解析 │
│ │
│ XHTML → BeautifulSoup → markdownify → Markdown │
│ Java → tree-sitter → Class/Method 切片 │
│ 其他 → 通用分割(按空行/换行) │
│ ↓ │
│ 结构化存储 │
│ │
│ knowledge_document (元数据 + content_hash) │
│ knowledge_chunk (切片文本 + FULLTEXT 索引) │
│ code_symbol (Java 符号关系) │
│ ↓ │
│ 向量化 │
│ │
│ bge-base-zh-v1.5 → 768 维向量 → ChromaDB (upsert) │
│ ↓ │
│ 检索 & 推理 │
│ │
│ 混合检索 (向量 + FULLTEXT + RRF + 语义重排) │
│ 代码图谱 (BFS 依赖追踪 + 跨模块源码聚合) │
│ LLM 生成 (Qwen3.5 RAG 回答) │
│ ↓ │
│ 对外服务 │
│ │
│ Chat API (OpenAI 兼容 /v1/chat/completions) ← NextChat / Chat UI │
│ MCP Server (/mcp/sse) ← Cursor / AI IDE │
│ REST API (/api/search, /api/admin) ← Console / 外部系统 │
└────────────────────────────────────────────────────────────────────────────┘
6.2 增量更新机制
Confluence:
- 比较 knowledge_document.version 与 API 返回的 version.number
- 版本不同 → 重新获取详情 → 重新切片 → 重新向量化
GitLab:
- git pull --ff-only(失败则 fetch + reset --hard)
- git diff old_head HEAD 获取变更文件列表
- 仅处理 added / modified 文件
7. 优雅停机与容错机制
7.1 停机触发方式
| 方式 | 说明 |
|---|---|
Ctrl+C / SIGINT |
终端中断 |
SIGTERM |
系统信号 |
scripts\stop.bat |
创建停止文件 |
data/.stop_worker |
手动创建停止文件 |
7.2 停机流程
停机信号
↓
_do_shutdown()
├── stop_sync_worker() → 设置标志位,循环自然退出
├── embed_worker.stop() → 设置标志位,等待当前 batch 完成
├── uvicorn.should_exit → API 停止接收新请求
└── _shutdown_event.set() → 通知主循环
↓
等待 Worker 完成当前任务(最多 30 秒)
↓
超时则强制取消
↓
清理资源
├── stop_scheduler() → 停止 APScheduler
├── chroma.close() → 持久化 ChromaDB 数据
├── redis_queue.close() → 关闭 Redis 连接
└── close_engine() → 关闭 MySQL 连接池
Windows 特殊处理:
- Windows 不支持 loop.add_signal_handler()
- 使用 signal.signal(SIGINT/SIGTERM, handler) + loop.call_soon_threadsafe() 替代
7.3 容错机制
| 场景 | 处理方式 |
|---|---|
| 单个文档处理失败 | 记录错误,继续处理下一个文档 |
| Embedding 编码失败 | 增加失败计数器,不中断整体流程 |
| Redis 连接断开 | 自动重连(5 秒重试) |
| Confluence Cookie 过期 | SSO 静默续签 |
| ChromaDB 索引损坏 | 删除 data/vectors,触发全量重建 |
| 端口被占用 | 捕获 SystemExit(code=3),日志提示并优雅退出 |
8. 运维工具与脚本
8.1 启动方式
| 入口文件 | 用途 | 说明 |
|---|---|---|
app/server.py |
统一启动(推荐) | API + Worker + 调度器一体化运行 |
app/run_workers.py |
独立 Worker | 仅启动 Sync Worker + Embed Worker + 调度器(不含 API) |
app/main.py |
纯 API | uvicorn app.main:create_app --factory,仅启动 FastAPI(含 MCP) |
8.2 运维脚本
scripts/ 目录下提供了多个运维和诊断工具:
| 脚本 | 用途 |
|---|---|
scripts/diagnose_search.py |
检索诊断工具:排查某篇文档为何未出现在检索结果中。检查文档 status、chunk 切片、vector_id、ChromaDB 向量、向量检索排名、FULLTEXT 检索命中等全链路环节 |
scripts/rebuild_symbols.py |
重建所有 Java 文件的代码符号索引(code_symbol 表) |
scripts/reset_all.py |
重置所有数据(清空 MySQL 表 + 删除 ChromaDB 向量 + 清空 Redis 队列) |
scripts/confluence_login.py |
手动触发 Confluence SSO 登录并获取 Cookie |
scripts/test_frontend.py |
前端功能测试 |
scripts/init_db.sql |
数据库初始化 SQL(建表) |
诊断脚本使用示例:
conda activate knowledge_hub
cd e:\workspace\python\knowledge_hub
python scripts/diagnose_search.py --doc-id 6941 --query "关于asetloan-push的工作流程"
9. 踩坑记录与解决方案
9.1 环境与部署
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 1 | PowerShell mysql < init_db.sql 报重定向错误 |
PowerShell 不支持 < 重定向 |
改用 pymysql 在 Python 中执行 SQL |
| 2 | pymysql 连接 SSL 报错 ASN1: NOT_ENOUGH_DATA |
MySQL 服务端 SSL 配置异常 | pymysql.connect(ssl_disabled=True) |
| 3 | pymysql 执行多语句 SQL 后报 Unknown database |
需要 CLIENT.MULTI_STATEMENTS 并消费所有 result set |
加 client_flag + while cursor.nextset(): pass |
| 4 | conda create 参数冲突 |
--prefix 和 -n 不能同时使用 |
去掉 --prefix |
| 5 | .env 中 PROXY_PORT=(空字符串)导致 ValidationError |
Pydantic 无法将空字符串转 int |
增加 model_validator 预处理空字符串 |
| 6 | 服务启动报 OSError: address already in use |
旧进程未退出 | start.bat 中加 taskkill 清理旧进程 |
9.2 Redis 连接
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 7 | unknown command HELLO |
老版本 Redis 不支持 RESP3 协议 | redis.asyncio.Redis(protocol=2) |
| 8 | Timeout reading 频繁出现 |
连接池默认参数不适合长任务 | 增加 socket_timeout、retry_on_timeout |
9.3 数据处理
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 9 | Data too long for column 'content' |
TEXT 列最大 64KB,大 SQL 文件超限 | 改为 MEDIUMTEXT |
| 10 | Embed Worker 报 文档无切片 |
Sync 和 Embed 竞态:chunk 未 commit 就 enqueue 了 | 每个文档 chunk 写入后立即 commit |
| 11 | 11 篇文档永远 pending | 空内容文件(如 .properties)chunk_count=0 |
chunk_count=0 直接标记 indexed |
| 12 | Session transaction rolled back |
异常后未 rollback,污染后续操作 | 加 await session.rollback() |
| 13 | Git 仓库拉取错误分支 | 数据库中分支名配置为 master | 统一改为 xxxxx 分支,重新 clone |
| 14 | 仓库目录不存在 |
相对路径在不同工作目录下解析不同 | Config 中 @property 确保绝对路径 |
9.4 LLM 集成
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 15 | LLM chat() 返回 NoneType has no attribute strip |
Qwen3.5 的 content 字段为 null |
优先 content,为空则回退 reasoning_content |
| 16 | 回答中包含 <think> 思考过程 |
Qwen3.5 特有的推理标签 | 正则过滤 <think>...</think> 等标签 |
| 17 | 回答被截断 | max_tokens=2048 不够,思考过程占用大量 token |
提升到 32192 + 超时 180s |
| 18 | 查询变体包含不相关内容 | LLM reasoning_content 被误当变体 | 严格解析 JSON 数组格式 |
| 18b | Qwen 模型在 content 中混入思考过程 |
Qwen 系列有时不分离推理过程 | _clean_thinking_content() 检测思考标记并提取最终答案 |
9.5 Confluence
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 19 | Playwright net::ERR_CONNECTION_TIMED_OUT |
Confluence 在内网,需要代理 | 配置 HTTP 代理 |
| 20 | SSO 脚本找不到 input[type="email"] |
Google 登录页 HTML 结构变化 | 使用多选择器回退 |
| 21 | test_connection 返回 Anonymous 但未触发续签 |
/rest/api/user/current 对匿名用户也返回 200 |
检测 displayName=="Anonymous" 后主动续签 |
| 22 | 受限 Space 请求返回 404 | 匿名用户看不到受限 Space | 对 content API 的 404 也视为认证失败 |
| 23 | SSO 脚本无输出 | Python stdout 缓冲 | functools.partial(print, flush=True) |
9.6 前端
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 24 | emptyState 元素消失 | innerHTML 更新销毁了 DOM 元素 |
更新前移出,更新后放回 |
| 25 | 流式传输中切换会话导致状态混乱 | 无并发保护 | 添加 isStreaming 检查 |
| 26 | 流式回答没有参考链接 | SSE 推送完成后未追加引用 | 在流式结束后追加参考 chunk |
9.7 向量存储
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 27 | ChromaDB 启动崩溃 (Access Violation) | HNSW 索引文件损坏(非正常退出) | 删除 data/vectors/ 重建;实现优雅停机 |
| 28 | HuggingFace 模型下载失败 | hf-mirror.com 不可达 | HF_ENDPOINT=https://huggingface.co |
9.8 代码分析
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 29 | Java 符号提取不完整 | 从 chunk 提取缺少 package/import 上下文 | 改从 git clone 目录读取完整原始文件 |
| 30 | LLM 意图识别选不出正确类 | 候选列表排序不佳,高相关类排名靠后 | 按关键词多维命中数排序候选 |
| 31 | LLM 返回的 JSON 无法解析 | Qwen3.5 将 JSON 嵌在 reasoning 文本中 | 搜索所有 [...] 结构,选最佳匹配 |
10. 性能与资源考量
10.1 内存占用估算
| 组件 | 占用 |
|---|---|
| Python 运行时 | ~100MB |
| bge-base-zh-v1.5 模型 | ~400MB |
| ChromaDB(13000 向量) | ~200MB |
| FastAPI + uvicorn | ~50MB |
| 合计 | ~750MB |
在 16GB 内存的机器上完全可以承受。
10.2 处理速度参考
| 操作 | 速度 |
|---|---|
| Java 文件 tree-sitter 解析 | ~2000 文件/分钟 |
| Embedding (batch=16, CPU) | ~200 chunks/分钟 |
| Confluence 页面同步 | ~10 页/秒(受限于 API 响应) |
| GitLab git pull | 秒级(增量) |
| 混合检索 | ~500ms/查询 |
10.3 数据规模
当前已索引数据:
| 指标 | 数量 |
|---|---|
| 数据源 | 11 个(10 GitLab + 1 Confluence) |
| 文档 | ~3000 篇 |
| 切片 | ~13000 个 |
| 向量 | ~13000 条 |
| 代码符号 | ~2000+ 个 |
11. 未来扩展方向
11.1 数据源扩展
- Jira / Linear(需求管理)
- Notion(文档协作)
- 钉钉/飞书文档
- 更多 Git 平台(GitHub、Gitee)
11.2 检索能力增强
- 引入 BGE-Reranker 做精排
- 支持多轮对话上下文理解
- 支持文档级别的权限控制
11.3 代码分析增强
- 支持更多语言(Python、Go、TypeScript)
- 方法级别的调用链追踪
- 代码变更影响分析
11.4 运维能力
- Docker 容器化部署
- 数据备份自动化
- 监控告警(如 Prometheus + Grafana)
- 多用户权限管理
11.5 模型升级
- 当 GPU 可用时,切换到更大的 Embedding 模型(BGE-M3)
- 接入更强的 LLM 获得更好的回答质量
暂无评论,快来抢沙发吧~