好,"helppeer.ai" 这个项目名很直白,拆开看就是help(帮助)+ peer(同伴/同行者)+ .ai(人工智能)。如果你在开发一个 AI 技能互助平台、AI 学习社区,或者以 AI 为核心的知识问答与同伴互助产品,那么这个名字非常贴切。本文会围绕这类 AI 互助平台的典型技术需求,从项目定位、技术选型、后端核心模块、AI 会话接入,到接口联调与生产部署,整理一套可以直接落地的实战方案。
无论你是正在规划类似产品,还是单纯想练习一个"AI + 业务系统"的综合项目,这篇文章都能给你一个完整的参考。代码以 Spring Boot 3 + 原生 HTTP 调用大模型接口为例,重点讲解设计思路、核心表结构、AI 模块接入方式与常见坑点,前端部分只给出基础交互逻辑,不展开复杂界面。
1. helppeer.ai 项目定位与技术架构
1.1 这类平台解决什么问题
在线学习或者技能成长过程中,最容易遇到的瓶颈是"遇到问题找不到人问"。
传统的问答社区存在几个体验问题:
- 提问后回答周期长,质量参差不齐。
- 新手不知道怎么描述问题,更不知道找谁问。
- 没有激励机制,高手不愿意持续回答。
- 没有沉淀,重复问题反复出现。
helppeer.ai 的核心思路,是把"人"和"AI"组合起来:AI 先即时回答,解决大部分通用问题;如果 AI 解决不了,再通过智能匹配把问题转给领域相近的同伴(Peer)。这种模式既能保证响应速度,又能保留人与人之间的互助价值。
1.2 核心功能模块
一个最小可用版本(MVP)至少要包含以下模块:
| 模块 | 功能说明 |
|---|---|
| 用户中心 | 注册、登录、个人资料、技能标签维护 |
| 互助广场 | 发布问题、浏览问题、搜索问题 |
| 智能问答 | 调用大模型 API 实现 AI 首轮回答 |
| 同伴匹配 | 根据问题标签匹配擅长该领域的用户 |
| 解答与评价 | 回答、采纳、点赞、评价 |
| 消息中心 | 问题被回答、被采纳时发送通知 |
1.3 技术选型建议
以 Java 技术栈为例,推荐如下组合:
- 后端框架:Spring Boot 3.x,简化配置,生态成熟。
- 持久层:MyBatis-Plus,代码生成效率高,适合快速迭代。
- 数据库:MySQL 8.x,存储业务数据。
- 缓存:Redis,保存会话上下文、热点数据与在线状态。
- AI 接入:使用 HTTP 接口调用大模型服务,不强制绑定某个 SDK。
- 前端:Vue 3 + Element Plus(本文只演示核心调用逻辑)。
- 部署:Docker + Docker Compose。
这套选型的特点是:技术栈通用性好,招聘成本低,资料多,且 AI 模块可以被替换——以后想换模型、换供应商,只需要改一个接口封装层。
2. 环境准备与项目初始化
2.1 本地开发环境
开始编码前,先确认以下工具已经安装:
JDK 17+ Maven 3.6+ MySQL 8.x Redis 6.x+ IntelliJ IDEA 或 Eclipse版本不必完全一致,但尽量使用较新的稳定版本。JDK 使用 17 是因为 Spring Boot 3.x 要求最低 JDK 17。
2.2 Spring Boot 项目创建
你可以通过 Spring Initializr 初始化项目,也可以直接用 IDEA 创建。需要引入的依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.25</version> </dependency> </dependencies>说明:Hutool 是工具类库,用来简化 HTTP 请求、日期处理、随机数生成等常见操作,属于可选依赖。如果不喜欢引入额外依赖,也可以直接使用 Spring 的RestTemplate或 JDK 11+ 的java.net.http.HttpClient。
2.3 项目目录结构
helppeer-ai/ ├── src/main/java/com/helppeer/ │ ├── HelppeerApplication.java │ ├── config/ │ │ ├── RedisConfig.java │ │ └── WebConfig.java │ ├── controller/ │ │ ├── UserController.java │ │ ├── QuestionController.java │ │ └── AiChatController.java │ ├── service/ │ │ ├── UserService.java │ │ ├── QuestionService.java │ │ ├── AiChatService.java │ │ └── PeerMatchService.java │ ├── mapper/ │ │ ├── UserMapper.java │ │ ├── QuestionMapper.java │ │ └── AnswerMapper.java │ ├── entity/ │ │ ├── User.java │ │ ├── Question.java │ │ └── Answer.java │ └── dto/ │ ├── QuestionDTO.java │ └── ChatRequestDTO.java └── src/main/resources/ ├── application.yml └── mapper/这个结构保留了经典的三层架构风格,职责清晰:Controller 只做参数接收与结果返回,Service 处理业务逻辑,Mapper 进行数据库操作。
3. 数据库设计与核心表结构
3.1 设计思路
互助平台的数据模型相比电商系统要简单一些,但有几个点需要提前想清楚:
- 用户除了基础信息外,需要有"技能标签",用于同类问题匹配。
- 问题与标签是多对多关系,建议单独建关联表,方便后续扩展标签体系。
- AI 回答需要保留记录,方便用户查看历史对话,也方便统计 token 消耗。
- 解答采纳后要更新问题状态,避免用户重复作答。
3.2 建表 SQL
下面是核心表的 SQL 设计,生产环境可以增加更多索引,MVP 阶段这些字段足够。
-- 用户表 CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '加密后的密码', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', `bio` VARCHAR(500) DEFAULT NULL COMMENT '个人简介', `level` INT DEFAULT 1 COMMENT '等级', `points` INT DEFAULT 0 COMMENT '积分', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 问题表 CREATE TABLE `question` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '提问人ID', `title` VARCHAR(200) NOT NULL COMMENT '问题标题', `content` TEXT COMMENT '问题详细描述', `status` TINYINT DEFAULT 0 COMMENT '状态:0-待解答,1-AI已回复,2-已采纳', `ai_answer` TEXT COMMENT 'AI首轮回答内容', `view_count` INT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题表'; -- 回答表 CREATE TABLE `answer` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `question_id` BIGINT NOT NULL, `user_id` BIGINT NOT NULL, `content` TEXT NOT NULL, `is_accepted` TINYINT DEFAULT 0 COMMENT '是否被采纳', `like_count` INT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_question_id` (`question_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回答表'; -- 用户技能标签表 CREATE TABLE `user_skill` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `skill_name` VARCHAR(50) NOT NULL, `level` TINYINT DEFAULT 1 COMMENT '熟练度 1-5', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_skill_name` (`skill_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户技能表'; -- AI对话记录表 CREATE TABLE `ai_chat_history` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `question_id` BIGINT DEFAULT NULL, `role` VARCHAR(20) NOT NULL COMMENT 'user/assistant/system', `content` TEXT NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话记录表';需要注意:
- 密码字段要和数据库字段保持一致,本文示例实体类用 String 类型,数据库用 VARCHAR。
ai_chat_history保存了完整的对话内容,可以支持多轮追问,也可以用于后续数据分析,比如统计高频问题、优化智能匹配。- 所有表都使用
utf8mb4,因为 utf8mb4 完整支持 emoji 和特殊字符,AI 返回内容中经常会有这类字符。
4. 后端核心模块实现
4.1 通用返回结果封装
在开发前后端分离项目时,最好定义一个统一的返回结果类,否则接口格式五花八门,前端联调会很痛苦。
package com.helppeer.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }4.2 用户注册与密码处理
用户注册的密码不能明文存储。BCrypt 是目前最普遍的密码哈希方案,Spring Security 的BCryptPasswordEncoder可以直接使用,也可以引入spring-security-crypto依赖单独使用。
先添加依赖:
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency>注意:spring-security-crypto独立使用时不需要额外配置,BCryptPasswordEncoder可以直接 new。
用户 Service 核心代码:
package com.helppeer.service.impl; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.helppeer.entity.User; import com.helppeer.mapper.UserMapper; import com.helppeer.service.UserService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); @Override public User register(String username, String password) { if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) { throw new RuntimeException("用户名和密码不能为空"); } Long count = userMapper.selectCount( new LambdaQueryWrapper<User>().eq(User::getUsername, username) ); if (count > 0) { throw new RuntimeException("用户名已存在"); } User user = new User(); user.setUsername(username); user.setPassword(encoder.encode(password)); user.setNickname(username); user.setLevel(1); user.setPoints(0); userMapper.insert(user); return user; } @Override public User login(String username, String password) { User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getUsername, username) ); if (user == null) { throw new RuntimeException("用户不存在"); } if (!encoder.matches(password, user.getPassword())) { throw new RuntimeException("密码错误"); } return user; } }代码里有两个关键点:
LambdaQueryWrapper是 MyBatis-Plus 提供的条件构造器,可以避免把 SQL 字符串写在代码里,减少拼写错误。encoder.matches(password, user.getPassword())用于校验明文密码与数据库中的哈希值是否匹配,不要直接调用equals比较。
4.3 发布问题并触发 AI 回答
当用户发布问题时,合理的顺序应该是:
- 校验参数。
- 保存问题到数据库。
- 调用 AI 服务生成首轮回答。
- 更新问题的
ai_answer字段和状态。 - 异步匹配可能擅长该问题的同伴。
这个流程中,AI 接口调用耗时可能较长,如果同步执行,用户会一直等待。MVP 阶段可以先同步调用,因为大模型接口通常 3-10 秒内就能返回;如果想优化体验,可以把 AI 调用和同伴匹配放入消息队列或线程池异步执行。
QuestionService 中的发布方法:
@Override public Question publishQuestion(QuestionDTO dto, Long userId) { Question question = new Question(); question.setUserId(userId); question.setTitle(dto.getTitle()); question.setContent(dto.getContent()); question.setStatus(0); question.setViewCount(0); questionMapper.insert(question); // 异步调用 AI,避免阻塞主流程 aiChatService.asyncGenerateFirstAnswer(question.getId()); return question; }这里使用了asyncGenerateFirstAnswer,方法内部的异步实现需要配合 Spring 的@Async注解。在启动类或配置类上加上@EnableAsync,然后在 Service 实现方法上标注@Async,Spring 会在线程池中执行该方法。
4.4 AI 智能问答模块
AI 模块是 helppeer.ai 的核心,也是最容易踩坑的部分。本节给出一个通用的 HTTP 调用示例,不绑定具体厂商,你可以直接对接 OpenAI 兼容接口、国内大模型平台的 HTTP 接口或者其他兼容接口。前提是对方提供 OpenAI 风格的chat/completions接口,这种格式现在已经成为事实标准。
4.4.1 配置管理
在application.yml中增加 AI 配置:
ai: api-key: ${AI_API_KEY:sk-xxxxxxxx} base-url: ${AI_BASE_URL:https://api.openai.com/v1} model: ${AI_MODEL:gpt-3.5-turbo} max-tokens: 1000 temperature: 0.7这里使用了环境变量占位符${AI_API_KEY:sk-xxxxxxxx},意思是优先从环境变量AI_API_KEY读取,如果不存在,则使用默认值。这样做的好处是敏感信息不会被提交到代码仓库。
4.4.2 AI 调用服务
使用 Spring 的RestTemplate发送 HTTP 请求。先注册 Bean:
package com.helppeer.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(30000); return new RestTemplate(factory); } }设置readTimeout=30000很重要。大模型接口生成内容需要时间,特别是较长的回答可能超过 10 秒。如果使用默认超时(通常很短),会出现频繁的 SocketTimeoutException。
AI 调用代码:
package com.helppeer.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j @Service public class AiChatServiceImpl implements AiChatService { @Resource private RestTemplate restTemplate; @Value("${ai.api-key}") private String apiKey; @Value("${ai.base-url}") private String baseUrl; @Value("${ai.model}") private String model; @Value("${ai.max-tokens}") private Integer maxTokens; @Value("${ai.temperature}") private Double temperature; private final ObjectMapper objectMapper = new ObjectMapper(); @Override public String chat(String userMessage, List<Map<String, String>> history) { String url = baseUrl + "/chat/completions"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); // 构建完整的消息序列 List<Map<String, String>> messages = new ArrayList<>(); messages.add(Map.of("role", "system", "content", "你是一个技术互助平台的AI助手,回答要简洁、准确、有条理。")); if (history != null) { messages.addAll(history); } Map<String, String> userMsg = new HashMap<>(); userMsg.put("role", "user"); userMsg.put("content", userMessage); messages.add(userMsg); Map<String, Object> body = new HashMap<>(); body.put("model", model); body.put("messages", messages); body.put("max_tokens", maxTokens); body.put("temperature", temperature); HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers); try { ResponseEntity<String> response = restTemplate.exchange( url, HttpMethod.POST, request, String.class); if (response.getStatusCode() != HttpStatus.OK) { log.error("AI接口返回异常状态码: {}", response.getStatusCode()); return "抱歉,AI服务暂时不可用,请稍后再试。"; } JsonNode root = objectMapper.readTree(response.getBody()); JsonNode content = root.path("choices").get(0).path("message").path("content"); return content.asText(); } catch (Exception e) { log.error("调用AI接口失败", e); return "抱歉,AI服务暂时不可用,请稍后再试。"; } } }代码说明:
headers.setBearerAuth(apiKey)对应 HTTP 请求头的Authorization: Bearer sk-xxx,这是目前 OpenAI 兼容接口的标准认证方式。- system 消息用来设定 AI 的人设和行为准则。在 helppeer.ai 的场景里,可以告诉 AI 它是互助平台的助手,回答要简洁、有条理。
history参数用于支持多轮对话,把之前几轮的消息按顺序传过去。- 异常处理非常关键。AI 接口可能因网络、限流、内容审核等原因失败,不能因为 AI 异常导致整个业务报错,必须降级处理。
4.4.3 同伴匹配逻辑(简化版)
同伴匹配的完整实现可以做得很复杂,比如基于向量相似度、用户活跃度、历史采纳率等。MVP 阶段先用一个简单规则:
- 从问题标题和内容中提取技能标签。
- 根据标签在
user_skill表中查找匹配用户。 - 排除提问者自己。
- 按技能熟练度排序,取前 N 个。
@Override public List<User> matchPeers(Long questionId, int limit) { Question question = questionMapper.selectById(questionId); List<String> keywords = extractKeywords(question.getTitle() + " " + question.getContent()); Set<Long> userIds = new HashSet<>(); for (String keyword : keywords) { List<UserSkill> skills = userSkillMapper.selectList( new LambdaQueryWrapper<UserSkill>() .eq(UserSkill::getSkillName, keyword) ); for (UserSkill skill : skills) { userIds.add(skill.getUserId()); } } userIds.remove(question.getUserId()); if (userIds.isEmpty()) { return new ArrayList<>(); } return userMapper.selectList( new LambdaQueryWrapper<User>() .in(User::getId, userIds) .last("LIMIT " + limit) ); }extractKeywords是关键词抽取方法,MVP 阶段可以直接用简单的规则去匹配数据库中的技能标签,不引入 NLP 库。例如把用户技能表中已有的标签拿出来,然后判断问题文本中是否包含该标签。
生产环境的升级方向是:
- 使用 Embedding 向量表示问题文本,检索相似技能标签。
- 结合消息中心,把推荐结果转成站内通知。
- 根据用户采纳率动态调整推荐权重。
5. 前端调用逻辑与接口联调示例
5.1 接口规范设计
后端接口建议统一使用/api前缀,方便后面配置网关和统一鉴权。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/user/register | 用户注册 |
| POST | /api/user/login | 用户登录 |
| POST | /api/question/publish | 发布问题 |
| GET | /api/question/list | 问题列表 |
| GET | /api/question/detail/{id} | 问题详情 |
| POST | /api/ai/chat | AI 多轮对话 |
| GET | /api/peer/match/{questionId} | 匹配同伴 |
5.2 Axios 调用 AI 接口示例
前端使用 Vue 3 + Axios 时,核心请求代码如下:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 60000 }) // 发布问题并获取 AI 回答 export async function publishQuestion(questionData) { const res = await request.post('/question/publish', questionData) return res.data } // AI 多轮对话 export async function sendChatMessage(historyList) { const res = await request.post('/ai/chat', { questionId: 123, history: historyList, userMessage: '具体的问题内容' }) return res.data }前端联调时需要注意:
- Axios 的
timeout需要设置得比后端接口耗时更长,比如 60 秒。 history列表结构要和后端定义一致,避免序列化失败。- 如果部署时存在跨域问题,后端需要配置 CORS 或使用 Nginx 反向代理。
5.3 Nginx 反向代理配置示例
前后端分离项目部署时,通常用 Nginx 代理前端静态资源和后端接口。
server { listen 80; server_name helppeer.ai; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 大模型返回较慢,需要提高超时时间 proxy_connect_timeout 60s; proxy_read_timeout 120s; } location / { try_files $uri $uri/ /index.html; } }注意proxy_read_timeout这个配置。默认情况下 Nginx 读取后端响应超时是 60 秒,如果大模型生成内容耗时较长,加上前端轮询或等待逻辑,容易在 Nginx 层超时。部署环境如果不是 Nginx,也要检查网关层的超时配置。
6. 运行与验证流程
6.1 启动后端服务
确认 MySQL 和 Redis 已启动,在application.yml中配置好连接信息后,直接启动 Spring Boot 应用。
mvn spring-boot:run看到启动日志中有类似以下内容,说明启动成功:
Tomcat started on port 8080 (http) with context path '' Started HelppeerApplication in 3.21 seconds6.2 用 curl 验证接口
注册用户:
curl -X POST http://localhost:8080/api/user/register \ -H "Content-Type: application/json" \ -d '{"username": "zhangsan", "password": "123456"}'预期返回:
{ "code": 200, "message": "success", "data": { "id": 1, "username": "zhangsan", "nickname": "zhangsan" } }发布问题:
curl -X POST http://localhost:8080/api/question/publish \ -H "Content-Type: application/json" \ -d '{"title": "Spring Boot 3 如何集成 Redis?", "content": "我按照文档配置了依赖和连接信息,但启动时报错 Unable to connect to Redis,请问是什么原因?"}'如果 AI 模块配置正确,过一段时间后查询问题详情,能看到ai_answer字段已经有内容。
6.3 AI 接口常见响应结构
正常情况下,OpenAI 兼容接口的响应是一个 JSON:
{ "id": "chatcmpl-xxx", "object": "chat.completion", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Spring Boot 3 集成 Redis 报错的原因通常有以下几种:..." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 50, "completion_tokens": 120, "total_tokens": 170 } }后端代码中解析的是choices[0].message.content。不同的模型供应商可能返回结构略有差异,但兼容 OpenAI 格式的接口通常都遵循这个结构。
7. 常见问题与排查思路
在开发 helppeer.ai 这类 AI 业务系统时,遇到频率最高的问题集中在以下几类。下面以表格形式给出排查方向,后面再展开说明。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| AI 接口调用超时 | RestTemplate 默认超时太短 | 设置 readTimeout 为 30-60 秒 |
| AI 返回内容被截断 | max_tokens 太小 | 调大 max_tokens,或开启流式输出 |
| API Key 报 401 | Key 配置不正确 | 检查环境变量配置,确认 Key 前后无空格 |
| Redis 连接失败 | 密码、端口、IP 配置错误 | 用 redis-cli 验证连接 |
| 接口返回中文乱码 | 数据库编码不是 utf8mb4 | 建库时指定 CHARACTER SET utf8mb4 |
| 发布问题后 AI 回答为空 | AI 调用异步执行失败被吞掉 | 查看日志,检查 AI 接口返回与异常堆栈 |
| 前后端联调跨域 | 后端未配置 CORS | 添加 CORS 配置或使用 Nginx 代理 |
7.1 AI 接口超时
这个问题的根源是 RestTemplate 默认的 readTimeout 太短。Spring 的SimpleClientHttpRequestFactory默认超时通常为 0 或很短,但大模型生成回答普遍需要几秒到几十秒。解决方法是像上文一样设置 connectTimeout 和 readTimeout,或者改用 WebClient。
7.2 AI 返回内容不完整
如果 AI 回答在中间被硬生生截断,大概率是max_tokens不够。注意,max_tokens限制的是生成的最大 token 数,不是字符数。对于中文内容,1 个 token 大约对应 1-2 个汉字。如果需要生成长文,建议把max_tokens设置为 2000 或更高。
7.3 异步 AI 调用失败无感知
使用@Async后,子线程中的异常默认不会被主线程捕获,如果不处理,日志中甚至看不到错误。建议在异步方法内部显式 try-catch:
@Async @Override public void asyncGenerateFirstAnswer(Long questionId) { try { String answer = chat(questionTitle, null); // 更新 question 的 ai_answer 字段 } catch (Exception e) { log.error("生成AI回答失败, questionId={}", questionId, e); } }这是很重要的一点。异步任务里的异常一定要自己兜底,否则排查问题时非常困难。
7.4 跨域问题
Spring Boot 后端可以直接配置 CORS:
package com.helppeer.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }生产环境不建议使用allowedOriginPatterns("*")配合allowCredentials(true),应该把允许的域名写死。开发阶段为了方便可以放行所有来源。
8. 最佳实践与工程建议
8.1 API Key 安全
AI 平台的 API Key 是敏感凭证,必须遵循以下规范:
- 不要硬编码在前端代码中,否则会被用户直接抓包获取。
- 不要提交到 Git 仓库,使用环境变量或配置中心管理。
- 定期轮换 Key,必要时对单个用户做调用频率限制。
- 如果团队规模较大,建议通过后端代理 AI 接口,而不是让前端直连。
8.2 对话上下文管理
AI 多轮对话需要保留上下文。MVP 阶段可以把最近 N 轮对话存入 Redis,设置过期时间,避免用户长时间不活跃导致上下文堆积。
示例伪代码:
public String chatWithContext(Long userId, String userMessage) { String historyKey = "chat:history:" + userId; // 从 Redis 读取最近 10 条记录 List<Map<String, String>> history = redisTemplate.opsForList().range(historyKey, -10, -1); String reply = aiChatService.chat(userMessage, history); // 写入本次问答 redisTemplate.opsForList().rightPush(historyKey, Map.of("role", "user", "content", userMessage)); redisTemplate.opsForList().rightPush(historyKey, Map.of("role", "assistant", "content", reply)); // 设置过期时间为 1 小时 redisTemplate.expire(historyKey, Duration.ofHours(1)); return reply; }通过 Redis 保存上下文的前提是,问题不敏感。如果涉及用户隐私,需要评估数据存储方案与合规要求。
8.3 降级与容灾
AI 服务再稳定也可能出现不可用,因此业务链路要做降级设计:
- AI 调用失败时,问题仍然要保存成功,只是
ai_answer为空。 - 前端展示时,AI 不可用区域显示"AI 服务繁忙,等待人工回答"。
- 对 AI 接口增加熔断机制,连续失败 N 次后暂停调用一段时间。
- 关键业务接口增加重试,但要注意避免重复扣费。
8.4 Token 消耗监控
大模型 API 是按 token 计费的,上线前必须对 token 消耗做监控。
建议在 AI 调用完成后,把响应中的 usage 字段保存到日志或数据库:
请求 ID、用户 ID、模型名称、提示词 token、生成 token、总计 token、耗时这样每天可以算出用户维度的成本,设置预算上限,避免单个用户异常消耗大量 token。
8.5 数据库与接口安全
- 所有 SQL 操作都通过 MyBatis-Plus 条件构造器或参数绑定,避免字符串拼接导致 SQL 注入。
- 发布问题和回答接口要做内容长度校验,防止超长内容打满数据库。
- 敏感操作(如删除问题、修改用户信息)需要做权限校验,不能只看前端隐藏按钮。
- 上线前给数据库加好索引,尤其是
question.status、question.user_id这些高频查询字段。
9. 总结与下一步学习方向
helppeer.ai 这个项目虽然看起来是一个垂直的互助社区,但把它拆开后,涉及的技术点其实覆盖了一条完整的技术链路:用户注册登录、业务数据建模、AI 大模型接入、异步任务、Redis 缓存、消息通知、部署上线。这是一道性价比很高的综合实战训练题。
本文给出的代码是 MVP 版本的核心骨架,你在实际开发中可以沿这个方向继续扩展:
- 接入 WebSocket,让用户实时收到"有人回答了你"的通知,提升互动率。
- 把 AI 回答改为流式输出(SSE),逐字展示回答内容,体验会好很多。
- 引入消息队列(RocketMQ 或 RabbitMQ),把 AI 调用、积分赠送、通知发送解耦。
- 用向量数据库 + Embedding 做技能标签推荐,让同伴匹配更准确。
- 给问题列表增加 Elasticsearch 或 MySQL 全文索引,优化搜索体验。
总体来看,helppeer.ai 这类产品能不能做好,除了工程实现外,更重要的是产品机制——怎么让提问者快速获得高质量答案,怎么让回答者愿意持续贡献。技术上,我们要保证的是:AI 回答快、同伴匹配准、系统稳定可扩展。
如果你正在搭建类似项目,可以从本文的代码骨架开始,先把"提问 → AI 回答 → 匹配同伴"这条主链路跑通,再逐步叠加消息通知、积分体系、搜索等模块。技术选型不是最难的,难的是把每个环节的异常处理和服务降级做扎实。希望这篇文章对你的项目有帮助,也欢迎在评论区交流你在开发 AI 互助平台时遇到的问题。