文章目录
- 文章详情页 AI 快速阅读功能实现
-
- 一、功能需求
- 二、技术选型
- 三、后端实现
-
- 1. 添加依赖
- 2. 配置文件
- 3. 配置属性类
- 4. AI 服务类
- 5. 控制器
- 四、前端实现
-
- 1. API 函数
- 2. 文章详情页添加快速阅读卡片
- 五、使用效果
- 六、遇到的问题
-
- 1. AI 返回格式问题
- 2. 文章内容过长
- 3. 用户体验
- 七、总结
- 八、完整代码文件清单
文章详情页 AI 快速阅读功能实现
最近给博客加了个小功能:用户进入文章详情页时,自动生成一段不超过 200 字的概要,用浅紫色半透明卡片展示。这样用户不用看完整个文章,就能快速了解文章的核心内容。
一、功能需求
- 进入文章详情页时,自动触发 AI 生成概要
- 概要不超过 200 字
- 用浅紫色半透明卡片展示,带 AI 图标
- 支持关闭和重试
二、技术选型
- 后端:Spring Boot + OkHttp + DeepSeek/硅基流动 API
- 前端:Vue 3 + TypeScript + Axios
三、后端实现
1. 添加依赖
如果你的项目还没有 OkHttp 依赖,需要在 pom.xml 中添加:
<!– OkHttp (用于 AI 流式请求) –>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
注意:快速阅读功能用的是同步调用,不需要 SSE 依赖。但如果你已经有 AI 对话功能,应该已经添加了。
2. 配置文件
在 application-dev.yml 或 application.yml 中添加 AI 配置:
# AI 对话配置
ai:
deepseek:
# API Key(从 https://siliconflow.cn 或 https://platform.deepseek.com 获取)
api-key: ${AI_API_KEY:sk–你的API密钥}
# API 地址(硅基流动免费,DeepSeek 需要付费)
api-url: ${AI_API_URL:https://api.siliconflow.cn/v1/chat/completions}
# 模型名称
model: ${AI_MODEL:deepseek–ai/DeepSeek–V3–0324}
# 系统提示词
system-prompt: ${AI_SYSTEM_PROMPT:你是一个博客智能助手,帮助用户解答技术问题。请用简洁、专业的中文回答,支持 Markdown 格式。}
获取 API Key:
- 硅基流动(推荐,免费):https://siliconflow.cn 注册账号,创建 API Key
- DeepSeek:https://platform.deepseek.com 注册账号,充值后创建 API Key
3. 配置属性类
创建 DeepSeekProperties.java(如果还没有的话):
package com.ican.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* DeepSeek AI 配置属性
*
* @author ican
*/
@Data
@Component
@ConfigurationProperties(prefix = "ai.deepseek")
public class DeepSeekProperties {
/**
* API Key
*/
private String apiKey;
/**
* API 地址
*/
private String apiUrl = "https://api.deepseek.com/chat/completions";
/**
* 模型名称
*/
private String model = "deepseek-chat";
/**
* 系统提示词
*/
private String systemPrompt = "你是一个博客智能助手,帮助用户解答技术问题。请用简洁、专业的中文回答,支持 Markdown 格式。";
}
4. AI 服务类
在 AiService.java 中添加快速阅读方法:
package com.ican.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ican.config.properties.DeepSeekProperties;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class AiService {
@Autowired
private DeepSeekProperties deepSeekProperties;
private final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
/**
* 快速阅读(200字概要)
*
* @param content 文章内容
* @return 200字左右的概要
*/
public String quickRead(String content) {
// 截取前 4000 字,避免 token 过长
String truncated = content.length() > 4000 ? content.substring(0, 4000) : content;
JSONArray messagesArray = new JSONArray();
// 系统提示词
JSONObject systemMsg = new JSONObject();
systemMsg.put("role", "system");
systemMsg.put("content", "你是一个文章速读助手。请根据文章内容,生成一段不超过200字的中文概要。" +
"要求:抓住文章核心要点,语言简洁流畅,不要使用 Markdown 格式,不要加前缀,直接输出概要。");
messagesArray.add(systemMsg);
// 用户消息
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", "请为以下文章生成200字以内的快速阅读概要:\\n\\n" + truncated);
messagesArray.add(userMsg);
JSONObject requestBody = new JSONObject();
requestBody.put("model", deepSeekProperties.getModel());
requestBody.put("messages", messagesArray);
requestBody.put("stream", false); // 非流式,同步返回
requestBody.put("temperature", 0.3); // 降低随机性,概要更稳定
requestBody.put("max_tokens", 400); // 限制输出长度,约 200 字
Request request = new Request.Builder()
.url(deepSeekProperties.getApiUrl())
.addHeader("Authorization", "Bearer " + deepSeekProperties.getApiKey())
.addHeader("Content-Type", "application/json")
.post(RequestBody.create(requestBody.toJSONString(), MediaType.parse("application/json")))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
String body = response.body() != null ? response.body().string() : "";
log.error("AI 快速阅读失败: {} {}", response.code(), body);
throw new RuntimeException("AI 快速阅读失败: " + response.code());
}
String responseBody = response.body().string();
JSONObject jsonResponse = JSON.parseObject(responseBody);
JSONArray choices = jsonResponse.getJSONArray("choices");
if (choices != null && !choices.isEmpty()) {
JSONObject message = choices.getJSONObject(0).getJSONObject("message");
if (message != null) {
String result = message.getString("content");
return result != null ? result.trim() : "";
}
}
return "";
} catch (IOException e) {
log.error("AI 快速阅读异常", e);
throw new RuntimeException("AI 快速阅读异常: " + e.getMessage());
}
}
}
关键点说明:
- stream: false:非流式调用,同步返回结果
- temperature: 0.3:降低随机性,概要更稳定
- max_tokens: 400:限制输出长度,约 200 字
- 截取前 4000 字:避免文章太长导致 token 超限
- 系统提示词明确要求:不要 Markdown,不要前缀,直接输出概要
5. 控制器
在 AiController.java 中添加接口:
package com.ican.controller;
import com.ican.model.vo.Result;
import com.ican.service.AiService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* AI 对话控制器
*
* @author ican
*/
@Api(tags = "AI 模块")
@RestController
public class AiController {
@Autowired
private AiService aiService;
/**
* AI 快速阅读(200字概要,前台文章详情页使用)
*
* @param body 请求体,包含 content 字段(文章内容)
* @return 200字左右的概要
*/
@ApiOperation(value = "AI 快速阅读")
@PostMapping("/ai/quick-read")
public Result<String> quickRead(@RequestBody Map<String, String> body) {
String content = body.get("content");
if (content == null || content.trim().isEmpty()) {
return Result.fail("文章内容不能为空");
}
return Result.success(aiService.quickRead(content));
}
}
接口放在 /ai/ 路径下,不需要登录(前台功能)。
四、前端实现
1. API 函数
在 api/article/index.ts 中添加:
import { PageQuery, PageResult, Result } from "@/model";
import request from "@/utils/request";
import { AxiosPromise } from "axios";
import { Article, ArticleInfo, ArticleRank, ArticleRecommend, ArticleSearch } from "./types";
/**
* AI 快速阅读
* @param content 文章内容
* @returns 200字概要
*/
export function quickReadArticle(content: string): AxiosPromise<Result<string>> {
return request({
url: "/ai/quick-read",
method: "post",
timeout: 60000, // AI 调用可能需要较长时间,设置 60 秒超时
data: { content },
});
}
注意设置了 timeout: 60000(60 秒),因为 AI 调用可能需要几秒到十几秒。
2. 文章详情页添加快速阅读卡片
在 views/Article/Article.vue 中:
<template>
<div class="bg">
<div class="main-container" v-if="article">
<div class="left-container" :class="app.sideFlag ? 'w-full' : ''">
<!– AI 快速阅读 –>
<div class="quick-read-card" v-if="quickReadVisible">
<div class="quick-read-header">
<div class="quick-read-title">
<svg-icon icon-class="ai" size="1.2rem" style="margin-right: 0.4rem"></svg-icon>
<span>AI 快速阅读</span>
</div>
<button class="quick-read-close" @click="quickReadVisible = false">
<svg-icon icon-class="close" size="0.8rem"></svg-icon>
</button>
</div>
<div class="quick-read-body">
<div v-if="quickReadLoading" class="quick-read-loading">
<span class="dot-loading"></span>
AI 正在阅读文章…
</div>
<div v-else-if="quickReadError" class="quick-read-error">
{{ quickReadError }}
<button class="retry-btn" @click="fetchQuickRead">重试</button>
</div>
<p v-else class="quick-read-content">{{ quickReadText }}</p>
</div>
</div>
<!– 文章内容 –>
<div class="article-container">
<!– … 原有文章内容 … –>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getArticle, likeArticle, quickReadArticle } from "@/api/article";
import { ArticleInfo, ArticlePagination } from "@/api/article/types";
import { useAppStore, useBlogStore, useUserStore } from "@/store";
import { formatDate } from "@/utils/date";
const user = useUserStore();
const app = useAppStore();
const blog = useBlogStore();
const route = useRoute();
// 文章数据
const article = ref<ArticleInfo | null>(null);
// AI 快速阅读
const quickReadVisible = ref(true);
const quickReadLoading = ref(false);
const quickReadText = ref("");
const quickReadError = ref("");
const fetchQuickRead = () => {
if (!article.value?.articleContent) return;
quickReadLoading.value = true;
quickReadError.value = "";
quickReadArticle(article.value.articleContent)
.then(({ data }) => {
if (data.flag && data.data) {
quickReadText.value = data.data;
} else {
quickReadError.value = data.msg || "生成失败";
}
})
.catch(() => {
quickReadError.value = "AI 服务暂时不可用,请稍后重试";
})
.finally(() => {
quickReadLoading.value = false;
});
};
onMounted(() => {
getArticle(Number(route.params.id)).then(({ data }) => {
article.value = data.data;
// … 其他逻辑 …
// 自动触发 AI 快速阅读
fetchQuickRead();
});
});
</script>
<style lang="scss" scoped>
/* AI 快速阅读卡片 */
.quick-read-card {
margin-bottom: 1rem;
border-radius: 0.75rem;
background: rgba(147, 112, 219, 0.12);
backdrop-filter: blur(10px);
border: 1px solid rgba(147, 112, 219, 0.25);
overflow: hidden;
box-shadow: 0 2px 12px rgba(147, 112, 219, 0.1);
animation: fadeInDown 0.5s ease;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.quick-read-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(147, 112, 219, 0.15);
}
.quick-read-title {
display: flex;
align-items: center;
font-size: 0.95rem;
font-weight: 600;
color: #6a3fbf;
}
.quick-read-close {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
border-radius: 50%;
color: var(–grey-5);
transition: all 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.06);
color: var(–grey-7);
}
}
.quick-read-body {
padding: 1rem 1.25rem;
}
.quick-read-content {
margin: 0;
font-size: 0.9rem;
line-height: 1.7;
color: var(–grey-7);
}
.quick-read-loading {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #6a3fbf;
}
.dot-loading {
display: inline-block;
width: 1.5rem;
height: 0.5rem;
&::after {
content: "…";
animation: dotAnim 1.5s steps(3, end) infinite;
font-size: 1.2rem;
letter-spacing: 2px;
}
}
@keyframes dotAnim {
0% { content: "."; }
33% { content: ".."; }
66% { content: "…"; }
}
.quick-read-error {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #e85d5d;
}
.retry-btn {
background: rgba(147, 112, 219, 0.15);
border: 1px solid rgba(147, 112, 219, 0.3);
border-radius: 0.3rem;
padding: 0.15rem 0.5rem;
color: #6a3fbf;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(147, 112, 219, 0.25);
}
}
</style>
五、使用效果
六、遇到的问题
1. AI 返回格式问题
问题:AI 可能返回带 Markdown 格式的文本,或者带前缀「概要:」。
解决:在系统提示词中明确要求「不要使用 Markdown 格式,不要加任何前缀,直接输出概要」。后端再做一些清理。
2. 文章内容过长
问题:文章可能有几万字,全部传给 AI 会超出 token 限制。
解决:后端截取前 4000 字,足够生成概要,也避免 token 超限。
3. 用户体验
问题:AI 调用可能需要几秒到十几秒,用户等待时需要有反馈。
解决:显示 loading 状态和加载动画,失败时显示错误信息和重试按钮。
七、总结
这个功能实现起来不算复杂:
关键是:
- 系统提示词要明确(不要 Markdown,不要前缀)
- 设置合适的 temperature 和 max_tokens
- 前端设置足够的超时时间
- 处理错误情况,给用户友好提示(loading、重试)
现在用户进入文章页时,可以快速了解文章内容,不用看完整个文章。如果生成的不满意,也可以关闭卡片,不影响正常阅读。
八、完整代码文件清单
后端:
- pom.xml – 添加 OkHttp 依赖
- application-dev.yml – 添加 AI 配置
- DeepSeekProperties.java – 配置属性类
- AiService.java – 添加 quickRead 方法
- AiController.java – 添加 /ai/quick-read 接口
前端:
- api/article/index.ts – 添加 quickReadArticle 函数
- views/Article/Article.vue – 添加快速阅读卡片和逻辑
按照上面的步骤,就可以在自己的项目中实现这个功能了。
网硕互联帮助中心







评论前必须登录!
注册