OpenAI配置相关介绍

 OpenAiApi openAiApi = OpenAiApi.builder()
                    .apiKey(apiKey)
                    .baseUrl(baseUrl)
                    .completionsPath(configExtAttrsDTO.getCompletionsPath())
                    .restClientBuilder(restClientBuilder)
                    .build();

completionsPath 动态覆盖接口路径:
在这里插入图片描述
2.restClientBuilder 就是生产级 HTTP 客户端配置
在这里插入图片描述

    Long timeoutMs = configExtAttrsDTO.getTimeoutMs();
    long readTimeoutMs = (timeoutMs != null && timeoutMs > 0) ? timeoutMs : 60_000L;
    long connectTimeoutMs = Math.min(readTimeoutMs, 10_000L);

    SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
    requestFactory.setConnectTimeout(Duration.ofMillis(connectTimeoutMs));
    requestFactory.setReadTimeout(Duration.ofMillis(readTimeoutMs));

    RestClient.Builder restClientBuilder = RestClient.builder()
            .requestFactory(requestFactory);

    // 3. 创建 OpenAiApi(带超时的 RestClient)
    OpenAiApi openAiApi = OpenAiApi.builder()
            .apiKey(apiKey)
            .baseUrl(baseUrl)
            .completionsPath(configExtAttrsDTO.getCompletionsPath())
            .restClientBuilder(restClientBuilder)
            .build();

OpenAiChatModel相关配置介绍:

            // 4. 创建 ChatModel(使用全局共享 ObservationRegistry,由 ObservationConfig 统一配置)
            OpenAiChatModel chatModel = OpenAiChatModel.builder()
                    .openAiApi(openAiApi)
                    .defaultOptions(chatOptions)
                    .retryTemplate(RetryUtils.DEFAULT_RETRY_TEMPLATE)
                    .observationRegistry(observationRegistry)
                    .toolCallingManager(ToolCallingManager.builder().observationRegistry(observationRegistry).build())
                    .build();

1.defaultOptions 是 Spring AI 框架中专门用于封装 ChatModel 的默认对话参数的配置类

当你调用 chatModel.call("你的问题")
Spring AI 会自动把 OpenAiChatOptions 序列化成 OpenAI API 需要的 JSON 请求体
发送给 baseUrl 对应的大模型服务
模型返回结果后,框架再解析响应
// 最常用配置(官网核心)
OpenAiChatOptions.builder()
    .model("gpt-3.5-turbo")    // 模型名称(必填)
    .temperature(0.7D)         // 温度:0=严谨,1=创意
    .maxTokens(1024)           // 最大响应token数
    .topP(1.0D)                // 核采样
    .frequencyPenalty(0.0D)    // 频率惩罚
    .presencePenalty(0.0D)     // 存在惩罚
    .stop(List.of("\n"))       // 停止词
    .stream(true)              // 是否流式输出
    .build();
// 设置为 ChatModel 的默认配置
OpenAiChatModel chatModel = OpenAiChatModel.builder()
        .defaultOptions(chatOptions)  // 全局默认
        .build();    

2.retryTemplate Spring AI 支持通过 Spring Retry 实现:
在这里插入图片描述
3.observationRegistry 集成 Spring 观测(Micrometer),实现监控、追踪、日志、指标

private final ObservationRegistry observationRegistry;

在这里插入图片描述
4.toolCallingManager 就是管理 AI 调用本地方法的核心管理器。
在这里插入图片描述
在这里插入图片描述
完整代码案列:

    public ChatModel createChatModel(String providerKey, String baseUrl, String apiKey,
                                     String modelKey, ConfigExtAttrsDTO configExtAttrsDTO) throws Exception {
        try {
            // 1. 验证配置
            if (!isConfigValid(baseUrl, apiKey)) {
                throw new ModelCallException(ModelCallException.ErrorCode.INVALID_PARAMETER,
                        "baseUrl 和 apiKey 不能为空");
            }


            log.debug("Creating OpenAiChatModel for provider: {}, model: {}, baseUrl: {}",
                    providerKey, modelKey, baseUrl);
            // 2. 超时时间:读取超时用配置的 timeoutMs,未配置默认 60s;连接超时取 min(timeoutMs, 10s)
            Long timeoutMs = configExtAttrsDTO.getTimeoutMs();
            long readTimeoutMs = (timeoutMs != null && timeoutMs > 0) ? timeoutMs : 60_000L;
            long connectTimeoutMs = Math.min(readTimeoutMs, 10_000L);

            SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
            requestFactory.setConnectTimeout(Duration.ofMillis(connectTimeoutMs));
            requestFactory.setReadTimeout(Duration.ofMillis(readTimeoutMs));

            RestClient.Builder restClientBuilder = RestClient.builder()
                    .requestFactory(requestFactory);

            // 3. 创建 OpenAiApi(带超时的 RestClient)
            OpenAiApi openAiApi = OpenAiApi.builder()
                    .apiKey(apiKey)
                    .baseUrl(baseUrl)
                    .completionsPath(configExtAttrsDTO.getCompletionsPath())
                    .restClientBuilder(restClientBuilder)
                    .build();

            // 3. 解析配置JSON,构建 OpenAiChatOptions
            OpenAiChatOptions chatOptions = buildChatOptions(modelKey, configExtAttrsDTO);

            // 4. 创建 ChatModel(使用全局共享 ObservationRegistry,由 ObservationConfig 统一配置)
            OpenAiChatModel chatModel = OpenAiChatModel.builder()
                    .openAiApi(openAiApi)
                    .defaultOptions(chatOptions)
                    .retryTemplate(RetryUtils.DEFAULT_RETRY_TEMPLATE)
                    .observationRegistry(observationRegistry)
                    .toolCallingManager(ToolCallingManager.builder().observationRegistry(observationRegistry).build())
                    .build();


            log.info("Successfully created OpenAiChatModel for model: {}", modelKey);
            return chatModel;

        } catch (ModelCallException e) {
            throw e;
        } catch (Exception e) {
            log.error("Failed to create OpenAiChatModel for model: {}", modelKey, e);
            throw new ModelCallException(
                    ModelCallException.ErrorCode.CHAT_MODEL_BUILD_FAILED,
                    "无法创建ChatModel: " + e.getMessage(),
                    e
            );
        }
    }

    private OpenAiChatOptions buildChatOptions(String modelKey, ConfigExtAttrsDTO config) {
        OpenAiChatOptions.Builder builder = OpenAiChatOptions.builder().model(modelKey);
        if (config == null) {
            return builder.build();
        }
        if (config.getTemperature() != null) {
            builder.temperature(config.getTemperature());
        }
        if (config.getMaxTokens() != null) {
            builder.maxTokens(config.getMaxTokens());
        }
        if (config.getTopP() != null) {
            builder.topP(config.getTopP());
        }
        if (config.getFrequencyPenalty() != null) {
            builder.frequencyPenalty(config.getFrequencyPenalty());
        }
        if (config.getPresencePenalty() != null) {
            builder.presencePenalty(config.getPresencePenalty());
        }
        if (config.getStopSequences() != null && !config.getStopSequences().isEmpty()) {
            builder.stop(config.getStopSequences());
        }
        if (config.getSeed() != null) {
            builder.seed(config.getSeed().intValue());
        }
        return builder.build();
    }

增加拦截器

        List<Advisor> advisors = new ArrayList<>();
            advisors.add(new SimpleLoggerAdvisor());
            advisors.addAll(snailChatAdvisors);
            ChatClient chatClient = ChatClient.builder(chatModel)
                    .defaultAdvisors(advisors.toArray(Advisor[]::new))
                    .build();

SimpleLoggerAdvisor 为日志拦截器 = Spring AI 自带的日志打印拦截器= 专门打印 AI 请求 + 响应 + 流式分片

我们自己可以重写他,只实现 ChatClientAdvisor 或者实现 StreamChatClientAdvisor
拦截器案列

     private final List<StreamAdvisor> snailChatAdvisors;
     实现这个StreamAdvisor接口;

在这里插入图片描述
在这里插入图片描述

MemoryInjectionAdvisor 记忆注入
在这里插入图片描述

在这里插入图片描述

mport com.aizuda.snail.ai.common.dto.agent.ChatDispatchRequest;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;

import java.util.ArrayList;
import java.util.List;

/**
 * 将 dispatch 中的 memory_context 与 history_messages 注入 Prompt。
 */
public class MemoryInjectionAdvisor implements BaseAdvisor {

    private static final String ROLE_USER = "user";
    private static final String ROLE_ASSISTANT = "assistant";

    @Override
    public String getName() {
        return "MemoryInjectionAdvisor";
    }

    @Override
    public int getOrder() {
        return 200;
    }

    @Override
    public ChatClientRequest before(ChatClientRequest request, AdvisorChain advisorChain) {
        Object d = request.context().get(ClientAdvisorKeys.DISPATCH);
        if (!(d instanceof ChatDispatchRequest dispatch)) {
            return request;
        }
        List<Message> messages = new ArrayList<>();
        addSystemMessage(messages, dispatch);
        addHistoryMessages(messages, dispatch.getHistoryMessages());
        addUserMessage(messages, dispatch.getUserMessage());
        Prompt p = new Prompt(messages, request.prompt().getOptions());
        return new ChatClientRequest(p, request.context());
    }

    @Override
    public ChatClientResponse after(ChatClientResponse response, AdvisorChain advisorChain) {
        return response;
    }

    private void addSystemMessage(List<Message> messages, ChatDispatchRequest request) {
        String systemPrompt = request.getSystemPrompt() != null ? request.getSystemPrompt() : "";
        String memoryContext = request.getMemoryContext() != null ? request.getMemoryContext() : "";
        if (!memoryContext.isEmpty()) {
            systemPrompt = systemPrompt + "\n\n" + memoryContext;
        }
        if (!systemPrompt.isEmpty()) {
            messages.add(new SystemMessage(systemPrompt));
        }
    }

    private void addHistoryMessages(List<Message> messages, List<ChatDispatchRequest.HistoryMessage> historyMessages) {
        if (historyMessages == null || historyMessages.isEmpty()) {
            return;
        }
        for (ChatDispatchRequest.HistoryMessage msg : historyMessages) {
            if (ROLE_USER.equals(msg.getRole())) {
                messages.add(new UserMessage(msg.getContent()));
            } else if (ROLE_ASSISTANT.equals(msg.getRole())) {
                messages.add(new AssistantMessage(msg.getContent()));
            }
        }
    }

    private void addUserMessage(List<Message> messages, String userMessage) {
        if (userMessage != null && !userMessage.isEmpty()) {
            messages.add(new UserMessage(userMessage));
        }
    }
}

InterceptorChainAdvisor(拦截器执行器)
在这里插入图片描述

public class InterceptorChainAdvisor implements BaseAdvisor {

    private final SnailAiInterceptorChain chain;

    public InterceptorChainAdvisor(List<SnailAiInterceptor> interceptors) {
        this.chain = new SnailAiInterceptorChain(interceptors);
    }

    @Override
    public String getName() {
        return "InterceptorChainAdvisor";
    }

    @Override
    public int getOrder() {
        return 300;
    }

    @Override
    public ChatClientRequest before(ChatClientRequest request, AdvisorChain advisorChain) {
        return chain.applyBefore(request);
    }

    @Override
    public ChatClientResponse after(ChatClientResponse response, AdvisorChain advisorChain) {
        return chain.applyAfter(response);
    }
}
/**
 * 拦截器链执行器
 * <p>
 * 管理 {@link SnailAiInterceptor} 列表的有序执行:
 * <ul>
 *   <li>{@code applyBefore}: 按 Order 正序执行 {@code beforeRequest}</li>
 *   <li>{@code applyAfter}: 按 Order 逆序执行 {@code afterResponse}</li>
 * </ul>
 *
 * @author opensnail
 */
public class SnailAiInterceptorChain {

    private final List<SnailAiInterceptor> interceptors;

    public SnailAiInterceptorChain(List<SnailAiInterceptor> interceptors) {
        this.interceptors = interceptors == null ? List.of() : interceptors.stream()
                .sorted(Comparator.comparingInt(SnailAiInterceptor::getOrder))
                .toList();
    }

    /**
     * 正序执行所有拦截器的 beforeRequest
     */
    public ChatClientRequest applyBefore(ChatClientRequest request) {
        ChatClientRequest current = request;
        for (SnailAiInterceptor interceptor : interceptors) {
            current = interceptor.beforeRequest(current);
        }
        return current;
    }

    /**
     * 逆序执行所有拦截器的 afterResponse
     */
    public ChatClientResponse applyAfter(ChatClientResponse response) {
        ChatClientResponse current = response;
        for (int i = interceptors.size() - 1; i >= 0; i--) {
            current = interceptors.get(i).afterResponse(current);
        }
        return current;
    }

    public List<SnailAiInterceptor> getInterceptors() {
        return interceptors;
    }

    public boolean isEmpty() {
        return interceptors.isEmpty();
    }
}
/**
 * 客户端 LLM 调用拦截器 SPI。
 */
public interface SnailAiInterceptor extends Ordered {

    default ChatClientRequest beforeRequest(ChatClientRequest request) {
        return request;
    }

    default ChatClientResponse afterResponse(ChatClientResponse response) {
        return response;
    }

    @Override
    default int getOrder() {
        return 0;
    }
}

/**
 * 可选的请求/响应日志拦截器。
 */
@Slf4j
public class LoggingInterceptor implements SnailAiInterceptor {

    @Override
    public ChatClientRequest beforeRequest(ChatClientRequest request) {
        int n = request.prompt() != null && request.prompt().getInstructions() != null
                ? request.prompt().getInstructions().size() : 0;
        log.info("LLM request: {} messages", n);
        return request;
    }

    @Override
    public ChatClientResponse afterResponse(ChatClientResponse response) {
        String finish = Optional.ofNullable(response)
                .map(ChatClientResponse::chatResponse)
                .map(ChatResponse::getResult)
                .map(Generation::getMetadata)
                .map(m -> String.valueOf(m.getFinishReason()))
                .orElse("unknown");
        log.info("LLM response: finishReason={}", finish);
        return response;
    }

    @Override
    public int getOrder() {
        return 100;
    }
}

TokenUsageCollectorAdvisor(Token 统计)
在这里插入图片描述

/**
 * 从流式响应的最终 chunk 中提取 Token 使用量,写入 {@link ClientStreamExecutionContext}。
 * <p>
 * 需要模型配置启用 {@code streamUsage(true)},OpenAI 才会在最终 SSE chunk 中返回 usage 数据。
 *
 * @author opensnail
 * @date 2026-04-20
 */
public class TokenUsageCollectorAdvisor implements StreamAdvisor {

    @Override
    public String getName() {
        return "TokenUsageCollectorAdvisor";
    }

    @Override
    public int getOrder() {
        return 350;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest request, StreamAdvisorChain chain) {
        Object st = request.context().get(ClientAdvisorKeys.STREAM_STATE);
        if (!(st instanceof ClientStreamExecutionContext state)) {
            return chain.nextStream(request);
        }

        return chain.nextStream(request)
                .doOnNext(response -> extractUsage(response, state));
    }

    private void extractUsage(ChatClientResponse response, ClientStreamExecutionContext state) {
        ChatResponse cr = response.chatResponse();
        if (cr == null) {
            return;
        }
        Usage usage = cr.getMetadata().getUsage();
        if (usage.getTotalTokens() > 0) {
            state.setPromptTokens(usage.getPromptTokens());
            state.setCompletionTokens(usage.getCompletionTokens());
        }
    }
}
/**
 * 单次流式调用的累积状态(由 Advisor 写入,完成时转为 {@link ClientChatExecutor.ChatCompletionResult})。
 */
public class ClientStreamExecutionContext {

    public final StringBuilder fullText = new StringBuilder();
    public final StringBuilder thinkingText = new StringBuilder();
    public final long startTime = System.currentTimeMillis();

    /** 累积的工具调用列表 (stream 模式下逐步收集) */
    private final List<AssistantMessage.ToolCall> toolCalls = new ArrayList<>();

    /** 流式最终 chunk 中的 Token 使用量 */
    @Setter
    private int promptTokens;
    @Setter
    private int completionTokens;

    public void addToolCall(AssistantMessage.ToolCall toolCall) {
        if (toolCall != null && !containsToolCall(toolCall.id())) {
            toolCalls.add(toolCall);
        }
    }

    public void addToolCalls(List<AssistantMessage.ToolCall> calls) {
        if (calls != null) {
            calls.forEach(this::addToolCall);
        }
    }

    public List<AssistantMessage.ToolCall> getToolCalls() {
        return new ArrayList<>(toolCalls);
    }

    public boolean hasToolCalls() {
        return !toolCalls.isEmpty();
    }

    private boolean containsToolCall(String id) {
        return toolCalls.stream().anyMatch(tc -> tc.id().equals(id));
    }

    public ClientChatExecutor.ChatCompletionResult toCompletionResult() {
        long duration = System.currentTimeMillis() - startTime;
        return new ClientChatExecutor.ChatCompletionResult(
                fullText.toString(), thinkingText.toString(), promptTokens, completionTokens, duration);
    }
}

ThinkingCollectorAdvisor(思考过程收集)
在这里插入图片描述

import com.aizuda.snail.ai.agent.common.context.AgentChatContextHolder;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import reactor.core.publisher.Flux;

import java.util.Map;
import java.util.function.Consumer;

/**
 * 累积流式思维链 metadata,完成时桥接到 {@link AgentChatContextHolder.ChatContext}
 * 供 GENERATION 观测的 {@code ThinkingContentExtractor} 读取。
 * <p>
 * 同时将完整 thinkingText 保留在 {@link ClientStreamExecutionContext},
 * 随 {@code ChatCompletionResult.fullThinking} 返回给调用方用于对话记录持久化。
 */
@RequiredArgsConstructor
public class ThinkingCollectorAdvisor implements StreamAdvisor {

    private static final String[] THINKING_KEYS = {"reasoningContent", "thinking", "reasoning"};

    @Override
    public String getName() {
        return "ThinkingCollectorAdvisor";
    }

    @Override
    public int getOrder() {
        return 400;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest request, StreamAdvisorChain chain) {
        Object st = request.context().get(ClientAdvisorKeys.STREAM_STATE);
        if (!(st instanceof ClientStreamExecutionContext state)) {
            return chain.nextStream(request);
        }

        @SuppressWarnings("unchecked")
        Consumer<String> thinkingConsumer = (Consumer<String>) request.context().get(ClientAdvisorKeys.THINKING_CONSUMER);

        return chain.nextStream(request)
                .doOnNext(response -> extractThinking(response, state, thinkingConsumer))
                .doOnComplete(() -> bridgeThinkingToContext(state));
    }

    private void extractThinking(ChatClientResponse response, ClientStreamExecutionContext state,
                                 Consumer<String> thinkingConsumer) {
        ChatResponse cr = response.chatResponse();
        if (cr == null || cr.getResult() == null) {
            return;
        }
        Generation generation = cr.getResult();
        Map<String, Object> metadata = generation.getOutput().getMetadata();
        for (String key : THINKING_KEYS) {
            Object value = metadata.get(key);
            if (value instanceof String s && !s.isEmpty()) {
                state.thinkingText.append(s);
                if (thinkingConsumer != null) {
                    thinkingConsumer.accept(s);
                }
                return;
            }
        }
    }

    /**
     * 流完成时将累积的思维链桥接到 ChatContext,供 ObservationHandler 的提取器读取
     */
    private void bridgeThinkingToContext(ClientStreamExecutionContext state) {
        String thinking = state.thinkingText.toString();
        if (thinking.isEmpty()) {
            return;
        }
        AgentChatContextHolder.ChatContext ctx = AgentChatContextHolder.getContext();
        if (ctx != null) {
            ctx.setCurrentThinkingContent(thinking);
        }
    }
}

StreamChunkForwarderAdvisor(转发文本分片)
在这里插入图片描述

/**
 * 将模型输出的文本增量回调给 {@link ClientAdvisorKeys#CHUNK_CONSUMER},并累积到 {@link ClientStreamExecutionContext}。
 */
public class StreamChunkForwarderAdvisor implements StreamAdvisor {

    @Override
    public String getName() {
        return "StreamChunkForwarderAdvisor";
    }

    @Override
    public int getOrder() {
        return 500;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest request, StreamAdvisorChain chain) {
        @SuppressWarnings("unchecked")
        Consumer<String> chunkConsumer = (Consumer<String>) request.context().get(ClientAdvisorKeys.CHUNK_CONSUMER);
        Object st = request.context().get(ClientAdvisorKeys.STREAM_STATE);
        ClientStreamExecutionContext state = st instanceof ClientStreamExecutionContext c ? c : null;

        return chain.nextStream(request).doOnNext(response -> {
            String text = extractText(response);
            if (text != null && !text.isEmpty()) {
                if (state != null) {
                    state.fullText.append(text);
                }
                if (chunkConsumer != null) {
                    chunkConsumer.accept(text);
                }
            }
        });
    }

    private static String extractText(ChatClientResponse response) {
        return Optional.ofNullable(response)
                .map(ChatClientResponse::chatResponse)
                .map(ChatResponse::getResult)
                .map(Generation::getOutput)
                .map(AssistantMessage::getText)
                .orElse(null);
    }
}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐