1、支持哪些聊天模型?
支持聊天模型,其实是支持接口风格。比如 DeepSeek-V3 官网的接口兼容 openai;在 ollama 平台是另一种接口风格;在阿里百炼则有两种接口风格,一种兼容 openai,另一种则是百炼专属风格;在模力方舟(ai.gitee)则是兼容 openai。
聊天模型的这种接口风格,称为聊天方言(简称,方言)。ChatConfig 通过 provider 或 apiUrl识别模型服务是由谁提供的。并自动选择对应的聊天方言适配。
框架内置的方言适配有:
| 言方 | 配置要求 | 描述 |
|---|---|---|
| openai | 兼容 openai 的接口规范(默认) | |
| ollama | provider=ollama |
兼容 ollama 的接口规范 |
| dashscope | provider=dashscope |
兼容 dashscope (阿里云的平台百炼)的接口规范(v3.5.1 后基于 apiUrl 自动识别,不要配置 provider) |
那支持哪些聊天模型?
- 所有兼容 openai 的平台服务(比如“模力方舟”、“硅基流动”、“魔搭社区(魔力空间)”、“Xinference”、“火山引擎”、“智谱”、“讯飞火星”、“百度千帆”、“阿里百炼” 等),都兼容
- 所有 ollama 平台上的模型,都兼容
- 所有 阿里百炼 平台上的模型(同时提供有 “百炼” 和 “openai” 两套接口),都兼容
构建示例:
ChatModel chatModel = ChatModel.of("http://127.0.0.1:11434/api/chat") //使用完整地址(而不是 api_base) .provider("ollama") .model("llama3.2") .headerSet("x-demo", "demo1") .build();
2、自带的方言依赖包
| 方言依赖包 | 描述 |
|---|---|
| org.noear:solon-ai | 包含 solon-ai-core 和下面所有的方言包。一般引用这个 |
| org.noear:solon-ai-dialect-openai | 兼容 openai 的方言包 |
| org.noear:solon-ai-dialect-ollama | 兼容 ollama 的方言包 |
| org.noear:solon-ai-dialect-dashscope | 兼容 dashscope 的方言包 |
提醒:一般匹配不到方言时?要么是 provider 配置有问题,要么是 pom 缺少相关的依赖包。
3、聊天方言接口定义
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());
方言定制参考:
import org.noear.snack4.ONode; import org.noear.solon.Utils; import org.noear.solon.ai.AiMedia; import org.noear.solon.ai.AiUsage; import org.noear.solon.ai.media.Audio; import org.noear.solon.ai.chat.ChatChoice; import org.noear.solon.ai.chat.ChatConfig; import org.noear.solon.ai.chat.ChatException; import org.noear.solon.ai.chat.ChatResponseDefault; import org.noear.solon.ai.chat.dialect.AbstractChatDialect; import org.noear.solon.ai.chat.message.AssistantMessage; import org.noear.solon.ai.chat.message.UserMessage; import org.noear.solon.ai.chat.tool.ToolCall; import org.noear.solon.ai.chat.tool.ToolCallBuilder; import org.noear.solon.ai.media.Image; import org.noear.solon.ai.media.Video; import org.noear.solon.core.util.DateUtil; import java.util.Date; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Ollama 聊天模型方言 * * @author noear * @since 3.1 */ //@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().then(n1 -> { for (Map.Entry<Integer, ToolCallBuilder> kv : toolCallBuilders.entrySet()) { //有可能没有 n1.addNew().set("id", kv.getValue().idBuilder.toString()) .set("type", "function") .getOrNew("function").then(n2 -> { n2.set("name", kv.getValue().nameBuilder.toString()); n2.set("arguments", ONode.ofJson(kv.getValue().argumentsBuilder.toString())); }); } }); return oNode; } @Override public boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String json) { //解析 ONode oResp = ONode.ofJson(json); if (oResp.isObject() == false) { return false; } if (oResp.hasKey("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, oResp)); if(resp.hasChoices() == false) { resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage(""))); } } } 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.ofJson(argStr); } Map<String, Object> argMap = null; if (n1fArgs.isObject()) { argMap = n1fArgs.toBean(Map.class); } return new ToolCall(index, callId, name, argStr, argMap); } }