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) {
	// 可选 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

绝地求生:经典杜卡迪与传奇杜卡迪的区别以及亮点

大家好&#xff0c;这里是闲游盒. 杜卡迪联名已正式加入PUBG&#xff0c;本次联名分为两个版本分别为&#xff1a;经典杜卡迪以及传奇杜卡迪 那接下来&#xff0c;就给大家展示一下经典杜卡迪&#xff08;红&#xff09;以及传奇版本杜卡迪&#xff08;暮光粉&#xff09;的区…

Acwing.4009 收集卡牌(期望dp)

题目 小林在玩一个抽卡游戏&#xff0c;其中有 n种不同的卡牌&#xff0c;编号为 1到 n。 每一次抽卡&#xff0c;她获得第 i种卡牌的概率为 pi。 如果这张卡牌之前已经获得过了&#xff0c;就会转化为一枚硬币。 可以用 k枚硬币交换一张没有获得过的卡。 小林会一直抽卡&…

2024大模型落地应用案例集(免费下载)

【1】扫码关注本公众号 【2】私信发送 2024大模型落地应用案例集 【3】获取本方案PDF下载链接&#xff0c;直接下载即可。

初识C++之内联函数 auto关键字

初识C之内联函数 auto关键字 文章目录 初识C之内联函数 auto关键字一、 内联函数1.1 定义1.2 应用1.3 特性 二、auto关键字2.1 简介2.2 auto的详细使用2.3 范围for&#xff08;C&#xff09;2.4 注意事项 一、 内联函数 1.1 定义 以inline修饰的函数叫做内联函数&#xff0c;…

UDP实现Mini版在线聊天室

实现原理 只有当客户端先对服务器发送online消息的时候&#xff0c;服务器才会把客户端加入到在线列表。当在线列表的用户发消息的时候&#xff0c;服务器会把消息广播给在线列表中的所有用户。而当用户输入offline时&#xff0c;表明自己要下线了&#xff0c;此时服务器把该用…

跨域问题一文解决

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;Vue ⛺️稳中求进&#xff0c;晒太阳 一、为什么会出现跨域的问题&#xff1f; 是浏览器的同源策略&#xff0c;跨域也是因为浏览器这个机制引起的&#xff0c;这个机制的存在还是在于安全…

【MATLAB源码-第6期】基于matlab的QPSK的误码率BER和误符号率SER仿真。

1、算法描述 QPSK&#xff0c;有时也称作四位元PSK、四相位PSK、4-PSK&#xff0c;在坐标图上看是圆上四个对称的点。通过四个相位&#xff0c;QPSK可以编码2位元符号。图中采用格雷码来达到最小位元错误率&#xff08;BER&#xff09; — 是BPSK的两倍. 这意味著可以在BPSK系统…

利用DataX工具,实现MySQL与OceanBase的数据同步实践

数据迁移是经常遇到的需求&#xff0c;市面上为此提供了众多同步工具。这里将为大家简要介绍DataX的使用。DataX 是阿里云 DataWorks数据集成 的开源版本&#xff0c;它作为离线数据同步的工具/平台&#xff0c;在阿里巴巴集团内部被广泛应用。DataX 能够实现多种异构数据源之间…

图书馆自习室|基于SSM的图书馆自习室座位预约小程序设计与实现(源码+数据库+文档)

图书馆自习室目录 基于SSM的图书馆自习室座位预约小程序设计与实现 一、前言 二、系统设计 三、系统功能设计 1、小程序端&#xff1a; 2、后台 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a…

[opencv]VideoWriter写出fourcc格式

fourcc支持的格式 fourcc全名Four-Character Codes&#xff0c;四字符代码&#xff0c;该编码由四个字符组成 cv2.VideoWriter_fourcc(O,O,O,O) cv2.VideoWriter_fourcc(*OOOO) 通常写法有上述两种形式&#xff0c;O代表一个字符&#xff0c;通常有 支持avi格式的有&#…

麒麟KOS删除鼠标右键新建菜单里不需要的选项

原文链接&#xff1a;麒麟KOS删除鼠标右键新建菜单里不需要的选项 Hello&#xff0c;大家好啊&#xff01;在日常使用麒麟KOS操作系统时&#xff0c;我们可能会发现鼠标右键新建菜单里包含了一些不常用或者不需要的选项。这不仅影响我们的使用效率&#xff0c;也让菜单显得杂乱…

【C++学习】C++11新特性(第一节)

文章目录 ♫一.文章前言♫二.C11新特性♫一.统一的列表初始化♫二.std::initializer_list♫三.声明♫四.decltype关键字♫五.nullptr♫六.新增加容器---静态数组array、forward_list以及unordered系列♫6.1unordered_map与unoredered_set♫6.2array♫6.3 forward_list&#xff…

上海亚商投顾:创业板指低开低走 低空经济概念股尾盘拉升

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数昨日集体调整&#xff0c;沪指午后跌超1%&#xff0c;深成指、创业板指盘中跌超2%&#xff0c;尾盘跌…

计算机视觉——基于深度学习UNet实现的复杂背景文档二值化算法实现与模型训练

1. 引言 阈值分割可以被视为一个分类问题&#xff0c;通常涉及两个类别&#xff0c;这也是为什么阈值分割也被称为二值化。对于文档图像&#xff0c;我们期望阈值算法能够正确地将墨水分类为黑色&#xff0c;将纸张分类为白色&#xff0c;从而得到二值化图像。对于数字灰度图像…

腾讯云人脸服务开通详解:快速部署,畅享智能体验

请注意&#xff0c;在使用人脸识别服务时&#xff0c;需要确保遵守相关的法律法规和政策规定&#xff0c;保护用户的合法权益&#xff0c;并依法收集、使用、存储用户信息。此外&#xff0c;腾讯云每个月会提供一定次数的人脸识别调用机会&#xff0c;对于一般的小系统登录来说…

头歌-机器学习 第11次实验 softmax回归

第1关&#xff1a;softmax回归原理 任务描述 本关任务&#xff1a;使用Python实现softmax函数。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;1.softmax回归原理&#xff0c;2.softmax函数。 softmax回归原理 与逻辑回归一样&#xff0c;softmax回归同样…

(三)、PTP时间精确协议如何工作的【Part1】

1、精确时钟需要校准 一个自由运行的时钟&#xff0c;本身每次的频率也不是绝对一致的&#xff08;每次频率都会有细微的差异&#xff09;&#xff0c;相位也是未知的。时间从来不是理想的&#xff0c;想到达到一个相对理想的准确时间&#xff0c;必须对时间进行调整&#xff0…

Electron 桌面端应用的使用 ---前端开发

Electron是什么&#xff1f; Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。 入门…

2024.4.8Morris中序遍历(线索二叉树)学习

这次博主在学习完知识点和代码之后&#xff0c;准备对这个知识重新进行整理总结。站在一个初学者的角度来看待这个知识点&#xff0c;在他人的讲解基础上加一点点自己的理解&#xff0c;并记录下来。以加深自己的理解&#xff0c;并且希望能够帮助到你。博主是一个初学者&#…

马云最新发声:AI时代刚刚到来,一切才刚开始,我们正当其时!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…