Spring AI 应用 - 智能记者

news2024/11/16 13:46:49

在这里插入图片描述
参考实现: 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) {
	// 可选 HTTPS
	System.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 input
	Scanner 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 body
	String body = "{\"q\":\"" + searchTerm + "\",\"hl\":\"en\",\"num\":10}";
	// Create entity
	HttpEntity<String> entity = new HttpEntity<>(body, serpApiHeader);
	// Execute request
	ResponseEntity<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 article
	String 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1595321.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python大数据分析——一元与多元线性回归模型

Python大数据分析——一元与多元线性回归模型 相关分析概念示例 一元线性回归模型概念理论分析函数示例 多元线性回归模型概念理论分析示例 线性回归模型的假设检验模型的F检验理论分析示例 模型的T检验理论分析示例 相关分析 概念 a 正相关&#xff1b;b 负相关&#xff1b;c…

大数据深度学习:基于Tensorflow深度学习卷积神经网络CNN算法垃圾分类识别系统

文章目录 大数据深度学习&#xff1a;基于Tensorflow深度学习卷积神经网络CNN算法垃圾分类识别系统一、项目概述二、深度学习卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;简称CNN&#xff09;三、部分数据库架构四、系统实现系统模型部分核心代码模型训…

JVM虚拟机(五)强引用、软引用、弱引用、虚引用

目录 一、强引用二、软引用三、弱引用四、虚引用五、总结 引文&#xff1a; 在 Java 中一共存在 4 种引用&#xff1a;强、软、弱、虚。它们主要指的是&#xff0c;在进行垃圾回收的时候&#xff0c;对于不同的引用垃圾回收的情况是不一样的。下面我们就一起来看一下这 4 种引用…

Qt 中默认代码

目录 主函数 widget的声明 widget的定义 form file .pro 文件 主函数 #include "widget.h" ​ #include <QApplication> ​ int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 上面就是 Qt 刚创建的一…

Redis入门到通关之Set命令

文章目录 ⛄ 概述⛄ Set类型的常见命令⛄RedisTemplate API❄️❄️ 添加Set缓存(值可以是一个&#xff0c;也可是多个)❄️❄️设置过期时间(单独设置)❄️❄️根据key获取Set中的所有值❄️❄️根据value从一个set中查询,是否存在❄️❄️获取Set缓存的长度❄️❄️移除指定的…

集合体系java

Collection:单列集合&#xff1a;每个元素只包含一个值 Collection集合存储的是地址 Collection的三种遍历方法如下 //迭代器是用来遍历集合的专用方式&#xff08;数组没有迭代器&#xff09;&#xff0c;在java中迭代器的代表是Iterator //boolean hasNext():询问当前位置…

9【原型模式】复制一个已存在的对象来创建新的对象

你好&#xff0c;我是程序员雪球。 今天我们来学习23种设计模式之原型模式&#xff0c;在平时开发过程中比较少见。我带你了解什么是原型模式&#xff0c;使用场景有哪些&#xff1f;有什么注意事项&#xff1f;深拷贝与浅拷贝的区别&#xff0c;最后用代码实现一个简单的示例…

机器学习-随机森林温度预测模型优化

文章目录 前言旧模型训练新模型训练参数查看组合参数训练学习模型评估 前言 在机器学习-随机森林算法预测温度一文中&#xff0c;通过增大模型训练数据集和训练特征的方式去优化模型的性能&#xff0c;本文将记录第三方种优化方式&#xff0c;通过调整随机森林创建模型参数的方…

专业照片编辑软件ON1 Photo RAW 2024 mac/win

ON1 Photo RAW 2024 for Mac是一款集专业性与易用性于一体的照片编辑软件。它拥有简洁直观的用户界面&#xff0c;即便对于摄影新手&#xff0c;也能快速上手。软件支持RAW格式照片处理&#xff0c;能够完整保留照片原始信息&#xff0c;让后期调整更加灵活。 在功能方面&#…

[Linux][基础IO][一][系统文件IO][文件描述符fd]详细解读

目录 0.预备知识1.系统文件I/O1.open2.write/read/close/lseek 2.文件描述符fd1.[0 & 1 & 2]2.什么是文件描述符&#xff1f;3.文件描述符的分配规则4.重定向5.使用dup2系统调用 -- 完成重定向6.FILE 0.预备知识 什么叫做文件呢&#xff1f; 站在系统的角度&#xff0…

【全网独家】oceanbase容器重启时报obshell failed错误,无法正常启动的问题处理

正常运行的oceanbase容器&#xff0c;重新启动该容器却启动不了&#xff0c;重启服务器也无法恢复&#xff0c;报obshell failed错误&#xff0c;无法正常启动&#xff0c;本文记录了问题处理过程。 一、问题现象 1、正常运行的oceanbase容器&#xff0c;重启却启动不了 2、运…

CSS中:root伪类的说明和使用

定义和用法 :root选择器用匹配文档的根元素。在HTML中根元素始终是HTML元素&#xff0c;所以也可以把:root理解为html根元素选择器&#xff0c;但是比html根元素的优先级高&#xff0c;:root伪类选择器常常被用于定义全局的CSS变量或者设置全局的CSS样式。CSS :root 选择器 | …

Win11 WSL2 install Ubuntu20.04 and Seismic Unix

Win11系统&#xff0c;先启用或关闭Windows功能&#xff0c;勾选“适用于Linux的Windows子系统”和“虚拟机平台”两项 设置wsl默认版本为wsl2&#xff0c;并更新 wsl --list --verbose # 查看安装版本及内容 wsl --set-default-version 2 # 设置wsl默认版本为wsl2 # 已安装…

Go微服务: go-micro集成consul的注册中心和配置中心

微服务与注册中心的关系图 这个图很好说明了微服务之间的关系&#xff0c;以及consul注册中心的重要性 环境准备 1 &#xff09;consul 集群 假设consul 集群已经搭建&#xff0c;已有5台server和2台client这里2台client被nginx做负载均衡&#xff0c;假设最终本地的访问地址…

rocketmq和rabbitmq总是分不清?

1. 官方解答 摘自百度搜索&#xff1a; 2. 通俗易懂的回答

蓝桥杯:握手问题和小球反弹问题

试题 A: 握手问题 本题总分&#xff1a; 5 分 【问题描述】 小蓝组织了一场算法交流会议&#xff0c;总共有 50 人参加了本次会议。在会议上&#xff0c; 大家进行了握手交流。按照惯例他们每个人都要与除自己以外的其他所有人进行一次握手&#xff08;且仅有一次&#x…

Hadoop概述及集群搭建

文章目录 一、Hadoop介绍二、Hadoop发展简史三、Hadoop核心组件四、Hadoop架构变迁1、Hadoop 1.02、Hadoop 2.03、Hadoop 3.0 五、Hadoop集群简介六、Hadoop部署模式七、Hadoop 集群搭建第一步&#xff1a;创建虚拟机第二步&#xff1a;安装Linux镜像第三步&#xff1a;网络配置…

Spring Cloud 集成 RabbitMQ

目录 前言步骤引入相关maven依赖添加相关配置 使用方法配置消息序列化创建第一个消息队列和交换机使用方法 总结 前言 在当今的微服务架构盛行的时代&#xff0c;消息队列作为一种重要的通信机制&#xff0c;在分布式系统中扮演着不可或缺的角色。RabbitMQ&#xff0c;作为一款…

(七)C++自制植物大战僵尸游戏关卡数据加载代码讲解

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/xjvbb 打开LevelData.h和LevelData.cpp文件。文件位置如下图所示。 LevelData.h 此头文件中定义了两个类&#xff0c;分别是OpenLevelData、LevelData&#xff0c;其中OpenLevelData用于加载文件数据。LevelData解析数据…

ansible创建用户账户和更新ansible库的密钥

1.创建⽤户帐户 从 http://materials/user_list.yml 下载要创建的⽤户的列表&#xff0c;并将它保存到 /home/greg/ansible 在本次考试中使⽤在其他位置创建的密码库 /home/greg/ansible/locker.yml 。创建名为 /home/greg/ansible/users.yml 的 playbook &#xff0c;从⽽…