说明 关于 Solon Cloud 目前正在准备「熔断与限流」 的部分,想使用 sentinel 的插件,但我之前只用过 Spring 集成的 Hystrix,还需要先学习下 sentinel,目前还在准备当中,不会那么快更新。Solon AI 是 《Solon 实用教程》的第六部分,算是新增章节,恰好之前有使用过 agents-flex,准备起来更容易些,因此先开始更新 Solon AI 部分。
Solon 将在 3.1.0 版本引入 AI 相关插件,现在已经发布 SNAPSHOT 版本,现在接口和功能还不稳定,只建议尝鲜。
从官网的介绍中(https://solon.noear.org/article/learn-solon-ai),可以看到Solon AI 对大模型的支持是比较完整的,聊天模型接口支持同步调用,流式调用,Function Call,记忆功能,多种消息角色和多种消息格式,提供 RAG 支持和流程编排。
在初体验中,我测试的是聊天模型的同步调用,流式调用,Function Call这几个功能。
前置 本地测试使用了 ollama,需要安装好 ollama (https://ollama.com/download),并下载好模型,我这里演示用的两个模型一个是deepseek-r1:7b(基于 qwen2.5 的蒸馏的一个推理模型)和 qwen2.5:7b(支持tools,在ollama 可以通过点击 tools 标签查看哪些模型支持 tools)。
1 2 ollama run deepseek-r1:7b ollama run qwen2.5:7b
如果无法调用 ollama 接口时,可做如下配置,主要处理跨域或者局域网调用。
1 2 3 4 5 6 vi .zshrc export OLLAMA_ORIGINS="*" export OLLAMA_HOST=0.0.0.0:11434source .zshrc
依赖 增加 solon-ai 的依赖,solon-web-rx 和 solon-web-sse 用于支持流式调用。
1 2 3 4 5 6 7 8 9 10 11 12 dependencies { implementation platform (project(":demo-parent" ) ) implementation ("org.noear:solon-web" ) implementation ("org.noear:solon-ai" ) implementation ("org.noear:solon-logging-logback" ) implementation ("org.noear:solon-openapi2-knife4j" ) implementation ("org.noear:solon-web-rx" ) implementation ("org.noear:solon-web-sse" ) testImplementation ("org.noear:solon-test" ) }
配置 我这里使用 ollama 服务时,需要配置 provider。如果使用云服务时,设置 apiKey。
这里配置 timeout 是避免使用推理模型时,接口调用时间过长报错问题,可以根据自己的模型和机器的配置情况进行调整。
1 2 3 4 5 6 7 8 demo: llm: apiUrl: "http://127.0.0.1:11434/api/chat" provider: "ollama" model: "qwen2.5:7b" timeout: 600s
实现 LlmConfig 通过注入的方式获取配置,并初始化ChatModel,这里可以根据自在的需求进一步配置ChatModel。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.example.demo.ai.llm.config;import org.noear.solon.ai.chat.ChatConfig;import org.noear.solon.ai.chat.ChatModel;import org.noear.solon.annotation.Bean;import org.noear.solon.annotation.Configuration;import org.noear.solon.annotation.Inject;@Configuration public class LlmConfig { @Bean public ChatModel build (@Inject("${demo.llm}") ChatConfig config) { return ChatModel.of(config).build(); } }
Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.example.demo.ai.llm.controller;import com.example.demo.ai.llm.service.LlmService;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import java.io.IOException;import org.noear.solon.annotation.*;import org.noear.solon.core.util.MimeType;import reactor.core.publisher.Flux;@Controller @Mapping("/llm") @Api("聊天") public class LlmController { @Inject private LlmService service; @ApiOperation("chat") @Post @Mapping("chat") public String chat (String prompt) { return service.chat(prompt); } @Produces(MimeType.TEXT_EVENT_STREAM_UTF8_VALUE) @Mapping("stream") public Flux<String> stream (String prompt) throws IOException { return service.stream(prompt); } @ApiOperation("functionCall") @Post @Mapping("functionCall") public String functionCall (String prompt) { return service.functionCall(prompt); } }
Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.example.demo.ai.llm.service;import java.io.IOException;import org.noear.solon.ai.chat.ChatModel;import org.noear.solon.ai.chat.ChatResponse;import org.noear.solon.annotation.Component;import org.noear.solon.annotation.Inject;import reactor.core.publisher.Flux;@Component public class LlmService { @Inject private ChatModel chatModel; public String chat (String prompt) { try { ChatResponse response = chatModel.prompt(prompt).call(); return response.getMessage().getContent(); } catch (IOException e) { throw new RuntimeException(e); } } public Flux<String> stream (String prompt) throws IOException { return Flux.from(chatModel.prompt(prompt).stream()) .filter(ChatResponse::hasChoices) .map(resp -> resp.getMessage().getContent()); } public String functionCall (String prompt) { try { ChatResponse response = chatModel.prompt(prompt).options(o -> o.functionAdd(new Tools())).call(); return response.getMessage().getContent(); } catch (IOException e) { throw new RuntimeException(e); } } }
Solon 提供了多种设置 Fuction Call 的方式,这里只是从官网拿过来的一个例子,更多内容看这里 https://solon.noear.org/article/921 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.example.demo.ai.llm.service; import org.noear.solon.ai.chat.annotation.FunctionMapping;import org.noear.solon.ai.chat.annotation.FunctionParam;public class Tools { @FunctionMapping (description = "获取指定城市的天气情况" ) public String get_weather ( @FunctionParam (name = "location" , description = "根据用户提到的地点推测城市" ) String location ) { if (location == null ) { throw new IllegalStateException("arguments location is null (Assistant recognition failure)" ); } return "晴,24度" ; } }
验证 同步调用
流式调用 可以在浏览器中直接进行测试,目前的情况是,如果使用了推理模型 think 可能不显示,如果推理时间过长,可能出现超时的情况。作者在最新版本的SNAPSHOT中已处理,但我暂时还拉取不到最新版本。
Function Call 我们可能看到这例子里面只简单返回了 “晴,24度” 的天气情况。经过大模型的处理,内容就更完整丰富了。
小结 这里我只测试了 Solon AI 的基础功能,可以说是非常容易上手,通过简单的配置就能调用本地的服务了,如果是云服务也是一样的,增加配置 apiKey 就可以了。后续我将继续测试 Solon AI 的 RAG 和 Flow 的功能。
广告 在 AI 来临的时候,越来越多的新奇的东西会占有我们的时间,我们的注意力,不少人甚至认为有了 AI 甚至可以不用学习了,然而现实有了 AI 能学的人越能学,不学的人越不会学,就像现在面对同样的 AI 有的人能做出精美的图片,有的人能写出生动的故事,但一些人面对 AI 还是不知所措。那么更关键的是即使没有 AI,自己也应该是一台学习机器、生产机器。不论学习还是生产都是需要占用时间的,或者更准确的说是注意力,这个注意力就好像是时间的利用率,注意力越高,时间利用率就越高,越能学习,越能生产。