fix: 统一适配 DeepSeek 工具与思考历史回传

- 结构化持久化 assistant/tool 历史,恢复真实消息链回放

- 为 DeepSeek 显式开启 thinking 协议配置,并补齐 public-api 与工具英文名兜底

- 增加聊天历史回放、tool 名称与请求参数解析相关测试
This commit is contained in:
2026-05-11 21:24:20 +08:00
parent c1590b0d8a
commit e27834ee0c
18 changed files with 1441 additions and 42 deletions

View File

@@ -5,55 +5,136 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 负责聚合单轮聊天中的 assistant thinking、tool 调用和最终回答,
* 并产出可用于历史回放的结构化 payload。
*/
public class ChatAssistantAccumulator {
private final StringBuilder content = new StringBuilder();
private final StringBuilder reasoning = new StringBuilder();
private final StringBuilder displayReasoning = new StringBuilder();
private final List<Map<String, Object>> chains = new ArrayList<>();
private final List<Map<String, Object>> messageChain = new ArrayList<>();
private final List<Map<String, Object>> toolMessages = new ArrayList<>();
private Map<String, Object> latestToolCallAssistant;
private boolean toolCallBatchOpen;
/**
* 追加当前 assistant 片段的文本内容。
*
* @param delta 内容增量
*/
public void appendContent(String delta) {
if (delta != null && !delta.isEmpty()) {
content.append(delta);
}
}
/**
* 追加当前 assistant 片段的 reasoning 内容。
*
* @param delta reasoning 增量
*/
public void appendReasoning(String delta) {
if (delta != null && !delta.isEmpty()) {
reasoning.append(delta);
displayReasoning.append(delta);
}
}
/**
* 记录 tool call同时把当前 assistant 片段固化为一条结构化 assistant 消息。
*
* @param id tool call id
* @param name tool 名称
* @param arguments tool 参数
*/
public void appendToolCall(String id, String name, Object arguments) {
Map<String, Object> chain = findToolChain(id, name);
chain.put("status", "TOOL_CALL");
chain.put("result", arguments);
chain.put("arguments", arguments);
Map<String, Object> assistantMessage = ensureToolCallAssistantMessage();
@SuppressWarnings("unchecked")
List<Map<String, Object>> toolCalls = (List<Map<String, Object>>) assistantMessage.computeIfAbsent("toolCalls",
key -> new ArrayList<Map<String, Object>>());
Map<String, Object> toolCall = new LinkedHashMap<>();
toolCall.put("id", id);
toolCall.put("name", name);
toolCall.put("arguments", arguments == null ? null : String.valueOf(arguments));
toolCalls.add(toolCall);
}
/**
* 记录 tool result并附加到结构化消息链中。
*
* @param id tool call id
* @param name tool 名称
* @param result tool 结果
*/
public void appendToolResult(String id, String name, Object result) {
Map<String, Object> chain = findToolChain(id, name);
chain.put("status", "TOOL_RESULT");
chain.put("result", result);
Map<String, Object> toolMessage = ChatRuntimeHistoryPayloadHelper.toolMessage(
id,
result == null ? null : String.valueOf(result)
);
toolMessages.add(toolMessage);
messageChain.add(ChatRuntimeHistoryPayloadHelper.deepCopyMap(toolMessage));
toolCallBatchOpen = false;
}
/**
* 获取当前 assistant 片段的文本内容。
*
* @return 文本内容
*/
public String getContent() {
return content.toString();
}
public Map<String, Object> buildPayload() {
Map<String, Object> payload = new LinkedHashMap<>();
/**
* 获取最近一次 tool-call assistant 的 reasoning 内容,供实时内存消息回写复用。
*
* @return reasoning 内容
*/
public String getLatestToolCallReasoning() {
return latestToolCallAssistant == null ? null : stringValue(latestToolCallAssistant.get("reasoningContent"));
}
/**
* 获取最近一次 tool-call assistant 的内容,供实时内存消息回写复用。
*
* @return 内容
*/
public String getLatestToolCallContent() {
return latestToolCallAssistant == null ? null : stringValue(latestToolCallAssistant.get("content"));
}
/**
* 产出结构化 payload。
*
* @param finalContent 最终 assistant 文本
* @return payload
*/
public Map<String, Object> buildPayload(String finalContent) {
List<Map<String, Object>> payloadChains = new ArrayList<>();
if (reasoning.length() > 0) {
if (displayReasoning.length() > 0) {
Map<String, Object> think = new LinkedHashMap<>();
think.put("reasoning_content", reasoning.toString());
think.put("reasoning_content", displayReasoning.toString());
think.put("thinkingStatus", "end");
think.put("thinlCollapse", Boolean.TRUE);
payloadChains.add(think);
}
payloadChains.addAll(chains);
if (!payloadChains.isEmpty()) {
payload.put("chains", payloadChains);
List<Map<String, Object>> payloadMessageChain = ChatRuntimeHistoryPayloadHelper.deepCopyList(messageChain);
Map<String, Object> finalAssistantMessage = buildFinalAssistantMessage(finalContent);
if (!finalAssistantMessage.isEmpty()) {
payloadMessageChain.add(finalAssistantMessage);
}
return payload;
return ChatRuntimeHistoryPayloadHelper.buildPayload(payloadMessageChain, toolMessages, payloadChains);
}
private Map<String, Object> findToolChain(String id, String name) {
@@ -71,4 +152,43 @@ public class ChatAssistantAccumulator {
chains.add(chain);
return chain;
}
private Map<String, Object> ensureToolCallAssistantMessage() {
if (toolCallBatchOpen && latestToolCallAssistant != null && !hasPendingAssistantContent()) {
return latestToolCallAssistant;
}
latestToolCallAssistant = ChatRuntimeHistoryPayloadHelper.assistantMessage(
content.length() == 0 ? null : content.toString(),
reasoning.length() == 0 ? null : reasoning.toString(),
null
);
messageChain.add(latestToolCallAssistant);
content.setLength(0);
reasoning.setLength(0);
toolCallBatchOpen = true;
return latestToolCallAssistant;
}
private Map<String, Object> buildFinalAssistantMessage(String finalContent) {
String assistantContent = finalContent;
if ((assistantContent == null || assistantContent.isEmpty()) && content.length() > 0) {
assistantContent = content.toString();
}
if ((assistantContent == null || assistantContent.isEmpty()) && reasoning.length() == 0) {
return new LinkedHashMap<>();
}
return ChatRuntimeHistoryPayloadHelper.assistantMessage(
assistantContent,
reasoning.length() == 0 ? null : reasoning.toString(),
null
);
}
private boolean hasPendingAssistantContent() {
return content.length() > 0 || reasoning.length() > 0;
}
private String stringValue(Object value) {
return value == null ? null : String.valueOf(value);
}
}

View File

@@ -0,0 +1,272 @@
package tech.easyflow.core.runtime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 聊天运行时历史 payload 的统一读写工具。
* <p>
* 该工具只处理 {@link Map} / {@link List} 结构,避免把 easy-agents 类型泄漏到通用协议模块。
*/
public final class ChatRuntimeHistoryPayloadHelper {
public static final String KEY_MESSAGE_CHAIN = "messageChain";
public static final String KEY_ASSISTANT_MESSAGE = "assistantMessage";
public static final String KEY_FINAL_ASSISTANT_MESSAGE = "finalAssistantMessage";
public static final String KEY_TOOL_MESSAGES = "toolMessages";
public static final String KEY_DISPLAY_CHAINS = "displayChains";
public static final String KEY_CHAINS = "chains";
private ChatRuntimeHistoryPayloadHelper() {
}
/**
* 构造 assistant 历史消息结构。
*
* @param content assistant 内容
* @param reasoningContent assistant reasoning 内容
* @param toolCalls assistant tool calls
* @return assistant 历史消息结构
*/
public static Map<String, Object> assistantMessage(String content,
String reasoningContent,
List<Map<String, Object>> toolCalls) {
Map<String, Object> message = new LinkedHashMap<>();
message.put("role", "assistant");
if (content != null) {
message.put("content", content);
}
if (reasoningContent != null && !reasoningContent.isEmpty()) {
message.put("reasoningContent", reasoningContent);
}
if (toolCalls != null && !toolCalls.isEmpty()) {
message.put("toolCalls", deepCopyList(toolCalls));
}
return message;
}
/**
* 构造 tool 历史消息结构。
*
* @param toolCallId tool call id
* @param content tool 结果内容
* @return tool 历史消息结构
*/
public static Map<String, Object> toolMessage(String toolCallId, String content) {
Map<String, Object> message = new LinkedHashMap<>();
message.put("role", "tool");
message.put("toolCallId", toolCallId);
if (content != null) {
message.put("content", content);
}
return message;
}
/**
* 构造统一的历史 payload。
*
* @param messageChain 完整 assistant/tool 历史链
* @param toolMessages tool 历史列表
* @param displayChains 前端展示链
* @return payload
*/
public static Map<String, Object> buildPayload(List<Map<String, Object>> messageChain,
List<Map<String, Object>> toolMessages,
List<Map<String, Object>> displayChains) {
Map<String, Object> payload = new LinkedHashMap<>();
List<Map<String, Object>> safeMessageChain = deepCopyList(messageChain);
List<Map<String, Object>> safeToolMessages = deepCopyList(toolMessages);
List<Map<String, Object>> safeDisplayChains = deepCopyList(displayChains);
if (!safeMessageChain.isEmpty()) {
payload.put(KEY_MESSAGE_CHAIN, safeMessageChain);
Map<String, Object> firstAssistant = findAssistant(safeMessageChain, false);
if (!firstAssistant.isEmpty()) {
payload.put(KEY_ASSISTANT_MESSAGE, firstAssistant);
}
Map<String, Object> lastAssistant = findAssistant(safeMessageChain, true);
if (!lastAssistant.isEmpty() && !lastAssistant.equals(firstAssistant)) {
payload.put(KEY_FINAL_ASSISTANT_MESSAGE, lastAssistant);
}
}
if (!safeToolMessages.isEmpty()) {
payload.put(KEY_TOOL_MESSAGES, safeToolMessages);
}
if (!safeDisplayChains.isEmpty()) {
payload.put(KEY_DISPLAY_CHAINS, safeDisplayChains);
payload.put(KEY_CHAINS, deepCopyList(safeDisplayChains));
}
return payload;
}
/**
* 读取结构化消息链。
*
* @param payload contentPayload
* @return 结构化消息链
*/
public static List<Map<String, Object>> getMessageChain(Map<String, Object> payload) {
return getMapList(payload == null ? null : payload.get(KEY_MESSAGE_CHAIN));
}
/**
* 读取 assistant 历史消息。
*
* @param payload contentPayload
* @return assistant 历史消息
*/
public static Map<String, Object> getAssistantMessage(Map<String, Object> payload) {
return getMap(payload == null ? null : payload.get(KEY_ASSISTANT_MESSAGE));
}
/**
* 读取最终 assistant 历史消息。
*
* @param payload contentPayload
* @return 最终 assistant 历史消息
*/
public static Map<String, Object> getFinalAssistantMessage(Map<String, Object> payload) {
return getMap(payload == null ? null : payload.get(KEY_FINAL_ASSISTANT_MESSAGE));
}
/**
* 读取 tool 历史消息列表。
*
* @param payload contentPayload
* @return tool 历史消息列表
*/
public static List<Map<String, Object>> getToolMessages(Map<String, Object> payload) {
return getMapList(payload == null ? null : payload.get(KEY_TOOL_MESSAGES));
}
/**
* 判断 payload 是否已经包含新结构化历史。
*
* @param payload contentPayload
* @return true 表示包含新结构
*/
public static boolean hasStructuredHistory(Map<String, Object> payload) {
return !getMessageChain(payload).isEmpty()
|| !getAssistantMessage(payload).isEmpty()
|| !getToolMessages(payload).isEmpty();
}
/**
* 读取对象为 Map。
*
* @param value 值
* @return Map 视图
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> getMap(Object value) {
if (!(value instanceof Map<?, ?> source)) {
return Collections.emptyMap();
}
Map<String, Object> result = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : source.entrySet()) {
if (entry.getKey() != null) {
result.put(String.valueOf(entry.getKey()), entry.getValue());
}
}
return result;
}
/**
* 读取对象为 Map 列表。
*
* @param value 值
* @return Map 列表
*/
public static List<Map<String, Object>> getMapList(Object value) {
if (!(value instanceof List<?> source)) {
return Collections.emptyList();
}
List<Map<String, Object>> result = new ArrayList<>(source.size());
for (Object item : source) {
Map<String, Object> map = getMap(item);
if (!map.isEmpty()) {
result.add(map);
}
}
return result;
}
/**
* 深拷贝 Map 列表。
*
* @param source 原始列表
* @return 深拷贝后的列表
*/
public static List<Map<String, Object>> deepCopyList(List<Map<String, Object>> source) {
if (source == null || source.isEmpty()) {
return new ArrayList<>();
}
List<Map<String, Object>> copy = new ArrayList<>(source.size());
for (Map<String, Object> item : source) {
copy.add(deepCopyMap(item));
}
return copy;
}
/**
* 深拷贝 Map。
*
* @param source 原始 Map
* @return 深拷贝后的 Map
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> deepCopyMap(Map<String, Object> source) {
if (source == null || source.isEmpty()) {
return new LinkedHashMap<>();
}
Map<String, Object> copy = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : source.entrySet()) {
Object value = entry.getValue();
if (value instanceof Map<?, ?> mapValue) {
copy.put(entry.getKey(), deepCopyMap((Map<String, Object>) mapValue));
} else if (value instanceof List<?> listValue) {
copy.put(entry.getKey(), deepCopyValueList((List<Object>) listValue));
} else {
copy.put(entry.getKey(), value);
}
}
return copy;
}
private static List<Object> deepCopyValueList(List<Object> source) {
List<Object> copy = new ArrayList<>(source.size());
for (Object item : source) {
if (item instanceof Map<?, ?> mapItem) {
copy.add(deepCopyMap((Map<String, Object>) mapItem));
} else if (item instanceof List<?> listItem) {
copy.add(deepCopyValueList((List<Object>) listItem));
} else {
copy.add(item);
}
}
return copy;
}
private static Map<String, Object> findAssistant(List<Map<String, Object>> messageChain, boolean reverse) {
if (messageChain == null || messageChain.isEmpty()) {
return Collections.emptyMap();
}
if (reverse) {
for (int i = messageChain.size() - 1; i >= 0; i--) {
Map<String, Object> item = messageChain.get(i);
if ("assistant".equalsIgnoreCase(String.valueOf(item.get("role")))) {
return deepCopyMap(item);
}
}
return Collections.emptyMap();
}
for (Map<String, Object> item : messageChain) {
if ("assistant".equalsIgnoreCase(String.valueOf(item.get("role")))) {
return deepCopyMap(item);
}
}
return Collections.emptyMap();
}
}