概述
2025年被视为智能元年。各行业纷纷致力于开发适用于自身业务场景的智能体,旨在使用智能体来提升生产力和节约成本。单一智能体能够处理好自己业务领域内的任务,但用户往往期望通过单一交互入口即可使用所有的智能体,而不是使用多个入口。
为了解决一个“超级入口”的问题,我开发了这个项目:基于意图识别的多智能体框架。
基于这个多智能体项目进行二次开发,再配备上一个前端展示,即可快速实现一个拥有超级入口、多智能体调度、业务处理、RAG知识查询的智能助理或者智能客服应用。
非常感谢Qwen2.5系列的开源大模型,这个项目是在Qwen2.5开源大模型强大的FunctionCall能力支持下诞生的。
https://www.modelscope.cn/models/Qwen/Qwen2.5-14B-Instruct
项目核心方案
完整代码托管于GitHub仓库 multi-agent-arch,提供了基础框架供开发者参考和扩展。请注意,此代码库仅作为一个起点,具体应用还需结合实际业务需求进行定制化开发。
下面我将介绍整个项目比较核心的部分,包括大模型选型、多智能体路由(意图识别)、智能体的业务调用、流式FunctionCall、用户会话管理。
大模型选型
本项目所使用的模型是阿里开源的Qwen2.5:14b文本大模型。从我使用的实际感受来看,Qwen2.5系列的大模型表现非常出色,尤其是在调用FunctionCall方面,总是能够精确地理解提示词中的含义,准确地调用到对应的方法,并且能够按照方法的参数要求来填充参数。
大家可以使用阿里云百炼官方的模型调用接口,官方注册新用户是有一定免费的Token额度的,而且一般情况下对于初学者来说够用了。https://www.aliyun.com/product/bailian
也可以像我一样,用ollama这款软件来在自己本机部署一个Qwen2.5的大模型。ollama这款软件非常简单,对于初学者非常友好,只需要ollama run qwen2.5:14b即可完成模型的部署,非常方便。
如果你有其他的像ollama一样的部署大模型的工具,如:LM-STUDIO等,也可以。
单独下载模型文件的话,访问ModelScope官网即可。https://www.modelscope.cn/models?name=Qwen&page=1
多智能体路由(意图识别)
也正因为Qwen2.5模型出色的方法调用能力和参数填充能力,我将本项目的核心能力“意图识别”基于方法调用来执行。
相比直接让模型输出意图分类,方法调用输出的参数更精准,不存在一些无关紧要的部分。例如:不使用方法调用的情况下,大模型分析后输出的内容会是:“用户的意图是XXX”
而使用方法调用时,大模型输出的内容会是一个toolcall,方法参数中的intent参数就是XXX。
这样一来,就解决了咱们传统的思路下,模型输出的内容太过于随机而导致很难利用。
调用Qwen2.5系列的大模型时,我们可以使用通义千问Dashscope的官方API来调用,也可以像下面我的代码一样,使用OPEN-AI的格式来调用Qwen2.5。注:因为Qwen系列是提供了OPEN-AI的兼容接口的,所以如果你之前的代码是用OPEN-AI的客户端写的,你只需要做一点点调整就可以来调用Qwen系列。通义千问Dashscope的官方API文档:https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api?spm=a2c4g.11186623.help-menu-2400256.d_3_3_0.4b865a0219YIgS
下面的代码就展示了我是如何使用Spring-AI框架+Qwen2.5 14b来实现基于Functioncall的意图识别来调用智能体,方法返回一个基于用户问题的,最能解决用户问题的智能体ID:
public class AgentDispatcher { name = "intentAgentProfile") ( private AgentProfile agentProfile; "${business.llm.api-key}") ( private String apiKey; "${business.llm.model}") ( private String model; "${business.llm.url}") ( private String url; public AgentDispatchResult dispatch(String question) { //这里我们使用Qwen2.5大模型的OPENAI的接口兼容模式,使用OPENAI的Client即可调用Qwen2.5 14b模型,并支持Stream和functioncall。 OpenAiApi api = new OpenAiApi(url, apiKey); //传入输出智能体Id的方法 List<OpenAiApi.FunctionTool> functionTools = agentProfile.getFunctionTools(); OpenAiApi.FunctionTool functionTool = functionTools.get(0); OpenAiApi.FunctionTool.Function function = functionTool.function(); String intentFunction = function.name(); OpenAiChatOptions chatOptions = OpenAiChatOptions.builder().withModel(model).withTemperature(agentProfile.getTemperature()).withTools(functionTools).withProxyToolCalls(true).build(); OpenAiChatModel chatModel = new OpenAiChatModel(api, chatOptions); String systemPrompt = agentProfile.getSystemPrompt(); SystemMessage systemMessage = new SystemMessage(systemPrompt); List<Message> messageList = new ArrayList<>(); messageList.add(systemMessage); UserMessage userMessage = new UserMessage(question); messageList.add(userMessage); Prompt prompt = new Prompt(messageList); org.springframework.ai.chat.model.ChatResponse response = chatModel.call(prompt); AssistantMessage message = response.getResult().getOutput(); List<AssistantMessage.ToolCall> toolCalls = message.getToolCalls(); AssistantMessage.ToolCall call = toolCalls.get(0); String name = call.name(); String arguments = call.arguments(); if (name.equals(intentFunction)) { IntentParams intentParams = JSON.parseObject(arguments, IntentParams.class); String agentProfileId = intentParams.getAgentId(); return new AgentDispatchResult(agentProfileId); } return null; } }
这里,我贡献出自己的智能体分发提示词,帮助大家更快地实现智能体分发。
注意:这里你需要给你的所有智能体添加一个描述,描述他们各自擅长的领域和问题,并且给他们分配一个智能体ID,便于大模型进行识别。
public AgentProfile getIntentAgentProfile() { AgentProfile agentProfile = new AgentProfile(); agentProfile.setTemperature(0.0); String systemPrompt = "## 角色\n" + "你是一名擅长处理问题的经理,你能够将用户的问题准确地分配给对应的员工进行处理。\n" + "## 流程\n" + "1. 你首先要了解下面的几个员工擅长处理的问题范围,知道哪些问题应该分配给哪个员工。\n" + "2. 你被给予一个用户提问的历史记录。你要基于历史记录中最后一条进行问题分析,分析出用户的这条问题最适合让谁来处理。你要认真对待最后一条提问,即使是很简单的提问。\n" + "3. 如果你分析不出来,或者需要更多的参考,你就参照历史记录中的上一条提问来继续分析。\n" + "4. 最终,你要调用select_person_id方法选择你要分配给哪个员工,并输出你的完整分析过程"; StringBuilder sb = new StringBuilder(systemPrompt); sb.append("##员工描述列表 "); List<AgentDescription> agentDescriptionList = agentProfileMap.entrySet().stream().map(entry -> new AgentDescription(entry.getKey(), entry.getValue().getDescription())).collect(Collectors.toList()); agentProfile.setSystemPrompt(systemPrompt + agentDescriptionList); String functioncall = "[ {\n" + " \"type\": \"function\",\n" + " \"function\": {\n" + " \"description\": \"选择员工Id\",\n" + " \"name\": \"select_agent_id\",\n" + " \"parameters\": {\n" + " \"type\": \"object\",\n" + " \"properties\": {\n" + " \"agentId\": {\n" + " \"type\": \"string\",\n" + " \"description\": \"智能体Id\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + " }]"; List<OpenAiApi.FunctionTool> toolList = JSON.parseArray(functioncall, OpenAiApi.FunctionTool.class); agentProfile.setFunctionTools(toolList); return agentProfile; }
意图识别,对智能体进行识别和问题分发的实际效果如下图:
各智能体的业务调用
为了解耦框架部分和业务部分,我设计了一个远程方法调用中心服务。调用中心负责一切的业务方法调用,让业务方法调用集中处理,避免在框架核心部分掺杂过多的业务,包括RAG的部分,我都放在的调用中心。并且,为了让大模型更好地理解调用的结果,比如成功与否,是否需要继续让大模型进行分析,我还设计了一个调用结果类。用来将方法调用的结果更直观、更明白地返回给大模型。
遇到functioncall时,处理如下,使用http调用服务:
private FunctionCallResult handleToolCall(AssistantMessage.ToolCall toolCall, Profile userProfile, Map<String, Object> extras, String enterpriseId,String agentId) { if (toolCall.type().equals(FunctionCallConstants.FUNCTION)) { String arguments = toolCall.arguments(); String name = toolCall.name(); FunctionCallRequest request = FunctionCallRequest.builder().functionName(name).functionArguments(arguments).userProfile(userProfile).extras(extras).enterpriseId(enterpriseId).agentId(agentId).build(); FunctionCallResult callResult = functionCallService.apply(request); return callResult; } return null; }
远程请求Client代码如下:
public class RemoteFunctionCallService implements FunctionCallService { private static final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); private static final String BASE_URL = "http://localhost:8090/api/function/call"; // 替换为实际的服务地址 public FunctionCallResult apply(FunctionCallRequest callRequest) { String requestJson = JSON.toJSONString(callRequest); RequestBody body = RequestBody.create( MediaType.parse("application/json; charset=utf-8"), requestJson ); Request request = new Request.Builder() .url(BASE_URL) .post(body) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); // 解析返回的 JSON 响应为 FunctionCallResult 对象 String responseBody = response.body().string(); return JSON.parseObject(responseBody, FunctionCallResult.class); } catch (IOException e) { log.error("Error calling remote function: ", e); // 构建错误响应 FunctionCallResult errorResult = new FunctionCallResult(); errorResult.setCardMessage(false); errorResult.setDirectReturn(true); errorResult.setContent("Error executing remote function: " + e.getMessage()); errorResult.setNextFinish(true); return errorResult; } } }
远程调用中心服务的Controller层如下,注意:此处缺少认证机制,需要你自己根据你的业务来添加,不然容易遭到恶意调用:
"/function") (public class FunctionCallController { private ApplicationContext context; private AgentFunctionMappingCacheService agentFunctionMappingCacheService; "/call") ( public ResponseEntity<FunctionCallResult> callFunction( FunctionCallRequest request) { try { // 从缓存中获取对应的beanName String beanName = agentFunctionMappingCacheService.getBeanNameByAgentId(request.getAgentId()); if (beanName == null) { return ResponseEntity.badRequest().body(FunctionCallResult.builder(). content("Agent not found or no corresponding handler.").build() ); } // 从Spring上下文中获取对应的bean FunctionHandler handler = context.getBean(beanName, FunctionHandler.class); // 创建结果对象 // 调用handler处理请求,并填充结果对象 FunctionCallResult callResult = handler.handle(request);// 假设handle方法可以接收结果对象作为参数 // 返回结果 return ResponseEntity.ok(callResult); } catch (Exception e) { // 构建错误响应 FunctionCallResult errorResult = FunctionCallResult.builder().content("Error executing function: " + e.getMessage()).build(); return ResponseEntity.status(500).body(errorResult); } } }
下面是使用本地运行的Qwen2.5产生的方法调用的实际效果,可以看到Qwen2.5可以很准确地匹配到对应的业务智能体,并且调用到对应的业务操作指令,填充好所需的业务参数。
Qwen2.5在我所有使用过的开源大模型中,实际效果是最好的,尤其是针对普通用户,没有太高的硬件设备,GPU相对较差的前提下,也能够让开发者能够体验和调试自己的智能体程序,实在是非常的难得,给Qwen所有的开发者点赞,加油,感谢🙏!
Qwen的流式FunctionCall
Qwen系列大模型支持流式返回,就是我们很多开发者想要的打字机效果。同时,它还支持使用functioncall的时候,同时指定为流式返回。这一特性给我们做智能客服或者智能助理类的场景,提供了相当大的便利,因为同时支持流式方法调用,我们可以让智能客服回复的同时,去调用业务接口,减少了在处理业务时,用户的等待感,提升了用户体验,这是Qwen系列的一个非常重要的特性。并且Ollama在最新的版本中也支持了这种特性。同样通义千问Dashscope的官方API也是支持这种调用的。
如果你使用Spring-AI或者alibaba-spring-ai的话也支持这样的调用。
Streaming-function的调用示例如下:
用户会话管理
为了支持“超级入口”下的用户连续会话,例如:用户在填写日程之后,想补充一下;或者用户缺少某些参数,大模型需要反问他,用户需要反向澄清。这种场景非常常见,所以保存用户的会话历史是非常重要的。
Qwen大模型等众多大模型都是无状态的,也就是说他没有session的概念,因此,大模型支持连续会话的就是MessageList,将每次调用大模型的MessageList追加,大模型就能够理解用户的上下文,把它看作是一个连续对话。因此,在应用程序中我们必须要用用户会话的概念,用户连续的会话要保存起来,便于下次取用。你可以使用ConcurrentHashMap等内存型的来存储,也可以使用redis、mysql、postgre等数据库来存储,注意处理好序列化和反序列化的问题。
智能体的会话存储如下面代码所示:
public AgentInstance createAgentInstance(String agentInstanceId, AgentProfile agentProfile) { AgentInstance agentInstance = new AgentInstance(); String systemPrompt = agentProfile.getSystemPrompt(); SystemMessage systemMessage = new SystemMessage(systemPrompt); List<Message> messageList = new ArrayList<>(); messageList.add(systemMessage); agentInstance.setAgentProfile(agentProfile); agentInstance.setMessageList(messageList); UUID uuid = UUID.randomUUID(); agentInstance.setInstanceId(uuid.toString()); agentInstanceMap.put(agentInstanceId, agentInstance); return agentInstance; } public AgentInstance getAgentInstanceById(String agentInstanceId) { return agentInstanceMap.get(agentInstanceId); } public AgentInstance updateAgentInstanceById(String agentId, List<Message> messageList) { String key = agentId; AgentInstance agentInstance = agentInstanceMap.get(key); if (agentInstance != null) { agentInstance.setMessageList(messageList); agentInstanceMap.put(key, agentInstance); return agentInstance; } else { return null; } }
下面的调用,演示了如何使用MessageList来调用Qwen2.5 14b模型。
项目具体实现方案
上面我介绍了整个项目最核心的部分,也是大多数开发者比较头疼的部分。
下面,我将按照整个调用流程来完整的解释一下用户问题的处理过程。
请求接口和用户信息提取(多租户)
实现一个接口,用来统一处理用户的请求,用户的请求中带着当前的问题、用户的token(用来识别用户身份)、以及一些额外的参数,这些额外的参数可以自定义,只是用来扩展你自定义的一些参数。
接口过滤器和用户信息保存
这个接口首先要经过过滤器,用token用来形成用户上下文,这样你就可以在后续处理中获取到用户的身份,如ID、手机号等,用来处理业务。具体如何使用token形成用户上下文,就看你业务本身的需求了。可以使用redis来存一下用户token,避免每次调用token认证中心。
我们拿到用户信息之后,就可以在controller层处理了。
用户请求处理
意图识别和智能体分配
在Controller层,我们先对用户的意图进行识别。
我们先把用户的当前提问存到数据库中,然后从数据库中取出5条用户最近的提问,拼成一个历史记录,让专门负责意图识别的智能体进行意图识别。
注意,这里的意图识别智能体,需要给他一个functioncall注册,让他输出意图的时候,调用这个方法,这个方法的参数就是用户的意图分类。
为什么要用functioncall来代替直接输出文字呢?
因为从观察来看,基于functiocall的输出的结果比较准确,不会出现一些无关的文字,funcationcall的参数中只会有intent这个变量的值。这是一个意图识别的技巧,是从实际开发获得的经验。
意图识别完成后,我们就需要拿着这个意图分类去数据库找对应的智能体。去哪里找无所谓,也可以在服务启动时,从数据库里读到内存,然后从HashMap中找。
找到对应的智能体后,把智能体的参数、提示词啥的都配置进当前调用中,就可以进行模型调用了。
智能体执行
- 如果你的智能体在执行过程中,需要调用funcationcall,我的建议做法是,设计一个funcationcall的调用中心,所有的functioncall都基于http进行远程调用,然后调用结果通过http返回回来。这样可以集中处理funcationcall,也可以将咱整个项目分成两个小的项目,一个是智能体框架本身,一个是方法调用中心。方法调用中心和业务强相关,可以做到框架和业务分离。
- 当然,这里要处理好functioncall的返回,我这里设计了一个functioncall的返回体的标准结构,用来控制是否把functioncall的结果直接返回,如果直接返回,那就不需要把functioncall的结果再次给大模型。如果不直接返回,那么functioncall的结果会继续给大模型,让大模型基于functioncall的结果继续分析。
- 为什么这样设计,是因为有这样的场景,业务处理完成后,就不需要让大模型再次分析了。比如说:更新了用户的日程。如果调用日程的接口完成之后,不需要让大模型知道,你就可以在此给用户返回一个精美的卡片,让用户体验更好。并且,日程更新完成后,这轮业务也算完成了。
- 我说的这个,核心思想是什么,是大模型不必知道一切细节,只要大模型能够完成你的要求就行,具体让大模型知道什么、对它屏蔽什么细节,是由你来决定的。并且,大模型本身的设计也是如此,大模型是无状态的,他并不会记住某次请求,他能分析的就是你每次传给他的MessageList,MessageList中的内容就是他解析的根本。
智能体核心处理代码如下:
public Flux<ChatResponse> stream(AgentInstance agentInstance, Profile userProfile, Map<String, Object> extra, String enterpriseId) { List<Message> messageList = agentInstance.getMessageList(); AgentProfile agentProfile = agentInstance.getAgentProfile(); //agentOptions中包含了这个agent中拥有的方法描述。 List<OpenAiApi.FunctionTool> functionTools = agentProfile.getFunctionTools(); Double temperature = agentProfile.getTemperature(); if (temperature == null) { temperature = 0.0; } OpenAiApi api = new OpenAiApi(url, apiKey); OpenAiChatOptions chatOptions = OpenAiChatOptions.builder().withModel(model).withTemperature(temperature).withTools(functionTools).withProxyToolCalls(true).build(); OpenAiChatModel chatModel = new OpenAiChatModel(api, chatOptions); return processToolCall(chatModel, messageList, Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(), OpenAiApi.ChatCompletionFinishReason.STOP.name()), toolCall -> handleToolCall(toolCall, userProfile, extra, enterpriseId,agentProfile.getId().toString()), agentInstance); } private Flux<ChatResponse> processToolCall(OpenAiChatModel chatModel, final List<Message> messageList, Set<String> finishReasons, Function<AssistantMessage.ToolCall, FunctionCallResult> customFunction, AgentInstance agentInstance) { try { String agentInstanceId = agentInstance.getInstanceId(); Prompt prompt = new Prompt(messageList); Flux<org.springframework.ai.chat.model.ChatResponse> chatResponses = chatModel.stream(prompt); //如果是纯文本的回复,就用个stringbuffer来积累,用于下一次大模型对话中的MessageList final StringBuffer sb = new StringBuffer(); return chatResponses.flatMap(chatResponse -> { //判断是不是funtioncall boolean isToolCall = toolCallHelper.isToolCall(chatResponse, finishReasons); if (isToolCall) { Optional<Generation> toolCallGeneration = chatResponse.getResults().stream().filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls())).findFirst(); AssistantMessage assistantMessage = toolCallGeneration.get().getOutput(); log.info("web助理大模型返回:" + JSON.toJSONString(assistantMessage)); List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>(); List<AssistantMessage.ToolCall> toolCalls = assistantMessage.getToolCalls(); AssistantMessage.ToolCall toolCall = null; toolCall = toolCalls.get(toolCalls.size() - 1); String arguments = toolCall.arguments(); String name = toolCall.name(); int lastIndexOf = arguments.lastIndexOf("{"); arguments = arguments.substring(lastIndexOf); toolCall = new AssistantMessage.ToolCall(toolCall.id(), toolCall.type(), toolCall.name(), arguments); String assistantMessageContent = assistantMessage.getContent(); assistantMessage = new AssistantMessage(assistantMessageContent, new HashMap<>(), Arrays.asList(toolCall)); //如果是正常的functioncall调用,就直接调用。 FunctionCallResult functionResponse = customFunction.apply(toolCall); //tips 这一步至关重要,方法调用的结果直接影响大模型的判断。 String responseContent = functionResponse.getContent(); toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(), ModelOptionsUtils.toJsonString(responseContent))); ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of()); //加入历史列表。 messageList.add(assistantMessage); messageList.add(toolMessageResponse); log.info("web用户当前会话历史:" + JSON.toJSONString(messageList)); //判断是不是卡片返回,如果是的话,在这里要返回卡片数据和卡片模版 //因为卡片是给用户看的,大模型不需要,所以不必给大模型返回。 boolean directReturn = functionResponse.isDirectReturn(); if (directReturn) { boolean isCardMessage = functionResponse.isCardMessage(); if (isCardMessage) { String content = functionResponse.getContent(); String cardTypeId = functionResponse.getCardTypeId(); Object cardDesc = cardFetcher.getCardDescById(cardTypeId); Object data = functionResponse.getData(); agentManager.updateAgentInstanceById(agentInstanceId, messageList); return Flux.just(ChatResponse.builder().status(Status.COMPLETED).content(content).data(data).design(cardDesc).build()); // 将 Mono 转换为 Flux } else { return Mono.just(ChatResponse.builder().status(Status.COMPLETED).build()); } } //如果是纯文本的内容,那就应该大模型返回了,大模型会继续分析这个调用结果。 //这个场景通常是是RAG的过程或者查询类的,需要大模型进一步的分析的。 agentManager.updateAgentInstanceById(agentInstanceId, messageList); return processToolCall(chatModel, messageList, finishReasons, customFunction, agentInstance); } //这里就是不需要functioncall的处理,纯文本的回复。 Generation generation = chatResponse.getResults().get(0); String content = generation.getOutput().getContent(); AssistantMessage message = generation.getOutput(); ChatGenerationMetadata metadata = generation.getMetadata(); String finishReason = metadata.getFinishReason(); sb.append(message.getContent()); if ("STOP".equals(finishReason)) { AssistantMessage assistantMessage = new AssistantMessage(sb.toString()); messageList.add(assistantMessage); // 使用 flatMap 将 Mono 转换为 Flux,并确保更新操作完成后再继续流 agentManager.updateAgentInstanceById(agentInstanceId, messageList); return Mono.just(ChatResponse.builder().status(Status.COMPLETED).build()); } if (StringUtils.isEmpty(content)) { return Flux.empty(); } return Flux.just(ChatResponse.builder().content(content).status(Status.REPLYING).build()); }); } catch (Exception e) { log.error(e.getMessage()); e.printStackTrace(); return Flux.error(e); } } private FunctionCallResult handleToolCall(AssistantMessage.ToolCall toolCall, Profile userProfile, Map<String, Object> extras, String enterpriseId,String agentId) { if (toolCall.type().equals(FunctionCallConstants.FUNCTION)) { String arguments = toolCall.arguments(); String name = toolCall.name(); FunctionCallRequest request = FunctionCallRequest.builder().functionName(name).functionArguments(arguments).userProfile(userProfile).extras(extras).enterpriseId(enterpriseId).agentId(agentId).build(); FunctionCallResult callResult = functionCallService.apply(request); return callResult; } return null; }
用户会话管理
当然,这就引申出了MessageList怎么保存的问题,以及MessageList的长度问题。咱的方案中,到现在还没有提到怎么更新用户的MessageList,以及MessageList到什么时候进行清空呢。因为MessageList不可能让你无限增长。
用户会话长度限制和会话列表重置
针对MessageList的更新问题,我们的方案是这样的:在匹配到对应的智能体后,先去内存中找用户之前有没有调用过这个智能体,如果有,过期没有(比如说设置15分钟内没有更新,就自动过期。)如果有,并且没有过期,就继续使用该智能体。
如果过期了,就重新实例化这个智能体,并存到内存中(当然也可以存到Redis这种缓存数据库中,要考虑序列化和反序列化的事)。
当大模型产生输出、用户的新提问、functioncall有结果的时候,这几种情况,需要更新MessageList,那就更新一下MessageList。当MessageList长度过长时,我们可以设置一下最大长度为6,或者在每一轮业务处理完成后,清空MessageList,并且设置一个LastMessage,用来手动记录下上一次会话中的重要信息,例如:用户提到过xxx日期,用户提到过xxx的待办,那么下一轮的MessageList中除了系统提示词以外,还有上一轮对话的重要信息(LastMessage)。就可以实现连续对话了。
当然,这种方案针对于单服务还是可以的。
考虑到负载均衡、多个微服务实例,需要考虑将智能体实例的存取做改造,支持分布式。
总结
- 整个项目是围绕着意图识别来进行多智能体路由的方案,利用了Qwen2.5大模型强大的functioncall能力来进行多智能体分发。
- 使用Qwen2.5的Open-AI的接口兼容模式和StreamingFunctioncall能力来当作智能客户或者智能助理的基础实现能力。
- 利用Spring-AI框架,实现了调用和控制Qwen2.5大模型,使用各种参数来优化大模型的表现。设计了远程方法调用中心,将业务调用集中起来,使得业务调用和多智能体框架核心分离结耦,使得代码逻辑更加清晰、扩展性和移植性都得到了提升。
- 使用内存型会话保持用户的连续会话,保证了用户的用户体验。
基于此项目进行二次开发,再配备上一个前端展示项目,即可实现一个智能助理或者智能客服的超级入口,用户可以在此超级入口中体验到多种智能体服务,并支持连续会话。
最后感谢阿里团队的开源Qwen系列大模型,让传统开发者能够尝试实现自己的大模型驱动的智能体应用,体验到本次科技革命带来的巨大能量!
附录
提示词样例
意图识别的提示词样例
## 角色 你是一名擅长处理问题的经理,你能够将用户的问题准确地分配给对应的员工进行处理。 ## 流程 1. 你首先要了解下面的几个员工擅长处理的问题范围,知道哪些问题应该分配给哪个员工。 2. 你被给予一个用户提问的历史记录。你要基于历史记录中最后一条进行问题分析,分析出用户的这条问题最适合让谁来处理。你要认真对待最后一条提问,即使是很简单的提问。 3. 如果你分析不出来,或者需要更多的参考,你就参照历史记录中的上一条提问来继续分析。 4. 最终,你要调用select_person_id方法选择你要分配给哪个员工,并输出你的完整分析过程
业务智能体提示词示例:待办
## 身份 你是待办小助手,你擅长分析用户的输入,将用户输入转化成一个操作指令。 ## 要求 你必须按照流程处理待办。 ## 认知 待办是一个记录用户待处理事项和已处理事项的模块。 待办有以下几个参数: 1. 待办的分类名。 2. 待办的发起人。 3. 待办的发起日期。 4. 待办的内容。 ## 流程 1. 分析用户的输入,识别用户想要进行的操作,用户想要进行的操作有四种:modify(修改)、query(查询、查看)、add(追加内容)。默认是查看。 2. 调用handleCommand方法。