现代人工作生活非常的紧凑,很多情况没有大块的时间进行阅读,所以我们可以借助SpringAI调用大模型来搭建一个个人的新闻助手。
首先,老生常谈,我们引用一下所需要的SpringAI相关的依赖。这里我们主要使用@Tool,只需要一些基本的依赖就可以。代码如下:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
然后,我们开始编写代码:
首先,我们去官方网站里获取新闻。这里我们使用的是人民日报官网的新闻数据。我试了一下SpringAI自带的tika,感觉人民日报官网上的干扰数据太多,所以使用jsoup来解析页面数据。说句题外话,tika里解析网站数据也使用了jsoup。
版权问题,这里获取人民日报数据,只用于个人学习。不要用于商业用途。
代码如下:
public void importNews(){
String today = DateUtil.format(DateUtil.date(), "yyyyMM/dd");
log.info("正在导入 {}", today);
for(int i=1;i<=20;i++) {
String layout = "node_" + StrUtil.fill(""+i,'0', 2, true) + ".html";
String url = "https://paper.people.com.cn/rmrb/pc/layout/" + today + "/" + layout;
String html = HttpUtil.get(url);
// log.info("html: {}", html);
Elements newsList = Jsoup.parse(html).select("ul.news-list > li");
for (Element news : newsList) {
Element link = news.select("a").first();
String title = link.text();
String href = link.attr("href").replace("../../..", "https://paper.people.com.cn/rmrb/pc");
log.info("title: {}, href: {}", title, href);
if (StrUtil.isNotBlank(href)) {
String subHtml = HttpUtil.get(href);
Elements articleList = Jsoup.parse(subHtml).select("div.article");
List<Document> docs = new ArrayList<>();
for (Element article : articleList) {
String content = article.text();
NewsEntity newsEntity = new NewsEntity();
newsEntity.setTitle(title);
newsEntity.setContent(content);
newsEntity.setLabel("news");
newsEntity.setDate(DateUtil.format(DateUtil.date(), "yyyy-MM-dd")));
log.info("newsEntity: {}", newsEntity);
elasticsearchTemplate.save(newsEntity);
}
}
}
}
}
这里我使用了定时任务每天跑一次,拉取今日的数据,存到数据库中。
然后我们定义四个tools。
- 获取今日时间;
- 获取某日的新闻标题;
- 获取某个新闻标题的总结;
- 获取某个新闻标题的原文。
代码如下:
@Tool(description = "获取今日日期")
public String getToday(){
return DateUtil.format(DateUtil.date(), "yyyy-MM-dd");
}
@Tool(description = "获取某日的新闻标题")
public List<String> getNewsByDate(@ToolParam(description = "日期") String date){
// 构建 NativeQuery
NativeQuery query = NativeQuery.builder()
.withQuery(Query.of(q -> q.match(MatchQuery.of(mq-> mq
.field("date")
.query(date)
.boost(1.0f)
)))).build();
SearchHits<NewsEntity> searchHits = elasticsearchTemplate.search(query, NewsEntity.class);
return searchHits.getSearchHits().stream().map(hit -> hit.getContent().getTitle()).toList();
}
@Tool(description = "获取某个新闻标题的总结")
public String getSummary(@ToolParam(description = "新闻标题") String title){
// 构建 NativeQuery
NativeQuery query = NativeQuery.builder()
.withQuery(Query.of(q -> q.match(MatchQuery.of(mq-> mq
.field("title")
.query(title)
.boost(1.0f)
)))).build();
SearchHits<NewsEntity> searchHits = elasticsearchTemplate.search(query, NewsEntity.class);
String customTemplate = """
阅读以下内容:
{context_str}
请用中文总结本节的核心要点(不超过100字):
""";
SummaryMetadataEnricher summaryMetadataEnricher = new SummaryMetadataEnricher(chatModel, ListUtil.of(SummaryMetadataEnricher.SummaryType.CURRENT),customTemplate, MetadataMode.ALL);
List<Document> docs = searchHits.getSearchHits().stream().map(hit -> hit.getContent().getContent()).map(Document::new).toList();
return summaryMetadataEnricher.apply(docs).stream().map(doc -> ""+doc.getMetadata().get("section_summary")).toList().get(0);
}
@Tool(description = "获取某个新闻标题的原文")
public String getNewsByTitle(@ToolParam(description = "新闻标题") String title){
// 构建 NativeQuery
NativeQuery query = NativeQuery.builder()
.withQuery(Query.of(q -> q.match(MatchQuery.of(mq-> mq
.field("title")
.query(title)
.boost(1.0f)
)))).build();
SearchHits<NewsEntity> searchHits = elasticsearchTemplate.search(query, NewsEntity.class);
return searchHits.getSearchHits().stream().map(hit -> hit.getContent().getContent()).toList().get(0);
}
这里四个工具需要解释下注意事项:
1 获取今日时间:
测试时发现大模型判断今日时间有事有错误。并且格式不太正确。所以加了这个工具。
2 获取某个新闻标题的总结:
这里用到一个SummaryMetadataEnricher工具类,他能通过大模型来辅助我们生成文章的总结。默认带的prompt不太适用,生成的数据是一些关键词加英文解释,很是奇怪。所以我们在这里换成新的prompt。
String customTemplate = """
阅读以下内容:
{context_str}
请用中文总结本节的核心要点(不超过100字):
""";
这样我们就可以用大模型来加载相关工具来生成对话了。代码如下:
public String queryLLM(String question, String label) {
// Calling the chat model with the question
ChatResponse chatResponse =chatClient.prompt()
.system("你是一个新闻评论员,请用中文回答相关时事问题。")
.advisors(
new SimpleLoggerAdvisor()
)
.tools(newsTool)
.user(question)
.call().chatResponse();
String response = Optional.ofNullable(chatResponse)
.map(ChatResponse::getResult)
.map(Generation::getOutput)
.map(AbstractMessage::getText)
.orElse("");
return response;
}
注意这里的newsTool一定要用spring注入进来。而不是new一个对象。很多教程直接new的对象,很是误导,new一个对象就脱离了spring的管理,没办法在方法里注入bean了。
到这里,所有的代码就完成了。我们看一下效果。
我们debug看一下,大模型是否调用了我们写的tools。
通过下断点,我们发现,大模型根据我们的问题,先通过第一个工具获取了今日时间;其次,通过第二个工具获取到今日新闻的所有标题;再次,通过第三个工具获取第一个新闻的总结概要。最后大模型将通过工具获得的数据进行整理,反馈给我们。这样,我们就得到了一个简单的新闻助手。当然,为了实现更多的需求,我们可以加入多个tools来辅助大模型调用。还可以利用mcp来做分布式架构开发。