Solon v3.2.1

chat - 聊天模型接口方言及定制

</> markdown

框架已内置 OllamaChatDialect、DashscopeChatDialect、OpenaiChatDialect(默认) 三种方言(基本够用),自动支持 Ollama 提供的模型接口、Dashscope 提供的模型接口及 Openai 规范的模型接口。

也可以通过定制,实现更多的模型兼容。

1、方言接口

public interface ChatDialect extends AiModelDialect {
    /**
     * 匹配检测
     *
     * @param config 聊天配置
     */
    boolean matched(ChatConfig config);

    /**
     * 构建请求数据
     *
     * @param config   聊天配置
     * @param options  聊天选项
     * @param messages 消息
     * @param isStream 是否流式获取
     */
    String buildRequestJson(ChatConfig config, ChatOptions options, List<ChatMessage> messages, boolean isStream);

    /**
     * 构建助理消息节点
     *
     * @param toolCallBuilders 工具调用构建器集合
     */
    ONode buildAssistantMessageNode(Map<Integer, ToolCallBuilder> toolCallBuilders);

    /**
     * 分析响应数据
     *
     * @param config   聊天配置
     * @param resp     响应体
     * @param respJson 响应数据
     */
    boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String respJson);

    /**
     * 分析工具调用
     *
     * @param resp     响应体
     * @param oMessage 消息节点
     */
    List<AssistantMessage> parseAssistantMessage(ChatResponseDefault resp, ONode oMessage);
}

2、内置的方言适配

言方配置要求描述
openai 兼容 openai 的接口规范(默认)
ollamaprovider=ollama兼容 ollama 的接口规范
dashscopeprovider=dashscope兼容 dashscope (阿里云的平台百炼)的接口规范

配置示例:

solon.ai.chat:
  demo:
    apiUrl: "http://127.0.0.1:11434/api/chat" # 使用完整地址(而不是 api_base)
    provider: "ollama" # 使用 ollama 服务时,需要配置 provider
    model: "llama3.2"

3、OllamaChatDialect 定制参考

如果方言有组件注解,会自动注册。否则,需要手动注册:

ChatDialectManager.register(new OllamaChatDialect());

方言定制:

//@Component
public class OllamaChatDialect extends AbstractChatDialect {
    private static OllamaChatDialect instance = new OllamaChatDialect();

    public static OllamaChatDialect getInstance() {
        return instance;
    }

    /**
     * 匹配检测
     *
     * @param config 聊天配置
     */
    @Override
    public boolean matched(ChatConfig config) {
        return "ollama".equals(config.getProvider());
    }

    @Override
    protected void buildChatMessageNodeDo(ONode oNode, UserMessage msg) {
        oNode.set("role", msg.getRole().name().toLowerCase());
        if (Utils.isEmpty(msg.getMedias())) {
            oNode.set("content", msg.getContent());
        } else {
            oNode.set("content", msg.getContent());

            AiMedia demo = msg.getMedias().get(0);
            if (demo instanceof Image) {
                oNode.set("images", msg.getMedias().stream().map(i -> i.toDataString(false)).collect(Collectors.toList()));
            } else if (demo instanceof Audio) {
                oNode.set("audios", msg.getMedias().stream().map(i -> i.toDataString(false)).collect(Collectors.toList()));
            } else if (demo instanceof Video) {
                oNode.set("videos", msg.getMedias().stream().map(i -> i.toDataString(false)).collect(Collectors.toList()));
            }
        }
    }

    @Override
    public ONode buildAssistantMessageNode(Map<Integer, ToolCallBuilder> toolCallBuilders) {
        ONode oNode = new ONode();
        oNode.set("role", "assistant");
        oNode.set("content", "");
        oNode.getOrNew("tool_calls").asArray().build(n1 -> {
            for (Map.Entry<Integer, ToolCallBuilder> kv : toolCallBuilders.entrySet()) {
                //有可能没有
                n1.addNew().set("id", kv.getValue().idBuilder.toString())
                        .set("type", "function")
                        .getOrNew("function").build(n2 -> {
                            n2.set("name", kv.getValue().nameBuilder.toString());
                            n2.set("arguments", ONode.loadStr(kv.getValue().argumentsBuilder.toString()));
                        });
            }
        });

        return oNode;
    }

    @Override
    public boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String json) {
        //解析
        ONode oResp = ONode.load(json);

        if (oResp.isObject() == false) {
            return false;
        }

        if (oResp.contains("error")) {
            resp.setError(new ChatException(oResp.get("error").getString()));
        } else {
            resp.setModel(oResp.get("model").getString());
            resp.setFinished(oResp.get("done").getBoolean());
            String done_reason = oResp.get("done_reason").getString();

            String createdStr = oResp.get("created_at").getString();
            if (createdStr != null) {
                createdStr = createdStr.substring(0, createdStr.indexOf(".") + 4);
            }
            Date created = DateUtil.parseTry(createdStr);
            List<AssistantMessage> messageList = parseAssistantMessage(resp, oResp.get("message"));
            for (AssistantMessage msg1 : messageList) {
                resp.addChoice(new ChatChoice(0, created, done_reason, msg1));
            }

            if (resp.isFinished()) {
                long promptTokens = oResp.get("prompt_eval_count").getLong();
                long completionTokens = oResp.get("eval_count").getLong();
                long totalTokens = promptTokens + completionTokens;

                resp.setUsage(new AiUsage(promptTokens, completionTokens, totalTokens));
            }
        }

        return true;
    }
}