fix: 统一适配 DeepSeek 工具与思考历史回传
- 结构化持久化 assistant/tool 历史,恢复真实消息链回放 - 为 DeepSeek 显式开启 thinking 协议配置,并补齐 public-api 与工具英文名兜底 - 增加聊天历史回放、tool 名称与请求参数解析相关测试
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user