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

相关文章

Spring Boot整合Redis注解实现增删改查功能(Redis注解使用)

《SpringBoot整合Redis注解实现增删改查功能(Redis注解使用)》文章介绍了如何使用SpringBoot整合Redis注解实现增删改查功能,包括配置、实体类、Repository、Se... 目录配置Redis连接定义实体类创建Repository接口增删改查操作示例插入数据查询数据删除数据更

Java Lettuce 客户端入门到生产的实现步骤

《JavaLettuce客户端入门到生产的实现步骤》本文主要介绍了JavaLettuce客户端入门到生产的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 目录1 安装依赖MavenGradle2 最小化连接示例3 核心特性速览4 生产环境配置建议5 常见问题

Java使用Swing生成一个最大公约数计算器

《Java使用Swing生成一个最大公约数计算器》这篇文章主要为大家详细介绍了Java使用Swing生成一个最大公约数计算器的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下... 目录第一步:利用欧几里得算法计算最大公约数欧几里得算法的证明情形 1:b=0情形 2:b>0完成相关代码第二步:加

Java 的ArrayList集合底层实现与最佳实践

《Java的ArrayList集合底层实现与最佳实践》本文主要介绍了Java的ArrayList集合类的核心概念、底层实现、关键成员变量、初始化机制、容量演变、扩容机制、性能分析、核心方法源码解析、... 目录1. 核心概念与底层实现1.1 ArrayList 的本质1.1.1 底层数据结构JDK 1.7

Java Map排序如何按照值按照键排序

《JavaMap排序如何按照值按照键排序》该文章主要介绍Java中三种Map(HashMap、LinkedHashMap、TreeMap)的默认排序行为及实现按键排序和按值排序的方法,每种方法结合实... 目录一、先理清 3 种 Map 的默认排序行为二、按「键」排序的实现方式1. 方式 1:用 TreeM

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node