经过前两篇文章,了解了SpringAI的基础使用,今天主要介绍SpringAI的常用的功能,并结合案例代码进行学习。
一、连续对话
连续对话对于AI来说就是要提供上下文对话记录,才能进行连续对话,SpringAI提供了相应的方案,即ChatMemory接口代表聊天对话历史的存储。它提供了向对话添加消息、从对话中检索消息和清除对话历史记录的方法。 目前有两种实现,InMemoryChatMemory和CassandraChatMemory,分别在内存中为聊天对话历史提供存储,并相应地随生存时间而持久化。
1.InMemoryChatMemory 根据条数进行处理历史会话数据。
ini 代码解读复制代码InMemoryChatMemory memory = new InMemoryChatMemory();
@GetMapping(value = "/api/stream")
public Flux stream(String query, String conversationId){
final ChatClient chatClient = ChatClient.builder(ollamaChatModel).defaultAdvisors(new MessageChatMemoryAdvisor(memory,conversationId,10)).build();
final Flux content = chatClient.prompt(query).stream().content();
return content ;
}
用MessageChatMemoryAdvisor类来完成构建,其中conversationId是可以通过参数传递,也可以不指定,后面10就是表示保留的记录数,超过会自动保留最新10条,这个根据需要设置即可,有了这个配置后,就可以实现连续对话了。
2.CassandraChatMemory 根据时间来进行处理历史会话数据
less 代码解读复制代码CassandraChatMemory memory = CassandraChatMemory.create(CassandraChatMemoryConfig.builder().withTimeToLive(Duration.ofDays(1)).build());
替换memory同上实例即可,官方文档有CassandraChatMemory类说明,但我在SpringAI 1.0.0-SNAPSHOT版本中没有发现CassandraChatMemory类,暂且先用InMemoryChatMemory来实现。
二、向量化Embedding RAG增强文档问答
- embedding向量化,需要配置相应的model,我使用本地ollama模型,所以使用OllamaEmbeddingModel类来实现,另外还需配置专门的文本向量化模型 application.yml配置向量模型
yaml 代码解读复制代码spring:
ai:
ollama:
init:
pull-model-strategy: when_missing
embedding:
model: mxbai-embed-large #这里可以改成其他模型
additional-models:
- mxbai-embed-large #这里可以改成其他模型
less 代码解读复制代码@GetMapping("/embedding")
public EmbeddingResponse embedding(String content){
return ollamaEmbeddingModel.call(new EmbeddingRequest(Collections.singletonList(content), OllamaOptions.builder().model(OllamaModel.MXBAI_EMBED_LARGE).truncate(false).build()));
}
通过这个一步就可以实现文本内容到向量化的转换。
- 使用ETL Piepeline 读取文件实现文档向量化(pdf、doc、json等)。
引入依赖(如果只需要指定格式可以只导具体的包就可以了,我这里为了方便,导入了多格式包)
xml代码解读复制代码
<dependency> <groupId>org.springframework.aigroupId> <artifactId>spring-ai-tika-document-readerartifactId> dependency>
上传文件实现文档向量化
ini 代码解读复制代码 @GetMapping("/uploadFile")
public List uploadFile(MultipartFile file){
if(file==null){
return Collections.emptyList();
}
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(file.getResource());
TokenTextSplitter splitter = new TokenTextSplitter(800, 350, 10, 5000, true);
final List documents = splitter.split(tikaDocumentReader.read());
return documents;
}
- RAG增强文档问答 要实现文档问答,我们可以使用SimpleVectorStore来测试向量化,修改文档上传代码 在构造函数中初始化SimpleVectorStore
arduino 代码解读复制代码private final VectorStore pvectorStore;
public XXXController(){
this.pvectorStore= SimpleVectorStore.builder(ollamaEmbeddingModel).build();
}
上传文档后将问答内容进行处理后进行向量化放到SimpleVectorStore向量存储器中
ini 代码解读复制代码
@GetMapping("/uploadFile")
public List uploadFile(MultipartFile file){
if(file==null){
return Collections.emptyList();
}
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(file.getResource());
TokenTextSplitter splitter = new TokenTextSplitter(800, 350, 10, 5000, true);
final List documents = splitter.split(tikaDocumentReader.read());
pvectorStore.add(documents);
return documents;
}
- 实现RAG增强问答,这里使用RetrievalAugmentationAdvisor类来实现,当然网上也有基于提示词来实现的
scss 代码解读复制代码@GetMapping("/streamRAG")
public Flux streamRAG(String query){
ChatClient chatClient = ChatClient.create(ollamaChatModel);
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder().queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(chatClient.mutate())
.build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(pvectorStore)
.build())
.build();
return chatClient.prompt().advisors(retrievalAugmentationAdvisor).user(query).stream().content();
}
- 使用提示词上下文实现方式
ini 代码解读复制代码@GetMapping("/streamRAG")
public Flux streamRAG(String query){
String ragPromptTemplate = """
Context information is below.
---------------------
{context}
---------------------
Given the context information and not prior knowledge, answer the question in Chinese.
You need to respond with content in context first, and then respond with your own database. When the given context doesn't help you answer the question, just say "I don't know."
Question: {question}
Answer:
""";
ChatClient chatClient = ChatClient.create(ollamaChatModel);
final List documents = pvectorStore.similaritySearch(SearchRequest.builder().query(query).similarityThreshold(0.5).topK(5).build());
final String context = documents.stream().map(Document::getText).collect(Collectors.joining(System.lineSeparator()));
final PromptTemplate template = new PromptTemplate(ragPromptTemplate);
final Prompt prompt = template.create(Map.of("context", context, "question", query));
return chatClient.prompt(prompt).stream().content();
当然了这里只是为了简单方便演示,所以使用基于内存的方式,一般向量化存储我们都会使用一些更可靠的向量数据库如Redis Stack、Postgresql等数据库作为持久化向量数据库,SpringAI官方文档有多种数据库场景使用的说明。我这里提供一下基于postgresql数据库的向量存储示例,首先要引入依赖。
xml 代码解读复制代码<dependency>
<groupId>org.springframework.aigroupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starterartifactId>
dependency>
配置application.yml
yaml 代码解读复制代码spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
batching-strategy: TOKEN_COUNT
max-document-batch-size: 10000
然后注入 VectorStore即可:
arduino 代码解读复制代码private final VectorStore pvectorStore;
public XXXController(VectorStore pvectorStore){
this.pvectorStore= pvectorStore;
}
三、函数工具调用 Tool Calling
在SpringAI中调用工具很简单,只需要在方法上加上@Tool注解即可,前提是调用的模型支持function call 功能,不然无法调用函数,接下来实现一下自定义工具方法,让ai调用自定义的工具方法。
自定义一个MyTools类,实现一个自定义工具方法,比如获取当前时间,MyTool类中可以定义多个自定义工具方法的,会根据不同的描述信息进行调用不同的工具方法。
kotlin 代码解读复制代码public class MyTools {
@Tool(description = "获取当前日期和时间")
public String getCurrentTime(){
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
实现模型调用,当我们询问当前时间获取今天日期时,模型会调用我们提供的工具方法实现获取当前时间并返回结果,这个过程是调用完我们的工具方法得到结果后ai进行组织回答的结果,如果我们想直接得到原始结果则需要在@Tool注解中配置直接返回即可returnDirect=true,如:@Tool(description = "获取当前日期和时间", returnDirect = true)。
typescript 代码解读复制代码@GetMapping("/callTool")
public Flux<String> callTool(String query){
return ChatClient.create(ollamaChatModel).prompt(query).tools(new MyTools()).stream().content();
}
关于智能体Agent,实际上也是一种Function call但是它内部还包含了一些Function call也就是tool。在Function Call中可以编写数据库查询,或者调用其他接口也就是AI的记忆部分了,在早期的SprnigAI版本中tool通过Function定义的时候,实现Agent,是不方便的,目前版本通过Tool注解,支持自定义类之后还是方便很多了,直接可以定义多个Tool方法来实现即可。
四、AI联网
之前没有做过AI开发,总是觉得AI联网问答很牛,因为AI的缺点之一就是无法获取实时信息,如何将AI接入互联网获取实时信息变得非常重要,接下来我就分享AI联网的两种方式给介绍一下,其实也就是基于提示词上下文和tool工具调用分别来实现联网问答功能。
- 基于上下文,如果当前模型不支持tool工具调用,使用上下文方式就可以解决了联网问题
ini 代码解读复制代码//调用搜索接口查询数据
private String getNetworkContent(String query){
final HttpRequest request = HttpRequest.newBuilder().GET().uri(URI.create("http://www.baidu.com/s?tn=json&rn=5&wd=".concat(query))).build();
HttpClient client = HttpClient.newHttpClient();
try {
final HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode()== HttpStatus.OK.value()){
String body = response.body();
final JsonNode jsonNode = objectMapper.readTree(body);
final JsonNode jsonNode1 = jsonNode.get("feed");
final Iterator entry = jsonNode1.get("entry").elements();
final StringBuilder sb = new StringBuilder();
while (entry.hasNext()){
final JsonNode item = entry.next();
final Object title = item.get("title");
final Object abs = item.get("abs");
if(title!=null&&abs!=null){
sb.append("来源:");
sb.append(title);
sb.append("\n");
sb.append("内容:");
sb.append(abs);
sb.append("\n\n");
}
}
final String s = sb.toString();
System.out.println(s);
return s;
}
} catch (Exception e) {
e.printStackTrace();
return "检索失败,请稍后再试";
}
return "检索失败,请稍后再试";
}
@GetMapping("/network")
public Flux network(String query){
String network_prompt = """
根据搜索的结果回答用户问题。如果信息不足,请明确说明。
搜索内容:
{context}
问题:{question}
答案:
""";
//调用网络搜索结果生成上下文信息
final String context = getNetworkContent(query);
final PromptTemplate template = new PromptTemplate(network_prompt);
final Prompt prompt = template.create(Map.of("context", context, "question", query));
return ChatClient.create(ollamaChatModel).prompt(prompt).stream().content();
}
- 使用tool实现联网问答,加入网络搜索工具方法即可
ini 代码解读复制代码public class MyTools {
private final ObjectMapper objectMapper = new ObjectMapper();
@Tool(description = "获取当前日期和时间", returnDirect = true)
public String getCurrentTime(){
System.out.println("时间执行了");
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
@Tool(description = "search 当需求了解实时信息时使用该工具联网搜索回答")
String search(String query){
System.out.println("联网搜索-执行了");
final HttpRequest request = HttpRequest.newBuilder().GET().uri(URI.create("http://www.baidu.com/s?tn=json&rn=5&wd=".concat(query))).build();
HttpClient client = HttpClient.newHttpClient();
try {
final HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode()== HttpStatus.OK.value()){
String body = response.body();
final JsonNode jsonNode = objectMapper.readTree(body);
final JsonNode jsonNode1 = jsonNode.get("feed");
final Iterator entry = jsonNode1.get("entry").elements();
final StringBuilder sb = new StringBuilder();
while (entry.hasNext()){
final JsonNode item = entry.next();
final Object title = item.get("title");
final Object abs = item.get("abs");
if(title!=null&&abs!=null){
sb.append("来源:");
sb.append(title);
sb.append("\n");
sb.append("内容:");
sb.append(abs);
sb.append("\n\n");
}
}
final String s = sb.toString();
System.out.println(s);
return s;
}
} catch (Exception e) {
e.printStackTrace();
return "检索失败,请稍后再试";
}
return "检索失败,请稍后再试";
}
//调用
@GetMapping("/callTool")
public Flux callTool(String query){
return ChatClient.create(ollamaChatModel).prompt(query).tools(new MyTools()).stream().content();
}
五、总结
本文主要介绍了SpringAI常用的功能:连续对话、向量化RAG增强问答文档向量化、工具函数调用智能体Agent以及AI联网问答这几个比较常用的功能,了解基本使用方法。通过这篇文章可以学习到这些功能的基本使用,可以更好的学习了解SpringAI,如果有什么问题欢迎指正,如果觉得对你有帮助,喜欢我的文章记得关注我😊。
评论记录:
回复评论: