• SpringAI 02 – Chat Client API

    ChatClient 提供了与AI模型交互的fluent API。它同时支持同步和流式编程模型。 ChatClient fluent API拥有构建传递给AI模型的提示(Prompt)的组成部分的方法。提示包含指导AI模型输出和行为的指令文本。从API的角度来看,提示由一系列消息组成。 AI模型处理两种主要类型的消息: 用户消息,即用户的直接输入。 系统消息,由系统生成以指导对话。 这些消息通常包含占位符,这些占位符在运行时会根据用户输入进行替换,以自定义AI模型对用户输入的响应。 还可以指定一些提示选项,例如: AI模型的名称,即要使用的AI模型的名称。 温度设置,控制生成输出的随机性或创造性。 这些功能使得ChatClient成为一个强大的工具,允许开发者以灵活的方式与AI模型进行交互,并通过定制化的提示和消息来优化AI模型的响应。 创建 ChatClient 使用 ChatClient.Builder 对象创建 ChatClient。你可以为任何 ChatModel SpringBoot 自动配置获取自动配置的 ChatClient.Builder 实例,或者手动创建一个。 使用自动配置的 ChatClient.Builder 在最简单的用例中,Spring AI 通过 SpringBoot 自动配置创建了一个 ChatClient.Builder 实例原型,使用时可以将其注入到类中。以下是一个简单的示例,用于检索对简单用户请求的字符串响应。 在这个简单的例子中,用户输入设置了用户消息的内容。call() 方法向 AI 模型发送请求,content() 方法将 AI 模型的响应作为字符串返回。 手动创建 ChatClient 可以通过设置属性 spring.ai.chat.client.enabled=false 来禁用 ChatClient.Builder 自动配置。当同时使用多个聊天模型时,这个配置会很有用。然后,为每个需要的 ChatModel 手动创建一个 ChatClient.Builder 实例: ChatClient Fluent API ChatClient fluent API 允许使用重载的 prompt() 方法以三种不同的方式创建提示(Prompt),以启动fluent API: prompt(): 此方法不接受任何参数就可以开始使用fluent API,允许您构建用户、系统和其它提示(prompt)部分。 prompt(Prompt prompt): 此方法接受一个 Prompt 实例作为参数,这个参数可以是一个非fluent API 创建的 Prompt 实例。 prompt(String content): 这是一个快捷方法,类似于之前重载的方法,它接受用户文本内容作为参数。 ChatClient 响应 ChatClient API 提供了几种格式化 AI 模型响应内容的方法。 返回 ChatResponse AI 模型的响应是一个由类型 ChatResponse 定义的复杂结构。ChatResponse中包含关于生成响应的元数据,并且还可以包含多个响应,称为 Generations,每个Generation都有自己的元数据。元数据还包括用于创建响应的token数量(每个token大概是 3/4 个单词)。这个信息很重要,因为托管的 AI 模型会根据每个请求中使用的token数量收费。 以下示例展示了如何获取包含元数据的 ChatResponse 对象的过程,ChatResponse实例是在 call() 方法后执行 chatResponse() 方法获得: 返回Entity 有时会希望将返回的字符串映射为某个特定的实体类的对象。entity() 方法提供了这个功能。 比如下面的 Java record类: 可以使用 entity() 方法轻松地将 AI 模型的输出结果映射为这个record类,如下所示: 还有一个重载的 entity() 方法,方法签名为 entity(ParameterizedTypeReference type),让您可以指定更复杂的类型,如List泛型: 流式响应 使用 stream() 方法可以像下面这样获得异步响应: 也可以使用 Flux<ChatResponse> chatResponse() 方法流式传输 ChatResponse 对象结果。 Spring AI将提供一个更便捷的方法,让开发者能够使用反应式 stream() 方法返回 Java Entity结果。与此同时,还可以使用结构化输出转换器( Structured Output Converter )来显式转换聚合的响应结果,就跟下面的例子一样。这个例子里也展示了fluent API 中参数的使用,具体将在文档的后续部分详细讨论。 call() 方法返回值 在 ChatClient 上指定 call() 方法后,有如下几种不同的响应类型选项。 String content(): 返回字符串格式的响应结果 ChatResponse chatResponse(): 返回包含多个Generation和响应元数据的 ChatResponse 对象,例如用于创建响应的token数量。 entity() 返回指定 Java 类型的返回结果 entity(ParameterizedTypeReference<T> type): 用于返回集合类型的结果(支持泛型)。 entity(Class type): 用于返回特定类型的结果。 entity(StructuredOutputConverter structuredOutputConverter): 可以指定 StructuredOutputConverter 实例,将字符串转换为需要的类型。 也可以使用 stream() 方法来替换 call()方法。 stream() 方法返回值 在 ChatClient 上指定 stream() 方法后,有几种响应类型选项: Flux<String> content(): 返回 AI 模型生成的字符串Flux对象。 Flux chatResponse(): 返回包含响应的额外元数据的 ChatResponse 对象的 Flux对象。 使用默认值

    [阅读更多...]
  • SpringAI 01 – AI概念

    模型 Model 模型是旨在处理和生成信息的算法,通常模仿人类认知功能。通过从大型数据集中学习模式和洞察力,这些模型可以进行预测、生成文本、图像或其他输出,增强各行业的应用。 当前有许多不同类型的 AI 模型,每种模型会适配特定的用例。虽然 ChatGPT 及其生成式 AI 功能通过文本输入和输出吸引了用户,但许多模型和公司提供了多样化的输入和输出。在 ChatGPT 之前,许多人对 Midjourney 和 Stable Diffusion 等文本到图像的生成模型着迷。 下表根据输入和输出类型对几种模型进行了分类: Spring AI 目前支持将输入和输出处理为语言、图像和音频的模型。上表中的最后一行,即接受文本作为输入并输出数字的那行,通常被称为嵌入文本,代表 AI 模型中使用的内部数据结构。Spring AI 支持嵌入(Embedding)以支持更先进的用例。 像 GPT 这种模型的独特之处在于它们的预训练性质,正如 GPT(Chat Generative Pre-trained Transformer)中的 “P” 所表示的。这种预训练特性将 AI 转变为一种通用开发工具,但不需要更多的机器学习或模型训练背景。 提示 Prompt 提示是基于语言的输入的基础,用于引导 AI 模型产生特定输出。对于熟悉 ChatGPT 的人来说,可能提示看起来只是输入到对话框中并发送到 API 的文本。然而,它远不止于此。在许多 AI 模型中,提示的文本不仅仅是一个简单的字符串。 ChatGPT 的 API 在一个提示中会有多个文本输入,每个文本输入都被分配了一个角色。例如,有系统角色,它告诉模型如何表现并设置交互的上下文。还有用户角色,通常就是用户的输入。 创建有效的提示既是科学也是艺术。ChatGPT 是为人类对话而设计的。这与使用 SQL 等特定的数据库查询语言来进行 “提问” 有很大不同。与 AI 模型进行交流必须要像与另一个真实的人交谈一样。 这种交互方式非常重要,以至于出现了像 “提示工程” 这样类似一门学科的名词。有大量提高提示有效性的技术正在涌现。花时间精心设计提示可以极大地改善最终输出的结果。 共享提示已成为一种常用的做法,并且在这个主题上正在进行积极的学术研究。作为创建有效提示可能有多违反直觉的一个例子(例如,与SQL比较),最近的一篇研究论文发现,最有效的一个提示可以用这样的语句开头: “深呼吸,一步一步地做这个”。这应该可以让你了解为什么语言如此重要。不幸的是我们还不完全理解如何最有效地利用这项技术,即使是在之前的迭代版本中(如 ChatGPT 3.5),更不用说正在开发的新版本了。 提示模板 Prompt Template 创建有效提示涉及建立请求的上下文,并将请求中指定的部分内容替换为让用户输入的值。 这个过程使用传统的基于文本的模板引擎进行提示创建和管理。Spring AI 为此使用了 OSS 库中的 StringTemplate。 例如下面就是一个简单的提示模板: Tell me a {adjective} joke about {content}. 在 Spring AI 中,提示模板可以类比为 Spring MVC 架构中的 “视图”。提供一个模型对象(通常是 java.util.Map)来填充模板中的占位符。这样“渲染” 后的字符串就成为提供给 AI 模型的提示内容。 发送给模型的提示的具体数据格式有很大差异。最初是简单的字符串,现在提示已经发展到包括多个消息,一条消息中的每个字符串代表模型的一个不同角色。 嵌入 Embedding 嵌入是文本、图像或视频的数值表示,用于捕获输入之间的关系。 嵌入通过将文本、图像和视频转换为浮点数数组(称为向量)来工作。这些向量被用来捕获文本、图像和视频的含义。嵌入数组的长度称为向量的维度。 通过计算两段文本的向量表示之间的数值距离,应用程序可以确定用于生成嵌入向量的对象之间的相似性。 作为探索 AI 的 Java 开发人员,不需要理解这些向量表示(vector representations)背后的复杂数学理论或具体实现,只需要对它们在 AI 系统中的作用和功能有基本的了解就足够了,特别是当你将 AI 功能集成到应用程序中时。 嵌入在像检索增强生成(RAG Retrieval Augmented Generation)模式这样的实际应用中特别相关。它们使数据能够表示为语义空间中的点,这类似于欧几里得几何中的二维空间,但维度更高。这意味着就像欧几里得几何中平面上的点根据其坐标可以近或远一样,在语义空间中,点的接近程度反映了含义的相似性。对应相似主题的语句在这个多维空间中位置更接近,就像图形上彼此靠近的点一样。这种接近有助于进行文本分类、语义搜索甚至产品推荐等任务,因为它允许 AI 根据它们在这个扩展的语义景观中的 “位置” 来识别和分组相关概念。

    [阅读更多...]
  • springboot入门14 – Kafka应用

    简述 这几天优化了一下之前写的一个springboot kafka组件。比较起原生的spring-kafka来,我希望能够简化kafka的使用,可以更聚焦于具体的消息处理逻辑。 接下来的内容是这个组件的用法。 使用方法 添加依赖 这个组件已经提交到了maven中央仓库,可以直接通过依赖的形式引入: 0.2.2是这两天刚发布的一个版本。 消费者Processor kafka-spring-boot-starter这个组件已经完成了kafka消费者的主要功能。 对于开发者来说,可以不必关注KafkaConsumer的创建,只需要实现Processor接口并注入到容器中即可。 下面是一个简单的示例: 如示例中,通过@Component注解完成了Processor实现类的实例的注入,并为注入的Bean提供了一个名称:zhyyy。记住这个名称,在之后的配置文件中会用到。 使用生产者 kafka-spring-boot-starter会根据配置主动创建KafkaProducer。开发使用时可以直接从容器中获取ProducerTemplate实例来发送消息: 如果写入kafka的消息的key和value的序列化方案采用的都是默认的字符串(反)序列化方案(StringDeserializer和StringSerializer),可以使用StringProducerTemplate实例: 发送消息时酌情调用不同的send()方法: 配置 下面是一个最简单的配置: 如上配置中: test-group00既是配置项的ID,也是消费组ID bootstrap-servers我想不需要多做解释。 topics对应的是一个数组结构,也可以写作[test-topic1]或[test-topic1,test-topic2],即支持同一个kafka集群上多个类似topic的统一处理 consumer是消费者相关配置,processor对应的是Processor实现类的Bean名称,count标识的是应用内消费线程的数量 默认的序列化方案采用的是字符串序列化方案。 虽然在配置中没有体现,但kafka-spring-boot-starter组件会基于已有的信息创建KafkaProducer,使用时可以通过ProducerTemplate执行消息发送。 比较完整的配置是这样子的: 其中common模块下是一些通用的配置,config模块下则是一或多组具体的配置(这里是两组)。common下的配置会被config下的配置覆盖。 此外还独立出来了一些常用的配置项,如autoOffsetReset,keyDeserializer等,以便在使用时进行配置。 其他 示例应用在 github / spring-boot-kafka。 kafka-spring-boot-starter这个组件的源码也在 github 。如果有定制化的需求可以据此进行调整。 End!

    [阅读更多...]
  • springboot入门12 – SpringBoot MyBatis读写分离

    概述 随业务量增长,数据库读写分离是迟早要面临的问题。另外,公司在上规模后一般也会要求统一采用主从分布式数据库。 我习惯的处理方案是在应用层进行隔离:即将以写为主的业务放在一个应用上,以读为主的业务放在其他应用上。这应该算是最简单粗暴的解决方案了,却也能帮我应对90%需要读写分离的场景。不过总还有10%的特殊场景需要思考下怎样在应用内实现读写分离。 在应用内做读写分离大体上需要考虑三件事情: 多数据源实现 读写请求识别 读写请求分流 其中后两点是执行读写分离的关键。 接下来详细介绍下怎么在Springboot+MyBatis的应用中实现读写分离。这里会用到H2数据库和dbcp2数据库连接池。在测试中不会真的创建一个数据库集群,我们只需要能够验证写入和读取是访问的两个不同的数据库即可。 1. 多数据源实现 之前在《SpringBoot自定义数据源及多数据源配置》这篇文里我有介绍过怎样做多数据源实现。这次的做法也差不多。 下面是在配置文件中做的多数据源配置: 这里配置了两个H2的内存数据库,我们权且当它们是一个集群吧。 然后是在一个配置类DsConfig中读取配置: 和之前那篇文章《SpringBoot自定义数据源及多数据源配置》略有不同,这次是用DataSourceProperties来表示读取的配置信息。 注意不要忽略了@Primary注解,不然会报错。 可以这样使用DataSourceProperties创建DataSource的实例: 这里只是想试试这种方案。按照老路子创建DataSource实例也是OK的,并且还会更简洁: 至此多数据源配置已经完成。 2. 读写请求识别 也看过其他人的读写分离方案,其中读写请求识别这一层多是通过自定义注解+AOP来实现的。这种方案当然没问题,但是稍嫌有些繁琐,如果忘掉了添加注解就会导致意外。我更想要的是一种‘润物细无声’的实现。 之前做过一个为写入数据库的实例赋默认值的方案:《MyBatis写入时null问题统一处理方案》。这个方案的思路是通过自定义实现MyBatis拦截器来拦截写数据库请求并补上未赋值的数据。稍稍变通下,就可以改为拦截并识别查询语句: 查询请求主要是由Executor类的query方法实现的,所以只要针对其进行拦截并执行判断即可。 因为判断读写请求和执行读写分流是在两个环节执行,我们需要找个地方将判断结果存储起来,并且保证线程安全,很自然可以想到使用ThreadLocal执行存储。DsContextHolder就是基于ThreadLocal执行的存储。看下实现: 接下来就可以使用DsContextHolder中存储的信息来执行分流了。 3. 读写请求分流 读写请求分流这一层主要依赖了AbstractRoutingDataSource这个类。核心是下面这个方法: 看名字也能知道,determineTargetDataSource决定了为之后的请求提供哪个数据源。根据代码可以看出来,我们至少需要做两件事: 为resolvedDataSources赋值,即设置相关数据源 实现分流方法determineCurrentLookupKey() 具体如何继承AbstractRoutingDataSource类并实现路由方案可以参考如下代码: 在afterPropertiesSet()方法中完成了自定义数据源的创建和设置,并且还将dsWrite设置为了默认数据源。 方法determineCurrentLookupKey()基于DsContextHolder中存储的内容提供了分流的关键字。 大体上就是这样了。具体实现代码已经上传到了GitHub : zhyea/database-wr End!! 参考文档 mybatis读写分离

    [阅读更多...]