初始化

This commit is contained in:
2026-02-22 18:55:40 +08:00
commit 8392cdd861
496 changed files with 45020 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.easyagents.llm.qwen;
import com.easyagents.core.model.chat.ChatConfig;
public class QwenChatConfig extends ChatConfig {
private static final String DEFAULT_MODEL = "qwen-turbo";
private static final String DEFAULT_ENDPOINT = "https://dashscope.aliyuncs.com";
private static final String DEFAULT_REQUEST_PATH = "/compatible-mode/v1/chat/completions";
public QwenChatConfig() {
setEndpoint(DEFAULT_ENDPOINT);
setRequestPath(DEFAULT_REQUEST_PATH);
setModel(DEFAULT_MODEL);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.easyagents.llm.qwen;
import com.easyagents.core.model.chat.OpenAICompatibleChatModel;
import com.easyagents.core.model.chat.ChatInterceptor;
import com.easyagents.core.model.chat.GlobalChatInterceptors;
import com.easyagents.core.model.client.ChatRequestSpecBuilder;
import java.util.List;
public class QwenChatModel extends OpenAICompatibleChatModel<QwenChatConfig> {
/**
* 构造一个聊天模型实例,不使用实例级拦截器。
*
* @param config 聊天模型配置
*/
public QwenChatModel(QwenChatConfig config) {
super(config);
}
/**
* 构造一个聊天模型实例,并指定实例级拦截器。
* <p>
* 实例级拦截器会与全局拦截器(通过 {@link GlobalChatInterceptors} 注册)合并,
* 执行顺序为:可观测性拦截器 → 全局拦截器 → 实例拦截器。
*
* @param config 聊天模型配置
* @param userInterceptors 实例级拦截器列表
*/
public QwenChatModel(QwenChatConfig config, List<ChatInterceptor> userInterceptors) {
super(config, userInterceptors);
}
@Override
public ChatRequestSpecBuilder getChatRequestSpecBuilder() {
return new QwenRequestSpecBuilder();
}
}

View File

@@ -0,0 +1,303 @@
package com.easyagents.llm.qwen;
import com.easyagents.core.model.chat.ChatOptions;
import com.alibaba.fastjson2.annotation.JSONField;
import java.util.List;
/**
* <link href="https://help.aliyun.com/zh/model-studio/use-qwen-by-calling-api">通义千问API参考</link>
*
* @author liutf
*/
public class QwenChatOptions extends ChatOptions {
/**
* 输出数据的模态,仅支持 Qwen-Omni 模型指定。(可选)
* 默认值为["text"]
* 可选值:
* ["text"]:输出文本。
*/
private List<String> modalities;
/**
* 控制模型生成文本时的内容重复度。(可选)
* 取值范围:[-2.0, 2.0]。正数会减少重复度,负数会增加重复度。
* <pre>
* 适用场景:
* 较高的presence_penalty适用于要求多样性、趣味性或创造性的场景如创意写作或头脑风暴。
* 较低的presence_penalty适用于要求一致性或专业术语的场景如技术文档或其他正式文档。
* </pre>
* 不建议修改QVQ模型的默认presence_penalty值。
*/
private Float presencePenalty;
/**
* 生成响应的个数取值范围是1-4。
* 对于需要生成多个响应的场景(如创意写作、广告文案等),可以设置较大的 n 值。
* <pre>
* 当前仅支持 qwen-plus 模型,且在传入 tools 参数时固定为1。
* 设置较大的 n 值不会增加输入 Token 消耗,会增加输出 Token 的消耗。
* </pre>
*/
private Integer n;
/**
* 是否开启并行工具调用。
* 参数为true时开启为false时不开启。
* 并行工具调用请参见https://help.aliyun.com/zh/model-studio/qwen-function-calling#cb6b5c484bt4x
*/
private Boolean parallelToolCalls;
/**
* 当您使用翻译模型时需要配置的翻译参数。
*/
private TranslationOptions translationOptions;
/**
* 用于控制模型在生成文本时是否使用互联网搜索结果进行参考。
* 取值如下:
* True启用互联网搜索模型会将搜索结果作为文本生成过程中的参考信息但模型会基于其内部逻辑判断是否使用互联网搜索结果。
* False默认关闭互联网搜索。
* 启用互联网搜索功能可能会增加 Token 的消耗。
* 当前支持 qwen-max、qwen-plus、qwen-turbo
*/
private Boolean enableSearch;
/**
* 思考模式预算,适用于 Qwen3 模型。
* 思考过程的最大长度,只在 enable_thinking 为 true 时生效。适用于 Qwen3 的商业版与开源版模型。
* 详情请参见限制思考长度https://help.aliyun.com/zh/model-studio/deep-thinking#e7c0002fe4meu
*/
private Integer thinkingBudget;
/**
* 联网搜索的策略。
* 仅当enable_search为true时生效。
*/
private SearchOptions searchOptions;
public static class TranslationOptions {
/**
* (必选)
* 源语言的英文全称
* 您可以将source_lang设置为"auto",模型会自动判断输入文本属于哪种语言。
* 支持的语言: https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#038d2865bbydc
*/
@JSONField(name = "source_lang")
private String sourceLang;
/**
* (必选)
* 目标语言的英文全称,
* 支持的语言: https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#038d2865bbydc
*/
@JSONField(name = "target_lang")
private String targetLang;
/**
* 在使用术语干预翻译功能时需要设置的术语数组。
* https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#2bf54a5ab5voe
*/
@JSONField(name = "terms")
private List<TranslationOptionsExt> terms;
/**
* 在使用翻译记忆功能时需要设置的翻译记忆数组。
* https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#17e15234e7gfp
*/
@JSONField(name = "tm_list")
private List<TranslationOptionsExt> tmList;
/**
* (可选)
* 在使用领域提示功能时需要设置的领域提示语句。
* 领域提示语句暂时只支持英文。
* https://help.aliyun.com/zh/model-studio/user-guide/machine-translation#4af23a31db7lf
*/
@JSONField(name = "domains")
private String domains;
public String getSourceLang() {
return sourceLang;
}
public TranslationOptions setSourceLang(String sourceLang) {
this.sourceLang = sourceLang;
return this;
}
public String getTargetLang() {
return targetLang;
}
public TranslationOptions setTargetLang(String targetLang) {
this.targetLang = targetLang;
return this;
}
public List<TranslationOptionsExt> getTerms() {
return terms;
}
public TranslationOptions setTerms(List<TranslationOptionsExt> terms) {
this.terms = terms;
return this;
}
public List<TranslationOptionsExt> getTmList() {
return tmList;
}
public TranslationOptions setTmList(List<TranslationOptionsExt> tmList) {
this.tmList = tmList;
return this;
}
public String getDomains() {
return domains;
}
public TranslationOptions setDomains(String domains) {
this.domains = domains;
return this;
}
}
public static class TranslationOptionsExt {
/**
* 源语言的术语/源语言的语句
*/
private String source;
/**
* 目标语言的术语/目标语言的语句
*/
private String target;
public String getSource() {
return source;
}
public TranslationOptionsExt setSource(String source) {
this.source = source;
return this;
}
public String getTarget() {
return target;
}
public TranslationOptionsExt setTarget(String target) {
this.target = target;
return this;
}
}
public static class SearchOptions {
/**
* 是否强制开启搜索。可选默认值为false
* 参数值true=强制开启false=不强制开启。
*/
@JSONField(name = "forced_search")
private Boolean forcedSearch;
/**
* 搜索互联网信息的数量,(可选)默认值为"standard"
* 参数值:
* standard在请求时搜索5条互联网信息
* pro在请求时搜索10条互联网信息。
*/
@JSONField(name = "search_strategy")
private String searchStrategy;
public Boolean getForcedSearch() {
return forcedSearch;
}
public SearchOptions setForcedSearch(Boolean forcedSearch) {
this.forcedSearch = forcedSearch;
return this;
}
public String getSearchStrategy() {
return searchStrategy;
}
public SearchOptions setSearchStrategy(String searchStrategy) {
this.searchStrategy = searchStrategy;
return this;
}
}
public List<String> getModalities() {
return modalities;
}
public QwenChatOptions setModalities(List<String> modalities) {
this.modalities = modalities;
return this;
}
public Float getPresencePenalty() {
return presencePenalty;
}
public QwenChatOptions setPresencePenalty(Float presencePenalty) {
this.presencePenalty = presencePenalty;
return this;
}
public Integer getN() {
return n;
}
public QwenChatOptions setN(Integer n) {
this.n = n;
return this;
}
public Boolean getParallelToolCalls() {
return parallelToolCalls;
}
public QwenChatOptions setParallelToolCalls(Boolean parallelToolCalls) {
this.parallelToolCalls = parallelToolCalls;
return this;
}
public TranslationOptions getTranslationOptions() {
return translationOptions;
}
public QwenChatOptions setTranslationOptions(TranslationOptions translationOptions) {
this.translationOptions = translationOptions;
return this;
}
public Boolean getEnableSearch() {
return enableSearch;
}
public QwenChatOptions setEnableSearch(Boolean enableSearch) {
this.enableSearch = enableSearch;
return this;
}
public Integer getThinkingBudget() {
return thinkingBudget;
}
public void setThinkingBudget(Integer thinkingBudget) {
this.thinkingBudget = thinkingBudget;
}
public SearchOptions getSearchOptions() {
return searchOptions;
}
public QwenChatOptions setSearchOptions(SearchOptions searchOptions) {
this.searchOptions = searchOptions;
return this;
}
}

View File

@@ -0,0 +1,30 @@
package com.easyagents.llm.qwen;
import com.easyagents.core.model.chat.ChatConfig;
import com.easyagents.core.model.chat.ChatOptions;
import com.easyagents.core.model.client.OpenAIChatRequestSpecBuilder;
import com.easyagents.core.prompt.Prompt;
import com.easyagents.core.util.CollectionUtil;
import com.easyagents.core.util.Maps;
public class QwenRequestSpecBuilder extends OpenAIChatRequestSpecBuilder {
@Override
protected Maps buildBaseParamsOfRequestBody(Prompt prompt, ChatOptions options, ChatConfig config) {
Maps params = super.buildBaseParamsOfRequestBody(prompt, options, config);
if (options instanceof QwenChatOptions) {
QwenChatOptions op = (QwenChatOptions) options;
params.setIf(CollectionUtil.hasItems(op.getModalities()), "modalities", op.getModalities());
params.setIf(op.getPresencePenalty() != null, "presence_penalty", op.getPresencePenalty());
params.setIf(op.getResponseFormat() != null, "response_format", op.getResponseFormat());
params.setIf(op.getN() != null, "n", op.getN());
params.setIf(op.getParallelToolCalls() != null, "parallel_tool_calls", op.getParallelToolCalls());
params.setIf(op.getTranslationOptions() != null, "translation_options", op.getTranslationOptions());
params.setIf(op.getEnableSearch() != null, "enable_search", op.getEnableSearch());
params.setIf(op.getEnableSearch() != null && op.getEnableSearch() && op.getSearchOptions() != null, "search_options", op.getSearchOptions());
params.setIf(op.getThinkingEnabled() != null, "enable_thinking", op.getThinkingEnabled());
params.setIf(op.getThinkingEnabled() != null && op.getThinkingEnabled() && op.getThinkingBudget() != null, "thinking_budget", op.getThinkingBudget());
}
return params;
}
}

View File

@@ -0,0 +1,113 @@
package com.easyagents.llm.qwen.test;
import com.easyagents.core.message.AiMessage;
import com.easyagents.core.model.chat.ChatModel;
import com.easyagents.core.model.chat.ChatOptions;
import com.easyagents.core.model.chat.response.AiMessageResponse;
import com.easyagents.core.model.exception.ModelException;
import com.easyagents.core.prompt.SimplePrompt;
import com.easyagents.llm.qwen.QwenChatConfig;
import com.easyagents.llm.qwen.QwenChatModel;
import com.easyagents.llm.qwen.QwenChatOptions;
import com.easyagents.llm.qwen.QwenChatOptions.SearchOptions;
import org.junit.Test;
public class QwenTest {
public static void main(String[] args) throws InterruptedException {
QwenChatConfig config = new QwenChatConfig();
//https://bailian.console.aliyun.com/?apiKey=1#/api-key
config.setApiKey("sk-28a6be3236****");
config.setModel("qwen-plus");
ChatModel chatModel = new QwenChatModel(config);
chatModel.chatStream("请写一个小兔子战胜大灰狼的故事", (context, response) -> {
AiMessage message = response.getMessage();
System.out.println(">>>> " + message.getContent());
});
Thread.sleep(10000);
}
@Test(expected = ModelException.class)
public void testForcedSearch() throws InterruptedException {
QwenChatConfig config = new QwenChatConfig();
config.setApiKey("sk-28a6be3236****");
config.setModel("qwen-max");
ChatModel chatModel = new QwenChatModel(config);
QwenChatOptions options = new QwenChatOptions();
options.setEnableSearch(true);
options.setSearchOptions(new SearchOptions().setForcedSearch(true));
String responseStr = chatModel.chat("今天是几号?", options);
System.out.println(responseStr);
}
@Test
public void testFunctionCalling() throws InterruptedException {
QwenChatConfig config = new QwenChatConfig();
config.setApiKey("sk-28a6be3236****");
config.setModel("qwen-turbo");
ChatModel chatModel = new QwenChatModel(config);
SimplePrompt prompt = new SimplePrompt("今天北京的天气怎么样");
prompt.addToolsFromClass(WeatherFunctions.class);
AiMessageResponse response = chatModel.chat(prompt);
System.out.println(response.executeToolCallsAndGetResults());
// "Today it will be dull and overcast in 北京"
}
/**
* 动态替换模型
*/
@Test
public void testDynamicModel() throws InterruptedException {
// 默认模型
QwenChatConfig config = new QwenChatConfig();
config.setApiKey("sk-28a6be3236****");
config.setModel("qwen-turbo");
// 运行时动态替换模型
ChatOptions options = new QwenChatOptions();
options.setModel("deepseek-r1");
ChatModel chatModel = new QwenChatModel(config);
chatModel.chatStream("请写一个小兔子战胜大灰狼的故事", (context, response) -> {
AiMessage message = response.getMessage();
System.err.println(message.getReasoningContent());
System.out.println(message.getFullContent());
System.out.println();
}, options);
Thread.sleep(10000);
}
/**
* 测试千问3 开启思考模式的开关
*/
@Test
public void testQwen3Thinking() throws InterruptedException {
QwenChatConfig config = new QwenChatConfig();
config.setApiKey("sk-28a6be3236****");
config.setModel("qwen3-235b-a22b");
ChatModel chatModel = new QwenChatModel(config);
QwenChatOptions options = new QwenChatOptions();
options.setThinkingEnabled(false);
//options.setThinkingBudget(1024);
chatModel.chatStream("你是谁", (context, response) -> {
AiMessage message = response.getMessage();
System.err.println(message.getReasoningContent());
System.out.println(message.getFullContent());
System.out.println();
}, options);
Thread.sleep(10000);
}
}

View File

@@ -0,0 +1,14 @@
package com.easyagents.llm.qwen.test;
import com.easyagents.core.model.chat.tool.annotation.ToolDef;
import com.easyagents.core.model.chat.tool.annotation.ToolParam;
public class WeatherFunctions {
@ToolDef(name = "get_the_weather_info", description = "get the weather info")
public static String getWeatherInfo(
@ToolParam(name = "city", description = "the city name") String name
) {
return "Today it will be dull and overcast in " + name;
}
}