fix: 兼容旧模型内联思考标签

- 在结构化 reasoning 为空时解析正文开头的 think/thinking 标签

- 将思考与正文映射为现有流式事件并覆盖 Agent 与 Bot 链路

- 补充跨分片和旁路条件测试
This commit is contained in:
2026-07-17 17:50:56 +08:00
parent 27e50a7624
commit 5a91323a2e
6 changed files with 725 additions and 47 deletions

View File

@@ -355,9 +355,10 @@ public class AgentRunService {
AtomicBoolean finished = new AtomicBoolean(false);
StringBuilder answer = new StringBuilder();
ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
LegacyThinkingTagParser legacyThinkingTagParser = new LegacyThinkingTagParser();
// 注册 emit 服务
registerEmitterCancellation(requestId, chatSseEmitter, chatContext, answer,
assistantAccumulator, finished, persistChatlog);
assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
AgentRunLock.Handle lockHandle = initialLockHandle;
try {
if (persistChatlog) {
@@ -394,10 +395,11 @@ public class AgentRunService {
owner,
lockHandle,
event -> handleRuntimeEvent(event, requestId, chatSseEmitter, answer,
assistantAccumulator, chatContext, finished, persistChatlog),
error -> handleRuntimeError(error, requestId, chatSseEmitter, chatContext, finished, persistChatlog),
() -> finishIfNeeded(requestId, chatSseEmitter, chatContext, answer,
assistantAccumulator, finished, persistChatlog)
assistantAccumulator, legacyThinkingTagParser, chatContext, finished, persistChatlog),
error -> handleRuntimeStreamError(error, requestId, chatSseEmitter, chatContext, answer,
assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog),
() -> finishRuntimeStream(requestId, chatSseEmitter, chatContext, answer,
assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)
);
agentRunRegistry.register(runContext);
lockHandle = null;
@@ -469,10 +471,11 @@ public class AgentRunService {
ChatRuntimeContext chatContext,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator,
LegacyThinkingTagParser legacyThinkingTagParser,
AtomicBoolean finished,
boolean persistChatlog) {
Runnable cancelTask = () -> cancelDisconnectedRun(requestId, chatContext, answer,
assistantAccumulator, finished, persistChatlog);
assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
SseEmitter emitter = chatSseEmitter.getEmitter();
emitter.onCompletion(cancelTask);
emitter.onTimeout(cancelTask);
@@ -483,6 +486,7 @@ public class AgentRunService {
ChatRuntimeContext chatContext,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator,
LegacyThinkingTagParser legacyThinkingTagParser,
AtomicBoolean finished,
boolean persistChatlog) {
if (!finished.compareAndSet(false, true)) {
@@ -498,6 +502,7 @@ public class AgentRunService {
}
agentRunRegistry.remove(requestId);
cancelPending(requestId, "客户端连接已断开Agent 运行已取消", persistChatlog);
appendAssistantSegments(legacyThinkingTagParser.finish(), answer, assistantAccumulator);
if (!persistChatlog) {
return;
}
@@ -517,32 +522,39 @@ public class AgentRunService {
ChatRuntimeContext chatContext,
AtomicBoolean finished,
boolean persistChatlog) {
handleRuntimeEvent(event, requestId, chatSseEmitter, answer, assistantAccumulator,
new LegacyThinkingTagParser(), chatContext, finished, persistChatlog);
}
private void handleRuntimeEvent(AgentRuntimeEvent event,
String requestId,
ChatSseEmitter chatSseEmitter,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator,
LegacyThinkingTagParser legacyThinkingTagParser,
ChatRuntimeContext chatContext,
AtomicBoolean finished,
boolean persistChatlog) {
if (event == null || event.getEventType() == null) {
return;
}
recordRuntimeEvent(requestId, chatContext, event, persistChatlog);
if (event.getEventType() == AgentRuntimeEventType.REASONING_STARTED) {
emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
legacyThinkingTagParser.reset();
return;
}
if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA) {
String text = stringPayload(event, "text");
if (text != null) {
answer.append(text);
assistantAccumulator.appendContent(text);
LOG.debug("Agent runtime message delta, requestId={}, deltaLength={}, answerLength={}, delta={}",
requestId, text.length(), answer.length(), toVisibleLogText(text));
if (!sendEnvelope(chatSseEmitter, ChatDomain.LLM, ChatType.MESSAGE, Map.of("delta", text, "role", "assistant"))) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
}
}
emitAssistantSegments(legacyThinkingTagParser.acceptContent(text), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
return;
}
if (event.getEventType() == AgentRuntimeEventType.REASONING_DELTA) {
Map<String, Object> payload = new LinkedHashMap<>();
String reasoning = firstText(stringPayload(event, "reasoning"), stringPayload(event, "text"));
assistantAccumulator.appendReasoning(reasoning);
payload.put("reasoning", reasoning);
payload.put("delta", reasoning);
if (!sendEnvelope(chatSseEmitter, ChatDomain.LLM, ChatType.THINKING, payload)) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
}
emitAssistantSegments(legacyThinkingTagParser.acceptReasoning(reasoning), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
return;
}
if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) {
@@ -550,17 +562,23 @@ public class AgentRunService {
agentRunRegistry.registerResumeToken(requestId, resumeToken);
recordApprovalRequired(requestId, chatContext, event, persistChatlog);
if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, ChatType.FORM_REQUEST, buildToolHitlPayload(requestId, event))) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
}
return;
}
if (isAsyncToolEvent(event.getEventType())) {
if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, asyncToolChatType(event), buildAsyncToolEventPayload(event))) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
}
return;
}
if (event.getEventType() == AgentRuntimeEventType.TOOL_CALL) {
if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
return;
}
LOG.info("Agent runtime tool call, requestId={}, toolCallId={}, payload={}, metadata={}",
requestId, event.getToolCallId(), event.getPayload(), event.getMetadata());
Map<String, Object> toolPayload = buildToolEventPayload(event);
@@ -571,7 +589,8 @@ public class AgentRunService {
firstNonNull(toolPayload.get("input"), toolPayload.get("toolInput"))
);
if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, ChatType.TOOL_CALL, toolPayload)) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
}
return;
}
@@ -587,15 +606,19 @@ public class AgentRunService {
toolPayload.get("text"))
);
if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, ChatType.TOOL_RESULT, toolPayload)) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
return;
}
legacyThinkingTagParser.reset();
return;
}
if (event.getEventType() == AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL) {
LOG.info("Agent runtime knowledge retrieval, requestId={}, payload={}, metadata={}",
requestId, event.getPayload(), event.getMetadata());
if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, buildKnowledgeRetrievalStatusPayload(event))) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
}
return;
}
@@ -604,7 +627,8 @@ public class AgentRunService {
LOG.info("Agent runtime memory compression, requestId={}, eventType={}, payload={}, metadata={}",
requestId, event.getEventType(), event.getPayload(), event.getMetadata());
if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, event.getPayload())) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
}
return;
}
@@ -612,7 +636,8 @@ public class AgentRunService {
LOG.info("Agent runtime suspended, requestId={}, payload={}, metadata={}",
requestId, event.getPayload(), event.getMetadata());
if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, buildSuspendedStatusPayload(event))) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
return;
}
AgentRunRegistry.AgentRunContext runContext = agentRunRegistry.get(requestId);
@@ -622,15 +647,20 @@ public class AgentRunService {
return;
}
if (event.getEventType() == AgentRuntimeEventType.COMPLETED) {
if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
return;
}
String finalText = stringPayload(event, "text");
if (finalText != null && !finalText.isBlank()) {
if (!legacyThinkingTagParser.isLegacyFormatDetected() && finalText != null && !finalText.isBlank()) {
answer.setLength(0);
answer.append(finalText);
}
List<Map<String, Object>> citations = buildKnowledgeCitationPayload(event);
if (!citations.isEmpty()) {
if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.CITATIONS, Map.of("items", citations))) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator, finished, persistChatlog);
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
return;
}
}
@@ -639,15 +669,150 @@ public class AgentRunService {
return;
}
if (event.getEventType() == AgentRuntimeEventType.CANCELLED) {
if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
return;
}
handleRuntimeCancelled(event, requestId, chatSseEmitter, chatContext, answer,
assistantAccumulator, finished, persistChatlog);
return;
}
if (event.getEventType() == AgentRuntimeEventType.FAILED) {
if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
return;
}
handleRuntimeError(new BusinessException(errorMessage(event)), requestId, chatSseEmitter, chatContext, finished, persistChatlog);
}
}
/**
* 将解析后的助手片段累计、持久化并发送到前端。
*
* @param segments 解析片段
* @param requestId 运行请求 ID
* @param chatSseEmitter SSE 发送器
* @param chatContext 聊天上下文
* @param answer 最终正文缓冲
* @param assistantAccumulator 结构化消息缓冲
* @param legacyThinkingTagParser 旧思考标签解析器
* @param finished 完成标记
* @param persistChatlog 是否持久化聊天记录
* @return 全部片段发送成功时为 {@code true}
*/
private boolean emitAssistantSegments(List<LegacyThinkingTagParser.Segment> segments,
String requestId,
ChatSseEmitter chatSseEmitter,
ChatRuntimeContext chatContext,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator,
LegacyThinkingTagParser legacyThinkingTagParser,
AtomicBoolean finished,
boolean persistChatlog) {
for (LegacyThinkingTagParser.Segment segment : segments) {
String text = segment.getText();
ChatType chatType;
Map<String, Object> payload = new LinkedHashMap<>();
if (segment.getType() == LegacyThinkingTagParser.SegmentType.REASONING) {
assistantAccumulator.appendReasoning(text);
payload.put("reasoning", text);
payload.put("delta", text);
chatType = ChatType.THINKING;
} else {
answer.append(text);
assistantAccumulator.appendContent(text);
payload.put("delta", text);
payload.put("role", "assistant");
chatType = ChatType.MESSAGE;
LOG.debug("Agent runtime message delta, requestId={}, deltaLength={}, answerLength={}, delta={}",
requestId, text.length(), answer.length(), toVisibleLogText(text));
}
if (!sendEnvelope(chatSseEmitter, ChatDomain.LLM, chatType, payload)) {
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
legacyThinkingTagParser, finished, persistChatlog);
return false;
}
}
return true;
}
/**
* 仅累计解析片段,用于连接已断开后的部分消息持久化。
*
* @param segments 解析片段
* @param answer 最终正文缓冲
* @param assistantAccumulator 结构化消息缓冲
*/
private void appendAssistantSegments(List<LegacyThinkingTagParser.Segment> segments,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator) {
for (LegacyThinkingTagParser.Segment segment : segments) {
if (segment.getType() == LegacyThinkingTagParser.SegmentType.REASONING) {
assistantAccumulator.appendReasoning(segment.getText());
} else {
answer.append(segment.getText());
assistantAccumulator.appendContent(segment.getText());
}
}
}
/**
* 在运行时自然结束但未显式发出完成事件时收口兼容解析器。
*
* @param requestId 运行请求 ID
* @param chatSseEmitter SSE 发送器
* @param chatContext 聊天上下文
* @param answer 最终正文缓冲
* @param assistantAccumulator 结构化消息缓冲
* @param legacyThinkingTagParser 旧思考标签解析器
* @param finished 完成标记
* @param persistChatlog 是否持久化聊天记录
*/
private void finishRuntimeStream(String requestId,
ChatSseEmitter chatSseEmitter,
ChatRuntimeContext chatContext,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator,
LegacyThinkingTagParser legacyThinkingTagParser,
AtomicBoolean finished,
boolean persistChatlog) {
if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
return;
}
finishIfNeeded(requestId, chatSseEmitter, chatContext, answer,
assistantAccumulator, finished, persistChatlog);
}
/**
* 在运行时异常结束前发送兼容解析器中尚未收口的片段。
*
* @param error 运行异常
* @param requestId 运行请求 ID
* @param chatSseEmitter SSE 发送器
* @param chatContext 聊天上下文
* @param answer 最终正文缓冲
* @param assistantAccumulator 结构化消息缓冲
* @param legacyThinkingTagParser 旧思考标签解析器
* @param finished 完成标记
* @param persistChatlog 是否持久化聊天记录
*/
private void handleRuntimeStreamError(Throwable error,
String requestId,
ChatSseEmitter chatSseEmitter,
ChatRuntimeContext chatContext,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator,
LegacyThinkingTagParser legacyThinkingTagParser,
AtomicBoolean finished,
boolean persistChatlog) {
if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
return;
}
handleRuntimeError(error, requestId, chatSseEmitter, chatContext, finished, persistChatlog);
}
private void finishIfNeeded(String requestId,
ChatSseEmitter chatSseEmitter,
ChatRuntimeContext chatContext,

View File

@@ -32,6 +32,7 @@ import tech.easyflow.core.runtime.ChatAssistantAccumulator;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import tech.easyflow.core.runtime.ChatRuntimeManager;
import tech.easyflow.core.runtime.ChatRuntimeMessage;
import tech.easyflow.core.runtime.LegacyThinkingTagParser;
import java.lang.reflect.Method;
import java.math.BigInteger;
@@ -176,6 +177,59 @@ public class AgentRunServiceDraftAndHitlTest {
Assert.assertEquals("正文增量", payload.get("delta"));
}
/**
* 验证旧模型写入 content 的思考标签即使跨增量拆分,也会转换为结构化思考事件。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void handleRuntimeEventShouldSplitLegacyThinkingTagsAcrossDeltas() throws Exception {
AgentRunService service = new AgentRunService();
setField(service, "agentRunRegistry", new AgentRunRegistry());
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
StringBuilder answer = new StringBuilder();
ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
LegacyThinkingTagParser parser = new LegacyThinkingTagParser();
AtomicBoolean finished = new AtomicBoolean(false);
for (String delta : List.of("<thi", "nk>先分析</thi", "nk>\n最终回答")) {
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA);
event.getPayload().put("text", delta);
invoke(service, "handleRuntimeEvent",
legacyRuntimeEventParameterTypes(),
event, "request-legacy-thinking", emitter, answer, assistantAccumulator,
parser, chatContext(), finished, false);
}
AgentRuntimeEvent completed = AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED);
completed.getPayload().put("text", "<think>先分析</think>\n最终回答");
invoke(service, "handleRuntimeEvent",
legacyRuntimeEventParameterTypes(),
completed, "request-legacy-thinking", emitter, answer, assistantAccumulator,
parser, chatContext(), finished, false);
StringBuilder reasoning = new StringBuilder();
StringBuilder content = new StringBuilder();
for (ChatEnvelope<?> envelope : emitter.envelopes) {
if (envelope.getDomain() != ChatDomain.LLM) {
continue;
}
@SuppressWarnings("unchecked")
Map<String, Object> payload = (Map<String, Object>) envelope.getPayload();
if (envelope.getType() == ChatType.THINKING) {
reasoning.append(payload.get("delta"));
} else if (envelope.getType() == ChatType.MESSAGE) {
content.append(payload.get("delta"));
}
}
Assert.assertEquals("先分析", reasoning.toString());
Assert.assertEquals("\n最终回答", content.toString());
Assert.assertEquals("\n最终回答", answer.toString());
Assert.assertTrue(emitter.envelopes.stream().anyMatch(envelope ->
envelope.getDomain() == ChatDomain.SYSTEM && envelope.getType() == ChatType.DONE));
}
/**
* 验证自动上下文压缩事件会作为业务状态发送给前端。
*
@@ -809,6 +863,12 @@ public class AgentRunServiceDraftAndHitlTest {
ChatRuntimeContext.class, AtomicBoolean.class, boolean.class};
}
private Class<?>[] legacyRuntimeEventParameterTypes() {
return new Class<?>[]{AgentRuntimeEvent.class, String.class, ChatSseEmitter.class, StringBuilder.class,
ChatAssistantAccumulator.class, LegacyThinkingTagParser.class,
ChatRuntimeContext.class, AtomicBoolean.class, boolean.class};
}
private AgentRunRegistry.AgentRunContext runContext(String requestId, String sessionId, boolean persistChatlog) {
return new AgentRunRegistry.AgentRunContext(
requestId,

View File

@@ -23,6 +23,7 @@ import tech.easyflow.core.runtime.ChatRuntimeContext;
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
import tech.easyflow.core.runtime.ChatRuntimeManager;
import tech.easyflow.core.runtime.ChatRuntimeMessage;
import tech.easyflow.core.runtime.LegacyThinkingTagParser;
import java.math.BigInteger;
import java.util.Date;
@@ -43,6 +44,7 @@ public class ChatStreamListener implements StreamResponseListener {
private final ChatRuntimeManager chatRuntimeManager;
private final ChatRuntimeContext runtimeContext;
private final ChatAssistantAccumulator assistantAccumulator;
private final LegacyThinkingTagParser legacyThinkingTagParser = new LegacyThinkingTagParser();
// 核心标记是否允许执行onStop业务逻辑仅最后一次无后续工具调用时为true
private boolean canStop = true;
// 辅助标记:是否进入过工具调用(避免重复递归判断)
@@ -65,6 +67,7 @@ public class ChatStreamListener implements StreamResponseListener {
@Override
public void onStart(StreamContext context) {
legacyThinkingTagParser.reset();
StreamResponseListener.super.onStart(context);
}
@@ -80,6 +83,7 @@ public class ChatStreamListener implements StreamResponseListener {
return;
}
if (aiMessage.isFinalDelta() && aiMessageResponse.hasToolCalls()) {
flushLegacyThinkingSegments();
this.canStop = false; // 工具调用期间禁止执行onStop
this.hasToolCall = true; // 标记已进入过工具调用
List<ToolCall> toolCalls = aiMessage.getToolCalls();
@@ -103,20 +107,7 @@ public class ChatStreamListener implements StreamResponseListener {
if (this.hasToolCall) {
this.canStop = true;
}
String reasoningContent = aiMessage.getReasoningContent();
if (reasoningContent != null && !reasoningContent.isEmpty()) {
assistantAccumulator.appendReasoning(reasoningContent);
chatRuntimeManager.recordAssistantDelta(runtimeContext, buildAssistantDeltaMessage(reasoningContent, ChatType.THINKING));
sendChatEnvelope(sseEmitter, reasoningContent, ChatType.THINKING);
} else {
String delta = aiMessage.getContent();
if (delta != null && !delta.isEmpty()) {
assistantAccumulator.appendContent(delta);
chatRuntimeManager.recordAssistantDelta(runtimeContext, buildAssistantDeltaMessage(delta, ChatType.MESSAGE));
sendChatEnvelope(sseEmitter, delta, ChatType.MESSAGE);
}
}
handleAssistantDelta(aiMessage);
}
} catch (Exception e) {
LOG.error("Chat stream onMessage failed, conversationId={}, message={}, exception={}",
@@ -137,6 +128,7 @@ public class ChatStreamListener implements StreamResponseListener {
sendSystemError(sseEmitter, context.getThrowable().getMessage(), context.getThrowable());
return;
}
flushLegacyThinkingSegments();
memoryPrompt.addMessage(context.getFullMessage());
chatRuntimeManager.recordAssistantCompleted(runtimeContext, buildAssistantCompletedMessage(context));
chatRuntimeManager.recordCompleted(runtimeContext);
@@ -186,6 +178,49 @@ public class ChatStreamListener implements StreamResponseListener {
}
}
/**
* 归一化并发送单个模型增量。
*
* @param aiMessage 模型增量消息
*/
private void handleAssistantDelta(AiMessage aiMessage) {
String reasoningContent = aiMessage.getReasoningContent();
if (StringUtil.hasText(reasoningContent)) {
emitAssistantSegments(legacyThinkingTagParser.acceptReasoning(reasoningContent));
return;
}
emitAssistantSegments(legacyThinkingTagParser.acceptContent(aiMessage.getContent()));
}
/**
* 发送兼容解析后的思考与正文片段。
*
* @param segments 解析片段
*/
private void emitAssistantSegments(List<LegacyThinkingTagParser.Segment> segments) {
for (LegacyThinkingTagParser.Segment segment : segments) {
String text = segment.getText();
if (segment.getType() == LegacyThinkingTagParser.SegmentType.REASONING) {
assistantAccumulator.appendReasoning(text);
chatRuntimeManager.recordAssistantDelta(runtimeContext,
buildAssistantDeltaMessage(text, ChatType.THINKING));
sendChatEnvelope(sseEmitter, text, ChatType.THINKING);
continue;
}
assistantAccumulator.appendContent(text);
chatRuntimeManager.recordAssistantDelta(runtimeContext,
buildAssistantDeltaMessage(text, ChatType.MESSAGE));
sendChatEnvelope(sseEmitter, text, ChatType.MESSAGE);
}
}
/**
* 收口并发送旧标签解析器仍保留的少量前缀。
*/
private void flushLegacyThinkingSegments() {
emitAssistantSegments(legacyThinkingTagParser.finish());
}
private void sendToolCallEnvelope(ToolCall toolCall) {
if (toolCall == null) {
return;
@@ -330,7 +365,10 @@ public class ChatStreamListener implements StreamResponseListener {
message.setRole("assistant");
message.setContentType("TEXT");
String fullContent = context != null && context.getFullMessage() != null ? context.getFullMessage().getContent() : null;
message.setContentText(StringUtil.hasText(fullContent) ? fullContent : assistantAccumulator.getContent());
String normalizedContent = assistantAccumulator.getContent();
message.setContentText(legacyThinkingTagParser.isLegacyFormatDetected()
? normalizedContent
: (StringUtil.hasText(fullContent) ? fullContent : normalizedContent));
message.setContentPayload(assistantAccumulator.buildPayload(message.getContentText()));
message.setCreatedAt(new Date());
message.setSenderId(runtimeContext.getAssistantId());