文章概述
随着智能技术的不断进步,2025年被视为智能体发展的关键一年。各行业纷纷致力于开发适用于自身业务场景的智能体,旨在提升用户体验和服务效率。然而,如何在保持智能体功能多样性的同时实现统一管理,成为了亟待解决的问题。用户期望通过单一交互入口访问所有类型的智能体服务,而不是针对每个智能体设置独立的入口。这一需求推动了本项目的启动,旨在构建一个能够整合多种智能体服务的平台。
项目代码
完整代码托管于GitHub仓库 multi-agent-arch,提供了基础框架供开发者参考和扩展。请注意,此代码库仅作为一个起点,具体应用还需结合实际业务需求进行定制化开发。
项目需求
本项目的目标是建立一个多智能体的统一交互入口,使用户能够通过一个交互入口能够体验不同类型的智能体服务,从而简化用户的操作流程,提高智能场景的准确性。
需求分析
每个智能体通常对应不同的业务,不同业务之间的问法应该是有区别的。比如:用户提问帮我创建一个日程,或者帮我查看一下我的待办。这是两个区分度很高的提问。我们可以基于大模型的分析能力来对用户的意图进行解析和分类,来分发到对应的智能体上,做一个智能体路由。
当然,这里面还有一些细节要考虑,比如说连续对话。连续对话中,我们不能总是对用户的每一条提问进行意图识别,因为用户当前说的,可能是对上一句话的补充或者澄清。比如说:明天、查一下后天的。诸如此类,我们的对应方案是,基于用户最近的五条提问,来对用户的最新意图进行识别。并且只针对于用户的提问,而不包含大模型的回答。因为大模型的回答是会影响到意图识别的准确度的。
技术栈
本项目采用Spring Boot、Spring-AI、Spring WebFlux、Redis、Qwen以及PostgreSQL等技术,构建了一个高性能、可扩展的多智能体服务平台。
架构概述
整个架构分为多个层次,从用户请求的接收、解析到智能体的选择与调用,再到结果返回,形成了一个完整的处理链路。以下是详细的实现步骤:
- 请求处理层
- 创建一个统一的API接口用于接收用户的请求。该接口负责接收用户的问题、身份令牌(token)以及其他自定义参数。
- 使用过滤器验证用户的身份令牌,形成用户上下文环境,便于后续处理中获取用户信息。
-1) // 确保此过滤器优先执行 (public class ApiSecurityFilter implements WebFilter { private ReactiveRedisTemplate<String, String> reactiveRedisTemplate; "${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()); 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")); }); }); } }
- 请求解析层
- 在Controller层首先保存用户的当前提问至数据库,随后提取最近五条提问记录作为历史对话背景,交由专门的意图识别模块进行处理。
"/chat") ( //Web端助理的Controller public class AssistantController { private AssistantService assistantService; value = "/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE) ( public Flux<String> completions( 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); } }
- 智能体选择层
- 意图识别模块利用
functioncall
机制输出用户意图分类,这种做法有助于提高意图识别的精度。 - 根据识别出的用户意图,查询数据库或内存缓存中的智能体映射表,找到对应的智能体实例。
- 对于智能体的配置,包括参数设置和提示词准备,均在此阶段完成。
4) (public class IntentAgent { name = "intentAgentOptions") ( private AgentOptions options; "${business.llm.api-key}") ( private String apiKey; "${business.llm.model}") ( private String model; "${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; } }
100) (public class AssistantService { private IntentAgent intentAgent; private AgentManager agentManager; private MessageRepository messageRepository; 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; } } }
- 智能体执行层
- 设计了一个集中式的
functioncall
调用中心,通过HTTP远程调用来处理智能体可能发起的功能调用请求。 - 对于
functioncall
的结果,根据具体情况决定是否直接返回给用户或是继续传递给大模型进行进一步分析。
//用来调用远程方法的类,大模型输出的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(); public FunctionCallResult apply(FunctionCallRequest callRequest) { FunctionCallResult callResult = new FunctionCallResult(); callResult.setContent(""); return callResult; } }
prefix = "business.llm", name = "model-series", havingValue = "qwen") (public class QwenServiceImpl implements LLMService { private ToolCallHelper toolCallHelper = new ToolCallHelper(); private FunctionCallService functionCallService; private CardFetcher cardFetcher; private AgentManager agentManager; "${business.llm.api-key}") ( private String apiKey; "${business.llm.model}") ( private String model; "${business.llm.url}") ( private String url; 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; } 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; } }
- 会话管理
- 引入MessageList概念来保存和更新用户的对话记录,控制其长度以防止无限增长,并在适当时候清空,保留重要信息供下一轮对话使用。
-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<>(); private AgentTypeRepository agentTypeRepository; 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特性。
具体实现设计
- 实现一个接口,用来统一处理用户的请求,用户的请求中带着当前的问题、用户的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中的内容就是他解析的根本。
- 当然,这就引申出了MessageList怎么保存的问题,以及MessageList的长度问题。咱的方案中,到现在还没有提到怎么更新用户的MessageList,以及MessageList到什么时候进行清空呢。因为MessageList不可能让你无限增长。
- 针对MessageList的更新问题,我们的方案是这样的:在匹配到对应的智能体后,先去内存中找用户之前有没有调用过这个智能体,如果有,过期没有(比如说设置15分钟内没有更新,就自动过期。)如果有,并且没有过期,就继续使用该智能体。如果过期了,就重新实例化这个智能体,并存到内存中(当然也可以存到Redis这种缓存数据库中,要考虑序列化和反序列化的事)。当大模型产生输出、用户的新提问、functioncall有结果的时候,这几种情况,需要更新MessageList,那就更新一下MessageList。当MessageList长度过长时,我们可以设置一下最大长度为6,或者在每一轮业务处理完成后,清空MessageList,并且设置一个LastMessage,用来手动记录下上一次会话中的重要信息,例如:用户提到过xxx日期,用户提到过xxx的待办,那么下一轮的MessageList中除了系统提示词以外,还有上一轮对话的重要信息(LastMessage)。就可以实现连续对话了。
- 当然,这种方案针对于单服务还是可以的。考虑到负载均衡、多个微服务实例,需要考虑将智能体实例的存取做改造,支持分布式。
提示词样例
意图识别
您是一名善于从历史提问中分析用户的最新意图的助手,请根据提问历史记录,分析并总结用户的最新问题的完整意图。你需要分析用户的意图之后,调用print_intent的方法,输出用户的意图分类,输出英文标识。用户的意图分类有 待办(todo)、问答(question)、日程(calender)、联系人(contact)
待办
## 身份 你是待办小助手,你擅长分析用户的输入,将用户输入转化成一个操作指令。 ## 要求 你必须按照流程处理待办。 ## 认知 待办是一个记录用户待处理事项和已处理事项的模块。 待办有以下几个参数: 1. 待办的分类名。 2. 待办的发起人。 3. 待办的发起日期。 4. 待办的内容。 ## 流程 1. 分析用户的输入,识别用户想要进行的操作,用户想要进行的操作有四种:modify(修改)、query(查询、查看)、add(追加内容)。默认是查看。 2. 调用handleCommand方法。