AI 零代码应用生成平台(覆盖源码实现深度思考)

一、前言

本文是通过修改LangChain4j的源码从而实现深度思考内容的输出,首先深度思考需要大模型的支持,具体可参考

模型的思考模式,目前大模型包含混合思考模式与仅思考模式。混合思考模式模型可以在思考后回复,也可直接回复;仅思考模式模型总会在回复前进行思考,且无法关闭。对于deepseek,其r1模型,只有思考模式。

由于该项目开发之初是使用1.1.0版本的LangChain4j作为ai开发框架,当时并不支持深度思考的输出,在后续1.2.0LangChain4j已有了onPartialThinking来获取深度思考的内容,但是考虑到项目的前部分工程项目生成已经修改了LangChain4j的源码,所以打算继续改造源码,而不是直接使用1.2.0版本。

二、开发实现

2.1、项目结构

源码文件目录结构:

UML类图如下:

2.2、添加装thinking的容器

首先,langchain4j框架已经处理过我们收到的stream流,不管是Flux<String>或者是TokenStream

因此我们需要修改AiMessage增加thinking类型的消息

具体代码如下:注意路径为 dev.langchain4j.data.message.AiMessage

java
复制代码
/** * Represents a message generated by AI (language model). * This message can contain: * <pre> * - {@link #text()}: textual content * - {@link #thinking()}: thinking/reasoning content * - {@link #toolExecutionRequests()}: requests to execute tools * - {@link #attributes()}: additional attributes, typically provider-specific * </pre> * <p> * In case this message contains tool execution requests, * the response to this message should be one {@link ToolExecutionResultMessage} for each tool execution request. */ public class AiMessage implements ChatMessage { private final String text; private final String thinking; private final List<ToolExecutionRequest> toolExecutionRequests; private final Map<String, Object> attributes; /** * Create a new {@link AiMessage} with the given text. * * @param text the text of the message. */ public AiMessage(String text) { this.text = ensureNotNull(text, "text"); this.thinking = null; this.toolExecutionRequests = List.of(); this.attributes = Map.of(); } /** * Create a new {@link AiMessage} with the given tool execution requests. * * @param toolExecutionRequests the tool execution requests of the message. */ public AiMessage(List<ToolExecutionRequest> toolExecutionRequests) { this.text = null; this.thinking = null; this.toolExecutionRequests = ensureNotEmpty(toolExecutionRequests, "toolExecutionRequests"); this.attributes = Map.of(); } /** * Create a new {@link AiMessage} with the given text and tool execution requests. * * @param text the text of the message. * @param toolExecutionRequests the tool execution requests of the message. */ public AiMessage(String text, List<ToolExecutionRequest> toolExecutionRequests) { this.text = text; this.thinking = null; this.toolExecutionRequests = copy(toolExecutionRequests); this.attributes = Map.of(); } /** * @since 1.2.0 */ public AiMessage(Builder builder) { this.text = builder.text; this.thinking = builder.thinking; this.toolExecutionRequests = copy(builder.toolExecutionRequests); this.attributes = copy(builder.attributes); } /** * Get the text of the message. * * @return the text of the message. */ public String text() { return text; } /** * Get the thinking/reasoning text of the message. * * @return the thinking/reasoning text of the message. * @since 1.2.0 */ @Experimental public String thinking() { return thinking; } /** * Get the tool execution requests of the message. * * @return the tool execution requests of the message. */ public List<ToolExecutionRequest> toolExecutionRequests() { return toolExecutionRequests; } /** * Check if the message has {@link ToolExecutionRequest}s. * * @return true if the message has {@link ToolExecutionRequest}s, false otherwise. */ public boolean hasToolExecutionRequests() { return !isNullOrEmpty(toolExecutionRequests); } /** * Returns additional attributes, typically provider-specific. * * @see #attribute(String, Class) * @since 1.2.0 */ @Experimental public Map<String, Object> attributes() { return attributes; } /** * Returns additional attribute by it's key. * * @see #attributes() * @since 1.2.0 */ @Experimental public <T> T attribute(String key, Class<T> type) { return (T) attributes.get(key); } @Override public ChatMessageType type() { return AI; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AiMessage that = (AiMessage) o; return Objects.equals(this.text, that.text) && Objects.equals(this.thinking, that.thinking) && Objects.equals(this.toolExecutionRequests, that.toolExecutionRequests) && Objects.equals(this.attributes, that.attributes); } @Override public int hashCode() { return Objects.hash(text, thinking, toolExecutionRequests, attributes); } @Override public String toString() { return "AiMessage {" + " text = " + quoted(text) + ", thinking = " + quoted(thinking) + ", toolExecutionRequests = " + toolExecutionRequests + ", attributes = " + attributes + " }"; } public static Builder builder() { return new Builder(); } public static class Builder { private String text; private String thinking; private List<ToolExecutionRequest> toolExecutionRequests; private Map<String, Object> attributes; public Builder text(String text) { this.text = text; return this; } /** * @since 1.2.0 */ @Experimental public Builder thinking(String thinking) { this.thinking = thinking; return this; } public Builder toolExecutionRequests(List<ToolExecutionRequest> toolExecutionRequests) { this.toolExecutionRequests = toolExecutionRequests; return this; } /** * @since 1.2.0 */ @Experimental public Builder attributes(Map<String, Object> attributes) { this.attributes = attributes; return this; } public AiMessage build() { return new AiMessage(this); } } /** * Create a new {@link AiMessage} with the given text. * * @param text the text of the message. * @return the new {@link AiMessage}. */ public static AiMessage from(String text) { return new AiMessage(text); } /** * Create a new {@link AiMessage} with the given tool execution requests. * * @param toolExecutionRequests the tool execution requests of the message. * @return the new {@link AiMessage}. */ public static AiMessage from(ToolExecutionRequest... toolExecutionRequests) { return from(asList(toolExecutionRequests)); } /** * Create a new {@link AiMessage} with the given tool execution requests. * * @param toolExecutionRequests the tool execution requests of the message. * @return the new {@link AiMessage}. */ public static AiMessage from(List<ToolExecutionRequest> toolExecutionRequests) { return new AiMessage(toolExecutionRequests); } /** * Create a new {@link AiMessage} with the given text and tool execution requests. * * @param text the text of the message. * @param toolExecutionRequests the tool execution requests of the message. * @return the new {@link AiMessage}. */ public static AiMessage from(String text, List<ToolExecutionRequest> toolExecutionRequests) { return new AiMessage(text, toolExecutionRequests); } /** * Create a new {@link AiMessage} with the given text. * * @param text the text of the message. * @return the new {@link AiMessage}. */ public static AiMessage aiMessage(String text) { return from(text); } /** * Create a new {@link AiMessage} with the given tool execution requests. * * @param toolExecutionRequests the tool execution requests of the message. * @return the new {@link AiMessage}. */ public static AiMessage aiMessage(ToolExecutionRequest... toolExecutionRequests) { return aiMessage(asList(toolExecutionRequests)); } /** * Create a new {@link AiMessage} with the given tool execution requests. * * @param toolExecutionRequests the tool execution requests of the message. * @return the new {@link AiMessage}. */ public static AiMessage aiMessage(List<ToolExecutionRequest> toolExecutionRequests) { return from(toolExecutionRequests); } /** * Create a new {@link AiMessage} with the given text and tool execution requests. * * @param text the text of the message. * @param toolExecutionRequests the tool execution requests of the message. * @return the new {@link AiMessage}. */ public static AiMessage aiMessage(String text, List<ToolExecutionRequest> toolExecutionRequests) { return from(text, toolExecutionRequests); } }

这个时候我们有了接收Thinking内容的容器,那么我们需要增加如何接收thinking的逻辑

2.3、获取加工thinking的逻辑

1)获取thinking

修改框架解析流式消息的载体Delta,新增reasoningContent字段以及相关的setter、getter和构建器等等

java
复制代码
@JsonDeserialize(builder = Delta.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public final class Delta { @JsonProperty private final String role; @JsonProperty private final String content; @JsonProperty private final String reasoningContent; @JsonProperty private final List<ToolCall> toolCalls; @JsonProperty @Deprecated private final FunctionCall functionCall; public Delta(Builder builder) { this.role = builder.role; this.content = builder.content; this.reasoningContent = builder.reasoningContent; this.toolCalls = builder.toolCalls; this.functionCall = builder.functionCall; } public String role() { return role; } public String content() { return content; } public String reasoningContent() { return reasoningContent; } public List<ToolCall> toolCalls() { return toolCalls; } @Deprecated public FunctionCall functionCall() { return functionCall; } @Override public boolean equals(Object another) { if (this == another) return true; return another instanceof Delta && equalTo((Delta) another); } private boolean equalTo(Delta another) { return Objects.equals(role, another.role) && Objects.equals(content, another.content) && Objects.equals(reasoningContent, another.reasoningContent) && Objects.equals(toolCalls, another.toolCalls) && Objects.equals(functionCall, another.functionCall); } @Override public int hashCode() { int h = 5381; h += (h << 5) + Objects.hashCode(role); h += (h << 5) + Objects.hashCode(content); h += (h << 5) + Objects.hashCode(reasoningContent); h += (h << 5) + Objects.hashCode(toolCalls); h += (h << 5) + Objects.hashCode(functionCall); return h; } @Override public String toString() { return "Delta{" + "role=" + role + ", content=" + content + ", reasoningContent=" + reasoningContent + ", toolCalls=" + toolCalls + ", functionCall=" + functionCall + "}"; } public static Builder builder() { return new Builder(); } @JsonPOJOBuilder(withPrefix = "") @JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static final class Builder { private String role; private String content; private String reasoningContent; private List<ToolCall> toolCalls; @Deprecated private FunctionCall functionCall; public Builder role(String role) { this.role = role; return this; } public Builder content(String content) { this.content = content; return this; } public Builder reasoningContent(String reasoningContent) { this.reasoningContent = reasoningContent; return this; } public Builder toolCalls(List<ToolCall> toolCalls) { if (toolCalls != null) { this.toolCalls = unmodifiableList(toolCalls); } return this; } @Deprecated public Builder functionCall(FunctionCall functionCall) { this.functionCall = functionCall; return this; } public Delta build() { return new Delta(this); } } }

定义流式消息的实体类,类似于工具调用定义的实体类

java
复制代码
public class PartialThinking { private final String text; public PartialThinking(String text) { ThrowUtils.throwIf(text == null, ErrorCode.PARAMS_ERROR, "text cannot be null"); this.text = text; } public String text() { return text; } @Override public boolean equals(final Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; PartialThinking that = (PartialThinking) object; return Objects.equals(text, that.text); } @Override public int hashCode() { return Objects.hashCode(text); } @Override public String toString() { return "PartialThinking{" + "text='" + text + '\'' + '}'; } }

dev.langchain4j.model.openai.OpenAiStreamingChatModel中新增解析Delta的reasoningContent得到thinking

具体代码如下:

java
复制代码
/** * Represents an OpenAI language model with a chat completion interface, such as gpt-4o-mini and o3. * The model's response is streamed token by token and should be handled with {@link StreamingResponseHandler}. * You can find description of parameters <a href="https://platform.openai.com/docs/api-reference/chat/create">here</a>. */ public class OpenAiStreamingChatModel implements StreamingChatModel { private final OpenAiClient client; private final OpenAiChatRequestParameters defaultRequestParameters; private final Boolean strictJsonSchema; private final Boolean strictTools; private final Boolean returnThinking; private final List<ChatModelListener> listeners; public OpenAiStreamingChatModel(OpenAiStreamingChatModelBuilder builder) { this.client = OpenAiClient.builder() .httpClientBuilder(builder.httpClientBuilder) .baseUrl(getOrDefault(builder.baseUrl, DEFAULT_OPENAI_URL)) .apiKey(builder.apiKey) .organizationId(builder.organizationId) .projectId(builder.projectId) .connectTimeout(getOrDefault(builder.timeout, ofSeconds(15))) .readTimeout(getOrDefault(builder.timeout, ofSeconds(60))) .logRequests(getOrDefault(builder.logRequests, false)) .logResponses(getOrDefault(builder.logResponses, false)) .userAgent(DEFAULT_USER_AGENT) .customHeaders(builder.customHeaders) .build(); ChatRequestParameters commonParameters; if (builder.defaultRequestParameters != null) { validate(builder.defaultRequestParameters); commonParameters = builder.defaultRequestParameters; } else { commonParameters = DefaultChatRequestParameters.EMPTY; } OpenAiChatRequestParameters openAiParameters; if (builder.defaultRequestParameters instanceof OpenAiChatRequestParameters openAiChatRequestParameters) { openAiParameters = openAiChatRequestParameters; } else { openAiParameters = OpenAiChatRequestParameters.EMPTY; } this.defaultRequestParameters = OpenAiChatRequestParameters.builder() // common parameters .modelName(getOrDefault(builder.modelName, commonParameters.modelName())) .temperature(getOrDefault(builder.temperature, commonParameters.temperature())) .topP(getOrDefault(builder.topP, commonParameters.topP())) .frequencyPenalty(getOrDefault(builder.frequencyPenalty, commonParameters.frequencyPenalty())) .presencePenalty(getOrDefault(builder.presencePenalty, commonParameters.presencePenalty())) .maxOutputTokens(getOrDefault(builder.maxTokens, commonParameters.maxOutputTokens())) .stopSequences(getOrDefault(builder.stop, commonParameters.stopSequences())) .toolSpecifications(commonParameters.toolSpecifications()) .toolChoice(commonParameters.toolChoice()) .responseFormat(getOrDefault(fromOpenAiResponseFormat(builder.responseFormat), commonParameters.responseFormat())) // OpenAI-specific parameters .maxCompletionTokens(getOrDefault(builder.maxCompletionTokens, openAiParameters.maxCompletionTokens())) .logitBias(getOrDefault(builder.logitBias, openAiParameters.logitBias())) .parallelToolCalls(getOrDefault(builder.parallelToolCalls, openAiParameters.parallelToolCalls())) .seed(getOrDefault(builder.seed, openAiParameters.seed())) .user(getOrDefault(builder.user, openAiParameters.user())) .store(getOrDefault(builder.store, openAiParameters.store())) .metadata(getOrDefault(builder.metadata, openAiParameters.metadata())) .serviceTier(getOrDefault(builder.serviceTier, openAiParameters.serviceTier())) .reasoningEffort(openAiParameters.reasoningEffort()) .build(); this.strictJsonSchema = getOrDefault(builder.strictJsonSchema, false); this.strictTools = getOrDefault(builder.strictTools, false); this.returnThinking = getOrDefault(builder.returnThinking, false); this.listeners = copy(builder.listeners); } @Override public OpenAiChatRequestParameters defaultRequestParameters() { return defaultRequestParameters; } @Override public void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler) { OpenAiChatRequestParameters parameters = (OpenAiChatRequestParameters) chatRequest.parameters(); validate(parameters); ChatCompletionRequest openAiRequest = toOpenAiChatRequest(chatRequest, parameters, strictTools, strictJsonSchema) .stream(true) .streamOptions(StreamOptions.builder() .includeUsage(true) .build()) .build(); OpenAiStreamingResponseBuilder openAiResponseBuilder = new OpenAiStreamingResponseBuilder(returnThinking); ToolExecutionRequestBuilder toolBuilder = new ToolExecutionRequestBuilder(); client.chatCompletion(openAiRequest) .onPartialResponse(partialResponse -> { openAiResponseBuilder.append(partialResponse); handle(partialResponse, toolBuilder, handler); }) .onComplete(() -> { if (toolBuilder.hasToolExecutionRequests()) { try { handler.onCompleteToolExecutionRequest(toolBuilder.index(), toolBuilder.build()); } catch (Exception e) { withLoggingExceptions(() -> handler.onError(e)); } } ChatResponse chatResponse = openAiResponseBuilder.build(); try { handler.onCompleteResponse(chatResponse); } catch (Exception e) { withLoggingExceptions(() -> handler.onError(e)); } }) .onError(throwable -> { RuntimeException mappedException = ExceptionMapper.DEFAULT.mapException(throwable); withLoggingExceptions(() -> handler.onError(mappedException)); }) .execute(); } private void handle(ChatCompletionResponse partialResponse, ToolExecutionRequestBuilder toolBuilder, StreamingChatResponseHandler handler) { if (partialResponse == null) { return; } List<ChatCompletionChoice> choices = partialResponse.choices(); if (choices == null || choices.isEmpty()) { return; } ChatCompletionChoice chatCompletionChoice = choices.get(0); if (chatCompletionChoice == null) { return; } Delta delta = chatCompletionChoice.delta(); if (delta == null) { return; } String content = delta.content(); if (!isNullOrEmpty(content)) { try { handler.onPartialResponse(content); } catch (Exception e) { withLoggingExceptions(() -> handler.onError(e)); } } String reasoningContent = delta.reasoningContent(); if (returnThinking && !isNullOrEmpty(reasoningContent)) { handler.onPartialThinking(new PartialThinking(reasoningContent)); } List<ToolCall> toolCalls = delta.toolCalls(); if (toolCalls != null) { for (ToolCall toolCall : toolCalls) { int index = toolCall.index(); if (toolBuilder.index() != index) { try { handler.onCompleteToolExecutionRequest(toolBuilder.index(), toolBuilder.build()); } catch (Exception e) { withLoggingExceptions(() -> handler.onError(e)); } toolBuilder.updateIndex(index); } String id = toolBuilder.updateId(toolCall.id()); String name = toolBuilder.updateName(toolCall.function().name()); String partialArguments = toolCall.function().arguments(); if (isNotNullOrEmpty(partialArguments)) { toolBuilder.appendArguments(partialArguments); ToolExecutionRequest partialToolExecutionRequest = ToolExecutionRequest.builder() .id(id) .name(name) .arguments(partialArguments) .build(); try { handler.onPartialToolExecutionRequest(index, partialToolExecutionRequest); } catch (Exception e) { withLoggingExceptions(() -> handler.onError(e)); } } } } } @Override public List<ChatModelListener> listeners() { return listeners; } @Override public ModelProvider provider() { return OPEN_AI; } public static OpenAiStreamingChatModelBuilder builder() { for (OpenAiStreamingChatModelBuilderFactory factory : loadFactories(OpenAiStreamingChatModelBuilderFactory.class)) { return factory.get(); } return new OpenAiStreamingChatModelBuilder(); } public static class OpenAiStreamingChatModelBuilder { private HttpClientBuilder httpClientBuilder; private String baseUrl; private String apiKey; private String organizationId; private String projectId; private ChatRequestParameters defaultRequestParameters; private String modelName; private Double temperature; private Double topP; private List<String> stop; private Integer maxTokens; private Integer maxCompletionTokens; private Double presencePenalty; private Double frequencyPenalty; private Map<String, Integer> logitBias; private String responseFormat; private Boolean strictJsonSchema; private Integer seed; private String user; private Boolean strictTools; private Boolean parallelToolCalls; private Boolean store; private Map<String, String> metadata; private String serviceTier; private Duration timeout; private Boolean returnThinking; private Boolean logRequests; private Boolean logResponses; private Map<String, String> customHeaders; private List<ChatModelListener> listeners; public OpenAiStreamingChatModelBuilder() { // This is public so it can be extended } public OpenAiStreamingChatModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { this.httpClientBuilder = httpClientBuilder; return this; } /** * Sets default common {@link ChatRequestParameters} or OpenAI-specific {@link OpenAiChatRequestParameters}. * <br> * When a parameter is set via an individual builder method (e.g., {@link #modelName(String)}), * its value takes precedence over the same parameter set via {@link ChatRequestParameters}. */ public OpenAiStreamingChatModelBuilder defaultRequestParameters(ChatRequestParameters parameters) { this.defaultRequestParameters = parameters; return this; } public OpenAiStreamingChatModelBuilder modelName(String modelName) { this.modelName = modelName; return this; } public OpenAiStreamingChatModelBuilder modelName(OpenAiChatModelName modelName) { this.modelName = modelName.toString(); return this; } public OpenAiStreamingChatModelBuilder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } public OpenAiStreamingChatModelBuilder apiKey(String apiKey) { this.apiKey = apiKey; return this; } public OpenAiStreamingChatModelBuilder organizationId(String organizationId) { this.organizationId = organizationId; return this; } public OpenAiStreamingChatModelBuilder projectId(String projectId) { this.projectId = projectId; return this; } public OpenAiStreamingChatModelBuilder temperature(Double temperature) { this.temperature = temperature; return this; } public OpenAiStreamingChatModelBuilder topP(Double topP) { this.topP = topP; return this; } public OpenAiStreamingChatModelBuilder stop(List<String> stop) { this.stop = stop; return this; } public OpenAiStreamingChatModelBuilder maxTokens(Integer maxTokens) { this.maxTokens = maxTokens; return this; } public OpenAiStreamingChatModelBuilder maxCompletionTokens(Integer maxCompletionTokens) { this.maxCompletionTokens = maxCompletionTokens; return this; } public OpenAiStreamingChatModelBuilder presencePenalty(Double presencePenalty) { this.presencePenalty = presencePenalty; return this; } public OpenAiStreamingChatModelBuilder frequencyPenalty(Double frequencyPenalty) { this.frequencyPenalty = frequencyPenalty; return this; } public OpenAiStreamingChatModelBuilder logitBias(Map<String, Integer> logitBias) { this.logitBias = logitBias; return this; } public OpenAiStreamingChatModelBuilder responseFormat(String responseFormat) { this.responseFormat = responseFormat; return this; } public OpenAiStreamingChatModelBuilder strictJsonSchema(Boolean strictJsonSchema) { this.strictJsonSchema = strictJsonSchema; return this; } public OpenAiStreamingChatModelBuilder seed(Integer seed) { this.seed = seed; return this; } public OpenAiStreamingChatModelBuilder user(String user) { this.user = user; return this; } public OpenAiStreamingChatModelBuilder strictTools(Boolean strictTools) { this.strictTools = strictTools; return this; } public OpenAiStreamingChatModelBuilder parallelToolCalls(Boolean parallelToolCalls) { this.parallelToolCalls = parallelToolCalls; return this; } public OpenAiStreamingChatModelBuilder store(Boolean store) { this.store = store; return this; } public OpenAiStreamingChatModelBuilder metadata(Map<String, String> metadata) { this.metadata = metadata; return this; } public OpenAiStreamingChatModelBuilder serviceTier(String serviceTier) { this.serviceTier = serviceTier; return this; } /** * This setting is intended for <a href="https://api-docs.deepseek.com/guides/reasoning_model">DeepSeek</a>. * <p> * Controls whether to return thinking/reasoning text (if available) inside {@link dev.langchain4j.data.message.AiMessage#thinking()} * and whether to invoke the {@link StreamingChatResponseHandler#onPartialThinking(PartialThinking)}callback. * Please note that this does not enable thinking/reasoning for the LLM; * it only controls whether to parse the {@code reasoning_content} field from the API response * and return it inside the {@link dev.langchain4j.data.message.AiMessage}. * <p> * Disabled by default. * If enabled, the thinking text will be stored within the {@link dev.langchain4j.data.message.AiMessage} and may be persisted. */ public OpenAiStreamingChatModelBuilder returnThinking(Boolean returnThinking) { this.returnThinking = returnThinking; return this; } public OpenAiStreamingChatModelBuilder timeout(Duration timeout) { this.timeout = timeout; return this; } public OpenAiStreamingChatModelBuilder logRequests(Boolean logRequests) { this.logRequests = logRequests; return this; } public OpenAiStreamingChatModelBuilder logResponses(Boolean logResponses) { this.logResponses = logResponses; return this; } public OpenAiStreamingChatModelBuilder customHeaders(Map<String, String> customHeaders) { this.customHeaders = customHeaders; return this; } public OpenAiStreamingChatModelBuilder listeners(List<ChatModelListener> listeners) { this.listeners = listeners; return this; } public OpenAiStreamingChatModel build() { return new OpenAiStreamingChatModel(this); } } }

该代码中新增了returnThinking字段

用于配置模型时,选择是否返回思考内容

dev.langchain4j.model.openai.OpenAiStreamingResponseBuilder中将thinking放入AiMessage

具体代码如下:

java
复制代码
/** * This class needs to be thread safe because it is called when a streaming result comes back * and there is no guarantee that this thread will be the same as the one that initiated the request, * in fact it almost certainly won't be. */ @Internal public class OpenAiStreamingResponseBuilder { private final StringBuffer contentBuilder = new StringBuffer(); private final StringBuffer reasoningContentBuilder; private final StringBuffer toolNameBuilder = new StringBuffer(); private final StringBuffer toolArgumentsBuilder = new StringBuffer(); private final Map<Integer, ToolExecutionRequestBuilder> indexToToolExecutionRequestBuilder = new ConcurrentHashMap<>(); private final AtomicReference<String> id = new AtomicReference<>(); private final AtomicReference<Long> created = new AtomicReference<>(); private final AtomicReference<String> model = new AtomicReference<>(); private final AtomicReference<String> serviceTier = new AtomicReference<>(); private final AtomicReference<String> systemFingerprint = new AtomicReference<>(); private final AtomicReference<TokenUsage> tokenUsage = new AtomicReference<>(); private final AtomicReference<FinishReason> finishReason = new AtomicReference<>(); private final boolean returnThinking; public OpenAiStreamingResponseBuilder(boolean returnThinking) { this.returnThinking = returnThinking; if (returnThinking) { this.reasoningContentBuilder = new StringBuffer(); } else { this.reasoningContentBuilder = null; } } public void append(ChatCompletionResponse partialResponse) { if (partialResponse == null) { return; } if (!isNullOrBlank(partialResponse.id())) { this.id.set(partialResponse.id()); } if (partialResponse.created() != null) { this.created.set(partialResponse.created()); } if (!isNullOrBlank(partialResponse.model())) { this.model.set(partialResponse.model()); } if (!isNullOrBlank(partialResponse.serviceTier())) { this.serviceTier.set(partialResponse.serviceTier()); } if (!isNullOrBlank(partialResponse.systemFingerprint())) { this.systemFingerprint.set(partialResponse.systemFingerprint()); } Usage usage = partialResponse.usage(); if (usage != null) { this.tokenUsage.set(tokenUsageFrom(usage)); } List<ChatCompletionChoice> choices = partialResponse.choices(); if (choices == null || choices.isEmpty()) { return; } ChatCompletionChoice chatCompletionChoice = choices.get(0); if (chatCompletionChoice == null) { return; } String finishReason = chatCompletionChoice.finishReason(); if (finishReason != null) { this.finishReason.set(finishReasonFrom(finishReason)); } Delta delta = chatCompletionChoice.delta(); if (delta == null) { return; } String content = delta.content(); if (!isNullOrEmpty(content)) { this.contentBuilder.append(content); } String reasoningContent = delta.reasoningContent(); if (returnThinking && !isNullOrEmpty(reasoningContent)) { this.reasoningContentBuilder.append(reasoningContent); } if (delta.functionCall() != null) { FunctionCall functionCall = delta.functionCall(); if (functionCall.name() != null) { this.toolNameBuilder.append(functionCall.name()); } if (functionCall.arguments() != null) { this.toolArgumentsBuilder.append(functionCall.arguments()); } } if (delta.toolCalls() != null) { System.out.println("OLOLO " + delta.toolCalls()); // TODO for (ToolCall toolCall : delta.toolCalls()) { ToolExecutionRequestBuilder builder = this.indexToToolExecutionRequestBuilder.computeIfAbsent( toolCall.index(), idx -> new ToolExecutionRequestBuilder() ); if (toolCall.id() != null) { builder.idBuilder.append(toolCall.id()); } FunctionCall functionCall = toolCall.function(); if (functionCall.name() != null) { builder.nameBuilder.append(functionCall.name()); } if (functionCall.arguments() != null) { builder.argumentsBuilder.append(functionCall.arguments()); } } } } public void append(CompletionResponse partialResponse) { if (partialResponse == null) { return; } Usage usage = partialResponse.usage(); if (usage != null) { this.tokenUsage.set(tokenUsageFrom(usage)); } List<CompletionChoice> choices = partialResponse.choices(); if (choices == null || choices.isEmpty()) { return; } CompletionChoice completionChoice = choices.get(0); if (completionChoice == null) { return; } String finishReason = completionChoice.finishReason(); if (finishReason != null) { this.finishReason.set(finishReasonFrom(finishReason)); } String token = completionChoice.text(); if (token != null) { this.contentBuilder.append(token); } } public ChatResponse build() { OpenAiChatResponseMetadata chatResponseMetadata = OpenAiChatResponseMetadata.builder() .id(id.get()) .modelName(model.get()) .tokenUsage(tokenUsage.get()) .finishReason(finishReason.get()) .created(created.get()) .serviceTier(serviceTier.get()) .systemFingerprint(systemFingerprint.get()) .build(); String text = contentBuilder.toString(); String toolName = toolNameBuilder.toString(); if (!toolName.isEmpty()) { ToolExecutionRequest toolExecutionRequest = ToolExecutionRequest.builder() .name(toolName) .arguments(toolArgumentsBuilder.toString()) .build(); // AiMessage aiMessage = isNullOrBlank(text) ? // AiMessage.from(toolExecutionRequest) : // AiMessage.from(text, singletonList(toolExecutionRequest)); String thinking = null; if (returnThinking) { thinking = reasoningContentBuilder.toString(); } AiMessage aiMessage = AiMessage.builder() .text(text) .thinking(thinking) .toolExecutionRequests(singletonList(toolExecutionRequest)) .build(); return ChatResponse.builder() .aiMessage(aiMessage) .metadata(chatResponseMetadata) .build(); } if (!indexToToolExecutionRequestBuilder.isEmpty()) { List<ToolExecutionRequest> toolExecutionRequests = indexToToolExecutionRequestBuilder.values().stream() .map(it -> ToolExecutionRequest.builder() .id(it.idBuilder.toString()) .name(it.nameBuilder.toString()) .arguments(it.argumentsBuilder.toString()) .build()) .collect(toList()); // AiMessage aiMessage = isNullOrBlank(text) ? // AiMessage.from(toolExecutionRequests) : // AiMessage.from(text, toolExecutionRequests); String thinking = null; if (returnThinking) { thinking = reasoningContentBuilder.toString(); } AiMessage aiMessage = AiMessage.builder() .text(text) .thinking(thinking) .toolExecutionRequests(toolExecutionRequests) .build(); return ChatResponse.builder() .aiMessage(aiMessage) .metadata(chatResponseMetadata) .build(); } if (!isNullOrBlank(text)) { AiMessage aiMessage = AiMessage.from(text); return ChatResponse.builder() .aiMessage(aiMessage) .metadata(chatResponseMetadata) .build(); } return null; } private static class ToolExecutionRequestBuilder { private final StringBuffer idBuilder = new StringBuffer(); private final StringBuffer nameBuilder = new StringBuffer(); private final StringBuffer argumentsBuilder = new StringBuffer(); } }

2)加工thinking

修改相关消息的handler

1、StreamingChatResponseHandler 新增处理PartialThinking

java
复制代码
/** * Invoked each time the model generates a partial thinking/reasoning text, usually a single token. * <p> * Please note that some LLM providers do not stream individual tokens, but send thinking tokens in batches. * In such cases, this callback may receive multiple tokens at once. * * @param partialThinking A partial thinking text, usually a single token. * @since 1.2.0 */ @Experimental default void onPartialThinking(PartialThinking partialThinking) {}

2、StreamingChatModel 中创建StreamingChatResponseHandler需要实现onPartialThinking方法

java
复制代码
@Override public void onPartialThinking(PartialThinking partialThinking) { handler.onPartialThinking(partialThinking); }

3、TokenStream 新增onPartialThinking回调方法

java
复制代码
/** * The provided consumer will be invoked every time a new partial thinking/reasoning text (usually a single token) * from a language model is available. * * @param partialThinkingHandler lambda that will be invoked when a model generates a new partial thinking/reasoning text * @return token stream instance used to configure or start stream processing */ default TokenStream onPartialThinking(Consumer<PartialThinking> partialThinkingHandler) { throw new UnsupportedOperationException("not implemented"); }

4、AiServiceTokenStream中注册回调方法,框架会自动调用该回调

具体代码如下:

java
复制代码
@Internal public class AiServiceTokenStream implements TokenStream { private final List<ChatMessage> messages; private final List<ToolSpecification> toolSpecifications; private final Map<String, ToolExecutor> toolExecutors; private final List<Content> retrievedContents; private final AiServiceContext context; private final Object memoryId; private final GuardrailRequestParams commonGuardrailParams; private final Object methodKey; private Consumer<String> partialResponseHandler; private Consumer<PartialThinking> partialThinkingHandler; private Consumer<List<Content>> contentsHandler; private Consumer<ToolExecution> toolExecutionHandler; private Consumer<ChatResponse> completeResponseHandler; private Consumer<Throwable> errorHandler; private BiConsumer<Integer, ToolExecutionRequest> partialToolExecutionRequestHandler; private BiConsumer<Integer, ToolExecutionRequest> completeToolExecutionRequestHandler; private int onPartialResponseInvoked; private int onPartialThinkingInvoked; private int onCompleteResponseInvoked; private int onRetrievedInvoked; private int onToolExecutedInvoked; private int onErrorInvoked; private int ignoreErrorsInvoked; /** * Creates a new instance of {@link AiServiceTokenStream} with the given parameters. * * @param parameters the parameters for creating the token stream */ public AiServiceTokenStream(AiServiceTokenStreamParameters parameters) { ensureNotNull(parameters, "parameters"); this.messages = copy(ensureNotEmpty(parameters.messages(), "messages")); this.toolSpecifications = copy(parameters.toolSpecifications()); this.toolExecutors = copy(parameters.toolExecutors()); this.retrievedContents = copy(parameters.gretrievedContents()); this.context = ensureNotNull(parameters.context(), "context"); ensureNotNull(this.context.streamingChatModel, "streamingChatModel"); this.memoryId = ensureNotNull(parameters.memoryId(), "memoryId"); this.commonGuardrailParams = parameters.commonGuardrailParams(); this.methodKey = parameters.methodKey(); } @Override public TokenStream onPartialResponse(Consumer<String> partialResponseHandler) { this.partialResponseHandler = partialResponseHandler; this.onPartialResponseInvoked++; return this; } @Override public TokenStream onPartialThinking(Consumer<PartialThinking> partialThinkingHandler) { this.partialThinkingHandler = partialThinkingHandler; this.onPartialThinkingInvoked++; return this; } @Override public TokenStream onPartialToolExecutionRequest(BiConsumer<Integer, ToolExecutionRequest> toolExecutionRequestHandler) { this.partialToolExecutionRequestHandler = toolExecutionRequestHandler; return this; } @Override public TokenStream onCompleteToolExecutionRequest(BiConsumer<Integer, ToolExecutionRequest> completedHandler) { this.completeToolExecutionRequestHandler = completedHandler; return this; } @Override public TokenStream onRetrieved(Consumer<List<Content>> contentsHandler) { this.contentsHandler = contentsHandler; this.onRetrievedInvoked++; return this; } @Override public TokenStream onToolExecuted(Consumer<ToolExecution> toolExecutionHandler) { this.toolExecutionHandler = toolExecutionHandler; this.onToolExecutedInvoked++; return this; } @Override public TokenStream onCompleteResponse(Consumer<ChatResponse> completionHandler) { this.completeResponseHandler = completionHandler; this.onCompleteResponseInvoked++; return this; } @Override public TokenStream onError(Consumer<Throwable> errorHandler) { this.errorHandler = errorHandler; this.onErrorInvoked++; return this; } @Override public TokenStream ignoreErrors() { this.errorHandler = null; this.ignoreErrorsInvoked++; return this; } @Override public void start() { validateConfiguration(); ChatRequest chatRequest = ChatRequest.builder() .messages(messages) .toolSpecifications(toolSpecifications) .build(); ChatExecutor chatExecutor = ChatExecutor.builder(context.streamingChatModel) .errorHandler(errorHandler) .chatRequest(chatRequest) .build(); var handler = new AiServiceStreamingResponseHandler( chatExecutor, context, memoryId, partialResponseHandler, partialThinkingHandler, partialToolExecutionRequestHandler, completeToolExecutionRequestHandler, toolExecutionHandler, completeResponseHandler, errorHandler, initTemporaryMemory(context, messages), new TokenUsage(), toolSpecifications, toolExecutors, commonGuardrailParams, methodKey); if (contentsHandler != null && retrievedContents != null) { contentsHandler.accept(retrievedContents); } context.streamingChatModel.chat(chatRequest, handler); } private void validateConfiguration() { if (onPartialResponseInvoked != 1) { throw new IllegalConfigurationException("onPartialResponse must be invoked on TokenStream exactly 1 time"); } if (onPartialThinkingInvoked > 1) { throw new IllegalConfigurationException("onPartialThinking can be invoked on TokenStream at most 1 time"); } if (onCompleteResponseInvoked > 1) { throw new IllegalConfigurationException("onCompleteResponse can be invoked on TokenStream at most 1 time"); } if (onRetrievedInvoked > 1) { throw new IllegalConfigurationException("onRetrieved can be invoked on TokenStream at most 1 time"); } if (onToolExecutedInvoked > 1) { throw new IllegalConfigurationException("onToolExecuted can be invoked on TokenStream at most 1 time"); } if (onErrorInvoked + ignoreErrorsInvoked != 1) { throw new IllegalConfigurationException( "One of [onError, ignoreErrors] " + "must be invoked on TokenStream exactly 1 time"); } } private ChatMemory initTemporaryMemory(AiServiceContext context, List<ChatMessage> messagesToSend) { var chatMemory = MessageWindowChatMemory.withMaxMessages(Integer.MAX_VALUE); if (!context.hasChatMemory()) { chatMemory.add(messagesToSend); } return chatMemory; } }

新增两个属性

private int onPartialThinkingInvoked;

private Consumer partialThinkingHandler;

其中onPartialThinkingInvoked为partialThinkingHandler的被调用次数

5、AiServiceStreamingResponseHandler 的构造函数添加partialThinkingHandler

同时实现了1中的StreamingChatResponseHandler,则需要实现onPartialThinking(PartialThinking partialThinking)方法,调用partialThinkingHandler处理器的accept()方法

java
复制代码
/** * Handles response from a language model for AI Service that is streamed token-by-token. Handles both regular (text) * responses and responses with the request to execute one or multiple tools. */ @Internal class AiServiceStreamingResponseHandler implements StreamingChatResponseHandler { private static final Logger LOG = LoggerFactory.getLogger(AiServiceStreamingResponseHandler.class); private final ChatExecutor chatExecutor; private final AiServiceContext context; private final Object memoryId; private final GuardrailRequestParams commonGuardrailParams; private final Object methodKey; private final Consumer<String> partialResponseHandler; private final Consumer<PartialThinking> partialThinkingHandler; private final BiConsumer<Integer, ToolExecutionRequest> partialToolExecutionRequestHandler; private final BiConsumer<Integer, ToolExecutionRequest> completeToolExecutionRequestHandler; private final Consumer<ToolExecution> toolExecutionHandler; private final Consumer<ChatResponse> completeResponseHandler; private final Consumer<Throwable> errorHandler; private final ChatMemory temporaryMemory; private final TokenUsage tokenUsage; private final List<ToolSpecification> toolSpecifications; private final Map<String, ToolExecutor> toolExecutors; private final List<String> responseBuffer = new ArrayList<>(); private final boolean hasOutputGuardrails; AiServiceStreamingResponseHandler( ChatExecutor chatExecutor, AiServiceContext context, Object memoryId, Consumer<String> partialResponseHandler, Consumer<PartialThinking> partialThinkingHandler, BiConsumer<Integer, ToolExecutionRequest> partialToolExecutionRequestHandler, BiConsumer<Integer, ToolExecutionRequest> completeToolExecutionRequestHandler, Consumer<ToolExecution> toolExecutionHandler, Consumer<ChatResponse> completeResponseHandler, Consumer<Throwable> errorHandler, ChatMemory temporaryMemory, TokenUsage tokenUsage, List<ToolSpecification> toolSpecifications, Map<String, ToolExecutor> toolExecutors, GuardrailRequestParams commonGuardrailParams, Object methodKey) { this.chatExecutor = ensureNotNull(chatExecutor, "chatExecutor"); this.context = ensureNotNull(context, "context"); this.memoryId = ensureNotNull(memoryId, "memoryId"); this.methodKey = methodKey; this.partialResponseHandler = ensureNotNull(partialResponseHandler, "partialResponseHandler"); this.partialThinkingHandler = partialThinkingHandler; this.partialToolExecutionRequestHandler = partialToolExecutionRequestHandler; this.completeToolExecutionRequestHandler = completeToolExecutionRequestHandler; this.completeResponseHandler = completeResponseHandler; this.toolExecutionHandler = toolExecutionHandler; this.errorHandler = errorHandler; this.temporaryMemory = temporaryMemory; this.tokenUsage = ensureNotNull(tokenUsage, "tokenUsage"); this.commonGuardrailParams = commonGuardrailParams; this.toolSpecifications = copy(toolSpecifications); this.toolExecutors = copy(toolExecutors); this.hasOutputGuardrails = context.guardrailService().hasOutputGuardrails(methodKey); } @Override public void onPartialResponse(String partialResponse) { // If we're using output guardrails, then buffer the partial response until the guardrails have completed if (hasOutputGuardrails) { responseBuffer.add(partialResponse); } else { partialResponseHandler.accept(partialResponse); } } @Override public void onPartialThinking(PartialThinking partialThinking) { if (partialThinkingHandler != null) { partialThinkingHandler.accept(partialThinking); } } @Override public void onPartialToolExecutionRequest(int index, ToolExecutionRequest partialToolExecutionRequest) { // If we're using output guardrails, then buffer the partial response until the guardrails have completed partialToolExecutionRequestHandler.accept(index, partialToolExecutionRequest); } @Override public void onCompleteResponse(ChatResponse completeResponse) { AiMessage aiMessage = completeResponse.aiMessage(); addToMemory(aiMessage); if (aiMessage.hasToolExecutionRequests()) { for (ToolExecutionRequest toolExecutionRequest : aiMessage.toolExecutionRequests()) { String toolName = toolExecutionRequest.name(); ToolExecutor toolExecutor = toolExecutors.get(toolName); String toolExecutionResult = toolExecutor.execute(toolExecutionRequest, memoryId); ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, toolExecutionResult); addToMemory(toolExecutionResultMessage); if (toolExecutionHandler != null) { ToolExecution toolExecution = ToolExecution.builder() .request(toolExecutionRequest) .result(toolExecutionResult) .build(); toolExecutionHandler.accept(toolExecution); } } ChatRequest chatRequest = ChatRequest.builder() .messages(messagesToSend(memoryId)) .toolSpecifications(toolSpecifications) .build(); var handler = new AiServiceStreamingResponseHandler( chatExecutor, context, memoryId, partialResponseHandler, partialThinkingHandler, partialToolExecutionRequestHandler, completeToolExecutionRequestHandler, toolExecutionHandler, completeResponseHandler, errorHandler, temporaryMemory, TokenUsage.sum(tokenUsage, completeResponse.metadata().tokenUsage()), toolSpecifications, toolExecutors, commonGuardrailParams, methodKey); context.streamingChatModel.chat(chatRequest, handler); } else { if (completeResponseHandler != null) { ChatResponse finalChatResponse = ChatResponse.builder() .aiMessage(aiMessage) .metadata(completeResponse.metadata().toBuilder() .tokenUsage(tokenUsage.add( completeResponse.metadata().tokenUsage())) .build()) .build(); // Invoke output guardrails if (hasOutputGuardrails) { if (commonGuardrailParams != null) { var newCommonParams = GuardrailRequestParams.builder() .chatMemory(getMemory()) .augmentationResult(commonGuardrailParams.augmentationResult()) .userMessageTemplate(commonGuardrailParams.userMessageTemplate()) .variables(commonGuardrailParams.variables()) .build(); var outputGuardrailParams = OutputGuardrailRequest.builder() .responseFromLLM(finalChatResponse) .chatExecutor(chatExecutor) .requestParams(newCommonParams) .build(); finalChatResponse = context.guardrailService().executeGuardrails(methodKey, outputGuardrailParams); } // If we have output guardrails, we should process all of the partial responses first before // completing responseBuffer.forEach(partialResponseHandler::accept); responseBuffer.clear(); } // TODO should completeResponseHandler accept all ChatResponses that happened? completeResponseHandler.accept(finalChatResponse); } } } private ChatMemory getMemory() { return getMemory(memoryId); } private ChatMemory getMemory(Object memId) { return context.hasChatMemory() ? context.chatMemoryService.getOrCreateChatMemory(memoryId) : temporaryMemory; } private void addToMemory(ChatMessage chatMessage) { getMemory().add(chatMessage); } private List<ChatMessage> messagesToSend(Object memoryId) { return getMemory(memoryId).messages(); } @Override public void onError(Throwable error) { if (errorHandler != null) { try { errorHandler.accept(error); } catch (Exception e) { LOG.error("While handling the following error...", error); LOG.error("...the following error happened", e); } } else { LOG.warn("Ignored error", error); } } }

至此,我们就实现了接收thinking的逻辑,而接收到了thinking如何处理由外层的调用方传入的回调函数决定

三、测试

首先需要在模型配置新增是否返回思考内容

只需要在原来的处理流方法processTokenStream添加onPartialThinking即可

OpenAiStreamingChatModel中的方法debug可以发现顺利拿到了resaoningContent

控制台输出:深度思考内容以及正式对话内容

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP