LLM大模型开发-SpringAI:ChatClient、Ollama、Advisor
ollama多个模型选择
构造自定义输入模型entity
package cn.kanyu.springai.entity;import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;import java.io.Serializable;@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class MoreModelConfig implements Serializable {/*** 模型*/private String model;/***温度*/private Double temperature;}
请求http://localhost:9999/ai/mulChatModel?message=你是谁&model=deepseek-r1:1.5b&temperature=0.9
请求模型为deepseek-r1:1.5b
返回
请求http://localhost:9999/ai/mulChatModel?message=你是谁&model=llama3:latest&temperature=0.9
返回
提示词模版
设置系统提示词模版
@Configuration
public class commonConfiguration {@BeanChatClient chatClient(ChatClient.Builder builder) {return builder.defaultSystem("你将作为一名机器人产品的专家,对于用户的使用需求作出解答,当前服务的用户姓名:{name},年龄:{age},性别:{sex}").build();}}
其中姓名:{name},年龄:{age},性别:{sex}为模版提示
在控制器下显示设置name、age、sex
@GetMapping(value = "/promptChat", produces = "text/stream;charset=UTF-8")public Flux<String> promptChat(@RequestParam("message")String message){return chatClient.prompt().system(p -> p.param("name","张三").param("age",16).param("sex","男") ).user(message).stream().content();}
此时发送请求
http://localhost:9999/ai/promptChat?message=你是谁
返回响应
可以看到用户的think思考过程考虑了在系统中设置的模版提示词,用户叫做张三同时16岁
当然也可以在user内设置伪提示词,但更好在system设置提示词优先级更高
提示词最佳设置经验
1,清晰化表达,描述足够清楚
- 补充必要背景信息:身份、场景、用途、已有内容,避免AI无端联想
- 避免“或许、可能”等模糊修饰语
2,任务描述越清楚,AI执行越具体
- 模糊:写一篇去北京的旅游攻略
- 清晰:完成一篇去北京的旅游攻略,要求覆盖北京最著名最好玩的景点,同时注意避开人流量最高峰的景点,错峰出行,在北京的计划旅游10天
3,格式清晰(结构化)
可以通过markdown模式,确定一二三级标题、列表之类的,易于模型理解和推理
公示:【角色设定】+【具体任务(技能)】+【限制条件(约束)】+【参考示例】
# 角色
你是一位专业的北京旅游导游
##技能
### 技能一:理解客户需求
- 询问了解客户的旅行偏好,包括但不限于目的地、预算、出行日期和交通工具等
- 根据用户的需求,个性化提供旅游攻略 ### 技能二:规划旅游路线
- 结合客户的旅行偏好,设计一条详细的旅游路线,包括形成安排、交通方式、住宿和餐饮建议
- 提供每个景点的详细介绍,包括历史背景、特色活动、最佳观看时间### 技能三:提供结合当地特色的实用建议
- 给出旅行中的实用建议,如必备物品清单、安全提示等
- 回答用户关于旅行的任何问题
- 若有不确定的问题,可以调用搜索工具来获取相关信息## 限制
- 只讨论与旅游相关话题
使用以上提示词进行请求
http://localhost:9999/ai/promptChat?message=你是谁
返回
可以看到AI给出了相关的推理返回
advisor实现日志记录
默认日志拦截SimpleAdvisor
spring ai官方的advisor有两个实现 一个callAdvisor用于直接返回的AI调用,streamAdvisor用于流失输出的AI调用
在构造器配置类中设置defaultAdvisor
@BeanChatClient chatClient(ChatClient.Builder builder) {return builder.defaultSystem("# 角色\n" +"你是一位专业的北京旅游导游\n" +"##技能 \n" +"### 技能一:理解客户需求 \n" +"- 询问了解客户的旅行偏好,包括但不限于目的地、预算、出行日期和交通工具等\n" +"- 根据用户的需求,个性化提供旅游攻略 \n" +"\n" +"### 技能二:规划旅游路线 \n" +"- 结合客户的旅行偏好,设计一条详细的旅游路线,包括形成安排、交通方式、住宿和餐饮建议\n" +"- 提供每个景点的详细介绍,包括历史背景、特色活动、最佳观看时间\n" +"\n" +"### 技能三:提供结合当地特色的实用建议 \n" +"- 给出旅行中的实用建议,如必备物品清单、安全提示等\n" +"- 回答用户关于旅行的任何问题\n" +"- 若有不确定的问题,可以调用搜索工具来获取相关信息\n" +"\n" +"## 限制\n" +"- 只讨论与旅游相关话题" ).defaultAdvisors(new SimpleLoggerAdvisor()).build();}
其次打开日志,设置下yml debug级别
logging:level:org.springframework.ai.chat.client.advisor: debugcn.kanyu: debug
可以看到控制态输出了对应的日志信息
request和response
那spirngAI怎样实现的呢
看下advisor的源码
@Override// AdvisedRequest 所有的AI请求输入都会记录 StreamAroundAdvisorChain 调用链 AOP思想public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {//记录请求日志advisedRequest = before(advisedRequest);//执行相关操作Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);//this::observeAfter 记录返回日志return new MessageAggregator().aggregateAdvisedResponse(advisedResponses, this::observeAfter);}
敏感词拦截
设置SafeGuardAdvisor
@BeanChatClient chatClient(ChatClient.Builder builder) {return builder.defaultSystem("# 角色\n" +"你是一位专业的北京旅游导游\n" +"##技能 \n" +"### 技能一:理解客户需求 \n" +"- 询问了解客户的旅行偏好,包括但不限于目的地、预算、出行日期和交通工具等\n" +"- 根据用户的需求,个性化提供旅游攻略 \n" +"\n" +"### 技能二:规划旅游路线 \n" +"- 结合客户的旅行偏好,设计一条详细的旅游路线,包括形成安排、交通方式、住宿和餐饮建议\n" +"- 提供每个景点的详细介绍,包括历史背景、特色活动、最佳观看时间\n" +"\n" +"### 技能三:提供结合当地特色的实用建议 \n" +"- 给出旅行中的实用建议,如必备物品清单、安全提示等\n" +"- 回答用户关于旅行的任何问题\n" +"- 若有不确定的问题,可以调用搜索工具来获取相关信息\n" +"\n" +"## 限制\n" +"- 只讨论与旅游相关话题" ).defaultAdvisors(new SimpleLoggerAdvisor(),new SafeGuardAdvisor(List.of("张三"))).build();}
请求
http://localhost:9999/ai/promptChat?message=张三在哪里
此时返回了I’m unable to respond to that due to sensitive content. Could we rephrase or discuss something else?
看下其源码实现
public class SafeGuardAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {private static final String DEFAULT_FAILURE_RESPONSE = "I'm unable to respond to that due to sensitive content. Could we rephrase or discuss something else?";private static final int DEFAULT_ORDER = 0;private final String failureResponse;private final List<String> sensitiveWords;private final int order;public SafeGuardAdvisor(List<String> sensitiveWords) {this(sensitiveWords, DEFAULT_FAILURE_RESPONSE, DEFAULT_ORDER);}public SafeGuardAdvisor(List<String> sensitiveWords, String failureResponse, int order) {Assert.notNull(sensitiveWords, "Sensitive words must not be null!");Assert.notNull(failureResponse, "Failure response must not be null!");this.sensitiveWords = sensitiveWords;this.failureResponse = failureResponse;this.order = order;}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {if (!CollectionUtils.isEmpty(this.sensitiveWords)&& this.sensitiveWords.stream().anyMatch(w -> advisedRequest.userText().contains(w))) {return createFailureResponse(advisedRequest);}return chain.nextAroundCall(advisedRequest);}private AdvisedResponse createFailureResponse(AdvisedRequest advisedRequest) {return new AdvisedResponse(ChatResponse.builder().generations(List.of(new Generation(new AssistantMessage(this.failureResponse)))).build(), advisedRequest.adviseContext());}
其中aroundCall会判断advisedRequest.userText().contains(w)
用户的输入是否包括敏感词
包括则返回createFailureResponse
自定义拦截器
自定义advisor
package cn.kanyu.springai.config;import org.springframework.ai.chat.client.advisor.api.*;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.rag.Query;
import reactor.core.publisher.Flux;import java.util.Map;public class ReReadingAdvisor implements BaseAdvisor {private static final String DEFAULT_USER_TEXT_ADVISE= """{re2_input_query}Read the question again:{re2_input_query}""";@Overridepublic AdvisedRequest before(AdvisedRequest request) {String contents = request.userText();Query originalQuery = Query.builder().text(new PromptTemplate(DEFAULT_USER_TEXT_ADVISE, request.userParams()).render(Map.of("re2_input_query",contents))).history(request.messages()).build();AdvisedRequest request1 = AdvisedRequest.from(request).userText(originalQuery.text()).build();return request1;}@Overridepublic AdvisedResponse after(AdvisedResponse advisedResponse) {return advisedResponse;}/*存在多个拦截器的时候定义拦截器的优先级 数字越小优先级越高先执行*/@Overridepublic int getOrder() {return 0;}
}
重新构造chatClient
@BeanChatClient chatClient(ChatClient.Builder builder) {return builder.defaultSystem( "你是一位旅游专家" )
// .defaultAdvisors(new SimpleLoggerAdvisor(),new SafeGuardAdvisor(List.of("张三"))).defaultAdvisors(new SimpleLoggerAdvisor(),new ReReadingAdvisor()).build();}
可以看到日志输出