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

简介: 随着智能技术的发展,2025年成为智能体发展的关键一年。本项目旨在构建一个多智能体统一交互平台,通过单一入口访问各类智能体服务,提升用户体验和效率。系统具备意图识别、连续对话支持和高效数据处理等特性,采用Spring Boot、Redis、Qwen等技术栈,实现智能体的选择与执行,并提供流式处理和会话管理功能。完整代码托管于GitHub仓库[multi-agent-arch](https://github.com/CoderSJX/multi-agent-arch)。

文章概述

随着智能技术的不断进步,2025年被视为智能体发展的关键一年。各行业纷纷致力于开发适用于自身业务场景的智能体,旨在提升用户体验和服务效率。然而,如何在保持智能体功能多样性的同时实现统一管理,成为了亟待解决的问题。用户期望通过单一交互入口访问所有类型的智能体服务,而不是针对每个智能体设置独立的入口。这一需求推动了本项目的启动,旨在构建一个能够整合多种智能体服务的平台。

项目代码

完整代码托管于GitHub仓库 multi-agent-arch,提供了基础框架供开发者参考和扩展。请注意,此代码库仅作为一个起点,具体应用还需结合实际业务需求进行定制化开发。

项目需求

本项目的目标是建立一个多智能体的统一交互入口,使用户能够通过一个交互入口能够体验不同类型的智能体服务,从而简化用户的操作流程,提高智能场景的准确性。

需求分析

每个智能体通常对应不同的业务,不同业务之间的问法应该是有区别的。比如:用户提问帮我创建一个日程,或者帮我查看一下我的待办。这是两个区分度很高的提问。我们可以基于大模型的分析能力来对用户的意图进行解析和分类,来分发到对应的智能体上,做一个智能体路由。

当然,这里面还有一些细节要考虑,比如说连续对话。连续对话中,我们不能总是对用户的每一条提问进行意图识别,因为用户当前说的,可能是对上一句话的补充或者澄清。比如说:明天、查一下后天的。诸如此类,我们的对应方案是,基于用户最近的五条提问,来对用户的最新意图进行识别。并且只针对于用户的提问,而不包含大模型的回答。因为大模型的回答是会影响到意图识别的准确度的。

技术栈

本项目采用Spring Boot、Spring-AI、Spring WebFlux、Redis、Qwen以及PostgreSQL等技术,构建了一个高性能、可扩展的多智能体服务平台。


架构概述

整个架构分为多个层次,从用户请求的接收、解析到智能体的选择与调用,再到结果返回,形成了一个完整的处理链路。以下是详细的实现步骤:

  1. 请求处理层
  1. 创建一个统一的API接口用于接收用户的请求。该接口负责接收用户的问题、身份令牌(token)以及其他自定义参数。
  2. 使用过滤器验证用户的身份令牌,形成用户上下文环境,便于后续处理中获取用户信息。
@Component
@Order(-1) // 确保此过滤器优先执行
public class ApiSecurityFilter implements WebFilter {
    @Autowired
    private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
    @Value("${business.token.expiration.seconds:300}")
    private long tokenExpirationSeconds;
    //这里可以改为你自己的oauth2.0认证中心,用token获取用户信息
    private String oauth2CenterUrl = "https://xxxx/oauth2.0/profile";
    private final Logger log = LoggerFactory.getLogger(getClass());
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        if ("OPTIONS".equals(request.getMethod().name())) {
            return chain.filter(exchange);
        }
        if (!request.getHeaders().containsKey("Authorization")) {
            return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing Authorization header"));
        }
        //用户的请求中必须带token:Bearer xxxx
        String authorizationHeader = request.getHeaders().getFirst("Authorization");
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Authorization header format"));
        }
        String token = authorizationHeader.substring(7); // Remove "Bearer "
        //这里是租户ID,用来支持多租户的。用户的请求中必须带current-enterprise
        String enterpriseId = request.getHeaders().getFirst("current-enterprise");
        return reactiveRedisTemplate.opsForValue()
                .get("AU:" + enterpriseId + ":" + token)
                .switchIfEmpty(fetchUserInfoFromAuthCenter(exchange, enterpriseId, token)).onErrorResume(e -> {
                    // 这里可以自定义错误处理逻辑,比如记录日志、返回特定错误信息给客户端等
                    log.error("Failed to fetch user info from auth center: ", e);
                    // 返回一个错误响应给客户端,这里假设使用HttpStatus.UNAUTHORIZED
                    return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Failed to authenticate user"));
                })
                .flatMap(userInfo -> {
                    exchange.getSession().doOnSuccess(session -> {
                        session.getAttributes().put("user", userInfo);
                        session.getAttributes().put("enterpriseId", enterpriseId);
                    });
                    exchange.getAttributes().put("userInfo", userInfo);
                    exchange.getAttributes().put("enterpriseId", enterpriseId);
                    return chain.filter(exchange);
                });
    }
    //从token认证中心获取用户信息,保存到redis中,便于下次直接认证通过,设置默认token过期时间。
    private Mono<String> fetchUserInfoFromAuthCenter(ServerWebExchange exchange, String enterpriseId, String token) {
        return WebClient.builder()
                .baseUrl(oauth2CenterUrl)
                .build()
                .get()
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .retrieve()
                .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Token expired or invalid")))
                .bodyToMono(String.class)
                // 假设返回JSON字符串形式的用户信息
                .flatMap(userInfo -> {
                    // 直接在流中处理Redis存储逻辑
                    return reactiveRedisTemplate.opsForValue()
                            .set("AU:" + enterpriseId + ":" + token, userInfo, Duration.ofSeconds(tokenExpirationSeconds))
                            .thenReturn(userInfo) // 返回userInfo,以便后续操作可以继续使用
                            .onErrorResume(e -> {
                                log.error("Failed to store user info in Redis: ", e);
                                // 根据需要决定是抛出新异常终止链路,还是返回一个默认值等
                                return Mono.error(new RuntimeException("Failed to store in Redis"));
                            });
                });
    }
}
  1. 请求解析层
  1. 在Controller层首先保存用户的当前提问至数据库,随后提取最近五条提问记录作为历史对话背景,交由专门的意图识别模块进行处理。
@CrossOrigin
@RestController
@RequestMapping("/chat")
@Slf4j
//Web端助理的Controller
public class AssistantController {
    @Autowired
    private AssistantService assistantService;
    @PostMapping(value = "/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> completions(@RequestBody ChatRequest request, ServerWebExchange exchange) {
        String userInfo = exchange.getAttribute("userInfo");
        beforeCompletion(request,exchange);
        String enterpriseId = exchange.getAttribute("enterpriseId");
        String content = request.getContent();
        Map<String, Object> extra = request.getExtra();
        log.info("接收到web用户消息:" + content);
        Profile userProfile = JSON.parseObject(userInfo, Profile.class);
        Agent agent = assistantService.getAgent(userProfile, enterpriseId);
        return assistantService.handleUserMessage(content, extra, userProfile, enterpriseId,agent);
    }
    public void beforeCompletion(ChatRequest request, ServerWebExchange exchange) {
        String userInfo = exchange.getAttribute("userInfo");
        String enterpriseId = exchange.getAttribute("enterpriseId");
        String content = request.getContent();
        Map<String, Object> extra = request.getExtra();
        Profile userProfile = JSON.parseObject(userInfo, Profile.class);
        assistantService.saveMessage(content,extra,userProfile,enterpriseId);
    }
}
  1. 智能体选择层
  1. 意图识别模块利用functioncall机制输出用户意图分类,这种做法有助于提高意图识别的精度。
  2. 根据识别出的用户意图,查询数据库或内存缓存中的智能体映射表,找到对应的智能体实例。
  3. 对于智能体的配置,包括参数设置和提示词准备,均在此阶段完成。

@Component
@Order(4)
public class IntentAgent {
    @Resource(name = "intentAgentOptions")
    private AgentOptions options;

    @Value("${business.llm.api-key}")
    private String apiKey;
    @Value("${business.llm.model}")
    private String model;
    @Value("${business.llm.url}")
    private String url;

    public AgentDispatchResult parse(String history) {

        OpenAiApi api = new OpenAiApi(url, apiKey);
        List<OpenAiApi.FunctionTool> functionTools = options.getFunctionTools();
        OpenAiApi.FunctionTool functionTool = functionTools.get(0);
        OpenAiApi.FunctionTool.Function function = functionTool.function();
        String intentFunction = function.name();
        OpenAiChatOptions chatOptions = OpenAiChatOptions.builder().withModel(model).withTemperature(0.0).withTools(functionTools).withProxyToolCalls(true).build();
        OpenAiChatModel chatModel = new OpenAiChatModel(api, chatOptions);
        String systemPrompt = options.getSystemPrompt();
        SystemMessage systemMessage = new SystemMessage(systemPrompt);
        List<Message> messageList = new ArrayList<>();
        messageList.add(systemMessage);
        UserMessage userMessage = new UserMessage(history);
        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 businessType = intentParams.getBusinessType();

            return AgentDispatchResult.builder().agentType(businessType).build();
        }

        return null;
    }

}
@Slf4j
@Service
@Order(100)
public class AssistantService {
    @Autowired
    private IntentAgent intentAgent;
    @Autowired
    private AgentManager agentManager;
    @Autowired
    private MessageRepository messageRepository;
    @Autowired
    private LLMService llmService;
    public Flux<String> handleUserMessage(String content, Map<String, Object> extra, Profile userProfile, String enterpriseId, Agent agent) {
        return processWithExistingAgent(agent, content, extra, userProfile, enterpriseId);
    }
    private Flux<String> processWithExistingAgent(Agent agent, String question, Map<String, Object> extra, Profile userProfile, String enterpriseId) {
        AgentOptions options = agent.getOptions();
        List<Message> messageList = agent.getMessageList();
        UserMessage userMessage = new UserMessage(question);
        messageList.add(userMessage);
        return llmService.stream(messageList, userProfile, extra, enterpriseId, options, agent.getId()).flatMap(response -> Flux.just(JSON.toJSONString(response)));
    }
    public void saveMessage(String content, Map<String, Object> extra, Profile userProfile, String enterpriseId) {
        UserMessageEntity message = new UserMessageEntity();
        message.setContent(content);
        String extraJSON = JSON.toJSONString(extra);
        message.setExtra(extraJSON);
        message.setEnterpriseId(enterpriseId);
        String userId = userProfile.getId();
        message.setUserId(userId);
        messageRepository.save(message);
    }
    public Agent getAgent(Profile userProfile, String enterpriseId) {
        String id = userProfile.getId();
        List<UserMessageEntity> userMessageEntityList = messageRepository.findTop5ByUserIdAndEnterpriseIdOrderByCreateDateDesc(id, enterpriseId);
        StringBuffer sb = new StringBuffer();
        if (!userMessageEntityList.isEmpty()) {
            sb.append("用户当前的会话历史为:");
            for (int i = 0; i < userMessageEntityList.size(); i++) {
                String content = userMessageEntityList.get(i).getContent();
                sb.append(i + ". " + content + "\n");
            }
        }
        AgentDispatchResult agentDispatchResult = intentAgent.parse(sb.toString());
        String agentType = agentDispatchResult.getAgentType();
        //agentId和用户强关联
        String agentId = "AGENT_INSTANCE:USER_ID:" + id + ":AGENT_TYPE:" + agentType;
        Agent agent = agentManager.getAgentByAgentId(agentId);
        if (agent == null) {
            AgentOptions agentOptions = agentManager.getAgentByType(agentType);
            agent = new Agent();
            String systemPrompt = agentOptions.getSystemPrompt();
            List<Message> messageList = new ArrayList<>();
            messageList.add(new SystemMessage(systemPrompt));
            agent.setMessageList(messageList);
            agent.setOptions(agentOptions);
            agent.setId(agentId);
            agentManager.createAgentInstance(agentId, agent);
            return agent;
        } else {
            return agent;
        }
    }
}
  1. 智能体执行层
  1. 设计了一个集中式的functioncall调用中心,通过HTTP远程调用来处理智能体可能发起的功能调用请求。
  2. 对于functioncall的结果,根据具体情况决定是否直接返回给用户或是继续传递给大模型进行进一步分析。
@Component
@Slf4j
//用来调用远程方法的类,大模型输出的functioncall都在此调用
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();


    @Override
    public FunctionCallResult apply(FunctionCallRequest callRequest) {
        FunctionCallResult callResult = new FunctionCallResult();
        callResult.setContent("");
        return callResult;
    }
}
@Slf4j
@Service
@ConditionalOnProperty(prefix = "business.llm", name = "model-series", havingValue = "qwen")
public class QwenServiceImpl implements LLMService {
    private ToolCallHelper toolCallHelper = new ToolCallHelper();
    @Autowired
    private FunctionCallService functionCallService;
    @Autowired
    private CardFetcher cardFetcher;
    @Autowired
    private AgentManager agentManager;
    @Value("${business.llm.api-key}")
    private String apiKey;
    @Value("${business.llm.model}")
    private String model;
    @Value("${business.llm.url}")
    private String url;
    @Override
    public Flux<ChatResponse> stream(List<Message> messageList, Profile userProfile, Map<String, Object> extra, String enterpriseId, AgentOptions options, String agentId) {
        //agentOptions中包含了这个agent中拥有的方法描述。
        List<OpenAiApi.FunctionTool> functionTools = options.getFunctionTools();
        OpenAiApi api = new OpenAiApi(url, apiKey);
        OpenAiChatOptions chatOptions = OpenAiChatOptions.builder().withModel(model).withTemperature(0.0).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), agentId);
    }
    private Flux<ChatResponse> processToolCall(OpenAiChatModel chatModel, final List<Message> messageList, Set<String> finishReasons, Function<AssistantMessage.ToolCall, FunctionCallResult> customFunction, String agentId) {
        try {
            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();
                    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 isCardMessage = functionResponse.isCardMessage();
                    if (isCardMessage) {
                        String content = functionResponse.getContent();
                        String cardTypeId = functionResponse.getCardTypeId();
                        Object cardDesc = cardFetcher.getCardDescById(cardTypeId);
                        Object data = functionResponse.getData();
                        agentManager.updateAgentInstanceById(agentId, messageList);
                        //这里可以选择是否清空会话历史
                        //agentManager.deleteAgentInstanceById(agentId);
                        return Flux.just(ChatResponse.builder().status(Status.COMPLETED).content(content).data(data).design(cardDesc).build());
                    }
                    //如果是纯文本的内容,那就应该大模型返回了,大模型会继续分析这个调用结果。
                    //这个场景通常是是RAG的过程或者查询类的,需要大模型进一步的分析的。
                    agentManager.updateAgentInstanceById(agentId, messageList);
                    return processToolCall(chatModel, messageList, finishReasons, customFunction, agentId);
                }
                //这里就是不需要functioncall的处理,纯文本的回复。
                //todo 这里出现过无法停止的回答,还没有定位到原因。
                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);
                    agentManager.updateAgentInstanceById(agentId, messageList);
                    return Flux.just(ChatResponse.builder().content(content).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);
        }
    }
    //所有的functioncall都在此处理,具体是远程调用,还是在本地处理,由你决定
    private FunctionCallResult handleToolCall(AssistantMessage.ToolCall toolCall, Profile userProfile, Map<String, Object> extras, String enterpriseId) {
        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).build();
            FunctionCallResult callResult = functionCallService.apply(request);
            return callResult;
        }
        return null;
    }
    @Override
    public org.springframework.ai.chat.model.ChatResponse call(List<Message> messageList) {
        OpenAiApi api = new OpenAiApi(url, apiKey);
        OpenAiChatOptions chatOptions = OpenAiChatOptions.builder().withModel(model).withTemperature(0.0).withProxyToolCalls(true).build();
        OpenAiChatModel chatModel = new OpenAiChatModel(api, chatOptions);
        Prompt prompt = new Prompt(messageList);
        org.springframework.ai.chat.model.ChatResponse response = chatModel.call(prompt);
        log.info(response.toString());
        return response;
    }
}
  1. 会话管理
  1. 引入MessageList概念来保存和更新用户的对话记录,控制其长度以防止无限增长,并在适当时候清空,保留重要信息供下一轮对话使用。
@Component
@Order(-1)
public class AgentManager {
    private final Map<String, AgentOptions> agentOptionsMap = new ConcurrentHashMap<>();
    private final Map<String, Agent> agentInstanceMap = new ConcurrentHashMap<>();
    // agent计时器,超过15分钟,自动过期
    private final Map<String, Long> agentCounter = new ConcurrentHashMap<>();
    @Autowired
    private AgentTypeRepository agentTypeRepository;
    @PostConstruct
    public void initialize() {
        loadAgentType();
    }
    public AgentOptions getAgentByType(String agentType) {
        return agentOptionsMap.get("AGENT_TYPE:" + agentType);
    }
    public Agent getAgentByAgentId(String agentId) {
        // 检查并清理过期的agent实例
        checkAndRemoveExpiredAgent(agentId);
        return agentInstanceMap.get(agentId);
    }
    public Agent createAgentInstance(String agentId, Agent agent) {
        String key = agentId;
        agentInstanceMap.put(key, agent);
        resetTimer(key); // 创建实例时重置计时器
        return agent;
    }
    public Agent updateAgentInstanceById(String agentId, List<Message> messageList) {
        String key = agentId;
        Agent agentInstance = agentInstanceMap.get(key);
        if (agentInstance != null) {
            agentInstance.setMessageList(messageList);
            agentInstanceMap.put(key, agentInstance);
            resetTimer(key); // 更新实例时重置计时器
            return agentInstance;
        } else {
            return null;
        }
    }
    public void deleteAgentInstanceById(String agentId) {
        String key = agentId;
        agentInstanceMap.remove(key);
        agentCounter.remove(key); // 从计时器映射中移除
    }
    private void resetTimer(String agentId) {
        agentCounter.put(agentId, System.currentTimeMillis());
    }
    private void checkAndRemoveExpiredAgent(String agentId) {
        long currentTime = System.currentTimeMillis();
        long lastAccessTime = agentCounter.getOrDefault(agentId, 0L);
        long expireDuration = 15 * 1000; // 15分钟转换为毫秒
        if (currentTime - lastAccessTime > expireDuration) {
            // 如果实例已过期,则从所有映射中移除它
            deleteAgentInstanceById(agentId);
        }
    }
    public void loadAgentType() {
        List<AgentTypeEntity> agents = agentTypeRepository.findAll();
        for (AgentTypeEntity agent : agents) {
            String type = agent.getType();
            Boolean isPublic = agent.getIsPublic();
            String description = agent.getDescription();
            Double temperature = agent.getTemperature();
            String enterpriseId = agent.getEnterpriseId();
            String functionTools = agent.getFunctionTools();
            String systemPrompt = agent.getSystemPrompt();
            AgentOptions agentOptions = new AgentOptions();
            agentOptions.setType(type);
            agentOptions.setDescription(description);
            agentOptions.setSystemPrompt(systemPrompt);
            if (StringUtils.isNotEmpty(functionTools)) {
                List<OpenAiApi.FunctionTool> toolList = JSON.parseArray(functionTools, OpenAiApi.FunctionTool.class);
                agentOptions.setFunctionTools(toolList);
            }
            agentOptions.setTemperature(temperature);
            agentOptionsMap.put("AGENT_TYPE:" + agent.getType(), agentOptions);
        }
    }
}

小细节

  • 大模型兼容性:本项目选用支持OpenAI格式的大模型,如spring-ai提供的openai-model,确保了良好的兼容性。
  • Agent实例存储:初始方案采用内存中的HashMap进行存储,可根据实际需求调整为更高效的ConcurrentHashMap或Redis。
  • 流式处理:支持Stream流式调用和返回,提供流畅的打字机效果,特别注意确认所选大模型是否支持StreamingFunctioncall特性。

具体实现设计

  1. 实现一个接口,用来统一处理用户的请求,用户的请求中带着当前的问题、用户的token(用来识别用户身份)、以及一些额外的参数,这些额外的参数可以自定义,只是用来扩展你自定义的一些参数。
  2. 这个接口首先要经过过滤器,用token用来形成用户上下文,这样你就可以在后续处理中获取到用户的身份,如ID、手机号等,用来处理业务。具体如何使用token形成用户上下文,就看你业务本身的需求了。可以使用redis来存一下用户token,避免每次调用token认证中心。
  3. 我们拿到用户信息之后,就可以在controller层处理了。
  4. 在Controller层,我们先对用户的意图进行识别。我们先把用户的当前提问存到数据库中,然后从数据库中取出5条用户最近的提问,拼成一个历史记录,让专门负责意图识别的智能体进行意图识别。注意,这里的意图识别智能体,需要给他一个functioncall注册,让他输出意图的时候,调用这个方法,这个方法的参数就是用户的意图分类。为什么要用functioncall来代替直接输出文字呢,因为从观察来看,基于functiocall的输出的结果比较准确,不会出现一些无关的文字,funcationcall的参数中只会有intent这个变量的值。这是一个意图识别的技巧,是从实际开发获得的经验。
  5. 意图识别完成后,我们就需要拿着这个意图分类去数据库找对应的智能体。去哪里找无所谓,也可以在服务启动时,从数据库里读到内存,然后从HashMap中找。
  6. 找到对应的智能体后,把智能体的参数、提示词啥的都配置进当前调用中,就可以进行模型调用了。
  7. 如果你的智能体在执行过程中,需要调用funcationcall,我的建议做法是,设计一个funcationcall的调用中心,所有的functioncall都基于http进行远程调用,然后调用结果通过http返回回来。这样可以集中处理funcationcall,也可以将咱整个项目分成两个小的项目,一个是智能体框架本身,一个是方法调用中心。方法调用中心和业务强相关,可以做到框架和业务分离。
  8. 当然,这里要处理好functioncall的返回,我这里设计了一个functioncall的返回体的标准结构,用来控制是否把functioncall的结果直接返回,如果直接返回,那就不需要把functioncall的结果再次给大模型。如果不直接返回,那么functioncall的结果会继续给大模型,让大模型基于functioncall的结果继续分析。为什么这样设计,是因为有这样的场景,业务处理完成后,就不需要让大模型再次分析了。比如说:更新了用户的日程。如果调用日程的接口完成之后,不需要让大模型知道,你就可以在此给用户返回一个精美的卡片,让用户体验更好。并且,日程更新完成后,这轮业务也算完成了。我说的这个,核心思想是什么,是大模型不必知道一切细节,只要大模型能够完成你的要求就行,具体让大模型知道什么、对它屏蔽什么细节,是由你来决定的。并且,大模型本身的设计也是如此,大模型是无状态的,他并不会记住某次请求,他能分析的就是你每次传给他的MessageList,MessageList中的内容就是他解析的根本。
  9. 当然,这就引申出了MessageList怎么保存的问题,以及MessageList的长度问题。咱的方案中,到现在还没有提到怎么更新用户的MessageList,以及MessageList到什么时候进行清空呢。因为MessageList不可能让你无限增长。
  10. 针对MessageList的更新问题,我们的方案是这样的:在匹配到对应的智能体后,先去内存中找用户之前有没有调用过这个智能体,如果有,过期没有(比如说设置15分钟内没有更新,就自动过期。)如果有,并且没有过期,就继续使用该智能体。如果过期了,就重新实例化这个智能体,并存到内存中(当然也可以存到Redis这种缓存数据库中,要考虑序列化和反序列化的事)。当大模型产生输出、用户的新提问、functioncall有结果的时候,这几种情况,需要更新MessageList,那就更新一下MessageList。当MessageList长度过长时,我们可以设置一下最大长度为6,或者在每一轮业务处理完成后,清空MessageList,并且设置一个LastMessage,用来手动记录下上一次会话中的重要信息,例如:用户提到过xxx日期,用户提到过xxx的待办,那么下一轮的MessageList中除了系统提示词以外,还有上一轮对话的重要信息(LastMessage)。就可以实现连续对话了。
  11. 当然,这种方案针对于单服务还是可以的。考虑到负载均衡、多个微服务实例,需要考虑将智能体实例的存取做改造,支持分布式。

提示词样例

意图识别

您是一名善于从历史提问中分析用户的最新意图的助手,请根据提问历史记录,分析并总结用户的最新问题的完整意图。你需要分析用户的意图之后,调用print_intent的方法,输出用户的意图分类,输出英文标识。用户的意图分类有 待办(todo)、问答(question)、日程(calender)、联系人(contact)

待办

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


目录
相关文章
|
7天前
|
调度 云计算 芯片
云超算技术跃进,阿里云牵头制定我国首个云超算国家标准
近日,由阿里云联合中国电子技术标准化研究院主导制定的首个云超算国家标准已完成报批,不久后将正式批准发布。标准规定了云超算服务涉及的云计算基础资源、资源管理、运行和调度等方面的技术要求,为云超算服务产品的设计、实现、应用和选型提供指导,为云超算在HPC应用和用户的大范围采用奠定了基础。
179585 20
|
14天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
16天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
9373 23
|
20天前
|
Cloud Native Apache 流计算
资料合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
5050 15
资料合集|Flink Forward Asia 2024 上海站
|
20天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。
|
28天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
16天前
|
人工智能 容器
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
本文介绍了如何利用千问开发一款情侣刮刮乐小游戏,通过三步简单指令实现从单个功能到整体框架,再到多端优化的过程,旨在为生活增添乐趣,促进情感交流。在线体验地址已提供,鼓励读者动手尝试,探索编程与AI结合的无限可能。
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
|
15天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
1187 72