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

@@ -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,