Solon v3.3.3

chat - 支持哪些模型?及方言定制

</> markdown

1、支持哪些聊天模型?

支持聊天模型,其实是支持接口风格。比如 DeepSeek-V3 官网的接口兼容 openai;在 ollama 平台是另一种接口风格;在阿里百炼则有两种接口风格,一种兼容 openai,另一种则是百炼专属风格;在模力方舟(ai.gitee)则是兼容 openai。

聊天模型的这种接口风格,称为聊天方言(简称,方言)。ChatConfig 通过 provider 标识模型服务是由谁提供的。并自动选择对应的聊天方言适配。

框架内置的方言适配有:

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

那支持哪些聊天模型?

  • 所有兼容 openai 的平台服务(比如“模力方舟”、“硅基流动”、“Xinference”、“火山引擎”、“智谱”、“讯飞火星”、“百度千帆”、“阿里百炼” 等),都兼容
  • 所有 ollama 平台上的模型,都兼容
  • 所有 阿里百炼 平台上的模型(同时提供有 “百炼” 和 “openai” 两套接口),都兼容

构建示例:

ChatModel chatModel = ChatModel.of("http://127.0.0.1:11434/api/chat")
                .provider("ollama")
                .model("llama3.2")
                .headerSet("x-demo", "demo1")
                .build();

2、聊天方言接口定义

public interface ChatDialect extends AiModelDialect {
    //是否为默认
    default boolean isDefault() {
        return false;
    }

    //匹配检测
    boolean matched(ChatConfig config);

    //构建请求数据
    String buildRequestJson(ChatConfig config, ChatOptions options, List<ChatMessage> messages, boolean isStream);

    //构建助理消息节点
    ONode buildAssistantMessageNode(Map<Integer, ToolCallBuilder> toolCallBuilders);

    //构建助理消息根据直接返回的工具消息
    AssistantMessage buildAssistantMessageByToolMessages(List<ToolMessage> toolMessages);

    //分析响应数据
    boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String respJson);

    //分析工具调用
    List<AssistantMessage> parseAssistantMessage(ChatResponseDefault resp, ONode oMessage);
}

3、OllamaChatDialect 定制参考

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

ChatDialectManager.register(new OllamaChatDialect());

方言定制参考:

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

    public static OllamaChatDialect getInstance() {
        return instance;
    }
    
    @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;
    }

    @Override
    protected ToolCall parseToolCall(ONode n1) {
        int index = -1; //n1.get("index").getInt();它是没有值的
        String callId = n1.get("id").getString();

        ONode n1f = n1.get("function");
        String name = n1f.get("name").getString();
        ONode n1fArgs = n1f.get("arguments");
        String argStr = n1fArgs.getString();

        index = name.hashCode();

        if (n1fArgs.isValue()) {
            //有可能是 json string
            n1fArgs = ONode.loadStr(argStr);
        }

        Map<String, Object> argMap = null;
        if (n1fArgs.isObject()) {
            argMap = n1fArgs.toObject(Map.class);
        }
        return new ToolCall(index, callId, name, argStr, argMap);
    }
}