在构建chatClient时,我们可以给chatClient传入一些Advisor。

构建Advisors

比如下面,我传入了两个 Advisor,第一个是 SimpleLoggerAdvisor,第二个是 RelAdvisor。 如果不指定order的大小,它默认会按照传入的顺序去执行Advisor。

  chatClient = ChatClient.builder(dashScopeChatModel)
          // 实现 Logger 的 Advisor
          .defaultAdvisors(
                  new SimpleLoggerAdvisor(), 
                  new RelAdvisor()
          ).defaultSystem("请用英文回答问题")
          // 设置 ChatClient 中 ChatModel 的 Options 参数
          .defaultOptions(
                  DashScopeChatOptions.builder()
                          .temperature(0.7)
                          .build()
          )
          .build();
public ChatClient.Builder defaultAdvisors(Advisor... advisors) {
    this.defaultRequest.advisors(advisors);
    return this;
}

可以看到这里面有一个addAll方法,就是按顺序把advisor添加到一个list里面,最终取出来也是一个一个取的。

private final List<Advisor> advisors;
public ChatClient.ChatClientRequestSpec advisors(Advisor... advisors) {
    Assert.notNull(advisors, "advisors cannot be null");
    Assert.noNullElements(advisors, "advisors cannot contain null elements");
    this.advisors.addAll(Arrays.asList(advisors));
    return this;
}

然后可以去调用 chatClient 的 call 或 stream 方法去对话。

call() 方法

buildAdvisorChain()

call方法里面有一个 buildAdvisorChain() 方法,其实就是把多个 Advisor编成一个链,就像是责任链模式,一个一个地去执行。

public ChatClient.CallResponseSpec call() {
    BaseAdvisorChain advisorChain = this.buildAdvisorChain();
    return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain, this.observationRegistry, this.chatClientObservationConvention);
}

再进去里面发现,它最终还会为 advisors集合添加两个Advisor,分别是 ChatModelCallAdvisor 和 ChatModelStreamAdvisor 。 主要是做 同步对话 和 流式对话中会使用到。

private BaseAdvisorChain buildAdvisorChain() {
    this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build());
    this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build());
    return DefaultAroundAdvisorChain.builder(this.observationRegistry).observationConvention(this.advisorObservationConvention).pushAll(this.advisors).build();
}

再点击进去 pushAll(this.advisors) 方法里面可以看到:它会过滤出 advisors 里面的 CallAdvisor 和 StreamAdvisor 的类型,存储到一个List里面。然后在遍历这些List,把它添加到一个栈里面,这也就是为啥Advisor有一定先后执行顺序的原因,栈是先进后出的数据结构。

private final Deque<CallAdvisor> callAdvisors;
private final Deque<StreamAdvisor> streamAdvisors;

public Builder pushAll(List<? extends Advisor> advisors) {
    Assert.notNull(advisors, "the advisors must be non-null");
    Assert.noNullElements(advisors, "the advisors must not contain null elements");
    if (!CollectionUtils.isEmpty(advisors)) {
        List<CallAdvisor> callAroundAdvisorList = advisors.stream().filter((a) -> a instanceof CallAdvisor).map((a) -> (CallAdvisor)a).toList();
        if (!CollectionUtils.isEmpty(callAroundAdvisorList)) {
            Deque var10001 = this.callAdvisors;
            Objects.requireNonNull(var10001);
            callAroundAdvisorList.forEach(var10001::push);
        }

        List<StreamAdvisor> streamAroundAdvisorList = advisors.stream().filter((a) -> a instanceof StreamAdvisor).map((a) -> (StreamAdvisor)a).toList();
        if (!CollectionUtils.isEmpty(streamAroundAdvisorList)) {
            // 栈
            Deque var4 = this.streamAdvisors;
            Objects.requireNonNull(var4);
            // 添加到栈里面
            streamAroundAdvisorList.forEach(var4::push);
        }

        this.reOrder();
    }

    return this;
}

在 pushAll() 的最后还有一个 reOrder() 方法,这个方法主要作用是:按照每一个Advisor内部的 order进行排序,order的数值越小,优先级越高,也就是先入,出的时候就第一个出。优先级越高,再栈的最顶端,因为pop出栈的时候就是第一个出的。

private final Deque<CallAdvisor> callAdvisors;
private final Deque<StreamAdvisor> streamAdvisors;

private void reOrder() {
    ArrayList<CallAdvisor> callAdvisors = new ArrayList(this.callAdvisors);
    // 先把 callAdvisors 按照内部的 order进行排序
    OrderComparator.sort(callAdvisors);
    // 先清空栈
    this.callAdvisors.clear();
    Deque var10001 = this.callAdvisors;
    Objects.requireNonNull(var10001);
    // 再重新把排序后的 advisor入队,保证执行先后的顺序
    callAdvisors.forEach(var10001::addLast);
    ArrayList<StreamAdvisor> streamAdvisors = new ArrayList(this.streamAdvisors);
    OrderComparator.sort(streamAdvisors);
    this.streamAdvisors.clear();
    var10001 = this.streamAdvisors;
    Objects.requireNonNull(var10001);
    streamAdvisors.forEach(var10001::addLast);
}

以上就是 call() 方法的 buildAdvisorChain() 的过程,

public ChatClient.CallResponseSpec call() {
    BaseAdvisorChain advisorChain = this.buildAdvisorChain();
    return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain, this.observationRegistry, this.chatClientObservationConvention);
}

DefaultChatClientUtils.toChatClientRequest(this)

然后就是 DefaultChatClientUtils.toChatClientRequest(this) ,这个方法是核心方法,构建一个 ChatClientRequest对象,后续与LLM交互都是用这个玩意。

final class DefaultChatClientUtils {
    private DefaultChatClientUtils() {
    }

    static ChatClientRequest toChatClientRequest(DefaultChatClient.DefaultChatClientRequestSpec inputRequest) {
        Assert.notNull(inputRequest, "inputRequest cannot be null");
        // 存储历史对话消息列表,最终会给到LLM
        List<Message> processedMessages = new ArrayList();
        // 获取系统提示词
        String processedSystemText = inputRequest.getSystemText();
        // 如果有系统提示词的话
        if (StringUtils.hasText(processedSystemText)) {
            if (!CollectionUtils.isEmpty(inputRequest.getSystemParams())) {
                processedSystemText = PromptTemplate.builder().template(processedSystemText).variables(inputRequest.getSystemParams()).renderer(inputRequest.getTemplateRenderer()).build().render();
            }
            // 往 processedMessages 添加一个【系统消息】SystemMessage
            processedMessages.add(SystemMessage.builder().text(processedSystemText).metadata(inputRequest.getSystemMetadata()).build());
        }

        // 如果 message() 不为空也会添加到 processedMessages
        if (!CollectionUtils.isEmpty(inputRequest.getMessages())) {
            processedMessages.addAll(inputRequest.getMessages());
        }

        // 看看用户的 Query是否为空, 不为空就也追加到 processedMessages
        String processedUserText = inputRequest.getUserText();
        if (StringUtils.hasText(processedUserText)) {
            if (!CollectionUtils.isEmpty(inputRequest.getUserParams())) {
                processedUserText = PromptTemplate.builder().template(processedUserText).variables(inputRequest.getUserParams()).renderer(inputRequest.getTemplateRenderer()).build().render();
            }

            processedMessages.add(UserMessage.builder().text(processedUserText).media(inputRequest.getMedia()).metadata(inputRequest.getUserMetadata()).build());
        }

        
        ChatOptions processedChatOptions = inputRequest.getChatOptions();

        // 如果存在工具
        if (!inputRequest.getToolNames().isEmpty() || !inputRequest.getToolCallbacks().isEmpty() || !inputRequest.getToolCallbackProviders().isEmpty() || !CollectionUtils.isEmpty(inputRequest.getToolContext())) {
            if (processedChatOptions == null) {
                processedChatOptions = new DefaultToolCallingChatOptions();
            } else if (processedChatOptions instanceof DefaultChatOptions) {
                DefaultChatOptions defaultChatOptions = (DefaultChatOptions)processedChatOptions;
                processedChatOptions = (ChatOptions)ModelOptionsUtils.copyToTarget(defaultChatOptions, ChatOptions.class, DefaultToolCallingChatOptions.class);
            }
        }

        // 如果 processedChatOptions是ToolCallingChatOptions类型
        if (processedChatOptions instanceof ToolCallingChatOptions toolCallingChatOptions) {
            // 如果工具不为空
            if (!inputRequest.getToolNames().isEmpty()) {
                // 合并所有的工具
                Set<String> toolNames = ToolCallingChatOptions.mergeToolNames(new HashSet(inputRequest.getToolNames()), toolCallingChatOptions.getToolNames());
                // 把工具名称的集合传入 toolCallingChatOptions
                toolCallingChatOptions.setToolNames(toolNames);
            }

            // 获取toolCallBack
            List<ToolCallback> allToolCallbacks = new ArrayList(inputRequest.getToolCallbacks());
            
            for(ToolCallbackProvider provider : inputRequest.getToolCallbackProviders()) {
                allToolCallbacks.addAll(List.of(provider.getToolCallbacks()));
            }

            if (!allToolCallbacks.isEmpty()) {
                List<ToolCallback> toolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(allToolCallbacks, toolCallingChatOptions.getToolCallbacks());
                ToolCallingChatOptions.validateToolCallbacks(toolCallbacks);
                toolCallingChatOptions.setToolCallbacks(toolCallbacks);
            }

            if (!CollectionUtils.isEmpty(inputRequest.getToolContext())) {
                Map<String, Object> toolContext = ToolCallingChatOptions.mergeToolContext(inputRequest.getToolContext(), toolCallingChatOptions.getToolContext());
                toolCallingChatOptions.setToolContext(toolContext);
            }
        }

        // 返回一个 ChatClientRequest 具体对象
        return ChatClientRequest.builder().prompt(Prompt.builder().messages(processedMessages).chatOptions(processedChatOptions).build()).context(new ConcurrentHashMap(inputRequest.getAdvisorParams())).build();
    }
}

最后 return的时候有一个 context属性 .context(new ConcurrentHashMap(inputRequest.getAdvisorParams()))

这个主要是支持为chatClient传入一些自定义的一些参数,这些参数会伴随一个 请求 到 响应的生命周期,可以在各个阶段中这个param一路传递过去,每个节点需要用到的时候就可以取出来。

在构建chatClient的时候就可以传入一些自定义属性,是kv结构的,其实最终就会被 DefaultChatClientUtils.toChatClientRequest() 方法里被赋值到返回的 ChatClientRequest对象里面的context属性里,也可以通过 ChatClientRequest对象拿到context里面的一些自定义属性。

this.chatClient = ChatClient.builder(chatModel)
                            .defaultAdvisors(spec -> {
                                spec.params(Map.of("name", "刘文灿")); // 自定义属性
                            })
                            .build();

可以看到defaultAdvisors()里面,最终会把 advisorSpec的params 传入 advisorParams Map里面,最终构建 ChatClientRequest的时候就可以拿到他赋值给 ChatClientRequest对象。

private final Map<String, Object> advisorParams;

public ChatClient.ChatClientRequestSpec advisors(Consumer<ChatClient.AdvisorSpec> consumer) {
    Assert.notNull(consumer, "consumer cannot be null");
    DefaultAdvisorSpec advisorSpec = new DefaultAdvisorSpec();
    consumer.accept(advisorSpec);
    this.advisorParams.putAll(advisorSpec.getParams());
    this.advisors.addAll(advisorSpec.getAdvisors());
    return this;
}

chatClient的content方法(核心)

点进去 content方法里面,可以看到 doGetObservableChatClientResponse

@Nullable
public String content() {
    ChatResponse chatResponse = this.doGetObservableChatClientResponse(this.request).chatResponse();
    return getContentFromChatResponse(chatResponse);
}

接着进去 this.advisorChain.nextCall

private ChatClientResponse doGetObservableChatClientResponse(ChatClientRequest chatClientRequest, @Nullable String outputFormat) {
    if (outputFormat != null) {
        chatClientRequest.context().put(ChatClientAttributes.OUTPUT_FORMAT.getKey(), outputFormat);
    }

    ChatClientObservationContext observationContext = ChatClientObservationContext.builder().request(chatClientRequest).advisors(this.advisorChain.getCallAdvisors()).stream(false).format(outputFormat).build();
    Observation observation = ChatClientObservationDocumentation.AI_CHAT_CLIENT.observation(this.observationConvention, DefaultChatClient.DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry);
    ChatClientResponse chatClientResponse = (ChatClientResponse)observation.observe(() -> {
        // 开始调用第一个advisor,里面是递归逻辑。
        ChatClientResponse response = this.advisorChain.nextCall(chatClientRequest);
        observationContext.setResponse(response);
        return response;
    });
    // 最终执行完所有的advisor后,把最终的response返回出去。
    return chatClientResponse != null ? chatClientResponse : ChatClientResponse.builder().build();
}

点进去,还可以看到一个 advisor.adviseCall

public ChatClientResponse nextCall(ChatClientRequest chatClientRequest) {
    Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");
    if (this.callAdvisors.isEmpty()) {
        throw new IllegalStateException("No CallAdvisors available to execute");
    } else {
        // 将存储Advisor的栈结构,进行pop,也就拿出优先级较高的那一个advisor去执行
        CallAdvisor advisor = (CallAdvisor)this.callAdvisors.pop();
        AdvisorObservationContext observationContext = AdvisorObservationContext.builder().advisorName(advisor.getName()).chatClientRequest(chatClientRequest).order(advisor.getOrder()).build();
        return (ChatClientResponse)AdvisorObservationDocumentation.AI_ADVISOR.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> {
            // 调用对应advisor的adviseCall方法, 去执行真正的逻辑
            ChatClientResponse chatClientResponse = advisor.adviseCall(chatClientRequest, this);
            observationContext.setChatClientResponse(chatClientResponse);
            return chatClientResponse;
        });
    }
}

我当前的编排第一个是 SimpleLoggerAdvisor,所以我先点击去对应的方法看看实现逻辑。可以看到它是一个递归调用,假如说我编排了多个advisor。

A -> B -> C -> ChatModelCallAdvisor

SpringAI框架会先去执行 A,然后在A里面递归去执行下一个B,B里面又会递归执行下一个C,C又会递归执行下一个 ChatModelCallAdvisor,最后这个也就是与LLM真正交互的Advisor,最后交互完后把结果层层往上一层返回,最终就输出一个完整 chatClientResponse 回去。

public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
    // 先打印请求前日志
    this.logRequest(chatClientRequest);
    // 再递归调用下一个advisor
    ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
    // 最后再把结果返回出去
    this.logResponse(chatClientResponse);
    return chatClientResponse;
}

真正与LLM交互的是最终的Advisor:ChatModelCallAdvisor

继续点击进去,找到 ChatModelCallAdvisor的实现类,找到其中的adviseCall方法。

public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
    Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");
    ChatClientRequest formattedChatClientRequest = augmentWithFormatInstructions(chatClientRequest);
    ChatResponse chatResponse = this.chatModel.call(formattedChatClientRequest.prompt());
    return ChatClientResponse.builder().chatResponse(chatResponse).context(Map.copyOf(formattedChatClientRequest.context())).build();
}

其中核心的就是 this.chatModel.call()方法,里面有一个 internalCall,这是最关键的实现,下面会讲。

public ChatResponse call(Prompt prompt) {
    Prompt requestPrompt = this.buildRequestPrompt(prompt);
    // 内部调用
    return this.internalCall(requestPrompt, (ChatResponse)null);
}

internalCall方法【最核心】真正与LLM对话的方法,里面的流程都在注释描述了。

public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) {
    OpenAiApi.ChatCompletionRequest request = this.createRequest(prompt, false);
    ChatModelObservationContext observationContext = ChatModelObservationContext.builder().prompt(prompt).provider(OpenAiApiConstants.PROVIDER_NAME).build();

    // 调用 LLM,并获取其中的响应
    ChatResponse response = (ChatResponse)ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> {
        ResponseEntity<OpenAiApi.ChatCompletion> completionEntity = (ResponseEntity)this.retryTemplate.execute((ctx) -> this.openAiApi.chatCompletionEntity(request, this.getAdditionalHttpHeaders(prompt)));
        OpenAiApi.ChatCompletion chatCompletion = (OpenAiApi.ChatCompletion)completionEntity.getBody();
        if (chatCompletion == null) {
            logger.warn("No chat completion returned for prompt: {}", prompt);
            return new ChatResponse(List.of());
        } else {
            List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
            if (choices == null) {
                logger.warn("No choices returned for prompt: {}", prompt);
                return new ChatResponse(List.of());
            } else {
                List<Generation> generations = choices.stream().map((choice) -> {
                    Map<String, Object> metadata = Map.of("id", chatCompletion.id() != null ? chatCompletion.id() : "", "role", choice.message().role() != null ? choice.message().role().name() : "", "index", choice.index() != null ? choice.index() : 0, "finishReason", this.getFinishReasonJson(choice.finishReason()), "refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "", "annotations", choice.message().annotations() != null ? choice.message().annotations() : List.of(Map.of()));
                    return this.buildGeneration(choice, metadata, request);
                }).toList();
                RateLimit rateLimit = OpenAiResponseHeaderExtractor.extractAiResponseHeaders(completionEntity);
                OpenAiApi.Usage usage = chatCompletion.usage();
                Usage currentChatResponseUsage = (Usage)(usage != null ? this.getDefaultUsage(usage) : new EmptyUsage());
                Usage accumulatedUsage = UsageCalculator.getCumulativeUsage(currentChatResponseUsage, previousChatResponse);
                ChatResponse chatResponse = new ChatResponse(generations, this.from(chatCompletion, rateLimit, accumulatedUsage));
                observationContext.setResponse(chatResponse);
                return chatResponse;
            }
        }
    });
        
    // 根据LLM的响应去判断是否需要工具调用
    // 其中判断我们应用层是否开启了工具自动调用 和 判断toolCallback是否为空
    if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) {
        // 调用工具
        ToolExecutionResult toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);
        // 判断 returnDirect 是否需要直接返回工具调用后的结果
        // 1. 不需要:直接返回原始工具执行后的结果
        // 2. 需要:再次把对话历史上下文 和 工具执行回来的结果交给LLM再一次润色后再返回【增多一次LLM的调用】
        return toolExecutionResult.returnDirect() ? ChatResponse.builder().from(response).generations(ToolExecutionResult.buildGenerations(toolExecutionResult)).build() : this.internalCall(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()), response);
    } else {
        // 不需要工具调用直接返回LLM执行回来的结果
        return response;
    }
}

在我们使用SpringAi框架去手搓实现一个 Agent的时候,是要关闭SpringAI框架层面的自动工具调用,是否需要调用工具将由我们自己应用层去实现。

在构建chatClient的时候可以指定一个chatOptions,chatOptions里面可以指定一个 .internalToolExecutionEnabled(false) ,代表关闭框架层工具自动调用。

private void initChatClient() {
    try {
        OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
            .streamUsage(true) // 这个开启的时候,在流式输出最后一个chunk里面会输出usage信息
            .toolCallbacks(tools)
            .internalToolExecutionEnabled(false)
            .build();

        ChatClient.Builder builder = ChatClient.builder(chatModel);
        if (!CollectionUtils.isEmpty(advisors)) {
            builder.defaultAdvisors(advisors);
        }
        this.chatClient = builder.defaultOptions(openAiChatOptions)
        .defaultToolCallbacks(tools)
        .build();
    } catch (Exception e) {
        throw new RuntimeException("ChatClient 初始化失败:" + e.getMessage(), e);
    }
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐