- 后端:新增 /api/audio/transcribe 接口,调用 Ollama whisper 进行语音识别 - 前端:新增 AudioTranscribePage.vue 页面,支持 MP3/WAV/M4A/OGG/FLAC 等格式 - 注册路由、工具卡片、智能助手欢迎语更新 Co-Authored-By: Claude Code <noreply@anthropic.com>
384 lines
17 KiB
Markdown
384 lines
17 KiB
Markdown
# pj0231-eai_agentplatforming 完整分析报告(对比 + 差距 + 路线图 + 设计稿)
|
||
|
||
> 版本:V1.0 | 日期:2026-08-16
|
||
> 对比对象:pj006-zhilianyuan2(智练猿)
|
||
> 范围:知识库能力、考试能力、测试/自测能力、岗位与知识对应能力
|
||
> 原则:以**实际代码**为准(本项目 PRD 文档存在过时,见 §1.3)
|
||
>
|
||
> **注:** 本文基于 2026-08-16 代码快照(后端 Go + Gin + GORM,检索为 brute-force 余弦 + bge-m3);数据库与向量栈已决策迁移至 **MySQL 8.0 + FAISS**,见 `docs/db_schema.md` / `docs/deploy.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/PRD.md` 写「FastAPI + MySQL + 不做向量检索」;实际代码是 **Go + Gin + GORM + SQLite**,检索已实现**向量(brute-force 余弦 + bge-m3)**。
|
||
- 本项目权威信息以 `eai_agentplatform_app/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 来自《对比分析_pj0231_vs_pj006-zhilianyuan2.md》,Part D 整合自《设计稿_岗位与知识对应能力.md》,Part E 为新增其余改进项设计要点。
|