SpringAI 从0.5到1

为什么是从0.5到1呢,因为Ollama的过程我就不再赘述,网上的教程也很多,本篇将使用Ollama+SpringAI进行叙述。

开始

创建一个SpringAI项目

你可以使用SpringIO的QuickStart,我这里贴出我的Pom(主要的一些东西)

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store</artifactId>
</dependency>

配置好Ollama的连接配置

spring:
ai:
ollama:
base-url: https://x x x x
chat:
options:
model: qwen2.5:32b
temperature: 0 # 温度给到0,
init:
embedding:
additional-models: bge-large-zh-v1.5

上手

配置一个ChatClient

@Configuration
public class OllamaConfig {
private final InMemoryChatMemory chatMemory = new InMemoryChatMemory();
// InMemoryChatMemory 是大模型和用户对话的上下文
@Bean
public ChatClient chatClient(OllamaChatModel ollamaChatModel) {
MessageChatMemoryAdvisor messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);
return ChatClient.builder(ollamaChatModel)
.defaultSystem("你的首选语言是中文。") // 系统提示词
.defaultAdvisors(messageChatMemoryAdvisor)
.build();
}
}

开始和大模型对话吧

 @RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class AGIController {
private static final Logger log = LoggerFactory.getLogger(AGIController.class);
private final ChatClient chatClient;

@RequestMapping(value = "test", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
// TEXT_EVENT_STREAM_VALUE 使用SSE进行前后端交互
public Flux<String> test(String user) { // 返回Flux表示返回多个元素
return chatClient.prompt("你是智能助手")
.user(user) // 用户提示词
.stream().content();
}
}

基本的一个大模型对话程序就完成了,但是这远远不到1的标准,我们还需要

给大模型添加记忆

chatClient.prompt("你是智能助手")
.user(user) // 用户提示词
.advisors(advisor -> advisor.param("chat_memory_conversation_id", 不同用户唯一ID)
.param("chat_memory_response_size", 50)) // 给到模型的最大上下文长度
.stream().content();

给予大模型其他能力

通过Tool Calling可以让大模型调用本地的方法

@Tool(description = "获取用户信息工具,可以根据用户提供的名称获取到该用户的详细信息。需要给客户返回友好的信息。")
public CustomerInfo getCustomerInfoTool(@ToolParam(description = "名称,例如王富贵") String name) {
return Mono.fromCallable(() -> customerService.getCustomerInfo(name).block())
.subscribeOn(Schedulers.boundedElastic()).block();
}

定义工具,Tool和ToolParam是强烈建议的,大模型会通过工具和入参的描述来决定是否使用该工具进行调用

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Tool {
String name() default "";

String description() default "";

boolean returnDirect() default false;

Class<? extends ToolCallResultConverter> resultConverter() default DefaultToolCallResultConverter.class;
}

Tool还有个属性是returnDirect设置为True之后,将直接返回该方法返回的信息,不再交给AI渲染

BEFORE

Providing additional contextual info to tools

AFTER

Returning tool call results directly to the caller

工具定义完成之后,我们需要调用大模型的时候告诉他,有这个工具

ChatClientRequestSpec tools(String... toolNames);

ChatClientRequestSpec tools(FunctionCallback... toolCallbacks);

ChatClientRequestSpec tools(List<ToolCallback> toolCallbacks);

ChatClientRequestSpec tools(Object... toolObjects);

ChatClientRequestSpec tools(ToolCallbackProvider... toolCallbackProviders);

多种方法都可以给到模型,我这边是直接将定义tool的类传递进去

chatClient.prompt("你是智能助手")
.user(user) // 用户提示词
.tools(userTool)
.advisors(advisor -> advisor.param("chat_memory_conversation_id", 不同用户唯一ID)
.param("chat_memory_response_size", 50)) // 给到模型的最大上下文长度
.stream().content();

当你问他,查询喵喵的信息的时候,大模型就会通过这个工具查询喵喵的信息。

你说你想在工具调用的时候将用户标识传进去,不想越权查询到其他用户的信息,怎么办

上下文传递

如果你使用的是SpringMVC,那么你直接使用ThredLocal就可以了,本节主要对Flux用户解决问题。

当你按照平常上下文传递的方式进行操作的时候,发现在Spring Tool内获取不到你保存的Context,怎么办,我们可以使用Tool Context来解决这个问题。

chatClient.prompt("你是智能助手")
.user(user) // 用户提示词
.tools(userTool)
.toolContext(Map.of("userinfo", userinfo)
.advisors(advisor -> advisor.param("chat_memory_conversation_id", 不同用户唯一ID)
.param("chat_memory_response_size", 50)) // 给到模型的最大上下文长度
.stream().content();

这样在下文中就可以通过Mono.deferContextual(ctx -> Mono.just(ctx.get("userinfo")));来获取到信息了。

这样看来就完事大吉了?开发过程中你会发现你的TraceID,里面的和外面的不一样,这还是Tool Calling上下文不一致的问题

那咱们就手动给他传traceid和spanid吧

private final Tracer tracer;

chatClient.prompt("你是智能助手")
.user(user) // 用户提示词
.tools(userTool)
.toolContext(Map.of("traceid", tracer.currentSpan().context().traceId(),
"spanid",tracer.currentSpan().context().spanId())
.advisors(advisor -> advisor.param("chat_memory_conversation_id", 不同用户唯一ID)
.param("chat_memory_response_size", 50)) // 给到模型的最大上下文长度
.stream().content();

对不起,这样传递进去,在Tool内不也不会用你给的信息,那咋办

日志上下文对象传递

private final Tracer tracer;

Mono.deferContextual(ctx -> Mono.just(ctx.get("micrometer.observation")).map(obj -> {
chatClient.prompt("你是智能助手")
.user(user) // 用户提示词
.tools(userTool)
.toolContext(Map.of("micrometer.observation",obj)
.advisors(advisor -> advisor.param("chat_memory_conversation_id", 不同用户唯一ID)
.param("chat_memory_response_size", 50)) // 给到模型的最大上下文长度
.stream().content();
});

直接给他强制替换,好了,这样终于统一了

SpringAI到1了吗,还没有,等我后期更新SpringAI的RAG应用吧

延展阅读

  1. Spring AI