文澜知微开发日志(三)——DeepSeek AI 大模型接入
一、完成的工作项
| 序号 | 工作项 | 状态 | 说明 |
|---|---|---|---|
| 1 | DeepSeek API 对接 | ✅ 完成 | 后端封装 API 调用,支持多轮对话 |
| 2 | 通古AI API 对接(预留) | ✅ 完成 | 古文专精模型接入 |
| 3 | 文心一言 API 接入(预留) | ✅ 完成 | 接口框架已搭建,待后续完善 |
| 4 | AI 配置管理 | ✅ 完成 | 通过 application.yml 管理多模型配置 |
| 5 | AI 对话接口 | ✅ 完成 | 实现 /api/ai/chat 和 /api/ai/models 接口 |
| 6 | AI 智读界面 | ✅ 完成 | 独立的 AI 对话页面 |
| 7 | 多模型切换 | ✅ 完成 | 支持 DeepSeek/通古AI/文心一言切换 |
| 8 | Markdown 渲染 | ✅ 完成 | AI 回复支持 Markdown 格式展示 |
| 9 | 语音朗读功能 | ✅ 完成 | 浏览器 TTS 实现 AI 回复朗读 |
二、具体工作内容
1. 后端开发
| 功能 | 说明 |
|---|---|
| 配置类 | 创建AIConfig.java,使用@ConfigurationProperties读取多模型配置 |
| 服务层 | 创建AIService.java,封装 DeepSeek/通古AI 的 API 调用逻辑 |
| 控制器层 | 创建AIController.java,实现/api/ai/chat和/api/ai/models接口 |
| 系统提示词 | 分别为 DeepSeek 和通古AI 设计专属的系统提示词 |
2. 前端开发
| 模块 | 文件 | 功能说明 |
|---|---|---|
| API 服务 | services/aiServ.ts | 封装 AI 对话的 API 请求方法 |
| AI 智读页面 | views/ai/AIChat.vue | 独立的 AI 对话界面 |
| 路由配置 | router/index.ts | 添加/AIChat路由 |
| 菜单配置 | 数据库menu表 | 添加「AI智读」一级菜单 |
三、技术实现详解
1. 整体架构
┌─────────────────────────────────────────────────────────────┐ │ 前端 (Vue3) │ ├─────────────────────────────────────────────────────────────┤ │ AIChat.vue │ │ - 模型选择栏(DeepSeek/通古AI/文心一言) │ │ - 对话消息列表 │ │ - 推荐问题 /自定义输入 │ │ - Markdown 渲染 + 语音朗读 │ └───────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 后端 (Spring Boot) │ ├─────────────────────────────────────────────────────────────┤ │ AIController.java │ AIService.java │ AIConfig.java │ │ - /api/ai/chat │ - 模型路由分发 │ - 多模型配置管理 │ │ - /api/ai/models │ - HTTP 请求封装 │ │ └─────────┬───────────┴────────┬─────────┴────────────────────┘ │ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ DeepSeek API │ │ 通古AI API │ │ 文心一言 API │ │ (通用大模型) │ │ (预留接口) │ │ (预留接口) │ └─────────────────┘ └─────────────────┘ └─────────────────┘
2. 多模型配置管理
(1)配置文件
在application.yml中配置多个模型的参数:
ai: deepseek: api-key: sk-eeb4972c394d41e7a84b934ec966f78c api-url: https://api.deepseek.com/v1/chat/completions model: deepseek-chat tonggu: api-key: your-tonggu-api-key-here api-url: https://api.tongguai.com/v1/chat/completions model: tonggu-classical(2)配置类实现 newConfig/AiConfig.java
@Component @ConfigurationProperties(prefix = "ai") public class AIConfig { private DeepSeek deepseek = new DeepSeek(); private Tonggu tonggu = new Tonggu(); public static class DeepSeek { private String apiKey; private String apiUrl; private String model; // getters and setters } public static class Tonggu { private String apiKey; private String apiUrl; private String model; // getters and setters } // getters and setters }3. API调用实现
调用deepseek api的官方文档示例:
由于官方未提供 Java SDK,我们参考 Python 示例,自行封装了 Java 版本的 API 调用工具类AIService.java,核心逻辑如下:
| 官方 Python 示例 | 我们的 Java 实现 |
|---|---|
requests.post() | CloseableHttpClient.execute(HttpPost) |
json=data | StringEntity(JSON.toJSONString(requestBody)) |
headers={"Authorization": "Bearer xxx"} | httpPost.setHeader("Authorization", "Bearer " + apiKey) |
response.json() | JSON.parseObject(responseBody) |
使用
CloseableHttpClient发起 POST 请求设置
Content-Type和Authorization请求头构建符合 OpenAI 格式的 JSON 请求体
解析返回的 JSON 响应,提取
choices[0].message.content
具体代码实现:
@Service public class AIService { @Autowired private AIConfig aiConfig; /** * 执行 HTTP 请求 */ private String executeHttpRequest(String url, String apiKey, String requestBody) throws IOException { // 1. 创建 HTTP 客户端 try (CloseableHttpClient httpClient = HttpClients.createDefault()) { // 2. 构建 POST 请求 HttpPost httpPost = new HttpPost(url); // 3. 设置请求头 httpPost.setHeader("Content-Type", "application/json"); httpPost.setHeader("Authorization", "Bearer " + apiKey); // 4. 设置请求体 httpPost.setEntity(new StringEntity(requestBody, "UTF-8")); // 5. 发送请求并获取响应 try (CloseableHttpResponse response = httpClient.execute(httpPost)) { String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); // 6. 解析响应 return parseResponse(responseBody); } } } /** * 解析 API 响应 * 对应 Python 的 response.json()["choices"][0]["message"]["content"] */ private String parseResponse(String responseBody) { JSONObject json = JSON.parseObject(responseBody); JSONArray choices = json.getJSONArray("choices"); if (choices != null && !choices.isEmpty()) { JSONObject choice = choices.getJSONObject(0); return choice.getJSONObject("message").getString("content"); } return "AI 响应格式异常"; } }3. DeepSeek API 接入
(1)获取 API Key
访问 DeepSeek 开放平台
注册账号并登录
进入「API Keys」页面,创建新的 API Key
复制保存 API Key(格式:
sk-xxxxxxxxxxxxxxxx)
(2)API 接口说明
DeepSeek 的对话接口与 OpenAI 兼容:
接口地址:
https://api.deepseek.com/v1/chat/completions请求方式:POST
认证方式:Bearer Token
请求体格式:
json
{ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是一位古籍助手"}, {"role": "user", "content": "学而时习之是什么意思?"} ], "temperature": 0.7, "max_tokens": 2000 }响应体格式:
json
{ "id": "xxx", "choices": [ { "message": { "role": "assistant", "content": "学而时习之的意思是..." } } ] }(3)服务层实现 newService/AIService.java
@Service public class AIService { @Autowired private AIConfig aiConfig; /** * 调用AI模型 */ public String chat(String model, String message, List<Map<String, String>> history) throws IOException { switch (model) { case "deepseek": return callDeepSeek(message, history); case "tonggu": return callTonggu(message, history); case "wenxin": return callWenxin(message, history); default: return "暂不支持该模型"; } } /** * 调用DeepSeek API */ private String callDeepSeek(String message, List<Map<String, String>> history) throws IOException { // 构建消息列表 JSONArray messages = new JSONArray(); // 添加系统提示 JSONObject systemMsg = new JSONObject(); systemMsg.put("role", "system"); systemMsg.put("content", getSystemPrompt()); messages.add(systemMsg); // 添加历史对话 - 增加空值检查 if (history != null) { for (Map<String, String> hist : history) { // 确保 content 不为 null String content = hist.get("content"); if (content == null) { content = ""; // 将 null 转为空字符串 } JSONObject histMsg = new JSONObject(); histMsg.put("role", hist.get("role")); histMsg.put("content", content); messages.add(histMsg); } } // 添加当前消息 - 确保 message 不为空 JSONObject userMsg = new JSONObject(); userMsg.put("role", "user"); userMsg.put("content", message == null ? "" : message); messages.add(userMsg); // 打印调试信息,查看消息内容 System.out.println("Messages to DeepSeek: " + messages.toJSONString()); // 构建请求体 JSONObject requestBody = new JSONObject(); requestBody.put("model", aiConfig.getDeepseek().getModel()); requestBody.put("messages", messages); requestBody.put("temperature", 0.7); requestBody.put("max_tokens", 2000); // 发送请求 return executeHttpRequest(aiConfig.getDeepseek().getApiUrl(), aiConfig.getDeepseek().getApiKey(), requestBody.toJSONString()); } /** * 系统提示词(通用) */ private String getSystemPrompt() { return "你是一位古籍智慧问答助手,专门解答关于中国古典文献、古文翻译、历史典故、诗词赏析等方面的问题。" + "请用中文回答,语言简洁明了,对于古文问题可以适当引用原文。"; } }服务层设计
AIService作为 AI 功能的核心服务类,采用策略模式实现多模型路由。通过switch语句根据前端传入的模型标识,分发到对应的 API 调用方法。这种设计使得新增模型时只需添加新的分支,不影响现有逻辑。
在调用 DeepSeek API 时,构建符合 OpenAI 格式的消息列表,依次添加系统提示词、历史对话和当前消息。系统提示词预设了古籍专家的角色,确保 AI 回答的专业性。多处空值检查体现了防御性编程思想,调试日志则便于开发阶段的问题排查。
流程:
- 用户发送消息
- AIController 接收请求
- 调用 AIService.chat(model, message, history)
- 根据 model 路由到对应方法
- 构建消息列表:( 1. 添加 system 提示词 2. 添加 history 历史对话 3. 添加当前 user 消息)
- 构建请求体(model + messages + temperature + max_tokens)
- 发送 HTTP POST 请求到 DeepSeek API
- 解析响应,提取 choices[0].message.content
- 返回 AI 回复给前端
5. 控制器层实现 newController/AIController.java
@CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api/ai") public class AIController { @Autowired private AIService aiService; /** * AI对话接口 */ @PostMapping("/chat") public DataResponse chat(@Valid @RequestBody DataRequest dataRequest) { String model = dataRequest.getString("model"); String message = dataRequest.getString("message"); List<Map<String, String>> history = dataRequest.getList("history"); // 防御性检查:确保消息不为空 if (message == null || message.trim().isEmpty()) { return CommonMethod.getReturnMessageError("消息内容不能为空"); } // 修复历史消息:确保角色交替 if (history != null && !history.isEmpty()) { List<Map<String, String>> fixedHistory = new ArrayList<>(); String expectedRole = "user"; for (Map<String, String> msg : history) { String role = msg.get("role"); if (role.equals(expectedRole)) { fixedHistory.add(msg); expectedRole = "assistant".equals(role) ? "user" : "assistant"; } } history = fixedHistory; } try { String reply = aiService.chat(model, message, history); Map<String, Object> result = new HashMap<>(); result.put("content", reply); result.put("model", model); result.put("timestamp", System.currentTimeMillis()); return CommonMethod.getReturnData(result); } catch (Exception e) { e.printStackTrace(); return CommonMethod.getReturnMessageError("AI服务暂时不可用"); } } /** * 获取可用模型列表 */ @PostMapping("/models") public DataResponse getModels() { List<Map<String, String>> models = new ArrayList<>(); Map<String, String> deepseek = new HashMap<>(); deepseek.put("id", "deepseek"); deepseek.put("name", "DeepSeek"); deepseek.put("description", "通用大模型,擅长各类问答"); models.add(deepseek); Map<String, String> tonggu = new HashMap<>(); tonggu.put("id", "tonggu"); tonggu.put("name", "通古AI"); tonggu.put("description", "古文专精模型,擅长古籍解读"); models.add(tonggu); Map<String, String> wenxin = new HashMap<>(); wenxin.put("id", "wenxin"); wenxin.put("name", "文心一言"); wenxin.put("description", "百度大模型,即将接入"); models.add(wenxin); return CommonMethod.getReturnData(models); } }流程:
- 前端发起 POST /api/ai/chat
- @CrossOrigin 处理 CORS
- @RequestBody 解析 JSON
- 参数提取(模型识别-model, 用户当前信息-message, 历史对话-history每个元素包含role和content)
- 防御性检查:message 是否为空,避免无效API调用
- 历史消息修复:确保role交替,用户会话和ai助手交替会话,只保留符合交替规则的消息,不符合的直接跳过。
- 调用服务处aiService.chat()
- 返回 JSON 响应给前端
6. Markdown 渲染
在 Web 开发中,Markdown 并不能直接被浏览器识别,需要借助解析器(Parser)将其转换为 HTML,再进行渲染。
本项目使用marked库来解析 Markdown
特点:
- 高性能,解析速度快
- 兼容性好,社区活跃
- API 灵活,可自定义渲染方式
适用场景:
- 注重性能的应用(如实时预览)
- 需要快速上手、语法覆盖率较广的项目
npm install marked在vue中
import { marked } from 'marked'; methods: { renderMarkdown(content: string): string { if (!content) return ''; try { return marked.parse(content, { async: false }) as string; } catch (error) { console.error('Markdown解析失败:', error); return content.replace(/\n/g, '<br>'); } } }<div class="message-text markdown-body" v-html="renderMarkdown(msg.content)" ></div>7. 前端 AI 智读界面
(1)页面结构设计
仅作为测试页面,后续将开发AI阅读助手功能
功能点:
支持多轮对话,上下文关联
AI 回复支持 Markdown 格式(标题、列表、粗体、代码等)
一键复制 AI 回复内容
语音朗读 AI 回复
加载状态提示
(2)语音朗读TTS(Text-to-Speech)
// 语音朗读 speakText(text: string) { if ('speechSynthesis' in window) { const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'zh-CN'; utterance.rate = 0.9; speechSynthesis.cancel(); speechSynthesis.speak(utterance); } else { console.log('浏览器不支持语音朗读'); } },SpeechSynthesisUtterance 常用属性
| 属性 | 说明 | 示例值 |
|---|---|---|
lang | 语言 | 'zh-CN' |
rate | 语速 | 0.9(0.1~10) |
pitch | 音调 | 1.0(0~2) |
volume | 音量 | 0.8(0~1) |
voice | 指定声音 | 通过getVoices()获取 |
Web Speech API 工作流程
- 用户点击「朗读」按钮
- 传入文字内容
- 创建 SpeechSynthesisUtterance 对象
- 配置语言、语速等参数
- 调用 speechSynthesis.speak()
- 浏览器调用系统 TTS 引擎
- 扬声器播放语音
四、下周计划
AI 侧边栏集成:将 AI 功能集成到阅读页侧边栏
其他大模型接入:接入通古/文心一言
选中文字提问:阅读页选中文字后一键发送给 AI
