一、简介
使用@Tool注解标注一个工具方法,使用@ToolParam来标记工具方法参数。
二、代码
DTO
/** * 航班订单信息 */@Data@BuilderpublicclassFlightOrder{/** id */privateLongid;/** 订单号 */privateStringorderNo;/** 航班号 */privateStringflightNumber;/** 乘客姓名 */privateStringname;/** 出发城市 */privateStringfromCity;/** 到达城市 */privateStringtoCity;}ChatEventVO
@Data@Builder@NoArgsConstructor@AllArgsConstructorpublicclassChatEventVO{/** * 文本内容 */privateObjecteventData;/** * 事件类型,1001-数据事件,1002-停止事件,1003-参数事件 */privateinteventType;}ChatEventTypeEnum
/** * 聊天消息事件类型 */@GetterpublicenumChatEventTypeEnum{DATA(1001,"数据事件"),STOP(1002,"停止事件"),PARAM(1003,"参数事件");privatefinalintvalue;privatefinalStringdesc;ChatEventTypeEnum(intvalue,Stringdesc){this.value=value;this.desc=desc;}@OverridepublicStringtoString(){returnthis.name();}}ToolResultHolder
/** * 工具结果保持器,用来存储tools中得到的结果,请求id 作为key, value为键值对数据 */publicclassToolResultHolder{privatestaticfinalMap<String,Map<String,Object>>HANDLER_MAP=newConcurrentHashMap<>();privateToolResultHolder(){}publicstaticvoidput(Stringkey,Stringfield,Objectresult){HANDLER_MAP.computeIfAbsent(key,k->newHashMap<>()).put(field,result);}publicstaticMap<String,Object>get(Stringkey){returnkey==null?null:HANDLER_MAP.get(key);}publicstaticObjectget(Stringkey,Stringfield){returnOptional.ofNullable(HANDLER_MAP.get(key)).map(map->map.get(field)).orElse(null);}publicstaticvoidremove(Stringkey){HANDLER_MAP.remove(key);}}Configuration
一般情况下配置类中只做一些公共的默认配置,如日志拦截器,消息记忆、消息优化等,像一些工具等都是放在Controller中单独配置。
@ConfigurationpublicclassSpringAiConfig{@BeanpublicChatClientchatClient(DeepSeekChatModeldeepSeekChatModel,ChatMemorychatMemory,MyToolsmyTools){returnChatClient.builder(deepSeekChatModel).defaultSystem("你是AI票务助手,回答用户的问题。").defaultTools(myTools).build();}}定义工具方法:航班订单信息查询 和 取消订单
- @ToolParam中的description可以写更多的和该参数相关的提示词。
- ToolContext :通过
toolContext用户大模型向工具传参
@ComponentpublicclassMyTools{@Tool(description="航班订单信息查询")publicFlightOrderqueryFlightOrder(@ToolParam(description="航班号")StringflightNumber,@ToolParam(description="姓名")Stringname,ToolContexttoolContext){System.out.println("航班订单信息查询:"+flightNumber+" "+name);varflightOrder=FlightOrder.builder().id(1L).orderNo("F1234").flightNumber(flightNumber).name(name).fromCity("北京").toCity("上海").build();varuserId=toolContext.getContext().get(Constant.USER_ID),varrequestId=Convert.toStr(toolContext.getContext().get(Constant.REQUEST_ID));varfield=StrUtil.format("{}_{}",StrUtil.lowerFirst(FlightOrder.class.getSimpleName()),flightOrder.getId());// 将接口的返回值放入到Holder中,便于在大模型中获取到封装成json返回给前端ToolResultHolder.put(requestId,field,courseInfo);returnflightOrder;}@Tool(description="取消订单")publicStringcancelOrder(@ToolParam(description="订单号")StringorderNo,@ToolParam(description="出发城市")StringfromCity,@ToolParam(description="到达城市")StringtoCity){// 参数校验System.out.println("取消订单:"+orderNo+" "+fromCity+"→"+toCity);return"订单取消成功";}}@RestControllerpublicclassSpringAIController{@AutowiredprivateChatClientchatClient;// 输出结束的标记privatestaticfinalChatEventVOSTOP_EVENT=ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build();@GetMapping(value="/chat",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<String>chat(@RequestParam("message")Stringmessage){// 生成请求idvarrequestId=IdUtil.fastSimpleUUID();// 获取用户idvaruserId=UserContext.getUser();Flux<String>content=chatClient.prompt()// 通过工具上下文传递参数.toolContext(Map.of(Constant.REQUEST_ID,requestId,Constant.USER_ID,userId)).user(message).stream().content().contactWith(.concatWith(Flux.defer(()->{// 通过请求id获取到参数列表,如果不为空,就将其追加到返回结果中vartoolResultMap=ToolResultHolder.get(requestId);if(CollUtil.isNotEmpty(toolResultMap)){ToolResultHolder.remove(requestId);// 清除参数列表// 响应给前端的参数数据varparamChatEvent=ChatEventVO.builder().eventData(toolResultMap).eventType(ChatEventTypeEnum.PARAM.getValue()).build();// 自定义一个结束标记,前端解析到就不会再请求,表示本轮会话全部结束returnFlux.just(paramChatEvent,STOP_EVENT);}});returncontent;}}航班订单信息查询 和 取消订单 都正常调用了。
三、最佳实践
- 对于工具方法大模型推测的参数值不能完全信任,一定要严格校验。
- 工具方法一定要注意性能,尽可能快的执行(能缓存就缓存)。
- 工具方法不要返回不确定Optional值。
- 通过SpringSecurity对用户进行必要的权限校验或者维护角色role和tool的关系,在方法内部通过用户id查询是否有权限,没有权限报错。
- 每个tool都会解析成json,如果tool过多,会导致占用上下文过大,也增加模型的选择难度。
现在是每次对话都会将工具作为上下文,当有的对话使用不到该工具时那么就会对上下文带来负面影响,好的方法使用使用Skill,按需加载工具。