# weknora-java
**Repository Path**: ym0609/weknora-java
## Basic Information
- **Project Name**: weknora-java
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-10-26
- **Last Updated**: 2026-10-26
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# WeKnora-Java
以 [WeKnora](https://github.com/Tencent/WeKnora)(腾讯开源的 RAG 知识库问答服务)为参照的 Java 复刻实现,**用于学习目的**。
**技术目标是"完整"而非"精简":** 377 个 HTTP 端点全部实现,验证同一个前端能否不改一行代码就接上另一个后端实现——这本身是一次有难度的学习练习。
---
## ⚠️ 声明
**本项目仅供个人学习与技术研究使用。**
- **非官方项目**,与腾讯公司及 WeKnora 官方团队无任何关联,未获授权、未获背书,也不代表官方立场。
- 目的是学习一个成熟 RAG 系统的工程结构(检索链路、Agent 运行时、文档解析、多租户模型),并练习 Java / Spring Boot / DDD 的落地方式。
- **不允许用于任何商业用途**。如需商用,请使用[原版 WeKnora](https://github.com/Tencent/WeKnora),并遵守其许可证条款。
- 原版 WeKnora 的著作权、商标权及其它一切权利归腾讯公司及相关权利人所有。
- 本项目不含原版任何代码资产。接口契约对齐原版,仅为验证"同一个前端能否连上另一个后端"这一学习目标,不构成对原版的分发或替代。
- 使用本项目产生的任何后果由使用者自行承担。作者不对可用性、安全性、合规性作任何明示或默示担保。
> 如果你是权利人,认为本项目有任何不妥之处,请通过 Issue 联系,我会立即处理。
---
## 一分钟了解
| | Go 原版 | 本项目 |
|---|---|---|
| 语言 / 框架 | Go + Gin | Java 21 + Spring Boot 3.5 |
| HTTP 端点 | 377 | **377(100%)** |
| 代码量 | 29.7 万行 | 9.9 万行 |
| 架构 | 分层 + 大量 service | **DDD 四层**,依赖单向 |
| 数据库 | PostgreSQL + pgvector | 同(Flyway 管表结构,Hibernate 只校验) |
**为什么代码量少一半:** 不是在"简化功能",而是 Java 的表达力、Spring Data 的样板消除,以及一部分能力用成熟的库代理(PDFBox / POI / jsoup)替掉了手写解析。
---
## 它是什么
一个知识库问答系统。你把文档传进去,它把文档切片、向量化、建索引,之后用自然语言提问,它检索出相关片段、交给大模型回答,并标注引用来源。
```mermaid
flowchart LR
A[上传文档] --> B[解析 PDF/Word/Excel/PPT...]
B --> C[分块]
C --> D[向量化 + 关键词索引]
E[用户提问] --> F[混合检索
向量 + 关键词 + RRF 融合]
D --> F
F --> G[重排 + 去冗]
G --> H[组装提示词]
H --> I[大模型生成]
I --> J[流式返回 + 引用标注]
K[Agent 模式] -.-> F
K -.-> L[工具调用
检索/读文档/抓网页/执行命令]
L --> I
```
---
## 快速开始
### 前置条件
- JDK 21+
- PostgreSQL 14+(需要 `pgvector` 扩展)
- Maven 3.9+
### 1. 准备数据库
```sql
CREATE DATABASE weknora;
\c weknora
CREATE EXTENSION IF NOT EXISTS vector;
```
表结构由 Flyway 自动创建,不需要手工建表。
### 2. 配置
改 `weknora-boot/src/main/resources/application.yml`,或用环境变量覆盖:
```powershell
$env:JWT_SECRET="至少32字节的随机串"
```
数据库连接默认 `localhost:5433 / weknora / weknora / weknora`(端口刻意用 5433,避免和本机其它 PostgreSQL 的 5432 冲突)。
### 3. 启动
```powershell
# 第一次:把依赖模块装进本地仓库
mvn -o install -DskipTests -pl weknora-domain,weknora-application,weknora-infrastructure,weknora-interfaces,weknora-docreader
# 之后每次启动
mvn -o -pl weknora-boot spring-boot:run
```
> **不要加 `-am`**。`-am` 会对依赖模块也执行 `spring-boot:run`,而根聚合 pom 没有主类,
> 报错信息里的项目名是 `weknora-java`(根项目)不是 `weknora-boot`,很容易看错。
看到这行就算起来了:
```
Tomcat started on port 8080
Started WeKnoraApplication in ~21 seconds
```
访问 http://127.0.0.1:8080/actuator/health 应返回 `{"status":"UP"}`。
### 4. 配模型
系统的问答能力来自大模型(自身不带模型)。启动后在**界面上配置**,或直接调 API:
```json
// POST /api/v1/models —— 对话模型
{
"name": "deepseek-chat",
"type": "KnowledgeQA",
"source": "deepseek",
"is_default": true,
"parameters": {
"base_url": "https://api.deepseek.com",
"api_key": "sk-..."
}
}
// POST /api/v1/models —— 向量模型(必须另配,对话模型不提供 embedding)
{
"name": "text-embedding-v3",
"type": "Embedding",
"source": "aliyun",
"is_default": true,
"parameters": {
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"api_key": "sk-...",
"dimension": 1024
}
}
```
**任何 OpenAI 兼容的服务都能用**:DeepSeek、阿里云百炼、豆包、硅基流动、Moonshot、vLLM、Ollama(`/v1`)、OpenAI 官方。
### 5. 联调数据(可选)
```powershell
python scripts/seed_testdata.py # 建知识库、文档、标签、智能体、MCP 端点
```
会创建账号 `e2e-admin@example.com` / `WeKnora@2026`,含 4 个知识库、7 篇多格式文档(全部解析完成)、3 个标签、2 个智能体。
---
## 架构
```
weknora-domain 领域模型 + 端口 (零框架依赖:不碰 Spring / JPA / HTTP)
weknora-application 用例编排 (只有业务逻辑)
weknora-infrastructure 端口实现 (JPA、HTTP、文件、解析器)
weknora-interfaces REST 控制器 + DTO (HTTP 契约)
weknora-boot 组合根 (@Bean 装配、配置)
weknora-docreader 独立文档解析服务 (可单独部署)
```
依赖方向严格单向:
```
interfaces ──> application ──> domain
↑
infrastructure ─────┘(只依赖 domain)
```
**为什么 domain 一行 Spring 都不许有:** 领域逻辑是业务真相,不该被框架绑架。这条规则在开发中真的拦住过错误——我最初把 Agent 工具放在 infrastructure,它需要调 application 层的检索用例,编译器直接报错,逼着重构到正确的层。
**为什么用例不标 `@Component`:** 全部在 `weknora-boot` 的 `@Configuration` 里用 `@Bean` 显式装配。代价是漏注册会启动失败,好处是装配关系一眼可见。这个代价是值得的——漏注册的问题被"上下文门禁测试"拦住了(见下)。
---
## 关键设计
### 引用用 `S1` 句柄,不是 chunkId
检索结果在提示词里标成 `[S1] 《文档标题》`,模型回答时引用这些句柄,服务端再映射回真实的 chunk 位置。这样前端拿到的是稳定的引用编号,不依赖模型是否会背长 ID。
### 混合检索用 RRF 融合,不是加权求和
向量分(余弦相似度 0~1)和关键词分(ts_rank_cd,无界)**量纲不可比**。RRF 只用排名,天然同量纲:
```
score = Σ weight_i / (k + rank_i)
```
### Agent 的工具范围钉在工具实例里
`search_knowledge` 工具在创建时就绑定了"这次能查哪几个知识库"。如果做成全局单例、把范围当参数传,模型就能自由填参数——等于把"能查什么"的决定权交给了模型。
### 沙箱是进程级,不是容器级
`shell_exec` 在隔离的工作目录里跑,有超时、输出上限、环境变量白名单。**它挡得住误操作,挡不住恶意攻击**(命令里写绝对路径仍能读写宿主文件)。所以这个工具**必须人工审批**才能执行。要开放给不可信用户,必须先换容器级沙箱。
### 高危工具默认不下发
`ToolRegistry` 的规则是:**没有白名单时只放行"不需要审批"的工具**。需要审批的高危工具必须被显式点名才会交给模型。
这不是细节——如果把"没配白名单"当成"全部允许",一个新建的、还没配过工具的智能体就会直接拿到 shell 执行能力,而用户根本没意识到自己给了这个权限。
---
## 功能覆盖
### 已完整实现
| 域 | 说明 |
|---|---|
| 认证与租户 | 注册/登录/JWT/刷新、多租户切换、邀请、OIDC |
| 知识库 | 增删改查、共享、复制、活动日志、置顶、收藏 |
| 文档 | 上传、URL 抓取、手工录入、移动、复制、批量下载、清空 |
| 解析 | **PDF / Word / Excel / PPT / Markdown / HTML / CSV / JSON / TXT** |
| 检索 | 向量 + 关键词混合、RRF 融合、重排、MMR 去冗、上下文扩展 |
| 问答 | SSE 流式、引用标注、多轮对话、会话分叉、消息搜索、导出 |
| Agent | ReAct 循环、工具调用、审批门、function calling |
| 评测 | 检索指标(Recall/Precision/MRR/NDCG)、数据集、多参数对比 |
| MCP | 作为客户端连外部 MCP;**作为服务端对外提供**知识库检索 |
| 模型管理 | 多厂商 CRUD、连通性探测、按租户选默认模型 |
| 其他 | 标签、Wiki、记忆、数据源同步、系统配置、审计日志 |
| 可观测性 | Langfuse / LiteFuse 链路追踪(OTLP 直连,默认关闭) |
### 明确不做
| 能力 | 原因 |
|---|---|
| **IM 消息运行时**(飞书/钉钉/企微回调) | 本项目定位本地运行,用不上。回调端点保留但返回 501,不假装成功 |
| **容器级沙箱**(E2B / Cube / Docker) | 依赖外部容器编排服务,本地环境不可用。已提供进程级替代 |
### 已知限制
| 项 | 说明 |
|---|---|
| **Agent 的 function calling 未跑通真实模型** | 代码就绪,但只在架构层验证过。**这是下一轮第一件事** |
| LLM 客户端只支持 OpenAI 兼容协议 | 不支持 Anthropic / Gemini 原生协议 |
| 长对话没有压缩 | 超长对话会因上下文溢出失败 |
| `/evaluation` 的 `dataset_id` 只读 JSON | parquet 需要先用 pyarrow 转一次(原因见偏差登记) |
| 异步任务用进程内虚拟线程 | 进程重启会丢掉在途任务 |
| 中文检索是子串匹配 | 没有中文分词,长句可能搜不到 |
---
## 质量保障
### 三条验收标准(每次改动都要跑)
```powershell
mvn -o clean test # 全量构建 + 全部测试
python scripts/api_missing.py # 端点对账,必须「缺失 0 条」
python scripts/bean_wiring_check.py # Bean 装配自检
```
### 为什么单测全绿不算数
本项目吃过三次亏——**单测都是绿的,应用却起不来**:
1. Spring Data 派生查询属性名写错(实体字段 `boolean temporary`,查询要写 `TemporaryFalse` 不是 `IsTemporaryFalse`)
2. 用例漏注册 `@Bean`
3. 测试探针上的 `@Configuration` 被组件扫描捡走,覆盖了真实 bean
所以有一道**上下文门禁测试**(`ApplicationContextGateTest`):在临时数据库上把**整个 Spring 上下文**起一遍,断言 46 个控制器都注册成功。这才是真正拦住「起不来」的那道门。
### 三个自动化检查脚本
| 脚本 | 查什么 |
|---|---|
| `check_transactions.py` | `@Modifying` 查询是否都在事务里(漏标会在运行时抛 `TransactionRequiredException`,编译期看不出来) |
| `check_route_coverage.py` | Go router 里有、但 swagger 清单里没有的路由(对账盲区) |
| `strip_bom.py` | 源码文件是否被写入了 UTF-8 BOM |
第二条的价值:`api_missing.py` 只对 swagger 清单负责,**没有 swagger 注解的路由永远不在清单里**,于是对账永远说"缺失 0 条",而实际整块功能没做。这个脚本发现了 8 条这样的路由。
---
## 文档索引
| 文档 | 内容 |
|---|---|
| [testdata/联调环境说明.md](testdata/联调环境说明.md) | **联调必读**:地址、账号、预置数据、注意事项 |
| [docs/当前状态.md](docs/当前状态.md) | 现在做到哪、还剩什么、怎么验证 |
| [docs/API等价清单.md](docs/API等价清单.md) | 377 条端点逐域状态(脚本生成) |
| [docs/移植踩坑清单.md](docs/移植踩坑清单.md) | **19 条实测踩坑**,改代码前值得扫一眼 |
| [docs/评测报告.md](docs/评测报告.md) | 检索指标与 7 组参数对比 |
| `docs/*-移植偏差登记.md` | 各域与原版的行为差异(不是缺陷,是已知取舍) |
| [tests/e2e-browser/README.md](tests/e2e-browser/README.md) | 浏览器 E2E 测试用法与 9 个踩坑记录 |
| [docs/archive/](docs/archive/README.md) | **已归档**:开工阶段的计划类文档(内容过时,仅作历史留存) |
---
## 常用命令
```powershell
# 全量验证(改动后必跑)
mvn -o clean test
# 启动
mvn -o -pl weknora-boot spring-boot:run
# 单独启动文档解析服务(remote 模式时)
mvn -o -pl weknora-docreader spring-boot:run # 端口 8081
# 灌联调数据
python scripts/seed_testdata.py
# 导出账号清单(Excel)
python scripts/export_users.py
# 端到端联调(真实模型)
python scripts/e2e_smoke.py
```
---
## 常见问题
### 启动报 `Unable to find a suitable main class, ... on project weknora-java`
用了 `-am`。去掉它:
```powershell
mvn -o -pl weknora-boot spring-boot:run # ✅
mvn -o -pl weknora-boot -am spring-boot:run # ❌
```
### 启动报 `Schema-validation: missing table`
Flyway 没跑成功,或数据库是旧的。检查 `flyway_schema_history` 表;开发环境可以直接删库重建。
### 改了代码但行为没变
Maven 增量编译可能跳过了整个模块。用 `mvn -o clean test` 而不是 `mvn -o test`。
### `mvn install` 报权限错误
本地仓库路径在工作空间外。`~/.m2/settings.xml` 里的 `` 指向的就是它。
### 问答说「知识库中没有找到相关内容」
按顺序排查:
1. 文档 `parse_status` 是 `completed` 吗(不是 `pending`/`failed`)
2. 配了 **Embedding 模型**吗(只有对话模型不够)
3. 检索阈值是不是太高(mock 嵌入模型下要调低到 0.01)
### 配了模型但回答还是模板文本
用的还是 `MockChatModel`(一个把检索结果照抄成回答的复读机)。它默认生效,用于验证链路。检查:
- 模型列表里 `KnowledgeQA` 类型有模型吗
- 该模型 `is_default` 是 `true` 吗(或者它是第一个 active 的)
---
## 致谢与许可
原版 [WeKnora](https://github.com/Tencent/WeKnora) 由腾讯开源,本项目在学习其公开接口契约的过程中受益良多。
**再次说明:本项目仅供学习研究,禁止商业使用。** 原版的一切权利归腾讯公司及相关权利人所有;如需在生产环境使用,请采用原版并遵守其许可证。
本项目自身代码同样以学习共享为目的,不提供任何担保。