SpringAI里使用openAi的一些经验
·
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);
}
}
更多推荐




所有评论(0)