Spring AI 应用 - 智能记者

2024-04-11 09:52
文章标签 java ai 应用 智能 spring 记者

本文主要是介绍Spring AI 应用 - 智能记者,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述
参考实现: https://github.com/mshumer/ai-journalist

上面是通过 Claude 配合 SERP 搜索 API,使用 Python 语言实现的,本文通过 GitHub Copilot 辅助改为了基于 Spring AI 的 Java 版本,本文使用的 OpenAI。

AIJournalist 实现

基本定义如下:

/*** 记者类,用于撰写和编辑文章*/
public class AIJournalist {private ChatClient   chatClient; // 聊天客户端private RestTemplate restTemplate = new RestTemplate(); // 用于发送HTTP请求的模板private HttpHeaders  serpApiHeader; // SERP API的请求头/*** 构造函数,初始化聊天客户端和SERP API的请求头** @param chatClient 聊天客户端* @param serpApiKey SERP API的密钥*/public AIJournalist(ChatClient chatClient, String serpApiKey) {this.chatClient = chatClient;this.serpApiHeader = new HttpHeaders();serpApiHeader.setContentType(MediaType.APPLICATION_JSON);serpApiHeader.set("X-API-KEY", serpApiKey);}

为了方便外部替换 ChatClient 实现,作为参数传递进去使用,搜索使用的 SERP,可以免费申请初始的额度用于搜索。

下面是 main 方法调用:

public static void main(String[] args) {// 可选 HTTPSSystem.setProperty("https.proxyHost", "localhost");System.setProperty("https.proxyPort", "7890");// 创建聊天客户端var openAiApi = new OpenAiApi("替换token");var chatClient = new OpenAiChatClient(openAiApi, OpenAiChatOptions.builder().withModel("gpt-4-turbo").withTemperature(0.4F).build());// 启动AIJournalist journalist = new AIJournalist(chatClient, "替换token");journalist.start();
}

这里选择的 gpt-4-turbo,在实现中会通过搜索引擎获取大量上下文,因此需要支持更大上下文的模型,gpt-4-turbo 支持的 token 上限为 128,000,如果遇到超出上下文的情况,还可以考虑尝试 gpt-4-32k 来支持 32,768 tokens。

下面看串起整个调用的 start() 方法。

/*** 开始撰写和编辑文章的过程*/
public void start() {// User inputScanner scanner = new Scanner(System.in);System.out.print("输入要写的主题:");String topic = scanner.nextLine();System.out.print("初稿完成后,是否要进行自动编辑?这可能会提高性能,但有点不可靠。回答“是”或“否”:");String doEdit = scanner.nextLine();

首先通过控制台输入主题,以及是否要进行自动编辑。比如输入如下信息:

输入要写的主题:How to use Obsidian?
初稿完成后,是否要进行自动编辑?这可能会提高性能,但有点不可靠。回答“是”或“否”:是

输入主题后,通过 getSearchTerms 方法根据主题生成搜索词:

// Generate search terms
List<String> searchTerms = getSearchTerms(topic);
System.out.println("\n------------------------------");
System.out.println("\n搜索词 '" + topic + "':");
System.out.println(String.join(", ", searchTerms));

getSearchTerms 方法会调用 AI 根据提示词生成:

/*** 根据给定的主题生成搜索词** @param topic 主题* @return 搜索词列表*/
public List<String> getSearchTerms(String topic) {List<Message> messages = new ArrayList<>();messages.add(new SystemMessage("你是一位世界级的记者。生成一个包含5个搜索词的列表,用于研究和撰写关于该主题的文章。"));messages.add(new UserMessage("主题: " + topic + "\n\n请提供一个与'" + topic + "'相关的5个搜索词的列表,用于研究和撰写文章。以逗号分隔的Java可解析列表形式回复。"));String responseText = call(messages);return Arrays.asList(responseText.replace("[", "").replace("]", "").replace("\"", "").split(","));
}

根据前面输入的主题,这里响应结果如下:

搜索词 'How to use Obsidian?':
Obsidian app tutorial,  Obsidian note-taking features,  Obsidian plugins,  Obsidian sync capabilities,  Obsidian vs Notion comparison

接下要搜调用搜索 API 分别搜索这几个搜索词

// Perform searches and select relevant URLs
List<String> relevantUrls = new ArrayList<>();
for (String term : searchTerms) {List<Map<String, Object>> searchResults = getSearchResults(term);List<String> urls = selectRelevantUrls(searchResults);relevantUrls.addAll(urls);
}

通过 getSearchResults 方法搜索(因为AI会用我们的语言编写文章,所以搜索英文资料会比中文效果更好):

/*** 根据给定的搜索词获取搜索结果** @param searchTerm 搜索词* @return 搜索结果*/
@SuppressWarnings({"unchecked", "rawtypes"})
public List<Map<String, Object>> getSearchResults(String searchTerm) {// Create request bodyString body = "{\"q\":\"" + searchTerm + "\",\"hl\":\"en\",\"num\":10}";// Create entityHttpEntity<String> entity = new HttpEntity<>(body, serpApiHeader);// Execute requestResponseEntity<Map> response = restTemplate.exchange("https://google.serper.dev/search",HttpMethod.POST,entity,Map.class);return (List<Map<String, Object>>) response.getBody().get("organic");
}

然后通过方法 selectRelevantUrls 使用 AI 筛选搜索结果中的 URL:

/*** 从给定的搜索结果中选择相关的URL** @param searchResults 搜索结果* @return 相关的URL列表*/
public List<String> selectRelevantUrls(List<Map<String, Object>> searchResults) {List<Message> messages = new ArrayList<>();messages.add(new SystemMessage("你是一位记者助手。从给定的搜索结果中,选择出看起来最相关和信息丰富的 URL,用于撰写关于该主题的文章。"));StringBuilder searchResultsText = new StringBuilder();for (int i = 0; i < searchResults.size(); i++) {searchResultsText.append(i + 1).append(". ").append(searchResults.get(i).get("link")).append("\n");}messages.add(new UserMessage("搜索结果:\n" + searchResultsText + "\n\n请选择看起来最相关和信息丰富的 URL 的编号,用于撰写关于该主题的文章。以逗号分隔的 Java 可解析列表形式回复(如 [1,2,4])。"));String responseText = call(messages);String[] numbers = responseText.replace("[", "").replace("]", "").replace("\"", "").split(",");List<String> relevantUrls = new ArrayList<>();for (String num : numbers) {int index = Integer.parseInt(num.trim()) - 1;relevantUrls.add((String) searchResults.get(index).get("link"));}return relevantUrls;
}

循环多个搜索关键字,拿到所有可以参考的 URL 链接,然后通过下面代码输出:

String urls = IntStream.range(0, relevantUrls.size()).mapToObj(i -> (i + 1) + ". " + relevantUrls.get(i)).collect(Collectors.joining("\n"));
System.out.println("\n------------------------------");
System.out.println("要阅读的相关 URL:\n" + urls);

当前主题输出的示例如下:

要阅读的相关 URL:
1. https://obsidian.rocks/getting-started-with-obsidian-a-beginners-guide/
2. https://bobbypowers.net/beginners-guide-to-obsidian/
3. https://thetotalliving.medium.com/the-ultimate-guide-to-obsidian-8de0a5ea5c20
4. https://obsidian.md/
5. https://www.makeuseof.com/what-is-obsidian-note-taking/
6. https://www.cloudwards.net/obsidian-review/
7. https://obsidian.md/plugins
8. https://github.com/obsidianmd/obsidian-releases
9. https://obsidianninja.com/best-obsidian-plugins/
10. https://www.dsebastien.net/2022-10-19-the-must-have-obsidian-plugins/
11. https://help.obsidian.md/Obsidian+Sync/Introduction+to+Obsidian+Sync
12. https://help.obsidian.md/Getting+started/Sync+your+notes+across+devices
13. https://help.obsidian.md/Obsidian+Sync/Sync+limitations
14. https://www.nuclino.com/solutions/obsidian-vs-notion
15. https://plaky.com/blog/obsidian-vs-notion/
16. https://clickup.com/blog/obsidian-vs-notion/
17. https://www.techrepublic.com/article/obsidian-vs-notion/
18. https://www.androidauthority.com/obsidian-vs-notion-3319050/

接下来获取 URL 的内容:

// Get article text from relevant URLs
List<String> articleTexts = new ArrayList<>();
for (String url : relevantUrls) {try {String text = getArticleText(url);if (text.length() > 75) {articleTexts.add(text);}} catch (Exception e) {e.printStackTrace();}
}

getArticleText 方法使用 Jsoup 获取并解析 html:

/*** 从给定的URL获取文章文本** @param url 文章的URL* @return 文章的文本*/
public String getArticleText(String url) {try {Document doc = Jsoup.connect(url).get();return doc.body().text();} catch (Exception e) {System.out.println("解析URL" + url + " 错误: " + e.getMessage());return "";}
}

控制输出参考文章:

System.out.println("\n------------------------------");
System.out.println("参考文章:" + articleTexts);

示例如下(太长,截取部分)

参考文章:[Obsidian Rocks Exploring knowledge management with Ob
- [[Interests MOC]]
- [[Work MOC]]
- [[Home MOC]]The text above may confuse you. What’s with the funky square br
2. Item two
3. Item three To create an unordered list, simply use asterisks 
* Item two
* Item three Blockquotes: To create a blockquote, simply type > 
> --Abraham Lincoln Note: Obsidian also has callouts, which are 

开始根据提供的上下文写作:

System.out.println("\n\n正在写文章...");
// Write the article
String article = writeArticle(topic, articleTexts);
System.out.println("\n------------------------------");
System.out.println("\n生成的文章:");
System.out.println(article);

通过提示词模板调用AI编写:

/*** 根据给定的主题和参考文章文本撰写文章** @param topic        主题* @param articleTexts 参考文章文本* @return 撰写的文章*/
public String writeArticle(String topic, List<String> articleTexts) {List<Message> messages = new ArrayList<>();messages.add(new SystemMessage("你是一位世界级的记者。根据以下的参考文章和主题,撰写一篇关于该主题的文章。"));StringBuilder articlesText = new StringBuilder();for (int i = 0; i < articleTexts.size(); i++) {String article = articleTexts.get(i);articlesText.append(i + 1).append(". ").append(article).append("\n");}messages.add(new UserMessage("参考文章:\n" + articlesText + "\n\n主题: " + topic + "\n\n请撰写一篇关于该主题的文章。"));return call(messages);
}

输出的内容放在了这里:掌握Obsidian:从入门到精通的全面指南

判断是否需要对写好的文章进行编辑:

if (doEdit.toLowerCase().contains("是")) {// Edit the articleString editedArticle = editArticle(article);System.out.println("\n------------------------------");System.out.println("\n编辑文章:");System.out.println(editedArticle);
}

编辑方法 editArticle 如下:

/*** 编辑文章以提高其质量** @param article 要编辑的文章* @return 编辑后的文章*/
public String editArticle(String article) {List<Message> messages = new ArrayList<>();messages.add(new SystemMessage("你是一位世界级的编辑。根据以下的文章,进行编辑以提高其质量。"));messages.add(new UserMessage("请编辑以下文章以提高其质量:\n" + article));return call(messages);
}

前面提示词是记者,这里是世界级编辑。

编辑后的内容看这里:全面掌握Obsidian:从新手到专家的实用指南

至此完成了文章的编写,编写的内容不一定很好,个人感觉搜索引擎搜索的结果以及从网页提取的方式对整个结果有很大的影响,如果上下文提供的好,效果应该能改善。

在本文中,Spring AI 只是起到了一个 Chat 的作用,Chat 提供了搜索词、筛选URL,以记者身份编写内容,以编辑身份修改内容。除此之外还用到了搜索 API,使用Jsoup解析HTML来提供上下文。一个复杂的 AI 就是以不同提示词、用法、身份进行多轮交互来产生最终的结果,没有特别复杂的东西。

本文代码较长,完整内容放在了 gist,地址如下:

https://gist.github.com/abel533/300e642cb4e2548830981ce824036586

这篇关于Spring AI 应用 - 智能记者的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/893751

相关文章

java对接海康摄像头的完整步骤记录

《java对接海康摄像头的完整步骤记录》在Java中调用海康威视摄像头通常需要使用海康威视提供的SDK,下面这篇文章主要给大家介绍了关于java对接海康摄像头的完整步骤,文中通过代码介绍的非常详细,需... 目录一、开发环境准备二、实现Java调用设备接口(一)加载动态链接库(二)结构体、接口重定义1.类型

SpringBoot读取ZooKeeper(ZK)属性的方法实现

《SpringBoot读取ZooKeeper(ZK)属性的方法实现》本文主要介绍了SpringBoot读取ZooKeeper(ZK)属性的方法实现,强调使用@ConfigurationProperti... 目录1. 在配置文件中定义 ZK 属性application.propertiesapplicati

Java Multimap实现类与操作的具体示例

《JavaMultimap实现类与操作的具体示例》Multimap出现在Google的Guava库中,它为Java提供了更加灵活的集合操作,:本文主要介绍JavaMultimap实现类与操作的... 目录一、Multimap 概述Multimap 主要特点:二、Multimap 实现类1. ListMult

Java中常见队列举例详解(非线程安全)

《Java中常见队列举例详解(非线程安全)》队列用于模拟队列这种数据结构,队列通常是指先进先出的容器,:本文主要介绍Java中常见队列(非线程安全)的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一.队列定义 二.常见接口 三.常见实现类3.1 ArrayDeque3.1.1 实现原理3.1.2

SpringBoot整合Apache Flink的详细指南

《SpringBoot整合ApacheFlink的详细指南》这篇文章主要为大家详细介绍了SpringBoot整合ApacheFlink的详细过程,涵盖环境准备,依赖配置,代码实现及运行步骤,感兴趣的... 目录1. 背景与目标2. 环境准备2.1 开发工具2.2 技术版本3. 创建 Spring Boot

springboot加载不到nacos配置中心的配置问题处理

《springboot加载不到nacos配置中心的配置问题处理》:本文主要介绍springboot加载不到nacos配置中心的配置问题处理,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录springboot加载不到nacos配置中心的配置两种可能Spring Boot 版本Nacos

Java反射实现多属性去重与分组功能

《Java反射实现多属性去重与分组功能》在Java开发中,​​List是一种非常常用的数据结构,通常我们会遇到这样的问题:如何处理​​List​​​中的相同字段?无论是去重还是分组,合理的操作可以提高... 目录一、开发环境与基础组件准备1.环境配置:2. 代码结构说明:二、基础反射工具:BeanUtils

在Java中将XLS转换为XLSX的实现方案

《在Java中将XLS转换为XLSX的实现方案》在本文中,我们将探讨传统ExcelXLS格式与现代XLSX格式的结构差异,并为Java开发者提供转换方案,通过了解底层原理、性能优势及实用工具,您将掌握... 目录为什么升级XLS到XLSX值得投入?实际转换过程解析推荐技术方案对比Apache POI实现编程

Java调用C#动态库的三种方法详解

《Java调用C#动态库的三种方法详解》在这个多语言编程的时代,Java和C#就像两位才华横溢的舞者,各自在不同的舞台上展现着独特的魅力,然而,当它们携手合作时,又会碰撞出怎样绚丽的火花呢?今天,我们... 目录方法1:C++/CLI搭建桥梁——Java ↔ C# 的“翻译官”步骤1:创建C#类库(.NET

Java中JSON格式反序列化为Map且保证存取顺序一致的问题

《Java中JSON格式反序列化为Map且保证存取顺序一致的问题》:本文主要介绍Java中JSON格式反序列化为Map且保证存取顺序一致的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未... 目录背景问题解决方法总结背景做项目涉及两个微服务之间传数据时,需要提供方将Map类型的数据序列化为co