企业AI助手从0到1(一):需求分析与架构设计
企业AI助手从0到1(一):需求分析与架构设计
开篇故事:5次需求会,5个不同方向
2025年9月,我接到一个咨询案例。
杭州某制造企业,员工1800人,IT部门8人,老板看完某大厂的AI发布会,第二天就找来技术总监陈建国:"我们也要做AI助手,下个季度上线。"
陈建国是我在星球里的同学,工作10年,Java老兵,带过3个人的小团队。他拉了我进项目群,从那天开始,我见证了这个项目最艰难的起点。
第一次需求会(第1天):老板要做"像ChatGPT一样的东西",能回答员工所有问题,包括设备操作手册、HR政策、客户报价……会议纪要里写着:"全能AI助手"。
第二次需求会(第5天):HR总监加入,说重点应该是员工自助查询考勤、请假流程,"我们每天处理200+个重复问题,太烦了"。方向变成"HR智能客服"。
第三次需求会(第9天):生产部总监来了,说他要的是设备维修助手,"我们有3000份设备手册,工人根本看不完"。方向又变成"设备知识库"。
第四次需求会(第14天):销售总监电话参会,他要的是客户问答机器人,能帮销售回答产品技术参数。方向第4次变化。
第五次需求会(第18天):全员参与,3个小时,争论不休,最后老板拍板:"先做HR助手,其他以后再说。"
陈建国跟我说:我们浪费了18天,但也值了——至少知道了从哪里开始。
这个故事,是所有企业AI项目的缩影。没有人天生知道"AI能做什么",需求会开多少次都正常。关键是:你要有方法,把混乱的需求变成清晰的架构。
今天这篇文章,我们就从这里开始,把企业AI助手从0到1的第一步——需求分析与架构设计——完整走一遍。
一、企业AI助手的典型需求分析
1.1 调研方法:三层访谈法
做企业AI项目,有个方法我用了很多次,效果很好,叫"三层访谈法":
决策层(1-2人):老板/CTO
→ 问:战略目标是什么?愿意投多少钱?6个月后想看到什么?
管理层(3-5人):部门总监
→ 问:你们部门最痛的问题是什么?每周浪费多少时间在重复工作上?
执行层(5-10人):实际用户
→ 问:你每天都在查什么?现在怎么查的?最烦的是什么?陈建国的调研结果(18天后整理):
| 层级 | 关键诉求 | 量化数据 |
|---|---|---|
| 决策层 | 降本增效,体现AI投入价值 | 期望3个月ROI |
| HR总监 | 减少重复问答,解放HR精力 | 每天200+咨询,3人处理 |
| 生产总监 | 设备手册太多,工人查不到 | 3000份文档,平均查询15分钟 |
| 普通员工 | 考勤规则、请假流程查起来麻烦 | 每周2-3次查询,找不到准确答案 |
1.2 需求优先级排列:RICE模型
混乱的需求要有方法排优先级,我推荐RICE模型:
R(Reach)= 影响用户数
I(Impact)= 解决痛点程度(1-3分)
C(Confidence)= 实现确定性(0-100%)
E(Effort)= 开发工作量(人周)
RICE得分 = (R × I × C) / E陈建国项目的RICE评估:
| 功能 | R | I | C | E | RICE分 |
|---|---|---|---|---|---|
| HR政策问答 | 1800 | 3 | 90% | 4 | 1215 |
| 设备手册查询 | 400 | 2 | 80% | 6 | 107 |
| 考勤自助查询 | 1800 | 2 | 85% | 3 | 1020 |
| 销售产品问答 | 50 | 3 | 70% | 5 | 21 |
结论:HR政策问答 + 考勤自助查询,第一期主攻。
1.3 需求文档模板
每个需求点,我要求用这个格式写清楚:
## 需求:HR政策问答
**用户故事**:
作为一名新员工,我想知道年假怎么申请,
这样我就不需要专门去找HR询问了。
**验收标准**:
1. 输入"年假怎么申请",能返回正确的申请流程(步骤数≤5)
2. 回答来源可追溯到具体文档
3. 如果文档中找不到,明确告知"我不知道"而非编造答案
4. 响应时间 < 5秒
**不包含**:
- 不处理具体的审批(跳转到OA系统)
- 不提供薪资相关计算
**优先级**:P0(MVP必须有)
**工作量估算**:3人周二、功能边界确定:AI能做什么、不能做什么
这是企业AI项目最容易翻车的地方。过度承诺,是项目失败的第一原因。
2.1 AI能做好的事
✅ 文档问答:从知识库检索并总结答案
✅ FAQ自动化:处理高频重复问题
✅ 内容摘要:长文档提炼要点
✅ 格式转换:非结构化→结构化
✅ 多轮对话:在一个主题内深入追问2.2 AI现阶段做不好的事
❌ 精确计算:别让AI算工资,会出错
❌ 实时数据查询:AI不连数据库就不知道今天考勤
❌ 跨系统操作:下单、审批、修改记录,需要系统对接
❌ 绝对权威回答:法律/财务问题,AI仅供参考
❌ 100%准确率:知识库有盲区时,AI可能产生幻觉2.3 给项目经理的沟通话术
跟业务方说清楚边界,我有个常用的说法:
"AI助手是一个聪明的搜索引擎加摘要工具,不是全知全能的员工。它能帮你节省找信息的时间,但重要决策仍需人工确认。"
陈建国用了这个说法后,业务方预期管理好了很多,验收压力减少了70%。
三、技术架构设计
确认了需求边界,我们开始设计架构。
3.1 整体架构图
3.2 各模块职责说明
| 模块 | 职责 | 核心技术 |
|---|---|---|
| 网关层 | 统一入口、JWT验证、限流(100 QPS)、请求日志 | Spring Cloud Gateway |
| 对话服务 | 会话管理、上下文注入、流式响应、日志记录 | Spring AI, WebSocket |
| 知识库服务 | 文档上传、向量化、检索、增量更新 | Spring AI, PGVector |
| 权限服务 | 用户认证、知识库权限、部门隔离 | Spring Security, JWT |
| 监控运营 | 使用统计、Token消耗、问题追踪 | Actuator, Micrometer |
3.3 请求流转时序图
四、技术选型决策
4.1 核心技术栈选择
框架:Spring Boot 3.3 + Spring AI 1.0
原因:Java生态首选,Spring AI已生产可用,社区活跃
放弃:LangChain4j(文档不如Spring AI完整)
LLM:通义千问 Qwen-Max(主)+ GPT-4o(备)
原因:
- 通义千问:中文理解最好,国内部署合规,API价格低40%
- GPT-4o:作为备用,处理英文技术文档效果更好
放弃:文心一言(API稳定性相对较差)
向量数据库:PGVector(初期)→ Milvus(规模大后迁移)
原因:
- 初期文档量<10万,PGVector够用,运维简单
- 超过百万向量后再迁Milvus,避免过早复杂化
放弃:Pinecone(国内网络不稳定,数据出境合规问题)
关系数据库:PostgreSQL 16
原因:PGVector插件在PostgreSQL上,统一数据库减少运维
缓存:Redis 7
原因:会话缓存、限流计数器、热点文档缓存,标配
认证:JWT + Spring Security 6
原因:无状态,适合分布式部署
对象存储:MinIO(私有化)
原因:企业文档不出内网,合规要求,比OSS更安全4.2 选型对比表
| 维度 | 我们的选择 | 备选方案 | 放弃原因 |
|---|---|---|---|
| AI框架 | Spring AI | LangChain4j | 文档质量差 |
| LLM | Qwen-Max | GPT-4o | 成本+合规 |
| 向量库 | PGVector | Milvus | 初期过重 |
| 存储 | MinIO | 阿里OSS | 数据合规 |
| 部署 | Docker Compose | K8s | 团队规模小 |
五、数据架构设计
5.1 整体数据模型
5.2 数据库建表SQL
-- 用户表
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
email VARCHAR(200) UNIQUE,
department VARCHAR(100),
role VARCHAR(50) DEFAULT 'employee',
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 知识库表
CREATE TABLE knowledge_bases (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
department VARCHAR(100),
access_level VARCHAR(50) DEFAULT 'department',
-- 'public': 全员可见
-- 'department': 部门内可见
-- 'private': 仅管理员
created_by BIGINT REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 文档表
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
kb_id BIGINT NOT NULL REFERENCES knowledge_bases(id),
title VARCHAR(500) NOT NULL,
original_filename VARCHAR(500),
file_path VARCHAR(1000),
file_size BIGINT,
file_type VARCHAR(50),
status VARCHAR(50) DEFAULT 'pending',
-- pending/processing/active/failed/deleted
chunk_count INTEGER DEFAULT 0,
error_message TEXT,
created_by BIGINT REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 文档分块表(含向量)
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE document_chunks (
id BIGSERIAL PRIMARY KEY,
doc_id BIGINT NOT NULL REFERENCES documents(id),
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
token_count INTEGER,
embedding vector(1536),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX ON document_chunks USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 会话表
CREATE TABLE sessions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
session_id VARCHAR(100) NOT NULL UNIQUE,
title VARCHAR(500),
kb_id BIGINT REFERENCES knowledge_bases(id),
message_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 消息表
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
session_id BIGINT NOT NULL REFERENCES sessions(id),
role VARCHAR(20) NOT NULL, -- 'user' / 'assistant'
content TEXT NOT NULL,
tokens_used INTEGER,
model_used VARCHAR(100),
latency_ms INTEGER,
retrieved_chunks JSONB, -- 检索到的文档片段ID列表
feedback VARCHAR(20), -- 'like'/'dislike'/null
feedback_text TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 使用统计表(按天汇总)
CREATE TABLE daily_stats (
id BIGSERIAL PRIMARY KEY,
stat_date DATE NOT NULL,
department VARCHAR(100),
total_messages INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
avg_latency_ms INTEGER,
satisfaction_rate DECIMAL(5,2),
unique_users INTEGER,
UNIQUE(stat_date, department)
);六、API设计
6.1 RESTful API总览
# 认证相关
POST /api/auth/login # 登录获取JWT
POST /api/auth/logout # 退出
GET /api/auth/me # 获取当前用户信息
# 对话相关
POST /api/chat/sessions # 创建会话
GET /api/chat/sessions # 查询会话列表
GET /api/chat/sessions/{id} # 查询会话详情
DELETE /api/chat/sessions/{id} # 删除会话
POST /api/chat/sessions/{id}/messages # 发送消息(非流式)
GET /api/chat/sessions/{id}/messages/stream # 发送消息(SSE流式)
GET /api/chat/sessions/{id}/messages # 获取历史消息
POST /api/chat/messages/{id}/feedback # 提交反馈
# 知识库相关
GET /api/knowledge-bases # 查询知识库列表
POST /api/knowledge-bases # 创建知识库(管理员)
GET /api/knowledge-bases/{id} # 查询知识库详情
POST /api/knowledge-bases/{id}/documents # 上传文档
GET /api/knowledge-bases/{id}/documents # 查询文档列表
DELETE /api/knowledge-bases/{id}/documents/{docId} # 删除文档
# 运营管理(管理员)
GET /api/admin/stats/daily # 每日统计
GET /api/admin/stats/users # 用户使用统计
GET /api/admin/queries/missed # 未命中查询分析6.2 关键接口定义
发送消息(非流式):
// Request
POST /api/chat/sessions/{sessionId}/messages
{
"message": "年假怎么申请",
"kbId": 1, // 指定知识库(可选)
"topK": 5 // 检索数量(可选,默认5)
}
// Response
{
"code": 200,
"data": {
"messageId": 12345,
"answer": "根据公司《员工手册》第三章...",
"sources": [
{
"docTitle": "员工手册2025版",
"chunkId": 789,
"similarity": 0.92,
"excerpt": "年假申请流程:1.登录OA系统..."
}
],
"tokensUsed": 856,
"latencyMs": 2340
}
}SSE流式接口:
GET /api/chat/sessions/{sessionId}/messages/stream?message=年假怎么申请
// SSE Events:
data: {"type":"start","sessionId":"xxx"}
data: {"type":"chunk","content":"根据"}
data: {"type":"chunk","content":"公司"}
data: {"type":"chunk","content":"《员工手册》"}
...
data: {"type":"sources","sources":[{"docTitle":"员工手册","similarity":0.92}]}
data: {"type":"done","tokensUsed":856,"latencyMs":2340}七、安全设计
7.1 安全架构总览
7.2 知识库权限矩阵
权限级别:
public → 全公司所有员工可访问
dept → 仅本部门员工可访问
private → 仅知识库管理员可访问
知识库访问规则:
- 用户只能查询自己有权限的知识库
- 向量检索时自动过滤无权限的文档
- 跨部门访问需要管理员审批7.3 安全配置代码
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
JwtAuthFilter jwtFilter) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}八、完整项目搭建:pom.xml与配置
8.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>com.enterprise</groupId>
<artifactId>ai-assistant</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>enterprise-ai-assistant</name>
<description>企业AI助手 - 核心服务</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.2</version>
</dependency>
<!-- 对象存储 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.12</version>
</dependency>
<!-- 监控 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.2</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>8.2 application.yml
server:
port: 8080
servlet:
context-path: /
spring:
application:
name: enterprise-ai-assistant
# 数据库配置
datasource:
url: jdbc:postgresql://localhost:5432/ai_assistant
username: ${DB_USERNAME:aiuser}
password: ${DB_PASSWORD:aipassword}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
# JPA配置
jpa:
hibernate:
ddl-auto: validate # 生产环境用validate,禁止auto
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
# Redis配置
data:
redis:
host: ${REDIS_HOST:localhost}
port: 6379
password: ${REDIS_PASSWORD:}
lettuce:
pool:
max-active: 16
max-idle: 8
# Spring AI配置
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
chat:
options:
model: ${AI_CHAT_MODEL:gpt-4o}
temperature: 0.1
max-tokens: 2048
embedding:
options:
model: text-embedding-3-small
vectorstore:
pgvector:
index-type: ivfflat
distance-type: cosine_distance
dimensions: 1536
# 文件上传
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
# JWT配置
app:
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-for-production-change-me}
expiration: 86400000 # 24小时(毫秒)
refresh-expiration: 604800000 # 7天
# MinIO配置
minio:
endpoint: ${MINIO_ENDPOINT:http://localhost:9000}
access-key: ${MINIO_ACCESS_KEY:minioadmin}
secret-key: ${MINIO_SECRET_KEY:minioadmin}
bucket: ai-documents
# 知识库配置
knowledge:
chunk-size: 512 # 每块Token数
chunk-overlap: 50 # 块间重叠Token数
top-k: 5 # 默认检索数量
similarity-threshold: 0.7 # 相似度阈值
# 限流配置
rate-limit:
requests-per-minute: 30 # 每用户每分钟
# 监控配置
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
# 日志配置
logging:
level:
com.enterprise: DEBUG
org.springframework.ai: INFO
org.springframework.security: WARN
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"8.3 核心领域模型
// entity/User.java
package com.enterprise.aiassistant.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "users")
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 100)
private String username;
@Column(unique = true, length = 200)
private String email;
@Column(length = 100)
private String department;
@Column(length = 50)
private String role = "employee";
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@Column(name = "is_active")
private boolean isActive = true;
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at")
private LocalDateTime updatedAt = LocalDateTime.now();
@PreUpdate
void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}// entity/KnowledgeBase.java
package com.enterprise.aiassistant.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "knowledge_bases")
public class KnowledgeBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(length = 100)
private String department;
@Column(name = "access_level", length = 50)
private String accessLevel = "department";
// public / department / private
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "created_by")
private User createdBy;
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
}// entity/Document.java
package com.enterprise.aiassistant.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "documents")
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "kb_id", nullable = false)
private KnowledgeBase knowledgeBase;
@Column(nullable = false, length = 500)
private String title;
@Column(name = "original_filename", length = 500)
private String originalFilename;
@Column(name = "file_path", length = 1000)
private String filePath;
@Column(name = "file_size")
private Long fileSize;
@Column(name = "file_type", length = 50)
private String fileType;
@Column(length = 50)
private String status = "pending";
@Column(name = "chunk_count")
private Integer chunkCount = 0;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "created_by")
private User createdBy;
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at")
private LocalDateTime updatedAt = LocalDateTime.now();
}九、项目计划:里程碑与交付物
9.1 项目时间线
9.2 里程碑与交付物
| 里程碑 | 时间 | 交付物 | 验收标准 |
|---|---|---|---|
| M1:架构评审 | 第2周末 | 架构设计文档、ERD图、API文档 | 技术评审通过 |
| M2:基础框架 | 第4周末 | 可运行的项目框架 + CI/CD | 单元测试通过率>90% |
| M3:知识库可用 | 第6周末 | 知识库管理+RAG搜索 | 10个标准问题准确率>80% |
| M4:对话可用 | 第8周末 | 对话系统+权限控制 | 端到端测试通过 |
| M5:灰度上线 | 第10周末 | 100用户可用的生产环境 | NPS≥4.0/5.0 |
| M6:全量上线 | 第12周末 | 1800用户全量上线 | 可用性≥99.5% |
十、环境搭建
10.1 Docker Compose开发环境
# docker-compose.yml
version: '3.9'
services:
postgres:
image: pgvector/pgvector:pg16
container_name: ai-postgres
environment:
POSTGRES_DB: ai_assistant
POSTGRES_USER: aiuser
POSTGRES_PASSWORD: aipassword
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aiuser -d ai_assistant"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: ai-redis
command: redis-server --requirepass redispassword
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
minio:
image: minio/minio:latest
container_name: ai-minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
volumes:
postgres_data:
redis_data:
minio_data:10.2 启动脚本
#!/bin/bash
# start-dev.sh
echo "=== 企业AI助手开发环境启动 ==="
# 1. 启动基础服务
docker-compose up -d
echo "等待服务启动..."
sleep 15
# 2. 初始化MinIO bucket
docker exec ai-minio mc alias set local http://localhost:9000 minioadmin minioadmin
docker exec ai-minio mc mb local/ai-documents --ignore-existing
# 3. 验证服务状态
echo ""
echo "=== 服务状态检查 ==="
echo -n "PostgreSQL: "
docker exec ai-postgres pg_isready -U aiuser && echo "✓ 正常" || echo "✗ 异常"
echo -n "Redis: "
docker exec ai-redis redis-cli ping | grep -q PONG && echo "✓ 正常" || echo "✗ 异常"
echo -n "MinIO: "
curl -sf http://localhost:9000/minio/health/live && echo "✓ 正常" || echo "✗ 异常"
echo ""
echo "=== 启动完成 ==="
echo "PostgreSQL: jdbc:postgresql://localhost:5432/ai_assistant"
echo "Redis: localhost:6379"
echo "MinIO API: http://localhost:9000"
echo "MinIO UI: http://localhost:9001"10.3 项目结构
enterprise-ai-assistant/
├── src/main/java/com/enterprise/aiassistant/
│ ├── AiAssistantApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── JwtConfig.java
│ │ └── MinioConfig.java
│ ├── controller/
│ │ ├── AuthController.java
│ │ ├── ChatController.java
│ │ ├── KnowledgeBaseController.java
│ │ └── AdminController.java
│ ├── service/
│ │ ├── AuthService.java
│ │ ├── ChatService.java
│ │ ├── KnowledgeBaseService.java
│ │ └── DocumentProcessingService.java
│ ├── repository/
│ │ ├── UserRepository.java
│ │ ├── SessionRepository.java
│ │ ├── MessageRepository.java
│ │ ├── KnowledgeBaseRepository.java
│ │ └── DocumentRepository.java
│ ├── entity/
│ │ ├── User.java
│ │ ├── Session.java
│ │ ├── Message.java
│ │ ├── KnowledgeBase.java
│ │ └── Document.java
│ ├── dto/
│ │ ├── request/
│ │ └── response/
│ ├── security/
│ │ ├── JwtAuthFilter.java
│ │ └── JwtTokenProvider.java
│ └── exception/
│ ├── GlobalExceptionHandler.java
│ └── BusinessException.java
├── src/main/resources/
│ ├── application.yml
│ └── application-prod.yml
├── sql/
│ └── init.sql
├── docker-compose.yml
└── pom.xml性能与预期效果数据
陈建国的项目,经过4个月开发,最终上线数据如下:
| 指标 | 目标 | 实际结果 |
|---|---|---|
| 首字延迟 | <3秒 | 平均1.8秒 |
| 知识库准确率 | >80% | 87.3% |
| HR日均咨询量减少 | >50% | 减少68% |
| 用户满意度 | >4.0/5 | 4.3/5 |
| 系统可用性 | >99.5% | 99.8% |
| 月均Token成本 | <5000元 | 3200元 |
FAQ
Q1:小团队(2-3人)做这个项目,工期怎么估?
A:如果只做HR问答这一个场景,核心功能(不含管理后台)2人3个月可以完成。我在文章里给的12周计划是包含完整管理后台的,实际可以裁剪。
Q2:必须用PGVector吗?我们已经有MySQL。
A:不一定非要PGVector,但向量存储必须有一个专门的方案。如果坚持用MySQL,可以考虑MyScale或者单独部署Milvus Lite,两者都有Spring AI的支持。迁移成本大概是2-3天。
Q3:LLM一定要用OpenAI/通义千问吗?私有化部署怎么办?
A:完全可以私有化。推荐方案:Ollama + Qwen2.5-7B本地部署,Spring AI的OllamaApi直接对接,代码改动不超过10行。效果上,7B模型对中文文档问答够用,但复杂推理题质量会下降30%左右。
Q4:18天的需求调研,是不是太长了?
A:实际上不是调研慢,是企业内部对齐慢。技术侧调研3天足够,但等各部门负责人开会、对齐、决策,2-3周是正常的。建议:提前发调研问卷,会议只用来确认和拍板,能节省50%时间。
Q5:项目上线后,AI说错了怎么办?
A:这是必须在上线前就回答清楚的问题。标准做法:AI所有回答附上来源文档,用户可以点击查看原文;重要决策页面加免责声明"仅供参考,最终以官方文件为准";设置反馈按钮,用户可以标记错误答案,运营人员48小时内处理。
系列预告
这是"企业AI助手从0到1"系列的第一篇,我们完成了:需求分析、架构设计、技术选型、数据模型、API设计、安全设计、项目计划和环境搭建。
下一篇(article-176),我们进入最核心的部分:知识库构建与RAG实现。知识库质量决定AI回答质量,这一篇会把文档预处理、向量化、检索调优、质量监控全部走一遍,附完整生产代码。
