feat: 按模型上下文自动配置智能体压缩阈值
- 从 llm.json 解析上下文与输出上限,并增强短模型 ID 匹配 - 切换模型、保存和试运行时按预算公式重算,目录缺失时回退 30K - 修复模型列表接口未返回能力元数据
This commit is contained in:
@@ -20,6 +20,7 @@ import com.easyagents.rerank.DefaultRerankModel;
|
||||
import com.easyagents.rerank.DefaultRerankModelConfig;
|
||||
import com.easyagents.rerank.gitee.GiteeRerankModel;
|
||||
import com.easyagents.rerank.gitee.GiteeRerankModelConfig;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.RelationManyToOne;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.entity.base.ModelBase;
|
||||
@@ -39,6 +40,18 @@ public class Model extends ModelBase {
|
||||
@RelationManyToOne(selfField = "providerId", targetField = "id")
|
||||
private ModelProvider modelProvider;
|
||||
|
||||
/**
|
||||
* 模型最大上下文窗口 Token 数。
|
||||
*/
|
||||
@Column(ignore = true)
|
||||
private Long contextWindowTokens;
|
||||
|
||||
/**
|
||||
* 模型最大输出 Token 数。
|
||||
*/
|
||||
@Column(ignore = true)
|
||||
private Long maxOutputTokens;
|
||||
|
||||
/**
|
||||
* 模型类型
|
||||
*/
|
||||
@@ -53,6 +66,42 @@ public class Model extends ModelBase {
|
||||
this.modelProvider = modelProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型最大上下文窗口。
|
||||
*
|
||||
* @return 最大上下文窗口 Token 数,目录未提供时返回 null
|
||||
*/
|
||||
public Long getContextWindowTokens() {
|
||||
return contextWindowTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型最大上下文窗口。
|
||||
*
|
||||
* @param contextWindowTokens 最大上下文窗口 Token 数
|
||||
*/
|
||||
public void setContextWindowTokens(Long contextWindowTokens) {
|
||||
this.contextWindowTokens = contextWindowTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型最大输出 Token 数。
|
||||
*
|
||||
* @return 最大输出 Token 数,目录未提供时返回 null
|
||||
*/
|
||||
public Long getMaxOutputTokens() {
|
||||
return maxOutputTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型最大输出 Token 数。
|
||||
*
|
||||
* @param maxOutputTokens 最大输出 Token 数
|
||||
*/
|
||||
public void setMaxOutputTokens(Long maxOutputTokens) {
|
||||
this.maxOutputTokens = maxOutputTokens;
|
||||
}
|
||||
|
||||
public ChatModel toChatModel() {
|
||||
String providerType = modelProvider.getProviderType();
|
||||
if (StringUtil.noText(providerType)) {
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ModelCapabilityCatalog {
|
||||
|
||||
/** 按规范化完整模型 ID 建立的目录索引。 */
|
||||
private final Map<String, ModelCatalogMetadata> metadataById;
|
||||
/** 仅在模型短 ID 唯一时建立的目录别名索引。 */
|
||||
/** 仅在规范化模型短 ID 唯一时建立的目录别名索引。 */
|
||||
private final Map<String, ModelCatalogMetadata> metadataByAlias;
|
||||
|
||||
/**
|
||||
@@ -83,7 +83,7 @@ public class ModelCapabilityCatalog {
|
||||
}
|
||||
}
|
||||
|
||||
String alias = shortId(normalizedId);
|
||||
String alias = canonicalAlias(normalizedId);
|
||||
return Optional.ofNullable(metadataByAlias.get(alias));
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ public class ModelCapabilityCatalog {
|
||||
}
|
||||
ModelCatalogMetadata metadata = toMetadata(normalizedId, field.getValue());
|
||||
fullIdIndex.put(normalizedId, metadata);
|
||||
registerAlias(shortId(normalizedId), metadata, aliasCandidates, ambiguousAliases);
|
||||
registerAlias(canonicalAlias(normalizedId), metadata, aliasCandidates, ambiguousAliases);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("无法加载模型能力库 " + CATALOG_RESOURCE, exception);
|
||||
@@ -147,20 +147,26 @@ public class ModelCapabilityCatalog {
|
||||
*/
|
||||
private ModelCapabilityResolution toCapability(String normalizedId, JsonNode node) {
|
||||
String modelType = resolveModelType(normalizedId);
|
||||
Long contextWindowTokens = positiveLongValue(node.path("limit"), "context");
|
||||
Long maxOutputTokens = positiveLongValue(node.path("limit"), "output");
|
||||
if (!Model.MODEL_TYPES[0].equals(modelType)) {
|
||||
return new ModelCapabilityResolution(
|
||||
modelType,
|
||||
Boolean.FALSE,
|
||||
Boolean.FALSE,
|
||||
Boolean.FALSE,
|
||||
ModelCapabilitySource.CATALOG);
|
||||
ModelCapabilitySource.CATALOG,
|
||||
contextWindowTokens,
|
||||
maxOutputTokens);
|
||||
}
|
||||
return new ModelCapabilityResolution(
|
||||
modelType,
|
||||
hasInputModality(node, "image"),
|
||||
booleanValue(node, "reasoning"),
|
||||
booleanValue(node, "tool_call"),
|
||||
ModelCapabilitySource.CATALOG);
|
||||
ModelCapabilitySource.CATALOG,
|
||||
contextWindowTokens,
|
||||
maxOutputTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,6 +197,22 @@ public class ModelCapabilityCatalog {
|
||||
return value != null && value.asBoolean(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取正整数长整型字段。
|
||||
*
|
||||
* @param node 字段所属节点
|
||||
* @param fieldName 字段名
|
||||
* @return 正整数值,字段缺失或非法时返回 null
|
||||
*/
|
||||
private Long positiveLongValue(JsonNode node, String fieldName) {
|
||||
JsonNode value = node.get(fieldName);
|
||||
if (value == null || !value.isIntegralNumber() || !value.canConvertToLong()) {
|
||||
return null;
|
||||
}
|
||||
long number = value.longValue();
|
||||
return number > 0L ? number : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断模型输入模态是否包含指定类型。
|
||||
*
|
||||
@@ -309,4 +331,24 @@ public class ModelCapabilityCatalog {
|
||||
int separator = normalizedId.lastIndexOf('/');
|
||||
return separator < 0 ? normalizedId : normalizedId.substring(separator + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成忽略厂商前缀、大小写和常见连接符的模型短 ID 索引键。
|
||||
*
|
||||
* @param normalizedId 已完成大小写与首尾空白规范化的模型 ID
|
||||
* @return 保留版本点号、移除横线、下划线和空白字符的索引键
|
||||
*/
|
||||
private String canonicalAlias(String normalizedId) {
|
||||
String modelId = shortId(normalizedId);
|
||||
StringBuilder alias = new StringBuilder(modelId.length());
|
||||
for (int index = 0; index < modelId.length(); index++) {
|
||||
char character = modelId.charAt(index);
|
||||
if (character == '-' || character == '_'
|
||||
|| Character.isWhitespace(character) || Character.isSpaceChar(character)) {
|
||||
continue;
|
||||
}
|
||||
alias.append(character);
|
||||
}
|
||||
return alias.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ public final class ModelCapabilityResolution {
|
||||
private final Boolean supportTool;
|
||||
/** 能力识别来源。 */
|
||||
private final ModelCapabilitySource source;
|
||||
/** 模型最大上下文窗口 Token 数,空值表示目录未提供。 */
|
||||
private final Long contextWindowTokens;
|
||||
/** 模型最大输出 Token 数,空值表示目录未提供。 */
|
||||
private final Long maxOutputTokens;
|
||||
|
||||
/**
|
||||
* 创建模型能力识别结果。
|
||||
@@ -30,11 +34,34 @@ public final class ModelCapabilityResolution {
|
||||
Boolean supportThinking,
|
||||
Boolean supportTool,
|
||||
ModelCapabilitySource source) {
|
||||
this(modelType, supportImage, supportThinking, supportTool, source, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建包含上下文窗口限制的模型能力识别结果。
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param supportImage 是否支持视觉输入
|
||||
* @param supportThinking 是否支持推理
|
||||
* @param supportTool 是否支持工具调用
|
||||
* @param source 能力识别来源
|
||||
* @param contextWindowTokens 模型最大上下文窗口 Token 数
|
||||
* @param maxOutputTokens 模型最大输出 Token 数
|
||||
*/
|
||||
public ModelCapabilityResolution(String modelType,
|
||||
Boolean supportImage,
|
||||
Boolean supportThinking,
|
||||
Boolean supportTool,
|
||||
ModelCapabilitySource source,
|
||||
Long contextWindowTokens,
|
||||
Long maxOutputTokens) {
|
||||
this.modelType = modelType;
|
||||
this.supportImage = supportImage;
|
||||
this.supportThinking = supportThinking;
|
||||
this.supportTool = supportTool;
|
||||
this.source = source;
|
||||
this.contextWindowTokens = contextWindowTokens;
|
||||
this.maxOutputTokens = maxOutputTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,6 +109,24 @@ public final class ModelCapabilityResolution {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型最大上下文窗口。
|
||||
*
|
||||
* @return 最大上下文窗口 Token 数,目录未提供时返回 null
|
||||
*/
|
||||
public Long getContextWindowTokens() {
|
||||
return contextWindowTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型最大输出 Token 数。
|
||||
*
|
||||
* @return 最大输出 Token 数,目录未提供时返回 null
|
||||
*/
|
||||
public Long getMaxOutputTokens() {
|
||||
return maxOutputTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否获得了模型库或命名规则证据。
|
||||
*
|
||||
|
||||
@@ -420,10 +420,16 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
String providerName = Optional.ofNullable(model.getModelProvider())
|
||||
ModelProvider provider = model.getModelProvider();
|
||||
String providerName = Optional.ofNullable(provider)
|
||||
.map(ModelProvider::getProviderName)
|
||||
.orElse("-");
|
||||
model.setTitle(providerName + "/" + model.getTitle());
|
||||
ModelCapabilityResolution resolution = modelCapabilityResolver.resolve(
|
||||
resolveProviderType(model.getProviderId(), provider),
|
||||
model.getModelName());
|
||||
model.setContextWindowTokens(resolution.getContextWindowTokens());
|
||||
model.setMaxOutputTokens(resolution.getMaxOutputTokens());
|
||||
}
|
||||
|
||||
private String buildOrderBy(String sortKey, String sortType) {
|
||||
|
||||
@@ -34,6 +34,28 @@ public class ModelCapabilityResolverTest {
|
||||
Assert.assertEquals(Boolean.TRUE, result.getSupportImage());
|
||||
Assert.assertEquals(Boolean.TRUE, result.getSupportThinking());
|
||||
Assert.assertEquals(Boolean.TRUE, result.getSupportTool());
|
||||
Assert.assertEquals(Long.valueOf(1_000_000L), result.getContextWindowTokens());
|
||||
Assert.assertEquals(Long.valueOf(64_000L), result.getMaxOutputTokens());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证目录匹配忽略厂商前缀、大小写和常见连接符。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveCatalogCapabilitiesByCanonicalShortId() {
|
||||
String[] modelIds = {
|
||||
"阿里百炼/QWEN3.7_PLUS",
|
||||
"custom/qwen3.7 plus",
|
||||
"qwen3.7plus"
|
||||
};
|
||||
|
||||
for (String modelId : modelIds) {
|
||||
ModelCapabilityResolution result = resolver.resolve(null, modelId);
|
||||
|
||||
Assert.assertEquals(ModelCapabilitySource.CATALOG, result.getSource());
|
||||
Assert.assertEquals(Long.valueOf(1_000_000L), result.getContextWindowTokens());
|
||||
Assert.assertEquals(Long.valueOf(64_000L), result.getMaxOutputTokens());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,5 +98,7 @@ public class ModelCapabilityResolverTest {
|
||||
Assert.assertNull(result.getSupportImage());
|
||||
Assert.assertNull(result.getSupportThinking());
|
||||
Assert.assertNull(result.getSupportTool());
|
||||
Assert.assertNull(result.getContextWindowTokens());
|
||||
Assert.assertNull(result.getMaxOutputTokens());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user