引言
最近DeepSeek可谓风光无限,AI可谓是目前互联网最火热的几个名词,我也一直在关注他的发展,从以前的人工智障,到chatGPT的高不可攀(价格太贵),再到DeepSeek的横空出世,才看到了AI从概念到应用的真正出路。可以这样说,随着DeepSeek的出现,势必会在接下来的几年让AI变得异常火热。最近接入了coze和DeepSeek两款,接下来简单介绍下Spring AI接入DeepSeek。
本地部署
首先,接入DeepSeek有两种方式,一种是通过DeepSeek官网接入API的方式,另一种是本地部署DeepSeek,通过Ollama接入API接口。这里我选择了本地DeepSeek接入的模式,因为手上正好有个项目可能会用到AI,甲方希望数据安全性,所以需要本地部署DeepSeek。
我们先去下载Ollama,我这里还在测试,所以用的自己电脑,下载好后直接下一步安装,没有任何难度
这里前文已经实践过了大家可以去参考一下
本地部署Deepseek
运行成功后我们可以开始下一步了
Spring 调用本地API
Spring AI包封装了大部分AI调用的工具,适用于很多大模型,这里因为我们用的是本地Ollama,所以也用对应的包。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-SNAPSHOT</version> <type>pom</type> <scope>import</scope>
</dependency>
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> <version>1.0.0-M5</version>
</dependency>
引入对应版本包,这里需要注意一点,Spring AI的SpringBoot版本需要3.2.x以上,jdk需要17版本以上,所以我这里将项目从2.3.7.RELEASE升级到了3.3.4。需要升级的时候也遇到不少坑,比如个别包名变了,或者其他三方包冲突,日志冲突等问题,旧的项目升级还是有一定难度的。
增加模型配置
spring:ai: ollama: base-url: http://localhost:11434 chat: model: deepseek-r1:1.5b openai: chat: options: response-format: json
默认地址是本地Ollama的地址,以后也可以改成远程,配置需要的大模型模板deepseek-r1:1.5b以及反参格式为json
大模型对话一般情况是不支持直接返回json格式的,SpringAI通过在对话增加关键词信息让AI回答以json格式返回,然后再通过框架将返回的内容解析成json,在解析成对象。这里可以不用在配置中指定json格式,也可以在每次对话的时候指定格式。
java">@Configuration
public class DeepSeekConfig {//注入模型,配置文件中的模型,或者可以在方法中指定模型@Autowiredprivate OllamaChatModel model;@Beanpublic ChatClient chatClient() {return ChatClient.builder(this.model).defaultAdvisors().build();}
}
配置chatChlient类
java"> @Autowired private ChatClient chatClient;@PostMapping("/testChat")public DeepSeekResponse testChat(@RequestBody AIChatReq param){//直接返回return chatClient.prompt(param.getMsg()).call().entity(DeepSeekResponse.class);}
新增一个测试接口,发起一个对话试试,这里我希望让AI根据我给出的公式和参数,计算出一个结果值,然后结果通过json返回给我。
以下是我的问题:
V=(A+2C+K×H)×H×L ,这是一个开挖土方计算公式,其中V表示基槽土方量,A表示槽底宽度,C表示工作面宽度,H表示基槽深度,L表示基槽长度,如果A=1.5,C=2,H=2,L=3,k=0,V等于多少
启动服务后,通过postman访问一下
看下日志报错
日志上发现DeepSeek确实回应了我的请求,最后也明确给出了正确结果,为何会转换失败?
跟踪源码,最后发现报错是在BeanOutputConverter的convert方法上
可以看到Spring AI包将AI回答转换成对象的逻辑
首先会根据反参类型格式化提问,就是通过实现FormatProvider接口实现,StructuredOutputConverter继承FormatProvider接口
BeanOutputConverter实现StructuredOutputConverter接口,同时主要实现的还有MapOutputConverter,ListOutputConverter两个类。
主要作用也很明晰,BeanOutputConverter是将结果转换成对象,MapOutputConverter转换成Map,ListOutputConverter转换成List
我们着重观察下BeanOutputConverter干的活儿
除了实现了上面说的convert方法外,还实现了getFormat方法
意思就是会在我们提问前加上上面那段话,希望AI将结果按JSON格式返回,不要包含markdown模块
然后我们再结合convert方法,将结果转换成对象
但是,实际DeepSeek返回结果时,会带上一大段思考过程的内容
即上面这一段,使用<think>标签包裹的内容
而Spring AI包在转换时会去判断字符串是否以 ``` 字符开头
由于DeepSeek并不是以此返回内容,导致对象转换出错
既然如此,思路就很简单了,我们需要改造BeanOutputConverter的convert方法
java">@Slf4j
public class DeepSeekBeanOutputConverter<T> extends BeanOutputConverter<T> {public DeepSeekBeanOutputConverter(Class<T> clazz) {super(clazz);}public DeepSeekBeanOutputConverter(Class<T> clazz, ObjectMapper objectMapper) {super(clazz, objectMapper);}public DeepSeekBeanOutputConverter(ParameterizedTypeReference<T> typeRef) {super(typeRef);}public DeepSeekBeanOutputConverter(ParameterizedTypeReference<T> typeRef, ObjectMapper objectMapper) {super(typeRef, objectMapper);}@Overridepublic T convert(@NonNull String text) {if (StrUtil.isBlank(text)) {return null;}log.info("deepseek response: {}", text);text = text.substring(text.indexOf("```json"));return (T) super.convert(text);}
}
新建一个类DeepSeekBeanOutputConverter继承自BeanOutputConverter,重写convert方法 在转换提取json里面的内容,再通过父类的convert方法实现回答和类的转化
java"> @PostMapping("/chat")public DeepSeekResponse chat(@RequestBody AIChatReq param){//直接返回DeepSeekBeanOutputConverter deepSeekBeanOutputConverter = new DeepSeekBeanOutputConverter<>(DeepSeekResponse.class);return (DeepSeekResponse) chatClient.prompt(param.getMsg()).call().entity(deepSeekBeanOutputConverter);}
新增一个方法,在我们提交对话时,将BeanOutputConverter替换成DeepSeekBeanOutputConverter的实现