后台自动生成文章标题功能实现
最近给博客后台加了个小功能:写文章时,标题输入框旁边加个按钮,点击后根据文章内容自动生成标题。不用自己想标题了,AI 帮你搞定。
一、功能需求
- 后台写文章时,标题输入框旁边有个「AI 生成标题」按钮
- 点击后根据文章内容自动生成标题
- 标题长度在 5-25 个字之间
- 生成后自动填入标题输入框
二、技术选型
- 后端:Spring Boot + OkHttp + DeepSeek/硅基流动 API
- 前端:Vue 3 + TypeScript + Axios + Element Plus
三、后端实现
1. 添加依赖
如果你的项目还没有 OkHttp 依赖,需要在 pom.xml 中添加:
xml
<!-- OkHttp (用于 AI 请求) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
2. 配置文件
在 application-dev.yml 中添加 AI 配置(如果还没有):
yaml
# AI 对话配置
ai:
deepseek:
api-key: ${AI_API_KEY:sk-你的API密钥}
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 格式。}
3. AI 服务类
在 AiService.java 中添加生成标题方法:
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();
/**
* 生成文章标题
*
* @param content 文章内容
* @return 标题文本
*/
public String generateTitle(String content) {
// 截取前 3000 字,避免 token 过长
String truncated = content.length() > 3000 ? content.substring(0, 3000) : content;
JSONArray messagesArray = new JSONArray();
// 系统提示词
JSONObject systemMsg = new JSONObject();
systemMsg.put("role", "system");
systemMsg.put("content", "你是一个文章标题生成助手。请根据文章内容,生成一个简洁、有吸引力的中文标题。" +
"要求:标题在5-25个字之间,不要加引号、书名号或其他标点包裹,直接输出标题文本。");
messagesArray.add(systemMsg);
// 用户消息
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", "请为以下文章生成一个标题:\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.7); // 提高创造性,标题更有吸引力
requestBody.put("max_tokens", 60); // 限制输出长度,约 5-25 字
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");
// 清理可能的引号包裹
if (result != null) {
result = result.trim().replaceAll("^[\"'《「【]|[\"'》」】]$", "");
}
return result;
}
}
return "";
} catch (IOException e) {
log.error("AI 标题生成异常", e);
throw new RuntimeException("AI 标题生成异常: " + e.getMessage());
}
}
}
关键点说明:
stream: false:非流式调用,同步返回结果temperature: 0.7:提高创造性,标题更有吸引力max_tokens: 60:限制输出长度,约 5-25 字- 截取前 3000 字:避免文章太长导致 token 超限
- 系统提示词明确要求:标题长度、不要标点包裹
- 后端清理:去除可能的引号、书名号等包裹
4. 控制器
在 AiController.java 中添加接口:
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 生成文章标题
*
* @param body 请求体,包含 content 字段(文章内容)
* @return 标题文本
*/
@ApiOperation(value = "AI 生成文章标题")
@PostMapping("/admin/ai/title")
public Result<String> generateTitle(@RequestBody Map<String, String> body) {
String content = body.get("content");
if (content == null || content.trim().isEmpty()) {
return Result.fail("文章内容不能为空");
}
return Result.success(aiService.generateTitle(content));
}
}
接口放在 /admin/ 路径下,需要登录才能访问(后台管理功能)。
四、前端实现
1. API 函数
在 api/article/index.ts 中添加:
typescript
import { Result } from "@/model";
import request from "@/utils/request";
import { AxiosPromise } from "axios";
/**
* AI 生成文章标题
* @param content 文章内容
* @returns 标题文本
*/
export function generateAiTitle(content: string): AxiosPromise<Result<string>> {
return request({
url: "/admin/ai/title",
method: "post",
timeout: 60000, // AI 调用可能需要较长时间,设置 60 秒超时
data: { content },
});
}
注意设置了 timeout: 60000(60 秒),因为 AI 调用可能需要几秒到十几秒。
2. 页面添加按钮
在 views/blog/article/write.vue 中,找到标题输入框,修改为:
vue
<template>
<div class="app-container">
<!-- 文章标题 -->
<div class="operation-container">
<el-input v-model="articleForm.articleTitle" placeholder="请输入文章标题">
<template #append>
<el-button :loading="aiTitleLoading" @click="handleAiTitle" title="AI 生成标题">
<el-icon><MagicStick /></el-icon>
</el-button>
</template>
</el-input>
<el-button type="danger" style="margin-left: 10px" @click="openModel">发布文章</el-button>
</div>
<!-- 文章内容编辑器 -->
<md-editor
ref="editorRef"
v-model="articleForm.articleContent"
:theme="isDark ? 'dark' : 'light'"
class="md-container"
:toolbars="toolbars"
@on-upload-img="uploadImg"
placeholder="请输入文章内容..."
>
<template #defToolbars>
<emoji-extension :on-insert="insert" />
</template>
</md-editor>
<!-- 发布文章对话框 -->
<!-- ... 原有发布对话框 ... -->
</div>
</template>
<script setup lang="ts">
import { generateAiTitle } from "@/api/article";
import { MagicStick } from '@element-plus/icons-vue';
import { ElMessage } from "element-plus";
import { useDark } from "@vueuse/core";
const isDark = useDark();
// 文章表单数据
const articleForm = ref({
articleTitle: "",
articleContent: "",
// ... 其他字段
});
// AI 生成标题
const aiTitleLoading = ref(false);
const handleAiTitle = async () => {
const content = articleForm.value.articleContent;
if (!content || content.trim() === "") {
ElMessage.warning("请先编写文章内容");
return;
}
aiTitleLoading.value = true;
try {
const { data } = await generateAiTitle(content);
if (data.flag && data.data) {
articleForm.value.articleTitle = data.data;
ElMessage.success("AI 标题生成成功");
} else {
ElMessage.error(data.msg || "AI 标题生成失败");
}
} catch {
ElMessage.error("AI 标题生成失败,请稍后重试");
} finally {
aiTitleLoading.value = false;
}
};
</script>
<style scoped>
.operation-container {
display: flex;
align-items: center;
margin-bottom: 1.25rem;
}
.md-container {
min-height: 300px;
height: calc(100vh - 200px);
}
</style>
按钮使用了 Element Plus 的 MagicStick 图标(魔法棒),放在输入框的 append 插槽中。
五、使用效果
- 在后台写文章页面,先写好文章内容
- 点击标题输入框右侧的魔法棒按钮
- 等待几秒(按钮显示 loading)
- AI 自动生成标题并填入输入框
- 可以手动修改后再发布
六、遇到的问题
1. AI 返回格式问题
问题 :AI 可能返回带引号或书名号的标题(如 "文章标题" 或 《文章标题》)。
解决 :在系统提示词中明确要求「不要加引号、书名号或其他标点包裹」。后端再用正则表达式清理:replaceAll("^[\"'《「【]|[\"'》」】]$", "")。
2. 标题长度控制
问题:AI 可能生成过短或过长的标题。
解决 :在系统提示词中明确要求「标题在5-25个字之间」。设置 max_tokens: 60,限制输出长度。
3. 文章内容为空
问题:用户没有写文章内容就点击生成标题。
解决:前端检查文章内容是否为空,为空时提示「请先编写文章内容」。
七、总结
这个功能实现起来不算复杂:
- 后端:新增一个同步调用的 AI 接口,专门用于生成标题
- 前端:加个按钮,调用 API,把结果填入输入框
关键是:
- 系统提示词要明确(标题长度、不要标点包裹)
- 设置合适的
temperature(提高创造性)和max_tokens(限制长度) - 前端设置足够的超时时间
- 处理错误情况,给用户友好提示
- 后端清理可能的标点包裹
现在写文章时,标题可以一键生成,省了不少时间。如果生成的不满意,还可以手动修改。
八、完整代码文件清单
后端:
pom.xml- 添加 OkHttp 依赖application-dev.yml- 添加 AI 配置AiService.java- 添加generateTitle方法AiController.java- 添加/admin/ai/title接口
前端:
api/article/index.ts- 添加generateAiTitle函数views/blog/article/write.vue- 添加标题生成按钮和逻辑
按照上面的步骤,就可以在自己的项目中实现这个功能了。