从0到1开发一个新闻智能体
摘要 本项目构建了一个基于大语言模型的新闻智能体系统,旨在解决传统新闻平台的痛点。系统采用分层架构,包含用户交互层、API网关层、业务服务层、AI能力层和数据存储层。核心功能包括智能摘要生成、语义检索、个性化推荐、情感分析和语音播报等。技术选型上采用Spring Boot、通义千问大模型、Elasticsearch向量数据库等组件。数据库设计包含新闻源表和新闻文章表,支持多语言处理和情感分析。该系
从0到1开发一个新闻智能体
基于大模型的智能体实战,覆盖新闻采集、摘要生成、语义检索、个性化推荐与语音播报全流程。
一、项目背景与目标
传统新闻聚合平台主要依赖编辑人工筛选和简单规则推荐,存在以下痛点:
- 信息过载:每天产生的新闻量巨大,用户难以快速获取感兴趣的内容
- 摘要质量差:基于模板或抽取式摘要,缺乏对语义的理解
- 推荐不精准:基于标签的关键词匹配,无法理解用户真实意图
- 交互形式单一:仅支持文字阅读,缺乏语音播报等沉浸式体验
本项目旨在构建一个新闻智能体,利用大语言模型(LLM)具备以下能力:
| 能力 | 说明 |
|---|---|
| 智能摘要 | 对新闻进行多维度、可控长度的语义摘要 |
| 语义检索 | 基于向量数据库的语义级别新闻搜索 |
| 个性化推荐 | 结合用户画像和新闻语义的精准推荐 |
| 情感分析 | 对新闻内容和用户评论进行细粒度情感判断 |
| 语音播报 | 将新闻摘要转为自然流畅的语音 |
| 多语言翻译 | 支持中英日等多语言新闻互译 |
二、整体架构
┌─────────────────────────────────────────────────────────────┐
│ 用户交互层 │
│ Web App / 小程序 / 语音助手 │
└──────────────┬──────────────────────────────────────────────┘
│
┌──────────────▼──────────────────────────────────────────────┐
│ API 网关层 │
│ Spring Cloud Gateway / Nginx │
└──────────────┬──────────────────────────────────────────────┘
│
┌──────────────▼──────────────────────────────────────────────┐
│ 业务服务层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │新闻采集 │ │智能摘要 │ │语义检索 │ │个性化推荐 │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────▼────────────▼────────────▼────────────▼─────┐ │
│ │ Agent 编排引擎 │ │
│ │ (Spring AI Alibaba / LangChain4j) │ │
│ └──────────────────┬───────────────────────────────┘ │
└─────────────────────┼───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ AI 能力层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ LLM 大模型│ │ Embedding│ │ ASR 语音 │ │ TTS 语音 │ │
│ │ 通义千问 │ │ 向量化 │ │ 识别 │ │ 合成 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ 数据存储层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ MySQL │ │Elasticsearch│ │ Redis │ │ OSS │ │
│ │ 业务数据 │ │ 向量+全文检索 │ │ 缓存 │ │ 文件存储 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
三、技术选型
| 层级 | 技术 | 选型理由 |
|---|---|---|
| 开发框架 | Spring Boot 3.x | Java 生态成熟,团队技术栈统一 |
| AI 集成 | Spring AI Alibaba | 阿里云官方 AI 框架,原生支持通义千问 |
| 大模型 | 通义千问(Qwen) | 中文能力强,支持 Function Calling |
| 向量数据库 | Elasticsearch 8.x | 原生支持 BM25 + kNN 混合检索 |
| 消息队列 | Kafka | 新闻采集管道的异步解耦 |
| 定时任务 | XXL-JOB | 采集任务的分布式调度 |
| 缓存 | Redis | 热点新闻缓存、用户会话管理 |
| 对象存储 | 阿里云 OSS | 音频文件、图片等资源存储 |
| 数据库 | MySQL | 业务元数据存储 |
四、数据库设计
4.1 新闻源表 news_source
记录配置的新闻数据来源。
CREATE TABLE `news_source` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL COMMENT '新闻源名称,如:新华网、澎湃新闻',
`source_type` TINYINT NOT NULL COMMENT '来源类型:1-RSS, 2-API, 3-网页爬取',
`url` VARCHAR(512) NOT NULL COMMENT 'RSS地址或API接口或网站首页URL',
`category` VARCHAR(64) DEFAULT NULL COMMENT '新闻分类:时政、财经、科技、体育等',
`language` VARCHAR(16) DEFAULT 'zh' COMMENT '语言:zh/en/ja',
`crawl_interval` INT DEFAULT 30 COMMENT '采集间隔(分钟)',
`enabled` TINYINT DEFAULT 1 COMMENT '是否启用',
`status` TINYINT DEFAULT 0 COMMENT '状态:0-正常, 1-异常',
`last_crawl_time` DATETIME DEFAULT NULL COMMENT '最近一次采集时间',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`del_flag` TINYINT DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新闻源配置表';
4.2 新闻文章表 news_article
存储采集到的新闻元数据。
CREATE TABLE `news_article` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`source_id` BIGINT NOT NULL COMMENT '新闻源ID',
`title` VARCHAR(512) NOT NULL COMMENT '标题',
`author` VARCHAR(128) DEFAULT NULL COMMENT '作者',
`content` LONGTEXT COMMENT '正文内容',
`content_hash` VARCHAR(64) DEFAULT NULL COMMENT '内容摘要Hash,用于去重',
`cover_image_url` VARCHAR(512) DEFAULT NULL COMMENT '封面图URL',
`publish_time` DATETIME DEFAULT NULL COMMENT '发布时间',
`category` VARCHAR(64) DEFAULT NULL COMMENT '分类',
`tags` VARCHAR(512) DEFAULT NULL COMMENT '标签,逗号分隔',
`language` VARCHAR(16) DEFAULT 'zh',
`sentiment` VARCHAR(32) DEFAULT NULL COMMENT '情感倾向:positive/negative/neutral',
`summary` TEXT DEFAULT NULL COMMENT 'AI生成的摘要',
`summary_en` TEXT DEFAULT NULL COMMENT '英文摘要',
`audio_url` VARCHAR(512) DEFAULT NULL COMMENT '语音播报音频URL',
`embedding_status` TINYINT DEFAULT 0 COMMENT '向量化状态:0-未处理, 1-已处理',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`del_flag` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_source_publish` (`source_id`, `publish_time`),
KEY `idx_category` (`category`),
KEY `idx_content_hash` (`content_hash`),
KEY `idx_publish_time` (`publish_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新闻文章表';
4.3 用户画像表 user_profile
存储用户的兴趣偏好。
CREATE TABLE `user_profile` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`preferred_categories` VARCHAR(256) DEFAULT NULL COMMENT '偏好分类,JSON数组',
`preferred_tags` VARCHAR(512) DEFAULT NULL COMMENT '偏好标签,JSON数组',
`language` VARCHAR(16) DEFAULT 'zh',
`interaction_count` INT DEFAULT 0 COMMENT '总交互次数',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户画像表';
4.4 用户交互记录表 user_interaction
记录用户与新闻的交互行为,用于推荐系统的特征工程。
CREATE TABLE `user_interaction` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`article_id` BIGINT NOT NULL,
`action` VARCHAR(32) NOT NULL COMMENT '行为类型:click/read/like/share/dislike',
`duration` INT DEFAULT NULL COMMENT '阅读时长(秒)',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_action` (`user_id`, `action`),
KEY `idx_article` (`article_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户交互记录表';
五、项目结构
news-agent-server/
├── pom.xml
├── src/main/java/com/example/newsagent/
│ ├── NewsAgentApplication.java
│ ├── config/
│ │ ├── DashScopeConfig.java # 大模型客户端配置
│ │ ├── ElasticsearchConfig.java # ES 向量检索配置
│ │ └── KafkaConfig.java # 消息队列配置
│ ├── agent/
│ │ ├── NewsAgent.java # 智能体编排核心
│ │ ├── tool/
│ │ │ ├── NewsSearchTool.java # 新闻搜索工具
│ │ │ ├── SummaryTool.java # 摘要生成工具
│ │ │ ├── TranslateTool.java # 翻译工具
│ │ │ └── SentimentTool.java # 情感分析工具
│ │ └── prompt/
│ │ └── PromptTemplate.java # Prompt 模板管理
│ ├── crawl/
│ │ ├── CrawlScheduler.java # 采集调度器
│ │ ├── RssCrawler.java # RSS 采集器
│ │ ├── WebCrawler.java # 网页采集器
│ │ └── CrawlPipeline.java # 采集后处理管道
│ ├── enrichment/
│ │ ├── ArticleEnrichmentService.java # 文章丰富化(分类、实体抽取、情感)
│ │ ├── SummaryService.java # 摘要生成
│ │ ├── EmbeddingService.java # 文章向量化
│ │ └── TtsService.java # 文本转语音
│ ├── search/
│ │ ├── NewsSearchService.java # 新闻搜索(混合检索)
│ │ └── RecommendService.java # 个性化推荐
│ ├── domain/
│ │ ├── NewsArticle.java
│ │ ├── NewsSource.java
│ │ ├── UserProfile.java
│ │ └── UserInteraction.java
│ ├── dao/
│ │ ├── NewsArticleMapper.java
│ │ ├── NewsSourceMapper.java
│ │ └── UserProfileMapper.java
│ └── controller/
│ ├── NewsController.java # 新闻查询接口
│ ├── ChatController.java # 智能体对话接口
│ └── UserController.java # 用户偏好管理接口
└── src/main/resources/
├── application.yml
├── mapper/
│ ├── NewsArticleMapper.xml
│ └── NewsSourceMapper.xml
└── prompts/
├── summary.txt # 摘要生成 Prompt
├── classify.txt # 分类 Prompt
└── sentiment.txt # 情感分析 Prompt
六、核心模块实现
6.1 大模型接入
引入 Spring AI Alibaba 依赖:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>1.0.0-M5</version>
</dependency>
配置文件 application.yml:
spring:
ai:
dashscope:
# TODO: 替换为你的 DashScope API Key
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen-plus
temperature: 0.7
embedding:
options:
model: text-embedding-v3
说明:通义千问通过阿里云 DashScope 平台提供 API 服务,支持 OpenAI 兼容接口。申请 API Key 请前往 阿里云百炼控制台。
主要模型选择:
qwen-turbo:速度快、成本低,适合分类、标签抽取等简单任务qwen-plus:均衡型,适合摘要生成、翻译等中等复杂度任务qwen-max:能力最强,适合复杂推理、多步骤任务编排
6.2 新闻采集模块
6.2.1 RSS 采集器
@Slf4j
@Component
public class RssCrawler {
@Resource
private NewsSourceMapper sourceMapper;
public List<NewsArticle> crawl(NewsSource source) {
List<NewsArticle> articles = new ArrayList<>();
try {
URL feedUrl = new URL(source.getUrl());
SyndFeedInput input = new SyndFeedInput();
SyndFeed feed = input.build(new XmlReader(feedUrl));
for (SyndEntry entry : feed.getEntries()) {
NewsArticle article = new NewsArticle();
article.setSourceId(source.getId());
article.setTitle(entry.getTitle());
article.setAuthor(entry.getAuthor());
article.setPublishTime(entry.getPublishedDate() != null
? new Timestamp(entry.getPublishedDate().getTime()) : null);
// 正文可能有 HTML,后续统一清洗
String content = entry.getDescription() != null
? entry.getDescription().getValue() : "";
article.setContent(HtmlUtil.cleanHtmlTag(content));
article.setContentHash(DigestUtil.md5Hex(article.getContent()));
article.setLanguage(source.getLanguage());
articles.add(article);
}
log.info("RSS采集完成,source:{},文章数:{}", source.getName(), articles.size());
} catch (Exception e) {
log.error("RSS采集失败,source:{}", source.getName(), e);
}
return articles;
}
}
6.2.2 网页采集器
@Slf4j
@Component
public class WebCrawler {
public List<NewsArticle> crawl(NewsSource source) {
List<NewsArticle> articles = new ArrayList<>();
try {
// 使用 Jsoup 抓取静态页面
Document doc = Jsoup.connect(source.getUrl())
.userAgent("Mozilla/5.0 NewsAgentBot/1.0")
.timeout(10000)
.get();
// 根据不同新闻源配置 CSS 选择器提取内容
Elements newsItems = doc.select(source.getCrawlRule());
for (Element item : newsItems) {
NewsArticle article = new NewsArticle();
article.setTitle(item.select(".title").text());
article.setContent(item.select(".content").text());
article.setContentHash(DigestUtil.md5Hex(article.getContent()));
// ... 其他字段提取
articles.add(article);
}
} catch (Exception e) {
log.error("网页采集失败,source:{}", source.getName(), e);
}
return articles;
}
}
6.2.3 采集后处理管道
采集到的原始数据需要经过去重、清洗、入库等步骤。
@Slf4j
@Component
public class CrawlPipeline {
@Resource
private NewsArticleMapper articleMapper;
@Resource
private KafkaTemplate<String, String> kafkaTemplate;
private static final String TOPIC_ENRICHMENT = "news-enrichment";
/**
* 采集后的统一处理管道
*/
public void process(List<NewsArticle> articles, Long sourceId) {
int savedCount = 0;
for (NewsArticle article : articles) {
// 1. 内容去重
if (articleMapper.existsByContentHash(article.getContentHash())) {
continue;
}
// 2. 内容清洗(去广告、去特殊字符等)
article.setContent(cleanContent(article.getContent()));
// 3. 入库
articleMapper.insert(article);
savedCount++;
// 4. 发送消息到 Kafka,触发异步 AI 处理
kafkaTemplate.send(TOPIC_ENRICHMENT, article.getId().toString());
}
log.info("管道处理完成,sourceId:{},新增:{},去重跳过:{}",
sourceId, savedCount, articles.size() - savedCount);
}
private String cleanContent(String content) {
if (StrUtil.isEmpty(content)) return content;
// 去除多余空白
content = content.replaceAll("\\s{2,}", " ");
// 去除常见广告文本
content = content.replaceAll("(广告)|\\[广告\\]", "");
return content.trim();
}
}
6.3 AI 文章丰富化
采集完成后,通过 Kafka 消费消息,异步调用大模型对文章进行丰富化处理。
@Slf4j
@Component
public class ArticleEnrichmentService {
@Resource
private ChatClient chatClient;
@Resource
private EmbeddingService embeddingService;
@Resource
private SummaryService summaryService;
@Resource
private NewsArticleMapper articleMapper;
/**
* 对单篇文章进行 AI 丰富化处理
*/
public void enrich(NewsArticle article) {
String content = article.getContent();
if (StrUtil.isEmpty(content) || content.length() < 50) {
return;
}
// 截断过长内容,避免超出 token 限制
String truncatedContent = content.length() > 3000
? content.substring(0, 3000) + "..." : content;
// 1. 生成摘要
String summary = summaryService.generate(truncatedContent);
// 2. 分类
String category = classify(truncatedContent);
// 3. 情感分析
String sentiment = analyzeSentiment(truncatedContent);
// 4. 提取标签
String tags = extractTags(truncatedContent);
// 更新数据库
NewsArticle update = new NewsArticle();
update.setId(article.getId());
update.setSummary(summary);
update.setCategory(category);
update.setSentiment(sentiment);
update.setTags(tags);
articleMapper.updateById(update);
// 5. 异步向量化(存入 Elasticsearch)
embeddingService.embedAndIndex(article.getId(), truncatedContent, summary, category);
log.info("文章丰富化完成,articleId:{}", article.getId());
}
/**
* 新闻分类
*/
private String classify(String content) {
String prompt = """
请对以下新闻内容进行分类,从以下类别中选择最匹配的一个:
[时政, 财经, 科技, 体育, 娱乐, 健康, 教育, 国际, 社会, 军事, 汽车, 房产]
只返回类别名称,不要其他内容。
新闻内容:%s
""".formatted(content);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
/**
* 情感分析
*/
private String analyzeSentiment(String content) {
String prompt = """
请分析以下新闻内容的情感倾向,从 positive、negative、neutral 中选择。
只返回情感标签,不要其他内容。
新闻内容:%s
""".formatted(content);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
/**
* 标签提取
*/
private String extractTags(String content) {
String prompt = """
请从以下新闻内容中提取3-5个关键标签,以逗号分隔返回。
标签应为具体的人名、地名、机构名或事件关键词。
新闻内容:%s
""".formatted(content);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
6.4 智能摘要生成
@Slf4j
@Service
public class SummaryService {
@Resource
private ChatClient chatClient;
/**
* 生成标准摘要(约150字)
*/
public String generate(String content) {
return generate(content, 150);
}
/**
* 生成可控长度摘要
*
* @param content 新闻正文
* @param maxWords 目标字数上限
*/
public String generate(String content, int maxWords) {
String prompt = """
你是一位专业的新闻编辑。请对以下新闻生成一段简洁准确的摘要。
要求:
1. 不超过%d个字
2. 包含核心事件和关键事实(人物、时间、地点、事件)
3. 保持客观中立的语气
4. 不要添加原文中没有的信息
新闻内容:
%s
摘要:
""".formatted(maxWords, content);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
/**
* 生成一句话标题摘要
*/
public String generateHeadline(String content) {
String prompt = """
用一句话(不超过30字)概括以下新闻的核心内容:
%s
""".formatted(content);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
/**
* 生成新闻简报(多篇文章汇总)
*/
public String generateBriefing(List<NewsArticle> articles) {
StringBuilder sb = new StringBuilder();
sb.append("请根据以下多条新闻生成一份简报,每条新闻用1-2句话概括,最后给出今日要闻总结。\n\n");
for (int i = 0; i < articles.size(); i++) {
sb.append(String.format("新闻%d [%s] %s\n%s\n\n",
i + 1,
articles.get(i).getCategory(),
articles.get(i).getTitle(),
articles.get(i).getSummary()));
}
return chatClient.prompt()
.user(sb.toString())
.call()
.content();
}
}
6.5 向量化与语义检索
6.5.1 文章向量化
@Slf4j
@Service
public class EmbeddingService {
@Resource
private EmbeddingModel embeddingModel;
@Resource
private ElasticsearchClient esClient;
private static final String INDEX_NAME = "news_articles";
/**
* 将文章向量化并写入 Elasticsearch
*/
public void embedAndIndex(Long articleId, String content, String summary, String category) {
// 拼接标题+摘要+正文前500字作为向量化输入
String textToEmbed = summary + "\n" + content.substring(0, Math.min(content.length(), 500));
// 调用大模型生成向量
float[] vector = embeddingModel.embed(textToEmbed);
// 写入 ES(包含向量字段)
Map<String, Object> doc = new HashMap<>();
doc.put("article_id", articleId);
doc.put("content_vector", vector);
doc.put("category", category);
doc.put("text", textToEmbed);
try {
esClient.index(i -> i.index(INDEX_NAME).id(articleId.toString()).document(doc));
} catch (IOException e) {
log.error("ES索引写入失败,articleId:{}", articleId, e);
}
}
}
向量化说明:调用阿里云 DashScope 的
text-embedding-v3模型,将文本转为 1024 维向量。Spring AI Alibaba 已内置 EmbeddingModel 接口,只需配置即可使用。
6.5.2 Elasticsearch 索引定义
PUT /news_articles
{
"mappings": {
"properties": {
"article_id": { "type": "long" },
"content_vector": {
"type": "dense_vector",
"dims": 1024,
"index": true,
"similarity": "cosine"
},
"text": { "type": "text", "analyzer": "ik_max_word" },
"category": { "type": "keyword" },
"publish_time": { "type": "date" }
}
}
}
6.5.3 混合检索(BM25 + 向量)
@Service
public class NewsSearchService {
@Resource
private ElasticsearchClient esClient;
@Resource
private NewsArticleMapper articleMapper;
private static final String INDEX_NAME = "news_articles";
/**
* 混合检索:BM25 关键词匹配 + 向量语义相似度
*
* @param query 用户查询
* @param category 可选的分类过滤
* @param size 返回数量
*/
public List<NewsArticle> hybridSearch(String query, String category, int size) {
try {
// 构建混合查询
NativeQuery searchQuery = NativeQuery.builder()
.withQuery(q -> q.bool(b -> {
// 子句1:向量语义检索
b.must(m -> m.scriptScore(ss -> ss
.query(inner -> inner.matchAll(ma -> ma))
.script(sc -> sc.inline(si -> si
.source("cosineSimilarity(params.query_vector, 'content_vector') + 1.0")
.params("query_vector", JsonData.of(embedQuery(query)))
))
));
// 子句2:BM25 关键词匹配
b.should(s -> s.match(m -> m.field("text").query(query)));
// 可选:分类过滤
if (StrUtil.isNotEmpty(category)) {
b.filter(f -> f.term(t -> t.field("category").value(category)));
}
return b;
}))
.withMaxResults(size)
.build();
SearchHits<Map> hits = elasticsearchTemplate.search(searchQuery, Map.class);
return hits.stream()
.map(hit -> articleMapper.selectById((Long) hit.getContent().get("article_id")))
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("混合检索失败,query:{}", query, e);
return Collections.emptyList();
}
}
/**
* 将用户查询文本转为向量
*/
private float[] embedQuery(String query) {
// TODO: 调用 DashScope Embedding API 将 query 转为向量
// float[] vector = embeddingModel.embed(query);
// return vector;
return new float[0];
}
}
为什么用混合检索?
- BM25 精确匹配关键词(人名、地名、专有名词),召回率高
- 向量检索 理解语义含义(“苹果降价” 和 “iPhone价格调整” 语义相近但关键词不同)
- 两者结合,既不遗漏精确匹配结果,也能捕获语义相关内容
6.6 智能体编排
利用 Spring AI 的 Tool Calling 和 Agent 能力,将新闻搜索、摘要、翻译等功能封装为工具,由大模型自主调度。
6.6.1 定义工具
@Slf4j
@Component
public class NewsTools {
@Resource
private NewsSearchService searchService;
@Resource
private SummaryService summaryService;
@Resource
private NewsArticleMapper articleMapper;
@Resource
private TranslateService translateService;
/**
* 工具:搜索新闻
*/
@Tool(description = "根据关键词搜索新闻,返回最相关的新闻列表。当用户询问新闻、热点、事件时使用。")
public List<NewsArticle> searchNews(
@ToolParam(description = "搜索关键词") String query,
@ToolParam(description = "新闻分类过滤,如:科技、财经、体育。不限制时传null") String category) {
return searchService.hybridSearch(query, category, 5);
}
/**
* 工具:生成摘要
*/
@Tool(description = "对指定新闻文章生成摘要。当用户要求总结、概括新闻时使用。")
public String summarizeArticle(
@ToolParam(description = "新闻文章ID") Long articleId) {
NewsArticle article = articleMapper.selectById(articleId);
if (article == null) return "未找到该文章";
return summaryService.generate(article.getContent());
}
/**
* 工具:翻译新闻
*/
@Tool(description = "将新闻内容翻译为指定语言。当用户要求翻译时使用。")
public String translateNews(
@ToolParam(description = "新闻文章ID") Long articleId,
@ToolParam(description = "目标语言,如:en、ja") String targetLanguage) {
NewsArticle article = articleMapper.selectById(articleId);
if (article == null) return "未找到该文章";
return translateService.translate(article.getContent(), targetLanguage);
}
/**
* 工具:生成新闻简报
*/
@Tool(description = "根据分类和数量生成今日新闻简报。当用户要求看简报、今日要闻时使用。")
public String generateBriefing(
@ToolParam(description = "新闻分类,如:科技、财经。all表示全部") String category,
@ToolParam(description = "简报中包含的新闻条数") int count) {
List<NewsArticle> articles;
if ("all".equals(category)) {
articles = articleMapper.selectTodayTopN(count);
} else {
articles = articleMapper.selectTodayByCategory(category, count);
}
return summaryService.generateBriefing(articles);
}
}
6.6.2 对话接口
@Slf4j
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Resource
private ChatClient chatClient;
@PostMapping
public String chat(@RequestBody ChatRequest request) {
String userMessage = request.getMessage();
Long userId = request.getUserId();
return chatClient.prompt()
.system(systemPrompt(userId))
.user(userMessage)
.functions("searchNews", "summarizeArticle", "translateNews", "generateBriefing")
.call()
.content();
}
private String systemPrompt(Long userId) {
return """
你是一位专业的新闻助手。你可以帮助用户:
1. 搜索和查询新闻
2. 对新闻进行摘要总结
3. 将新闻翻译为其他语言
4. 生成新闻简报
回答要求:
- 基于检索到的新闻内容回答,不编造信息
- 引用新闻来源时注明出处
- 保持客观中立
- 如果找不到相关新闻,诚实告知用户
""";
}
}
工作原理:当用户提问时,大模型会根据问题自动决定调用哪些工具。例如:
| 用户提问 | 大模型决策 |
|---|---|
| “最近有什么科技新闻?” | 调用 searchNews("科技新闻", "科技") |
| “帮我总结一下第123条新闻” | 调用 summarizeArticle(123L) |
| “把这条新闻翻译成英文” | 调用 translateNews(articleId, "en") |
| “给我来一份今日财经简报” | 调用 generateBriefing("财经", 5) |
6.7 语音播报
使用阿里云 CosyVoice 将新闻摘要转为自然语音。
说明:以下为阿里云 CosyVoice TTS 接入预留位置,具体接入方式请参考阿里云文档。
@Slf4j
@Service
public class TtsService {
@Value("${news.tts.voice:longxiaochun}")
private String voiceName;
@Value("${news.tts.output-path:/tmp/news-audio/}")
private String outputPath;
@Resource
private OssUploader ossUploader;
/**
* 将文本转为语音文件并上传 OSS
*
* @param text 待合成文本
* @param fileName 文件名
* @return OSS 音频访问地址
*/
public String textToSpeech(String text, String fileName) {
// TODO: 接入阿里云 CosyVoice TTS
// CosyVoice 使用 WebSocket 协议进行实时语音合成
// 1. 建立 WebSocket 连接: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
// 2. 发送文本指令
// 3. 接收音频流并写入文件
// 4. 上传至 OSS 返回访问地址
//
// 参考模型: fun-cosyvoice-3.0
// 支持音色: longxiaochun, longshu, longlaotie 等
//
// 示例伪代码:
// String localPath = outputPath + fileName + ".mp3";
// CosyVoiceClient.synthesize(text, voiceName, localPath);
// return ossUploader.upload(localPath);
log.info("TTS合成完成,text长度:{}, fileName:{}", text.length(), fileName);
return null;
}
}
6.8 个性化推荐
@Service
public class RecommendService {
@Resource
private NewsSearchService searchService;
@Resource
private UserProfileMapper profileMapper;
@Resource
private UserInteractionMapper interactionMapper;
@Resource
private EmbeddingModel embeddingModel;
/**
* 基于用户画像的个性化推荐
*/
public List<NewsArticle> recommend(Long userId, int size) {
// 1. 获取用户画像
UserProfile profile = profileMapper.selectByUserId(userId);
if (profile == null) {
// 冷启动:返回热门新闻
return getHotNews(size);
}
// 2. 构建用户兴趣向量
// 将用户偏好的分类和标签拼接,转为向量
String interestText = StrUtil.join(",",
profile.getPreferredCategories(),
profile.getPreferredTags());
float[] userVector = embeddingModel.embed(interestText);
// 3. 获取用户最近交互过的文章ID,用于排除
List<Long> readArticleIds = interactionMapper.selectRecentArticleIds(userId, 50);
// 4. 基于用户向量进行相似度检索
return searchService.vectorSearch(userVector, readArticleIds, size);
}
/**
* 热门新闻(冷启动兜底)
*/
private List<NewsArticle> getHotNews(int size) {
// 基于阅读量和时间衰减排序
return Collections.emptyList(); // TODO: 实现热门排序逻辑
}
}
6.9 用户偏好更新
@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
private UserProfileMapper profileMapper;
@Resource
private UserInteractionMapper interactionMapper;
/**
* 记录用户行为并更新画像
*/
@PostMapping("/interact")
public Result interact(@RequestBody UserInteraction interaction) {
// 1. 记录交互行为
interactionMapper.insert(interaction);
// 2. 更新用户画像(基于最近 N 次行为重新计算偏好)
List<UserInteraction> recentInteractions = interactionMapper
.selectRecentByUserId(interaction.getUserId(), 100);
updateUserProfile(interaction.getUserId(), recentInteractions);
return Result.success();
}
/**
* 根据最近交互记录更新用户偏好
* 使用 LLM 从行为记录中提取偏好
*/
private void updateUserProfile(Long userId, List<UserInteraction> interactions) {
// 将最近浏览的文章分类和标签汇总
// 统计高频分类和标签作为偏好
// 更新 user_profile 表
// 实际项目中可用 LLM 进行更精细的偏好推断
}
}
七、异步处理架构
新闻采集到 AI 处理是典型的异步场景,使用 Kafka 解耦:
┌──────────┐ Kafka ┌───────────────────┐
│ 采集服务 │ ──────────> │ news-enrichment │ ──> AI丰富化
│ (Producer)│ │ (Consumer) │
└──────────┘ └───────────────────┘
│
▼
┌───────────────────┐
│ news-embedding │ ──> 向量化
│ (Consumer) │
└───────────────────┘
│
▼
┌───────────────────┐
│ news-tts │ ──> 语音合成
│ (Consumer) │
└───────────────────┘
Kafka Consumer 示例:
@Slf4j
@Component
public class NewsEnrichmentConsumer {
@Resource
private ArticleEnrichmentService enrichmentService;
@Resource
private NewsArticleMapper articleMapper;
@KafkaListener(topics = "news-enrichment", groupId = "news-agent-group")
public void onMessage(ConsumerRecord<String, String> record) {
Long articleId = Long.parseLong(record.value());
try {
NewsArticle article = articleMapper.selectById(articleId);
if (article != null) {
enrichmentService.enrich(article);
}
} catch (Exception e) {
log.error("文章丰富化处理失败,articleId:{}", articleId, e);
}
}
}
八、定时任务调度
使用 XXL-JOB 调度新闻采集:
@Slf4j
@Component
public class NewsCrawlJobHandler {
@Resource
private NewsSourceMapper sourceMapper;
@Resource
private CrawlPipeline crawlPipeline;
@Resource
private RssCrawler rssCrawler;
@Resource
private WebCrawler webCrawler;
/**
* 定时采集所有启用的新闻源
*/
@XxlJob("newsCrawlJob")
public void execute() {
List<NewsSource> sources = sourceMapper.selectEnabled();
log.info("开始采集,共{}个新闻源", sources.size());
for (NewsSource source : sources) {
try {
List<NewsArticle> articles;
if (source.getSourceType() == 1) {
articles = rssCrawler.crawl(source);
} else {
articles = webCrawler.crawl(source);
}
crawlPipeline.process(articles, source.getId());
// 更新最近采集时间
NewsSource update = new NewsSource();
update.setId(source.getId());
update.setLastCrawlTime(new Date());
sourceMapper.updateById(update);
} catch (Exception e) {
log.error("采集新闻源失败,sourceId:{}", source.getId(), e);
// 标记异常状态
NewsSource update = new NewsSource();
update.setId(source.getId());
update.setStatus(1);
sourceMapper.updateById(update);
}
}
log.info("采集任务执行完毕");
}
}
九、关键 Prompt 设计
Prompt 质量直接决定输出效果,以下是几个核心场景的 Prompt 模板。
9.1 新闻摘要
你是一位拥有10年经验的专业新闻编辑。请对以下新闻内容生成摘要。
规则:
1. 字数控制在{maxWords}字以内
2. 必须包含:核心事件、关键人物、时间地点、结果影响
3. 保持客观中立,不添加个人观点
4. 不编造原文中没有的信息
5. 使用简洁的新闻语言风格
新闻标题:{title}
新闻正文:{content}
请输出摘要:
9.2 新闻分类
对以下新闻进行分类。从给定类别中选择最匹配的一个。
类别列表:时政、财经、科技、体育、娱乐、健康、教育、国际、社会、军事、汽车、房产
新闻内容:{content}
只输出类别名称:
9.3 多语言翻译
你是一位专业的新闻翻译。请将以下新闻翻译为{targetLanguage}。
要求:
1. 准确传达原文含义
2. 保持新闻体裁的语言风格
3. 专有名词(人名、地名、机构名)采用标准译法
4. 数字、日期格式遵循目标语言习惯
原文:
{content}
译文:
9.4 新闻简报
你是一位新闻主播。请根据以下新闻条目,生成一份简明新闻播报稿。
风格要求:
- 开头用一句话概括今日整体要闻
- 每条新闻用1-2句话概括
- 结尾给出简要评论或展望
- 语言简洁有力,适合口语播报
新闻列表:
{articles}
播报稿:
十、性能优化策略
10.1 LLM 调用优化
| 策略 | 说明 |
|---|---|
| 模型分级 | 分类/标签用 qwen-turbo(快+便宜),摘要/推理用 qwen-plus |
| 内容截断 | 新闻正文截取前 3000 字送入 LLM,覆盖核心信息即可 |
| 并发调用 | 多篇文章的丰富化处理使用线程池并行 |
| 结果缓存 | 相同内容的 LLM 结果写入 Redis,避免重复调用 |
| 降级策略 | LLM 不可用时降级为规则提取(正则匹配分类/标签) |
10.2 检索优化
| 策略 | 说明 |
|---|---|
| 混合检索 | BM25 + 向量加权融合,召回率提升 20%+ |
| 时间衰减 | 搜索评分中加入时间权重,新闻越新得分越高 |
| 分类预过滤 | ES 的 term filter 在向量检索前缩小范围 |
| 查询扩展 | 用 LLM 将用户短查询扩展为更丰富的检索词 |
10.3 采集优化
| 策略 | 说明 |
|---|---|
| 增量采集 | 记录 last_crawl_time,只采集新发布的内容 |
| 内容去重 | 基于内容 MD5 去重,避免重复入库 |
| 限速控制 | 遵守 robots.txt,控制请求频率 |
| 失败重试 | Kafka 消费失败时重试 3 次,超过则转入死信队列 |
十一、部署架构
┌─────────────────────────────────────────────────┐
│ Nginx / SLB │
└────────────┬────────────────┬───────────────────┘
│ │
┌────────▼─────┐ ┌──────▼──────┐
│ News-Agent │ │ Crawl-Job │
│ 服务 (x2) │ │ 采集服务(x1)│
└──────┬───────┘ └──────┬──────┘
│ │
▼ ▼
┌──────────────────────────────┐
│ Kafka 集群 │
└──────────┬───────────────────┘
│
┌──────────▼───────────────────┐
│ Enrichment Worker (x2) │
│ (AI处理消费服务) │
└──────────┬───────────────────┘
│
┌──────────▼───────────────────┐
│ 中间件集群 │
│ MySQL / ES / Redis / OSS │
└──────────────────────────────┘
│
┌──────────▼───────────────────┐
│ 阿里云 DashScope API │
│ (通义千问 / Embedding / TTS) │
└──────────────────────────────┘
十二、开发路线图
| 阶段 | 内容 |
|---|---|
| Phase 1 | 基础框架搭建、新闻采集(RSS + 网页)、数据库 CRUD |
| Phase 2 | AI 文章丰富化(摘要、分类、情感、标签)、向量化入库 |
| Phase 3 | 语义检索(混合检索)、用户画像、个性化推荐 |
| Phase 4 | 智能体编排(Tool Calling)、对话式新闻助手 |
| Phase 5 | 语音播报(CosyVoice TTS)、新闻简报定时推送 |
| Phase 6 | 性能优化、监控告警、前端页面 |
十三、总结
本文从实际项目出发,介绍了从0到1构建新闻智能体的完整过程。核心思路:
- 采集先行:先建立稳定可靠的新闻数据管道
- AI 赋能:利用大模型在摘要、分类、检索等环节提效
- 智能体编排:通过 Tool Calling 让 LLM 自主调用工具,实现对话式新闻助手
- 混合检索:BM25 + 向量结合,兼顾精确匹配和语义理解
- 异步解耦:采集与 AI 处理通过 Kafka 解耦,保障系统稳定
技术选型上,Spring AI Alibaba 降低了大模型接入门槛,Elasticsearch 8.x 原生支持向量检索,Java 生态的成熟度也为企业级部署提供了保障。
更多推荐
所有评论(0)