使用Qwen2.5+SpringBoot+SpringAI+SpringWebFlux的基于意图识别的多智能体架构方案

简介: 本项目旨在解决智能体的“超级入口”问题,通过开发基于意图识别的多智能体框架,实现用户通过单一交互入口使用所有智能体。项目依托阿里开源的Qwen2.5大模型,利用其强大的FunctionCall能力,精准识别用户意图并调用相应智能体。核心功能包括:- 意图识别:基于Qwen2.5的大模型方法调用能力,准确识别用户意图。- 业务调用中心:解耦框架与业务逻辑,集中处理业务方法调用,提升系统灵活性。- 会话管理:支持连续对话,保存用户会话历史,确保上下文连贯性。- 流式返回:支持打字机效果的流式返回,增强用户体验。感谢Qwen2.5系列大模型的支持,使项目得以顺利实施。

概述

2025年被视为智能元年。各行业纷纷致力于开发适用于自身业务场景的智能体,旨在使用智能体来提升生产力和节约成本。单一智能体能够处理好自己业务领域内的任务,但用户往往期望通过单一交互入口即可使用所有的智能体,而不是使用多个入口。

为了解决一个“超级入口”的问题,我开发了这个项目:基于意图识别的多智能体框架。

基于这个多智能体项目进行二次开发,再配备上一个前端展示,即可快速实现一个拥有超级入口、多智能体调度、业务处理、RAG知识查询的智能助理或者智能客服应用。

非常感谢Qwen2.5系列的开源大模型,这个项目是在Qwen2.5开源大模型强大的FunctionCall能力支持下诞生的。

https://www.modelscope.cn/models/Qwen/Qwen2.5-14B-Instruct

image.png

项目核心方案

完整代码托管于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

image.png

image.png

image.png

多智能体路由(意图识别)

也正因为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 {
    @Resource(name = "intentAgentProfile")
    private AgentProfile agentProfile;

    @Value("${business.llm.api-key}")
    private String apiKey;
    @Value("${business.llm.model}")
    private String model;
    @Value("${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;
    }


意图识别,对智能体进行识别和问题分发的实际效果如下图:

image.png

各智能体的业务调用

为了解耦框架部分和业务部分,我设计了一个远程方法调用中心服务。调用中心负责一切的业务方法调用,让业务方法调用集中处理,避免在框架核心部分掺杂过多的业务,包括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代码如下:

@Component
@Slf4j
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"; // 替换为实际的服务地址

    @Override
    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层如下,注意:此处缺少认证机制,需要你自己根据你的业务来添加,不然容易遭到恶意调用:

@RestController
@RequestMapping("/function")
public class FunctionCallController {

    @Autowired
    private ApplicationContext context;

    @Autowired
    private AgentFunctionMappingCacheService agentFunctionMappingCacheService;

    @PostMapping("/call")
    public ResponseEntity<FunctionCallResult> callFunction(@RequestBody 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所有的开发者点赞,加油,感谢🙏!

image.png

image.png

Qwen的流式FunctionCall

Qwen系列大模型支持流式返回,就是我们很多开发者想要的打字机效果。同时,它还支持使用functioncall的时候,同时指定为流式返回。这一特性给我们做智能客服或者智能助理类的场景,提供了相当大的便利,因为同时支持流式方法调用,我们可以让智能客服回复的同时,去调用业务接口,减少了在处理业务时,用户的等待感,提升了用户体验,这是Qwen系列的一个非常重要的特性。并且Ollama在最新的版本中也支持了这种特性。同样通义千问Dashscope的官方API也是支持这种调用的。

如果你使用Spring-AI或者alibaba-spring-ai的话也支持这样的调用。

Streaming-function的调用示例如下:

image.png

用户会话管理

为了支持“超级入口”下的用户连续会话,例如:用户在填写日程之后,想补充一下;或者用户缺少某些参数,大模型需要反问他,用户需要反向澄清。这种场景非常常见,所以保存用户的会话历史是非常重要的。

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模型。

image.png



项目具体实现方案

上面我介绍了整个项目最核心的部分,也是大多数开发者比较头疼的部分。

下面,我将按照整个调用流程来完整的解释一下用户问题的处理过程。

请求接口和用户信息提取(多租户)

实现一个接口,用来统一处理用户的请求,用户的请求中带着当前的问题、用户的token(用来识别用户身份)、以及一些额外的参数,这些额外的参数可以自定义,只是用来扩展你自定义的一些参数。

接口过滤器和用户信息保存

这个接口首先要经过过滤器,用token用来形成用户上下文,这样你就可以在后续处理中获取到用户的身份,如ID、手机号等,用来处理业务。具体如何使用token形成用户上下文,就看你业务本身的需求了。可以使用redis来存一下用户token,避免每次调用token认证中心。

我们拿到用户信息之后,就可以在controller层处理了。

用户请求处理

意图识别和智能体分配

在Controller层,我们先对用户的意图进行识别。

我们先把用户的当前提问存到数据库中,然后从数据库中取出5条用户最近的提问,拼成一个历史记录,让专门负责意图识别的智能体进行意图识别

注意,这里的意图识别智能体,需要给他一个functioncall注册,让他输出意图的时候,调用这个方法,这个方法的参数就是用户的意图分类。

为什么要用functioncall来代替直接输出文字呢?

因为从观察来看,基于functiocall的输出的结果比较准确,不会出现一些无关的文字,funcationcall的参数中只会有intent这个变量的值。这是一个意图识别的技巧,是从实际开发获得的经验。


意图识别完成后,我们就需要拿着这个意图分类去数据库找对应的智能体。去哪里找无所谓,也可以在服务启动时,从数据库里读到内存,然后从HashMap中找。

找到对应的智能体后,把智能体的参数、提示词啥的都配置进当前调用中,就可以进行模型调用了。

智能体执行

  1. 如果你的智能体在执行过程中,需要调用funcationcall,我的建议做法是,设计一个funcationcall的调用中心,所有的functioncall都基于http进行远程调用,然后调用结果通过http返回回来。这样可以集中处理funcationcall,也可以将咱整个项目分成两个小的项目,一个是智能体框架本身,一个是方法调用中心。方法调用中心和业务强相关,可以做到框架和业务分离。
  2. 当然,这里要处理好functioncall的返回,我这里设计了一个functioncall的返回体的标准结构,用来控制是否把functioncall的结果直接返回,如果直接返回,那就不需要把functioncall的结果再次给大模型。如果不直接返回,那么functioncall的结果会继续给大模型,让大模型基于functioncall的结果继续分析。
  3. 为什么这样设计,是因为有这样的场景,业务处理完成后,就不需要让大模型再次分析了。比如说:更新了用户的日程。如果调用日程的接口完成之后,不需要让大模型知道,你就可以在此给用户返回一个精美的卡片,让用户体验更好。并且,日程更新完成后,这轮业务也算完成了。
  4. 我说的这个,核心思想是什么,是大模型不必知道一切细节,只要大模型能够完成你的要求就行,具体让大模型知道什么、对它屏蔽什么细节,是由你来决定的。并且,大模型本身的设计也是如此,大模型是无状态的,他并不会记住某次请求,他能分析的就是你每次传给他的MessageList,MessageList中的内容就是他解析的根本。

智能体核心处理代码如下:

@Override
    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)。就可以实现连续对话了。

当然,这种方案针对于单服务还是可以的。

考虑到负载均衡、多个微服务实例,需要考虑将智能体实例的存取做改造,支持分布式。

总结

  1. 整个项目是围绕着意图识别来进行多智能体路由的方案,利用了Qwen2.5大模型强大的functioncall能力来进行多智能体分发。
  2. 使用Qwen2.5的Open-AI的接口兼容模式和StreamingFunctioncall能力来当作智能客户或者智能助理的基础实现能力。
  3. 利用Spring-AI框架,实现了调用和控制Qwen2.5大模型,使用各种参数来优化大模型的表现。设计了远程方法调用中心,将业务调用集中起来,使得业务调用和多智能体框架核心分离结耦,使得代码逻辑更加清晰、扩展性和移植性都得到了提升。
  4. 使用内存型会话保持用户的连续会话,保证了用户的用户体验。


基于此项目进行二次开发,再配备上一个前端展示项目,即可实现一个智能助理或者智能客服的超级入口,用户可以在此超级入口中体验到多种智能体服务,并支持连续会话。

最后感谢阿里团队的开源Qwen系列大模型,让传统开发者能够尝试实现自己的大模型驱动的智能体应用,体验到本次科技革命带来的巨大能量!

image.png

附录

提示词样例

意图识别的提示词样例

## 角色
你是一名擅长处理问题的经理,你能够将用户的问题准确地分配给对应的员工进行处理。
## 流程
1. 你首先要了解下面的几个员工擅长处理的问题范围,知道哪些问题应该分配给哪个员工。
2. 你被给予一个用户提问的历史记录。你要基于历史记录中最后一条进行问题分析,分析出用户的这条问题最适合让谁来处理。你要认真对待最后一条提问,即使是很简单的提问。
3. 如果你分析不出来,或者需要更多的参考,你就参照历史记录中的上一条提问来继续分析。
4. 最终,你要调用select_person_id方法选择你要分配给哪个员工,并输出你的完整分析过程

业务智能体提示词示例:待办

## 身份
你是待办小助手,你擅长分析用户的输入,将用户输入转化成一个操作指令。
## 要求
你必须按照流程处理待办。
## 认知
待办是一个记录用户待处理事项和已处理事项的模块。
待办有以下几个参数:
1. 待办的分类名。
2. 待办的发起人。
3. 待办的发起日期。
4. 待办的内容。
## 流程
1. 分析用户的输入,识别用户想要进行的操作,用户想要进行的操作有四种:modify(修改)、query(查询、查看)、add(追加内容)。默认是查看。
2. 调用handleCommand方法。


目录
相关文章
|
1月前
|
存储 SQL 关系型数据库
Mysql高可用架构方案
本文阐述了Mysql高可用架构方案,介绍了 主从模式,MHA模式,MMM模式,MGR模式 方案的实现方式,没有哪个方案是完美的,开发人员在选择何种方案应用到项目中也没有标准答案,合适的才是最好的。
164 3
Mysql高可用架构方案
|
1月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
22天前
|
消息中间件 架构师 数据库
本地消息表事务:10Wqps 高并发分布式事务的 终极方案,大厂架构师的 必备方案
45岁资深架构师尼恩分享了一篇关于分布式事务的文章,详细解析了如何在10Wqps高并发场景下实现分布式事务。文章从传统单体架构到微服务架构下分布式事务的需求背景出发,介绍了Seata这一开源分布式事务解决方案及其AT和TCC两种模式。随后,文章深入探讨了经典ebay本地消息表方案,以及如何使用RocketMQ消息队列替代数据库表来提高性能和可靠性。尼恩还分享了如何结合延迟消息进行事务数据的定时对账,确保最终一致性。最后,尼恩强调了高端面试中需要准备“高大上”的答案,并提供了多个技术领域的深度学习资料,帮助读者提升技术水平,顺利通过面试。
本地消息表事务:10Wqps 高并发分布式事务的 终极方案,大厂架构师的 必备方案
|
26天前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
87 5
|
24天前
|
缓存 NoSQL Java
Spring Boot中的分布式缓存方案
Spring Boot提供了简便的方式来集成和使用分布式缓存。通过Redis和Memcached等缓存方案,可以显著提升应用的性能和扩展性。合理配置和优化缓存策略,可以有效避免常见的缓存问题,保证系统的稳定性和高效运行。
43 3
|
1月前
|
缓存 关系型数据库 MySQL
高并发架构系列:数据库主从同步的 3 种方案
本文详解高并发场景下数据库主从同步的三种解决方案:数据主从同步、数据库半同步复制、数据库中间件同步和缓存记录写key同步,旨在帮助解决数据一致性问题。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
高并发架构系列:数据库主从同步的 3 种方案
|
8天前
|
弹性计算 负载均衡 安全
云端问道-Web应用上云经典架构方案教学
本文介绍了企业业务上云的经典架构设计,涵盖用户业务现状及挑战、阿里云业务托管架构设计、方案选型配置及业务初期低门槛使用等内容。通过详细分析现有架构的问题,提出了高可用、安全、可扩展的解决方案,并提供了按量付费的低成本选项,帮助企业在业务初期顺利上云。
|
8天前
|
弹性计算 负载均衡 安全
企业业务上云经典架构方案整体介绍
本次课程由阿里云产品经理晋侨分享,主题为企业业务上云经典架构。内容涵盖用户业务架构现状及挑战、阿里云业务托管经典架构设计、方案涉及的产品选型配置,以及业务初期如何低门槛使用。课程详细介绍了企业业务上云的全流程,帮助用户实现高可用、稳定、可扩展的云架构。
|
22天前
|
弹性计算 API 持续交付
后端服务架构的微服务化转型
本文旨在探讨后端服务从单体架构向微服务架构转型的过程,分析微服务架构的优势和面临的挑战。文章首先介绍单体架构的局限性,然后详细阐述微服务架构的核心概念及其在现代软件开发中的应用。通过对比两种架构,指出微服务化转型的必要性和实施策略。最后,讨论了微服务架构实施过程中可能遇到的问题及解决方案。
|
1月前
|
Cloud Native Devops 云计算
云计算的未来:云原生架构与微服务的革命####
【10月更文挑战第21天】 随着企业数字化转型的加速,云原生技术正迅速成为IT行业的新宠。本文深入探讨了云原生架构的核心理念、关键技术如容器化和微服务的优势,以及如何通过这些技术实现高效、灵活且可扩展的现代应用开发。我们将揭示云原生如何重塑软件开发流程,提升业务敏捷性,并探索其对企业IT架构的深远影响。 ####
43 3

热门文章

最新文章