docs: 重构仓库文档目录并迁移训练素材
按当前架构重组 docs 目录,统一中文命名与目录分层,并将训练原材料迁移到独立目录以保持架构文档边界清晰。
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
# pj0231-eai_agentplatforming 完整分析报告(对比 + 差距 + 路线图 + 设计稿)
|
||||
|
||||
> 版本:V1.0 | 日期:2026-08-16
|
||||
> 对比对象:pj006-zhilianyuan2(智练猿)
|
||||
> 范围:知识库能力、考试能力、测试/自测能力、岗位与知识对应能力
|
||||
> 原则:以**实际代码**为准(本项目 PRD 文档存在过时,见 §1.3)
|
||||
>
|
||||
> **注:** 本文基于 2026-08-16 代码快照(后端 Go + Gin + GORM,检索为 brute-force 余弦 + bge-m3);相关数据库口径可参见 `docs/04_Backend/01_平台治理底座/BG04_数据库总览.md` / `docs/02_Architecture/部署文档.md`。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- Part A —— 现状对比
|
||||
- Part B —— 差距汇总与反向差异
|
||||
- Part C —— 改进路线图
|
||||
- Part D —— 设计稿:岗位与知识对应能力(核心)
|
||||
- Part E —— 设计要点:其余改进项
|
||||
- Part F —— 待拍板决策点与下一步
|
||||
|
||||
---
|
||||
|
||||
# Part A —— 现状对比
|
||||
|
||||
## A1. 一句话结论
|
||||
|
||||
两个项目**不是同一量级**:
|
||||
|
||||
- **pj0231** 是一个**极简单租户、内部专属培训平台**——Go 后端约 4,900 行,11 张表,2 个角色,聚焦「看资料 → AI 答疑 → 考试验收」的窄闭环。
|
||||
- **pj006** 是一个**多租户、多角色、带完整 AI 对练 / 岗位能力模型的通用销售培训平台**——Python 后端约 1.7 万行 + 独立 engine 库,23 版 schema、40+ 张表,4 个前端。
|
||||
|
||||
在四个对比维度上,pj006 全部**能力更全、更深**;但 pj0231 在个别点上有「更实」的实现(见 §B3)。
|
||||
|
||||
## A2. 系统定位与技术架构
|
||||
|
||||
| 维度 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 定位 | 博昇内部培训(单企业) | 通用 AI 销售培训平台(多企业 SaaS) |
|
||||
| 后端 | Go + Gin + GORM + SQLite | Python 3.10 + FastAPI + SQLite(per-org 分库) |
|
||||
| 前端 | 1 个:Vue3 + Vite + Element Plus | 4 个:app / orgadmin / admin / platformadmin |
|
||||
| 角色 | 2(employee / admin) | 多级(学员 / 组织管理员 oadmin / 平台管理员 padmin / 教练 trainer) |
|
||||
| 租户 | 单租户 | 多租户(`orgs` 注册表 + 每 org 一个 `<org>.db`) |
|
||||
| 代码量 | ~4,900 行 Go | ~17,200 行 Python + `zlylibs/engine` |
|
||||
| 数据表 | 11 张 | 40+ 张(schema v23) |
|
||||
| AI 底座 | 内网 OpenAI 兼容接口(Ollama LLM + bge-m3) | OpenAI 兼容路由(trainee/coach/eval 三 slot) |
|
||||
|
||||
## A3. 重要前提:pj0231 文档已过时
|
||||
|
||||
- `docs/06_Product_Lines/Historical_PRD_And_Designs/产品需求文档.md` 写「FastAPI + MySQL + 不做向量检索」;实际代码是 **Go + Gin + GORM + SQLite**,检索已实现**向量(brute-force 余弦 + bge-m3)**。
|
||||
- 本项目权威信息以 `eai_agentplatform/CLAUDE.md` 与**实际代码**为准。
|
||||
|
||||
---
|
||||
|
||||
## A4. 知识库能力对比
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 素材摄入 | ✅ 上传→审批→转换→切片(真实跑通) | ⚠️ 知识工厂骨架完备,Phase 1 解析抽取「不真跑、停 pending」 |
|
||||
| 文档转换 | LibreOffice + pdftotext,切片入 `knowledge_chunk` | 设计 parse/structured/extraction_blocks 多阶段管线,未实跑 |
|
||||
| 审批流 | ✅ `pending/approved/rejected`,审批前置 | ✅ 更细:`working_knowledge_points` 待复核 + `kb_publish_records` 发布链 |
|
||||
| 知识组织 | 扁平 `knowledge_chunk` + `knowledge_source` | 多层:`knowledge_points` → `knowledge_meta` → `knowledge_units`(统一主干)→ `knowledge_chunks`(带 `embedding_status`) |
|
||||
| 术语/规则 | ❌ 无 | ✅ `knowledge_terms`、`industry_rules` |
|
||||
| 检索 | ✅ **混合检索**(向量余弦 + 关键词兜底) | ⚠️ 实跑 jieba 分词 + LIKE;向量仅文档宣称 |
|
||||
| 自动出题 | ❌ 无 | ✅ 设计:`working_questions` 自动生成(待复核) |
|
||||
| 知识-题目/场景关联 | 仅题目挂 `course_id` | ✅ `question_knowledge_units`、`scene_knowledge_units`、`knowledge_unit_links` |
|
||||
|
||||
## A5. 考试能力对比
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 题型 | 单选 / 多选 / 判断 | 单选 / 多选 / 判断 / **简答 essay** |
|
||||
| 简答评分 | ❌ 无 | ✅ LLM 按 rubric 评分,得分率持久化 `exam_session_grades` |
|
||||
| 组卷 | ✅ 按知识域 + 随机抽题 | ⚠️ 固定 `question_ids` 列表;蓝图组卷未接考试服务 |
|
||||
| 考试次数 | 正式考只允许一次 | ✅ `max_attempts` + `deadline` |
|
||||
| 自测 | ✅ 自测(即时对错、不存记录) | ✅ 练习走独立 `practice` 模块 |
|
||||
| 判分 | 确定性比对 | 选择题确定性 + 简答 LLM |
|
||||
| 成绩分析 | 仅存得分/正确率/明细 | ✅ 维度能力分析(weak/ok/strong)+ 薄弱题 + 改进建议 |
|
||||
| 错题联动 | ❌ 无 | ✅ 交卷错题自动入 `mistake_records` |
|
||||
|
||||
## A6. 测试 / 自测 / 评估能力对比
|
||||
|
||||
### A6.1 面向学员(产品能力)
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 自测练习 | ✅ 自测(不存记录) | ✅ 每日一练 `practice`(游戏化:段位/星/连击/经验/积分) |
|
||||
| 错题本 | ❌ 无 | ✅ `mistake_records`(练习+考试聚合) |
|
||||
| 笔记/收藏 | ❌ 无 | ✅ `study_notes` + 收藏 |
|
||||
| 学习档案/学情 | ❌ **PRD 明确禁止** | ✅ 学习档案 dashboard + 能力对标 |
|
||||
| AI 对练复盘评分 | ❌ 无 | ✅ `dialogue_review_scores` + `ReviewScoreService` |
|
||||
|
||||
### A6.2 面向软件工程(自动化测试)
|
||||
|
||||
| 维度 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 后端测试 | ❌ **0 个 `_test.go`** | ✅ 31 个 pytest 文件 |
|
||||
| 引擎测试 | 无 engine 层 | ✅ `zlylibs/engine/tests` 约 20 个 |
|
||||
| 测试策略文档 | ❌ 无 | ✅ `docs/12_Test_Strategy` |
|
||||
| CI/依赖管理 | 无 | ✅ pyproject dev deps |
|
||||
|
||||
## A7. 岗位与知识对应能力(差距最大维度)
|
||||
|
||||
pj0231 是**结构性缺失**:用户只有 `role`,完全没有岗位概念,也没有岗位→知识→考试的能力链。
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 岗位表 | ❌ 无 | ✅ `positions` |
|
||||
| 岗位↔用户绑定 | ❌ 无 | ✅ `user_positions` |
|
||||
| 岗位能力模型(洋葱) | ❌ 无 | ✅ `competency_layers` + `competency_levels`(L1-L4)+ `competency_details` |
|
||||
| 能力模型自动生成 | ❌ 无 | ✅ `generate_competency_model` |
|
||||
| 岗位↔知识映射 | ❌ 无 | ✅ `position_knowledge_units` + `auto_match` |
|
||||
| 岗位考试蓝图 | ❌ 无 | ✅ `position_exam_blueprints` |
|
||||
| 标准岗位/职级代码 | ❌ 无 | ✅ `SD01_Standard_Job_Codes` |
|
||||
| 岗位驱动学练考 | ❌ 无 | ✅ A11 学习 / A12 练习 / A13 蓝图组卷 |
|
||||
|
||||
---
|
||||
|
||||
# Part B —— 差距汇总与反向差异
|
||||
|
||||
## B1. 差距严重度总表
|
||||
|
||||
| 维度 | pj0231 现状 | 差距严重度 | 是否 pj0231 定位内该补 |
|
||||
|---|---|---|---|
|
||||
| 岗位与知识对应 | 完全缺失 | 🔴 高 | ✅ 是(本次核心诉求) |
|
||||
| 错题本 / 学习闭环 | 完全缺失 | 🟠 中高 | ✅ 是(考试「考完即止」无闭环) |
|
||||
| 简答题 + LLM 评分 | 缺失 | 🟠 中 | ✅ 是(LLM 底座已有,成本低) |
|
||||
| 自动化测试 | 0 测试 | 🟠 中高(工程风险) | ✅ 是 |
|
||||
| 多租户 / NPC 人格 / KPI | 缺失 | 🔵 低 | ❌ 否(与「极简内网单企业」冲突) |
|
||||
| 向量检索 | ✅ 已实现 | — | —(反而领先 pj006) |
|
||||
| 随机 + 按域组卷 | ✅ 已实现 | — | —(反而领先 pj006) |
|
||||
|
||||
## B2. 反向差异:pj0231 反而更「实」的点
|
||||
|
||||
1. **向量检索是真实现**:pj0231 混合检索端到端可用;pj006 实跑 jieba 关键词,向量仅字段预留。
|
||||
2. **随机 + 按知识域自动抽题组卷**:pj0231 支持;pj006 考试为固定题目列表。
|
||||
3. **文档→文本→检索链路真实跑通**:pj0231 LibreOffice/pdftotext 切片入库完整;pj006 知识工厂 Phase 1 未实跑。
|
||||
4. **AI 算力点计费 + 审计日志**:pj0231 有 `ai_call_log`;pj006 未见同等计费。
|
||||
|
||||
---
|
||||
|
||||
# Part C —— 改进路线图
|
||||
|
||||
## C1. 优先级(按 ROI)
|
||||
|
||||
| 优先级 | 项 | 理由 |
|
||||
|---|---|---|
|
||||
| P0 | 岗位模型 + 岗位-知识映射 + 岗位驱动考试 | 最本质差距,本次核心诉求 |
|
||||
| P1 | 学员「我的岗位应学清单」+ 成绩按岗位聚合 | P0 的自然延伸 |
|
||||
| P2 | 错题本 + 轻量成绩档案 | 补考试闭环 |
|
||||
| P2 | 简答题 + LLM 评分 | 复用 LLM 底座,成本低 |
|
||||
| P2 | 后端自动化测试 | 消除 0 测试隐患 |
|
||||
| 不做 | 多租户 / NPC 人格 / HEP 引擎 / KPI | 与极简定位冲突 |
|
||||
|
||||
## C2. 分阶段总览
|
||||
|
||||
```
|
||||
P0(本次落地):岗位 + 映射 + 岗位驱动考试组卷
|
||||
↓
|
||||
P1:我的岗位应学清单 · 成绩按岗位聚合 · 岗位考试蓝图
|
||||
↓
|
||||
P2:错题本 · 简答题 LLM 评分 · 自动化测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Part D —— 设计稿:岗位与知识对应能力(核心)
|
||||
|
||||
> 完整设计见《设计稿_岗位与知识对应能力.md》,此处为完整版正文。
|
||||
|
||||
## D1. 目标与原则
|
||||
|
||||
**目标**:落地最小岗位驱动闭环 —— `岗位 → 用户绑定 → 岗位知识映射 → 岗位驱动考试组卷`。
|
||||
|
||||
**原则**:保持极简;复用现有 `AutoMigrate` / `RequireAdmin` / `pickQuestions`;级别词汇对齐 pj006 L1-L4 但**不建独立级别表**;单岗位优先。
|
||||
|
||||
## D2. 改动点清单
|
||||
|
||||
| 现有资产 | 位置 | 改动 |
|
||||
|---|---|---|
|
||||
| `User` | `internal/model/user.go` | 加列 `PositionID *uint`(可空) |
|
||||
| `ExamPaper` | `internal/model/exam_paper.go` | 加列 `PositionID *uint`(可空) |
|
||||
| 迁移 | `internal/store/db.go` | AutoMigrate 追加 2 张新表 |
|
||||
| 路由 | `internal/api/router.go` | 新增岗位端点(admin 组) |
|
||||
| 抽题 | `internal/api/exam.go` `pickQuestions` | 增加岗位知识映射过滤分支 |
|
||||
| 种子 | `internal/store/seed.go` | 可选预置默认岗位 |
|
||||
|
||||
## D3. 数据模型
|
||||
|
||||
### 3.1 `position`(岗位表)
|
||||
|
||||
```go
|
||||
type Position struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Code string `gorm:"size:32;uniqueIndex;not null" json:"code"`
|
||||
Name string `gorm:"size:64;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Status string `gorm:"size:16;not null;default:active;index" json:"status"` // active / inactive
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
func (Position) TableName() string { return "position" }
|
||||
```
|
||||
|
||||
### 3.2 `position_knowledge`(岗位知识映射,核心)
|
||||
|
||||
```go
|
||||
type PositionKnowledge struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
PositionID uint `gorm:"not null;index" json:"position_id"`
|
||||
Domain string `gorm:"size:16;not null;index" json:"domain"` // company / product / sales
|
||||
CourseID *uint `gorm:"index" json:"course_id"` // 可空
|
||||
ProductID *uint `gorm:"index" json:"product_id"` // 可空
|
||||
RequiredLevel string `gorm:"size:16;not null;default:L1" json:"required_level"` // L1/L2/L3/L4
|
||||
Weight float64 `gorm:"not null;default:1" json:"weight"`
|
||||
IsMandatory bool `gorm:"not null;default:true" json:"is_mandatory"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
func (PositionKnowledge) TableName() string { return "position_knowledge" }
|
||||
```
|
||||
|
||||
### 3.3 级别常量
|
||||
|
||||
```go
|
||||
const (
|
||||
LevelL1 = "L1" // 入门级
|
||||
LevelL2 = "L2" // 稳定执行级
|
||||
LevelL3 = "L3" // 复杂处理级
|
||||
LevelL4 = "L4" // 体系策略级
|
||||
)
|
||||
var validLevels = map[string]bool{LevelL1: true, LevelL2: true, LevelL3: true, LevelL4: true}
|
||||
```
|
||||
|
||||
## D4. API 设计
|
||||
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/positions` | admin | 岗位列表 |
|
||||
| POST | `/api/positions` | admin | 新建岗位 |
|
||||
| PUT | `/api/positions/:id` | admin | 编辑岗位 |
|
||||
| DELETE | `/api/positions/:id` | admin | 停用(软删) |
|
||||
| GET | `/api/positions/:id/knowledge` | admin | 岗位知识映射列表 |
|
||||
| PUT | `/api/positions/:id/knowledge` | admin | 保存映射(整表覆盖) |
|
||||
| PUT | `/api/users/:id/position` | admin | 设置/清除用户岗位 |
|
||||
| GET | `/api/my/position` | 员工 | 我的岗位 + 应学范围(P1) |
|
||||
|
||||
## D5. 岗位驱动考试组卷(核心算法改动)
|
||||
|
||||
修改 `pickQuestions`,在 `PositionID != nil` 时按岗位知识映射圈定题池,否则回退原 `domain` 抽题:
|
||||
|
||||
```go
|
||||
func pickQuestions(p model.ExamPaper) ([]model.Question, error) {
|
||||
pool := store.DB.Where("status = ?", "active")
|
||||
|
||||
if p.PositionID != nil {
|
||||
var pks []model.PositionKnowledge
|
||||
if err := store.DB.Where("position_id = ?", *p.PositionID).Find(&pks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domains := make([]string, 0, len(pks))
|
||||
courseIDs := make([]uint, 0, len(pks))
|
||||
for _, pk := range pks {
|
||||
domains = append(domains, pk.Domain)
|
||||
if pk.CourseID != nil {
|
||||
courseIDs = append(courseIDs, *pk.CourseID)
|
||||
}
|
||||
}
|
||||
domains = dedupe(domains)
|
||||
if len(domains) > 0 {
|
||||
pool = pool.Where("domain IN ?", domains)
|
||||
}
|
||||
if len(courseIDs) > 0 {
|
||||
pool = pool.Where("course_id IN ? OR course_id IS NULL", courseIDs)
|
||||
}
|
||||
} else {
|
||||
domains := splitDomains(p.Domain)
|
||||
if len(domains) > 0 {
|
||||
pool = pool.Where("domain IN ?", domains)
|
||||
}
|
||||
}
|
||||
|
||||
var qs []model.Question
|
||||
if err := pool.Order("id ASC").Find(&qs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(qs) < p.QuestionCount {
|
||||
return nil, fmt.Errorf("题库题目不足:需要 %d,可用 %d", p.QuestionCount, len(qs))
|
||||
}
|
||||
if p.Randomize {
|
||||
rand.Shuffle(len(qs), func(i, j int) { qs[i], qs[j] = qs[j], qs[i] })
|
||||
}
|
||||
return qs[:p.QuestionCount], nil
|
||||
}
|
||||
```
|
||||
|
||||
配套:`CreatePaper` / `UpdatePaper` 追加校验——岗位考试必须已配置至少一条知识映射,否则拒绝。
|
||||
|
||||
## D6. 前端设计
|
||||
|
||||
- 导航:管理员「系统管理」下拉新增 **「岗位管理」**。
|
||||
- 页面:岗位列表(CRUD)+ 岗位知识映射编辑面板(domain 下拉 + 课程/产品下拉 + L1-L4 + 权重 + 必学开关)。
|
||||
- 改造:用户管理页加「岗位」下拉;考试配置表单加「关联岗位」下拉。
|
||||
|
||||
## D7. 分阶段
|
||||
|
||||
- **P0**:表 + API + 岗位 CRUD + 用户设岗 + 岗位驱动组卷 + 前端岗位管理页。
|
||||
- **P1**:学员「我的岗位应学清单」+ 成绩按岗位聚合 + 岗位考试蓝图。
|
||||
- **P2**:见 Part E。
|
||||
|
||||
## D8. 明确不做
|
||||
|
||||
❌ 不建洋葱三层模型;❌ 不引入多租户;❌ 不做 LLM 自动生成能力模型;❌ 不做多岗位绑定;❌ 不做学情分析。
|
||||
|
||||
---
|
||||
|
||||
# Part E —— 设计要点:其余改进项
|
||||
|
||||
## E1. 错题本 + 轻量成绩档案(P2)
|
||||
|
||||
> ⚠️ 与 PRD 冲突需先拍板:PRD 第 7 节「无学情分析」。**错题本**是学员自助工具(不做管理层分析),可不冲突;**学习档案/能力雷达**属「学情分析」范畴,需产品重新确认是否放开。
|
||||
|
||||
### 数据模型
|
||||
|
||||
```go
|
||||
// MistakeRecord 错题本
|
||||
type MistakeRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
QuestionID uint `gorm:"not null;index" json:"question_id"`
|
||||
Source string `gorm:"size:16;not null" json:"source"` // self_test / formal
|
||||
UserAnswer string `gorm:"type:text" json:"user_answer"`
|
||||
CorrectAnswer string `gorm:"type:text" json:"correct_answer"`
|
||||
Resolved bool `gorm:"not null;default:false;index" json:"resolved"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
func (MistakeRecord) TableName() string { return "mistake_record" }
|
||||
```
|
||||
|
||||
### 联动点
|
||||
|
||||
- 正式考 `ExamSubmit` 判分循环里,错题 `INSERT` 进 `mistake_record`(`source=formal`);自测可选不写。
|
||||
- 学员端「我的错题本」页:列表 + 「已掌握」标记(`resolved=true`)。
|
||||
|
||||
## E2. 简答题 + LLM 评分(P2)
|
||||
|
||||
### 数据模型改动
|
||||
|
||||
- `question.type` 增加 `essay`;`answer` 存 rubric 评分要点文本;`options` 空。
|
||||
- `exam_record.detail_json` 已存答题明细,可直接承载简答文本。
|
||||
|
||||
### 评分流程
|
||||
|
||||
- 复用 `internal/ai/llm.go`,新增「评分」能力路由(或在 `ai_call_log.capability` 加 `essay_grade`)。
|
||||
- 交卷时对 `essay` 题调 LLM 按 rubric 打分(0~1 得分率),持久化到 `detail_json`。
|
||||
- 选择题仍确定性判分;总分 = Σ得分率/题数×100(对齐 pj006 算法)。
|
||||
|
||||
## E3. 自动化测试(P2)
|
||||
|
||||
- **后端**:新增 `_test.go`,优先覆盖纯逻辑函数:`isCorrect`(判分)、`splitDomains`、`pickQuestions`(组卷)、`ai.Retrieve` 的 `splitTerms`/`cosine`。
|
||||
- **命令**:`go test ./...`;接入 `start_dev` 或 CI 脚本。
|
||||
- **前端**:可选,暂缓。
|
||||
|
||||
---
|
||||
|
||||
# Part F —— 待拍板决策点与下一步
|
||||
|
||||
## F1. 需产品/技术拍板
|
||||
|
||||
1. **岗位考试课程过滤语义**(D5):岗位绑定具体课程时,抽题用 `course_id IN ?`(严格)还是 `course_id IN ? OR course_id IS NULL`(宽松含域匹配)?
|
||||
2. **错题本 vs 学情分析边界**(E1):PRD 禁止「学情分析」,错题本是否属于允许范围?
|
||||
3. **岗位知识映射去重**(D3.2):是否加唯一约束防重复?
|
||||
4. **简答题是否本期做**:P0 还是 P2?
|
||||
|
||||
## F2. 建议的下一步
|
||||
|
||||
1. 确认 F1 四个决策点;
|
||||
2. 进入 P0 编码(改动文件已列于 D2,共 6 处);
|
||||
3. P0 完成后补 `isCorrect`/`pickQuestions` 单测(借 P0 一并补测试债)。
|
||||
|
||||
---
|
||||
|
||||
> 本文件为整合版:Part A/B/C 来自《对比分析_项目0231与项目006智链源二.md》,Part D 整合自《设计稿_岗位与知识对应能力.md》,Part E 为新增其余改进项设计要点。
|
||||
@@ -0,0 +1,161 @@
|
||||
# pj0231-eai_agentplatforming vs pj006-zhilianyuan2 能力对比分析
|
||||
|
||||
> 生成日期:2026-08-16
|
||||
> 范围:知识库能力、考试能力、测试/自测能力、岗位与知识对应能力
|
||||
> 原则:以**实际代码**为准(本项目 PRD 文档存在过时,见 §0)
|
||||
>
|
||||
> **注:** 本文基于 2026-08-16 代码快照(后端 Go + Gin + GORM,检索为 brute-force 余弦 + bge-m3);相关数据库口径可参见 `docs/04_Backend/01_平台治理底座/BG04_数据库总览.md` / `docs/02_Architecture/部署文档.md`。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话结论
|
||||
|
||||
两个项目**不是同一量级**:
|
||||
|
||||
- **pj0231** 是一个**极简单租户、内部专属的培训平台**——Go 后端约 4,900 行,11 张表,2 个角色,聚焦「看资料 → AI 答疑 → 考试验收」的窄闭环。
|
||||
- **pj006** 是一个**多租户、多角色、带完整 AI 对练 / 岗位能力模型的通用销售培训平台(智练猿)**——Python 后端约 1.7 万行 + 独立 engine 库,23 版 schema、40+ 张表,4 个前端,具备岗位洋葱模型、知识工厂、AI 角色扮演道场、能力对标等。
|
||||
|
||||
在本文对比的四个维度上,pj006 全部**能力更全、更深**;但 pj0231 在个别点上有「更实」的实现(见 §6)。
|
||||
|
||||
> ⚠️ **重要前提:pj0231 的 PRD 文档已过时。**
|
||||
> `docs/06_Product_Lines/Historical_PRD_And_Designs/产品需求文档.md` 写「FastAPI + MySQL + 不做向量检索」,但实际代码是 **Go + Gin + GORM + SQLite**,且检索已实现**向量(brute-force 余弦 + bge-m3 embedding)**。本报告以实际代码为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统定位与技术架构对照
|
||||
|
||||
| 维度 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 定位 | 博昇内部培训(单企业) | 通用 AI 销售培训平台(多企业 SaaS) |
|
||||
| 后端 | Go + Gin + GORM + SQLite | Python 3.10 + FastAPI + SQLite(per-org 分库) |
|
||||
| 前端 | 1 个:Vue3 + Vite + Element Plus | 4 个:app / orgadmin / admin / platformadmin |
|
||||
| 角色 | 2(employee / admin) | 多级(学员 / 组织管理员 oadmin / 平台管理员 padmin / 教练 trainer) |
|
||||
| 租户 | 单租户 | 多租户(`orgs` 注册表 + 每 org 一个 `<org>.db`,路由键 `org_code_id`) |
|
||||
| 代码量 | ~4,900 行 Go | ~17,200 行 Python + `zlylibs/engine` 引擎库 |
|
||||
| 数据表 | 11 张 | 40+ 张(schema 版本 v23) |
|
||||
| AI 底座 | 内网 OpenAI 兼容接口(Ollama LLM + bge-m3 embedding) | OpenAI 兼容路由(trainee/coach/eval 三 slot,可配多模型) |
|
||||
| 交付 | 单二进制 + systemd + Clonezilla | 多服务 + 多端 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 知识库能力对比
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| **素材摄入** | ✅ 上传→审批→转换→切片(真实跑通) | ⚠️ 知识工厂骨架完备,但 Phase 1 解析抽取**「不真跑、停 pending」**(见 `schema.py` 中 `kb_asset_tasks` 注释) |
|
||||
| 文档转换 | LibreOffice + pdftotext,切片入 `knowledge_chunk` | 设计了 parse / structured / extraction_blocks 多阶段管线,但未实跑 |
|
||||
| 审批流 | ✅ 三层 `pending / approved / rejected`,审批前置 | ✅ 更细:`working_knowledge_points` 待复核 + `kb_publish_records` 发布链 |
|
||||
| **知识组织** | 扁平 `knowledge_chunk`(段落切片)+ `knowledge_source`(markdown 知识源) | 多层:`knowledge_points` → `knowledge_meta`(语料)→ `knowledge_units`(统一主干,含 domain/section/dimension/sub_dimension/role/position/level/kind)→ `knowledge_chunks`(带 `embedding_status`) |
|
||||
| 术语 / 规则 | ❌ 无 | ✅ `knowledge_terms`(别名/禁用词)、`industry_rules` |
|
||||
| **检索** | ✅ **混合检索**:向量(brute-force 余弦,bge-m3)+ 关键词兜底,去重合并 topK | ⚠️ **实跑的是 jieba 分词 + SQLite LIKE 关键词检索**;向量(ChromaDB/Neo4j)仅文档宣称,`embedding_status` 字段预留 |
|
||||
| **自动出题** | ❌ 无 | ✅ 设计:`working_questions` 从知识块自动生成题目(待复核) |
|
||||
| 知识-题目/场景关联 | 仅题目挂 `course_id` | ✅ `question_knowledge_units`、`scene_knowledge_units`、`knowledge_unit_links` 三张桥接表 |
|
||||
|
||||
**小结**:pj0231 的「文档→文本→检索」链路**真实端到端跑通**(且检索更强,含向量);pj006 的知识模型**层级深、可扩展强**(知识主干 + 术语 + 规则 + 自动出题 + 发布链),但知识工厂的自动解析在 Phase 1 尚未实跑。
|
||||
|
||||
---
|
||||
|
||||
## 3. 考试能力对比
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 题型 | 单选 / 多选 / 判断 | 单选 / 多选 / 判断 / **简答 essay** |
|
||||
| 简答题评分 | ❌ 无 | ✅ LLM 按 rubric 评分(`engine.eval.grade_short_answer`),得分率持久化 `exam_session_grades` |
|
||||
| **组卷** | ✅ 按知识域 + 随机抽题(`randomize`、`domain` 逗号分隔) | ⚠️ 实际是**固定 `question_ids` 列表**(手动组卷);蓝图组卷(`position_exam_blueprints`)已落表+CRUD,但考试服务未接蓝图抽题 |
|
||||
| 考试次数 | 正式考**只允许一次**(重考即 409) | ✅ `max_attempts`(最多考 N 次)、`deadline` 截止 |
|
||||
| 自测模式 | ✅ 自测(不限次、即时对错、**不存记录**) | ✅ 练习走独立 `practice` 模块(见 §4) |
|
||||
| 判分 | 确定性比对(`isCorrect`) | 选择题确定性 + 简答 LLM;总分 = Σ得分率/题数×100 |
|
||||
| **成绩分析** | 仅存记录:得分 / 正确率 / 答题明细 | ✅ **维度能力分析**(`competency_breakdown`:weak<70% / ok / strong≥90%)、薄弱题、改进建议 |
|
||||
| 错题联动 | ❌ 无 | ✅ 交卷错题自动写入 `mistake_records`(错题本) |
|
||||
| 成绩档案 | `exam_record`(永久存档) | `exam_sessions` + 报告回溯(`review_exam`) |
|
||||
|
||||
**小结**:pj006 考试在**题型(简答)、评分(LLM)、维度分析、错题联动、考试次数控制**上全面领先;pj0231 仅在**自动随机抽题组卷**这一点上更「自动化」(pj006 实际为手动固定组卷)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试 / 自测 / 评估能力对比
|
||||
|
||||
### 4.1 面向学员的自测与评估(产品能力)
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 自测练习 | ✅ 自测(即时对错,不存记录) | ✅ 每日一练 `practice`(**游戏化**:段位/星/连击/经验/积分 `practice_profile`) |
|
||||
| 错题本 | ❌ 无 | ✅ `mistake_records`(练习+考试两来源聚合,可标记已掌握) |
|
||||
| 学习笔记 / 收藏 | ❌ 无 | ✅ `study_notes` + 收藏(`study_progress.favorited`) |
|
||||
| 学习档案 / 学情 | ❌ **明确禁止**(PRD 第 7 节) | ✅ 学习档案 dashboard:学习天数、**能力对标**(来自对练复盘评分)、AI 对练摘要 |
|
||||
| **AI 对练复盘评分** | ❌ 无(仅快捷动作「扮演销售」的文本生成) | ✅ `dialogue_review_scores` + `ReviewScoreService`:维度评分、整体等级、趋势、强项/风险维度、复发问题、改进建议 |
|
||||
|
||||
### 4.2 面向软件工程的自动化测试(工程能力)
|
||||
|
||||
| 维度 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 后端测试 | ❌ **0 个 `_test.go`** | ✅ 31 个 pytest 文件(exam/eval/position/profile/dojo/dialogue/review/kb/…) |
|
||||
| 引擎测试 | 无 engine 层 | ✅ `zlylibs/engine/tests` 约 20 个(HEP 8层、DSRP loader、UPP、KB、eval、e2e smoke) |
|
||||
| 测试策略文档 | ❌ 无 | ✅ `docs/12_Test_Strategy`(可见点击测试、完整旅程 E2E 测试计划、legacy prompt) |
|
||||
| CI / 依赖管理 | 无 | ✅ pyproject `[dependency-groups] dev = pytest` |
|
||||
|
||||
**小结**:这是**差距最大**的一项。pj006 有完整的 pytest 测试体系 + 测试策略文档 + E2E 测试计划;pj0231 **一行测试都没有**。
|
||||
|
||||
---
|
||||
|
||||
## 5. 岗位与知识对应能力对比(差距最大的维度)
|
||||
|
||||
这是 pj0231 的**结构性缺失**——PRD 明确「不做能力档案、不做复杂权限」,代码里用户只有 `role` 字段,**完全没有岗位概念**。
|
||||
|
||||
| 能力 | pj0231 | pj006 |
|
||||
|---|---|---|
|
||||
| 岗位表 | ❌ 无 | ✅ `positions`(code/name/description,按 org 隔离) |
|
||||
| 岗位↔用户绑定 | ❌ 无 | ✅ `user_positions` |
|
||||
| **岗位能力模型(洋葱模型)** | ❌ 无 | ✅ `competency_layers`(C1知识技能/C2能力素质/C3职业素养)+ `competency_levels`(**L1-L4**)+ `competency_details` |
|
||||
| 能力模型自动生成 | ❌ 无 | ✅ `generate_competency_model`(自动生成三层 + L1-L4 初稿,可人工保存) |
|
||||
| **岗位↔知识映射** | ❌ 无 | ✅ `position_knowledge_units`(required_level / weight / is_mandatory),`auto_match_knowledge_units` 自动匹配 + 人工修正 |
|
||||
| **岗位考试蓝图** | ❌ 无 | ✅ `position_exam_blueprints`(维度/级别/题型/题量/权重) |
|
||||
| 标准岗位 / 职级代码 | ❌ 无 | ✅ `SD01_Standard_Job_Codes`(JobFunction + JobRank 双正交编码)+ `SD06_PROFILE_CODE_ENUM.json` |
|
||||
| 岗位驱动学练考 | ❌ 无 | ✅ 设计文档 `DA00_07`(A11 学习分发 / A12 练习分发 / A13 考试蓝图组卷) |
|
||||
| 用户身份 / 角色分级 | 2 角色 | 学员/教练/组织管理员/平台管理员 + NPC 人格(`UPP`) |
|
||||
|
||||
**小结**:pj006 把岗位从「一个标签」升级成了「知识分发与能力成长的**主控制器**」——岗位决定学什么(知识单元映射)、练什么(薄弱维度反查)、考什么(考试蓝图)。pj0231 完全没有这条能力链,这也是两项目最本质的定位差异。
|
||||
|
||||
---
|
||||
|
||||
## 6. 反向差异:pj0231 有、pj006 反而没有 / 较弱的点
|
||||
|
||||
诚实地讲,pj0231 并非全面落后,有几个点它反而更「实」:
|
||||
|
||||
1. **向量检索是真实现**:pj0231 的混合检索(向量余弦 + 关键词)端到端可用;pj006 实际跑的是 jieba 关键词,向量仅停在文档与字段预留层。
|
||||
2. **随机 + 按知识域自动抽题组卷**:pj0231 支持;pj006 考试实际为固定题目列表(蓝图组卷未接)。
|
||||
3. **文档→文本→检索链路真实跑通**:pj0231 的 LibreOffice / pdftotext 切片入库是完整的;pj006 的知识工厂「解析抽取」在 Phase 1 未实跑(仅表结构 + 状态机)。
|
||||
4. **AI 算力点计费 + 审计日志**:pj0231 有 `ai_call_log`(按用户扣点、按能力计费、延迟/错误审计);pj006 未见同等的算力计费。
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键文件索引(证据)
|
||||
|
||||
**pj0231**(本项,实际代码为准):
|
||||
- 数据模型:`eai_agentplatform/backend-go/internal/model/`(11 张表)
|
||||
- 检索:`eai_agentplatform/backend-go/internal/ai/retrieve.go`(混合检索)
|
||||
- 考试:`eai_agentplatform/backend-go/internal/api/exam.go`(组卷 / 判分 / 存档)
|
||||
- AI 聊天:`eai_agentplatform/backend-go/internal/api/ai_chat.go`(PathCoach + 3 快捷动作)
|
||||
|
||||
**pj006**(对比项):
|
||||
- 数据模型:`zlyapp/backend/app/db/schema.py`(v23,40+ 表,含 `orgs` 多租户根)
|
||||
- 考试:`zlyapp/backend/app/exam/service.py`(简答 LLM 评分、维度分析、错题联动)
|
||||
- 评估:`zlyapp/backend/app/eval/service.py`(学习档案、错题本、对练摘要)
|
||||
- 岗位:`zlyapp/backend/app/position/service.py`(洋葱模型生成、岗位知识自动匹配、考试蓝图)
|
||||
- 检索:`zlyapp/backend/app/kb/sqlite_retriever.py`(jieba + LIKE)
|
||||
- 引擎:`zlylibs/engine/`(HEP 8层、DSRP、UPP、KB、eval)+ 约 20 测试
|
||||
- 岗位洋葱设计:`docs/07_Data_Assets/DA00_07_Position_Onion_Model.md`
|
||||
- 测试策略:`docs/12_Test_Strategy/`
|
||||
|
||||
---
|
||||
|
||||
## 8. 给下一步的建议(不改代码,仅判断)
|
||||
|
||||
若目标是让 pj0231 **向 pj006 对齐**,优先级建议(按 ROI):
|
||||
|
||||
1. **补岗位模型 + 岗位-知识映射**(`positions` / `user_positions` / `position_knowledge_units`)——这是两项目最本质的差距,也是「岗位与知识对应」这一诉求的核心。
|
||||
2. **补错题本 + 学习档案**(`mistake_records`、维度能力分析)——pj0231 考试目前「考完就结束」,无闭环。
|
||||
3. **补简答题题型 + LLM 评分**——已有 LLM 底座,复用成本低。
|
||||
4. **补自动化测试**——当前 0 测试是工程隐患。
|
||||
|
||||
不建议盲目引入的:多租户分库、NPC 人格 / HEP 引擎、KPI 模块——这些与 pj0231「极简内网单企业」定位冲突,ROI 低。
|
||||
@@ -0,0 +1,8 @@
|
||||
# 02_Project_Comparisons
|
||||
|
||||
本目录用于存放本项目与其他项目的能力比较、整合分析,以及从比较结论中推导出的设计稿。
|
||||
|
||||
归档原则:
|
||||
|
||||
1. 差距分析、对照分析、整合版报告,归入本目录
|
||||
2. 由对比分析直接推导出的设计稿,也归入本目录,避免继续散落在根目录
|
||||
@@ -0,0 +1,406 @@
|
||||
# pj0231 岗位与知识对应能力 —— 落地设计稿
|
||||
|
||||
> 版本:V0.1(草案) | 日期:2026-08-16
|
||||
> 上位文档:《对比分析_项目0231与项目006智链源二.md》§5
|
||||
> 目标:补齐 pj0231 缺失的「岗位 → 知识 → 考试」能力链,对齐 pj006 的岗位驱动思想,但**保持 pj0231 的极简单租户定位,不照搬 pj006 的洋葱模型与多租户机制**。
|
||||
>
|
||||
> **状态:已落地。** 本设计的 P0(岗位 → 知识映射 → 用户设岗 → 岗位驱动组卷)已在 V1.4 实现,`position` / `position_knowledge` / `position_exam_blueprint` 等表与接口见 `docs/01_System_Overall/变更日志.md` V1.4–V1.5。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 现状痛点
|
||||
|
||||
对比分析确认 pj0231 存在**结构性缺失**:
|
||||
|
||||
- 用户只有 `role`(employee/admin),**没有岗位概念**;
|
||||
- 题库只有粗粒度 `domain`(company / product / sales)+ 可选 `course_id`,**没有「某岗位该掌握哪些知识/课程/产品」的对应关系**;
|
||||
- 考试组卷只能按 `domain` 抽题,**无法做到「按岗位应学范围出题」**;
|
||||
- 因此「该岗位该学什么、该考什么」完全靠人工记忆,无法系统化。
|
||||
|
||||
### 1.2 本次目标
|
||||
|
||||
落地一条**最小的岗位驱动闭环**:
|
||||
|
||||
```
|
||||
岗位(Position) ──绑定──> 用户(User)
|
||||
│
|
||||
└──映射──> 岗位知识要求(PositionKnowledge)
|
||||
├── 知识域 domain(company/product/sales)
|
||||
├── 课程 course_id(可选)
|
||||
└── 产品 product_id(可选)
|
||||
|
||||
考试(ExamPaper.position_id) ──> 按岗位知识要求自动抽题组卷
|
||||
```
|
||||
|
||||
### 1.3 非目标(明确不做,见 §9)
|
||||
|
||||
- 不引入 pj006 的 `competency_layers / levels / details` 三层洋葱模型;
|
||||
- 不引入多租户 `org` 组织模型;
|
||||
- 不做 LLM 自动生成岗位能力模型初稿;
|
||||
- 不做学习进度 / 学情分析(依赖学习进度表,另行立项)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计原则(强制)
|
||||
|
||||
1. **保持极简**:只加「岗位 + 映射」这一条能力链,不为对齐 pj006 而堆表。
|
||||
2. **复用现有机制**:考试组卷复用 `exam.go` 的 `pickQuestions`;路由复用 `RequireAdmin`;迁移复用 GORM `AutoMigrate`。
|
||||
3. **级别词汇对齐 pj006 的 L1-L4**,但**不建独立级别表**——`required_level` 用普通字符串字段 + Go 常量,降低复杂度。
|
||||
4. **单岗位优先**:一个用户一个岗位(`user.position_id` 可空),满足绝大多数内部员工场景;多岗位需求出现时再升级为映射表。
|
||||
5. **不破坏现有数据**:所有新列可空、新表独立,AutoMigrate 幂等。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现状盘点(改动点清单)
|
||||
|
||||
| 现有资产 | 位置 | 改动 |
|
||||
|---|---|---|
|
||||
| `User` | `internal/model/user.go` | **加列** `PositionID *uint`(可空) |
|
||||
| `ExamPaper` | `internal/model/exam_paper.go` | **加列** `PositionID *uint`(可空) |
|
||||
| `Question` | `internal/model/question.go` | 不改 |
|
||||
| `Course` / `Product` | `internal/model/` | 不改 |
|
||||
| 迁移 | `internal/store/db.go` `AutoMigrate` | 追加 2 张新表 |
|
||||
| 路由 | `internal/api/router.go` | 新增岗位相关端点(admin 组) |
|
||||
| 抽题 | `internal/api/exam.go` `pickQuestions` | 增加「岗位知识映射」过滤分支 |
|
||||
| 种子 | `internal/store/seed.go` | 可选:预置默认岗位 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据模型设计
|
||||
|
||||
### 4.1 新增表:`position`(岗位主表)
|
||||
|
||||
```go
|
||||
// internal/model/position.go
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Position 岗位表
|
||||
type Position struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Code string `gorm:"size:32;uniqueIndex;not null" json:"code"`
|
||||
Name string `gorm:"size:64;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Status string `gorm:"size:16;not null;default:active;index" json:"status"` // active / inactive
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Position) TableName() string { return "position" }
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS position (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_position_status ON position(status);
|
||||
```
|
||||
|
||||
### 4.2 新增表:`position_knowledge`(岗位知识要求,核心映射)
|
||||
|
||||
一条记录 = 「某岗位在某个知识域(可细化到课程/产品)上要求达到某级别」。
|
||||
|
||||
```go
|
||||
// internal/model/position_knowledge.go
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// PositionKnowledge 岗位知识要求(岗位 ↔ 知识域/课程/产品 映射)
|
||||
// domain 必填;course_id / product_id 可选(三者共同圈定岗位应学范围)。
|
||||
type PositionKnowledge struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
PositionID uint `gorm:"not null;index" json:"position_id"`
|
||||
Domain string `gorm:"size:16;not null;index" json:"domain"` // company / product / sales
|
||||
CourseID *uint `gorm:"index" json:"course_id"` // 绑定具体课程,可空
|
||||
ProductID *uint `gorm:"index" json:"product_id"` // 绑定具体产品,可空
|
||||
RequiredLevel string `gorm:"size:16;not null;default:L1" json:"required_level"` // L1/L2/L3/L4
|
||||
Weight float64 `gorm:"not null;default:1" json:"weight"`
|
||||
IsMandatory bool `gorm:"not null;default:true" json:"is_mandatory"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (PositionKnowledge) TableName() string { return "position_knowledge" }
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS position_knowledge (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
position_id INTEGER NOT NULL,
|
||||
domain TEXT NOT NULL, -- company / product / sales
|
||||
course_id INTEGER, -- 可空
|
||||
product_id INTEGER, -- 可空
|
||||
required_level TEXT NOT NULL DEFAULT 'L1', -- L1/L2/L3/L4
|
||||
weight REAL NOT NULL DEFAULT 1,
|
||||
is_mandatory INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pk_position ON position_knowledge(position_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pk_domain ON position_knowledge(domain);
|
||||
```
|
||||
|
||||
> 说明:不做 `UNIQUE(position_id, domain, course_id, product_id)` 的唯一约束,允许同一岗位同一域多条记录(如按产品细分)。如后续需要防重复,可在 service 层做去重校验。
|
||||
|
||||
### 4.3 改动:`user` 加岗位列
|
||||
|
||||
> 在 [user.go](../eai_agentplatform/backend-go/internal/model/user.go) 的 `User` 中追加:
|
||||
|
||||
```go
|
||||
PositionID *uint `gorm:"index" json:"position_id"` // 所属岗位,可空(历史用户/管理员可为空)
|
||||
```
|
||||
|
||||
### 4.4 改动:`exam_paper` 加岗位列
|
||||
|
||||
> 在 [exam_paper.go](../eai_agentplatform/backend-go/internal/model/exam_paper.go) 的 `ExamPaper` 中追加:
|
||||
|
||||
```go
|
||||
PositionID *uint `gorm:"index" json:"position_id"` // 关联岗位考试,可空;设置后按岗位知识映射抽题
|
||||
```
|
||||
|
||||
### 4.5 级别常量(Go)
|
||||
|
||||
```go
|
||||
// internal/model/position.go 或独立 constants 处
|
||||
const (
|
||||
LevelL1 = "L1" // 入门级:概念/定义/标准流程
|
||||
LevelL2 = "L2" // 稳定执行级:标准应用/独立执行
|
||||
LevelL3 = "L3" // 复杂处理级:跨场景迁移/带教
|
||||
LevelL4 = "L4" // 体系策略级:方案设计/组织约束
|
||||
)
|
||||
|
||||
var validLevels = map[string]bool{LevelL1: true, LevelL2: true, LevelL3: true, LevelL4: true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 设计
|
||||
|
||||
### 5.1 端点总表
|
||||
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/positions` | admin | 岗位列表 |
|
||||
| POST | `/api/positions` | admin | 新建岗位 |
|
||||
| PUT | `/api/positions/:id` | admin | 编辑岗位 |
|
||||
| DELETE | `/api/positions/:id` | admin | 停用岗位(软删除 status=inactive) |
|
||||
| GET | `/api/positions/:id/knowledge` | admin | 某岗位知识映射列表 |
|
||||
| PUT | `/api/positions/:id/knowledge` | admin | 保存映射(整表覆盖) |
|
||||
| PUT | `/api/users/:id/position` | admin | 设置/清除用户岗位 |
|
||||
| GET | `/api/my/position` | 员工 | 我的岗位 + 应学范围(P1) |
|
||||
|
||||
路由注册(`router.go` admin 组内追加):
|
||||
|
||||
```go
|
||||
admin.GET("/positions", ListPositions)
|
||||
admin.POST("/positions", CreatePosition)
|
||||
admin.PUT("/positions/:id", UpdatePosition)
|
||||
admin.DELETE("/positions/:id", DeletePosition)
|
||||
admin.GET("/positions/:id/knowledge", ListPositionKnowledge)
|
||||
admin.PUT("/positions/:id/knowledge", SavePositionKnowledge)
|
||||
admin.PUT("/users/:id/position", SetUserPosition)
|
||||
```
|
||||
|
||||
### 5.2 关键请求/响应
|
||||
|
||||
**PUT `/api/positions/:id/knowledge`(整表覆盖保存映射)**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{ "domain": "product", "course_id": null, "product_id": 3, "required_level": "L2", "weight": 1, "is_mandatory": true },
|
||||
{ "domain": "sales", "course_id": 2, "product_id": null, "required_level": "L3", "weight": 1.5, "is_mandatory": true },
|
||||
{ "domain": "company", "course_id": null, "product_id": null, "required_level": "L1", "weight": 1, "is_mandatory": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
响应:保存后的完整映射列表(含 `id`)。
|
||||
|
||||
**PUT `/api/users/:id/position`**
|
||||
|
||||
```json
|
||||
{ "position_id": 5 } // 设岗位
|
||||
{ "position_id": null } // 清除岗位
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 岗位驱动考试组卷(核心算法改动)
|
||||
|
||||
修改 [exam.go](../eai_agentplatform/backend-go/internal/api/exam.go) 的 `pickQuestions`,在原有 `domain` 抽题之前,先判断 `p.PositionID` 是否为空:
|
||||
|
||||
```go
|
||||
// pickQuestions 按 paper 抽题:优先按岗位知识映射圈定题池,否则回退 domain 抽题
|
||||
func pickQuestions(p model.ExamPaper) ([]model.Question, error) {
|
||||
pool := store.DB.Where("status = ?", "active")
|
||||
|
||||
if p.PositionID != nil {
|
||||
// 岗位考试:圈定岗位应学范围
|
||||
var pks []model.PositionKnowledge
|
||||
if err := store.DB.Where("position_id = ?", *p.PositionID).Find(&pks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domains := make([]string, 0, len(pks))
|
||||
courseIDs := make([]uint, 0, len(pks))
|
||||
for _, pk := range pks {
|
||||
domains = append(domains, pk.Domain)
|
||||
if pk.CourseID != nil {
|
||||
courseIDs = append(courseIDs, *pk.CourseID)
|
||||
}
|
||||
}
|
||||
domains = dedupe(domains)
|
||||
if len(domains) > 0 {
|
||||
pool = pool.Where("domain IN ?", domains)
|
||||
}
|
||||
if len(courseIDs) > 0 {
|
||||
// 岗位明确绑定了课程:题目要么属于这些课程,要么未绑定课程但域匹配
|
||||
pool = pool.Where("course_id IN ? OR course_id IS NULL", courseIDs)
|
||||
}
|
||||
} else {
|
||||
// 原有逻辑:按 exam_paper.domain 抽题
|
||||
domains := splitDomains(p.Domain)
|
||||
if len(domains) > 0 {
|
||||
pool = pool.Where("domain IN ?", domains)
|
||||
}
|
||||
}
|
||||
|
||||
var qs []model.Question
|
||||
if err := pool.Order("id ASC").Find(&qs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(qs) < p.QuestionCount {
|
||||
return nil, fmt.Errorf("题库题目不足:需要 %d,可用 %d", p.QuestionCount, len(qs))
|
||||
}
|
||||
if p.Randomize {
|
||||
rand.Shuffle(len(qs), func(i, j int) { qs[i], qs[j] = qs[j], qs[i] })
|
||||
}
|
||||
return qs[:p.QuestionCount], nil
|
||||
}
|
||||
```
|
||||
|
||||
> 说明:
|
||||
> - `p.PositionID == nil` 时走原逻辑,**对现有考试零影响**;
|
||||
> - 岗位映射为空(`len(pks)==0`)时 `domains` 为空,`pool` 不过滤域,会抽到全部题——需要在 `CreatePaper/UpdatePaper` 校验:**岗位考试必须存在至少一条知识映射**,否则拒绝创建/更新。
|
||||
|
||||
### 6.1 `CreatePaper` / `UpdatePaper` 追加校验
|
||||
|
||||
```go
|
||||
if p.PositionID != nil {
|
||||
var n int64
|
||||
store.DB.Model(&model.PositionKnowledge{}).Where("position_id = ?", *p.PositionID).Count(&n)
|
||||
if n == 0 {
|
||||
web.Fail(c, web.NewBadRequest("岗位考试必须先配置岗位知识映射"))
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 前端设计
|
||||
|
||||
### 7.1 导航位置
|
||||
|
||||
管理员「系统管理」下拉新增 **「岗位管理」**,位于「用户账号管理」之前:
|
||||
|
||||
```
|
||||
系统管理【管理员】─┬─ 岗位管理 ← 新增
|
||||
├─ 用户账号管理
|
||||
├─ 全部考试成绩
|
||||
└─ 系统参数配置
|
||||
```
|
||||
|
||||
### 7.2 页面清单
|
||||
|
||||
| 页面 | 路由 | 说明 |
|
||||
|---|---|---|
|
||||
| 岗位列表 | `/admin/positions` | 表格:编码/名称/描述/状态;新建/编辑/停用 |
|
||||
| 岗位知识映射 | 岗位列表行内抽屉或独立页 | 勾选知识域 + 绑定课程/产品下拉 + 选 L1-L4 + 权重 + 必学开关 |
|
||||
| 用户管理改造 | 现有 `UserManage.vue` | 用户行加「岗位」下拉(可选岗位/清空) |
|
||||
| 考试配置改造 | 现有题库管理/考试配置 | 考试表单加「关联岗位」下拉(可选,空=普通考试) |
|
||||
|
||||
### 7.3 岗位知识映射编辑面板交互(要点)
|
||||
|
||||
- 上方:岗位基本信息(只读展示)。
|
||||
- 主体:映射条目列表,每行 = `domain 下拉` + `课程下拉(可空)` + `产品下拉(可空)` + `L1-L4 下拉` + `权重` + `必学开关` + 删除。
|
||||
- 底部:「新增一行」+「保存」(整体 PUT 覆盖)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 分阶段实施
|
||||
|
||||
### P0(本次落地,核心闭环)
|
||||
|
||||
1. 新增 `Position` / `PositionKnowledge` model + 追加 `User.PositionID` / `ExamPaper.PositionID`;
|
||||
2. `db.go` AutoMigrate 追加 2 张表;
|
||||
3. 岗位 CRUD + 映射保存 API(admin);
|
||||
4. 用户设岗位 API + 用户管理页岗位下拉;
|
||||
5. `pickQuestions` 岗位过滤分支 + `CreatePaper/UpdatePaper` 校验;
|
||||
6. 前端「岗位管理」页 + 映射编辑面板 + 考试配置「关联岗位」下拉。
|
||||
|
||||
### P1(随后)
|
||||
|
||||
1. 学员端「我的岗位 + 应学清单」`GET /api/my/position`(按岗位映射返回应学课程/产品/知识域);
|
||||
2. 考试成绩按岗位聚合(`ExamRecord` 冗余 `position_id` 快照,或 join `user.position_id`);
|
||||
3. 岗位考试蓝图 `position_exam_blueprint`(维度/题型/题量/权重,对齐 pj006),驱动更精细组卷。
|
||||
|
||||
### P2(视需要,另行立项)
|
||||
|
||||
1. 错题本 + 学习档案(依赖学习进度表);
|
||||
2. 简答题题型 + LLM 评分(复用现有 LLM 底座);
|
||||
3. L1-L4 能力洋葱模型(若确需能力档案再上);
|
||||
4. 后端 pytest 测试补齐。
|
||||
|
||||
---
|
||||
|
||||
## 9. 明确不做(边界)
|
||||
|
||||
- ❌ 不建 `competency_layers / competency_levels / competency_details` 三层洋葱模型;
|
||||
- ❌ 不引入多租户 `orgs` / per-org 分库;
|
||||
- ❌ 不做 LLM 自动生成岗位能力模型初稿;
|
||||
- ❌ 不做多岗位绑定(单岗位优先,`user.position_id` 单列);
|
||||
- ❌ 不做学习进度 / 学情分析 / 能力雷达图。
|
||||
|
||||
---
|
||||
|
||||
## 10. 迁移与回滚
|
||||
|
||||
- **迁移**:GORM `AutoMigrate` 自动建新表 + 给既有表加可空列,幂等、对现有数据零破坏。历史用户 `position_id` 为空,不影响登录与现有考试。
|
||||
- **回滚**:新表删除即可(`DROP TABLE position; DROP TABLE position_knowledge;`);加列不影响回滚(可留空)。
|
||||
- **部署**:单二进制,无额外依赖;`clonezilla` 交付前清数据即可。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与注意事项
|
||||
|
||||
1. **岗位映射为空导致抽全题**:已在 `CreatePaper/UpdatePaper` 加校验拦截(§6.1)。
|
||||
2. **course_id 与 domain 双重过滤语义**:岗位绑定了具体课程时,`course_id IN ? OR course_id IS NULL` 会把「未绑定课程但域匹配」的题也纳入,需与产品确认是否符合预期(倾向:岗位考试以课程为强约束时,改为 `course_id IN ?` 严格匹配)。
|
||||
3. **级别字段暂不参与抽题**:P0 中 `required_level` 只做展示与后续 P1 蓝图使用,不参与 P0 组卷过滤(避免一开始就复杂化)。
|
||||
4. **多岗位需求**:若未来一人多岗,`user.position_id` 单列不够,届时升级为 `user_position` 映射表(参考 pj006 `user_positions`)。
|
||||
|
||||
---
|
||||
|
||||
## 12. 与 pj006 的差异对照(本稿的取舍)
|
||||
|
||||
| 项 | pj006 做法 | 本稿(pj0231)取舍 |
|
||||
|---|---|---|
|
||||
| 岗位能力模型 | 三层洋葱 + L1-L4 独立表 | 仅 `required_level` 字符串 + 常量,不建级别表 |
|
||||
| 岗位↔知识映射 | `position_knowledge_units`(挂知识单元) | `position_knowledge`(挂 domain/course/product,因 pj0231 无知识单元) |
|
||||
| 考试蓝图 | `position_exam_blueprints` | P0 用 `exam_paper.position_id` 直接驱动,蓝图列 P1 |
|
||||
| 用户绑定 | `user_positions` 独立表 | `user.position_id` 单列(极简) |
|
||||
| 多租户 | `orgs` + per-org 分库 | 不做(单企业) |
|
||||
|
||||
---
|
||||
|
||||
> 本稿为草案,落地前需与产品确认 §11.2 的课程/域双重过滤语义,以及 §4.2 是否需要对映射去重约束。
|
||||
Reference in New Issue
Block a user