- 基线:
main@61d471c(行号均以此为准) - 状态:已评审定案,待实施
- 范围:
crates/plugin-api(模型契约)、src-tauri/src/core/model/(宿主模型核心)、src-tauri/src/builtin_plugin/config/(模型配置组件)、src-ui/i18n/(三语言文案) - 兼容性策略:不做旧代码兼容。旧配置字段静默丢弃,旧 embedding 缓存随 namespace 变化自然失效(见 §7)
当前 task_type 是 ModelEmbeddingRequest 的可选参数(None = 裸传),模板解析为三层优先级:用户配置 → 内置档案 → gemma 兜底(model_profiles.rs:12-15)。该设计存在以下问题:
- 模板是模型实现细节(qwen3 的 Instruct 格式、gemma 的 task 前缀),把它的编辑权交给终端用户属于知识归属错位——内置模板尚且会写错(61d471c 修复的 gemma 文档侧占位符误用),用户手写错误率更高;
None(裸传)与PlainText(裸传任务)语义重复,是两条路径做同一件事,且产生等价向量却两个缓存条目;- 无档案模型的 gemma 兜底对 bge/gte/m3e 等未见过该前缀的模型不是中性操作,效果可能劣化;
- 调用方不声明任务时,非对称检索的 query/document 两侧无法保证模板配套,检索质量静默劣化。
目标:模板归档案库(唯一来源)、任务归调用方(必填声明)、用户只管选模型。
| # | 决策 | 内容 |
|---|---|---|
| D1 | 无档案模型一律裸传 | 删除 gemma 兜底路径。模型无内置档案时,所有任务(含 retrieval_query 等)原样透传 input,不套任何前缀。GEMMA_FALLBACK 常量仅作为 gemma-embedding 档案自身的模板表保留 |
| D2 | task_type 必填 | 序列化形状保持 Option<String> + #[serde(default)] 不变,宿主入口将 None/空串/未知值统一拒绝为 ModelError::InvalidRequest,保证插件拿到明确业务错误而非反序列化泛型错误 |
| D3 | 删除 EmbeddingCapability::TaskType 能力位 |
模板组装是宿主侧行为(provider 只收到拼好的最终字符串),模型层面不存在"不支持 taskType";强制必填后能力门控必然失效。保留 OutputDimensions |
| D4 | 删除 template_args 契约字段 |
61d471c 后所有内置模板只引用 {0},无任何调用方传参。gemma 文档侧未来若需 title,由宿主从候选元数据自取,不跨 RPC 传 |
另含一项连带修复(§6):cosine_similarity 实现真余弦 + 修正 types.rs 的"归一化向量"假注释。
宿主 embedding 入口(ModelManager::embedding)按以下顺序校验:
task_type为None或空串 →InvalidRequest("embedding 调用必须指明 task_type(retrieval_document / retrieval_query / semantic_similarity / classification / clustering / plain_text)")- 非上述六个序列化名之一 →
InvalidRequest("未知的 task_type: {task}")(沿用现有文案) - 校验通过后由
compose层解析为SemanticTask枚举继续处理
profile_for(model_id) 命中档案:
task 在档案 task_templates 中命中 → 渲染模板(仅 {0} 占位符替换为 input 文本)
task 在档案中未命中(如未来新增枚举但档案未更新)→ 裸传
profile_for 未命中(无档案模型):
所有任务一律裸传
- 不存在用户模板层、不存在 gemma 兜底层、不存在 "不传 task_type 裸传" 分支
- 模板渲染只支持
{0};模板中的其他字面量(含{1}等)原样输出 SemanticTask枚举与六个序列化名保持不变(model_profiles.rs:26-48)
| 场景 | 旧行为 | 新行为 |
|---|---|---|
不传 task_type |
裸传 | InvalidRequest 拒绝 |
task_type = "plain_text" |
档案/兜底模板 {0}(等效裸传) |
不变(所有模型裸传) |
无档案模型 + retrieval_query |
gemma 兜底模板 task: search result | query: {0} |
裸传 |
| 有档案模型 + 任意任务 | 用户配置 > 档案 > 兜底 | 档案(唯一来源) |
| 模型未勾 taskType 能力 + 带 task_type | InvalidRequest(能力门控) | 能力位已删除,行为只由档案决定 |
请求携带 template_args |
参与模板渲染 | 字段已删除,多余 JSON 键被 serde 忽略 |
| 位置 | 删除内容 |
|---|---|
model_profiles.rs:183-189 |
gemma_fallback_template() 函数整体删除——无模板时不兜底,此函数是兜底的唯一入口 |
compose.rs:133-135 |
template_for(...).or_else(|| gemma_fallback_template(task)...) 的 .or_else 兜底调用,以及 expect("模板查询恒命中(回退保底)")——新逻辑下 template_for 返回 None 即裸传,永不 panic |
model_profiles.rs:88-93 |
GEMMA_FALLBACK 的文档注释中"无档案模型的 gemma 回退模板"语义——常量本体保留(它是 gemma 档案模板表),建议改名 GEMMA_TEMPLATES 并重写注释为"gemma-embedding 档案模板表" |
model_profiles.rs:12-15 |
模块头注释中"用户配置 → 内置档案 → gemma 兜底"三层描述、"无档案模型且 task_type 非空时使用 gemma 中性前缀模板,仅 task_type 为空时原样透传"整段 |
| 位置 | 删除内容 |
|---|---|
settings.rs:101-110 |
TaskTemplateItem 结构体整体删除 |
settings.rs:94-98 |
EmbeddingModelConfig.task_templates 字段删除 |
settings.rs:119 |
Default impl 中的 task_templates: Vec::new() 行 |
model_profiles.rs:167-181 |
template_for() 的 user_templates 参数及用户配置优先分支(:172-174) |
model_profiles.rs:191-204 |
auto_task_templates() 整体删除(用途是填充用户配置) |
model_profiles.rs:206-234 |
apply_profile_defaults() 整体删除 |
model_profiles.rs:236-250 |
apply_profiles_to_entries() 整体删除 |
model_profiles.rs:17 |
use super::settings::ModelEntryConfig(删除后无引用) |
model_openai_config.rs:336-337 |
apply_settings 中 apply_profiles_to_entries 调用及 :336 注释 |
model_ollama_config.rs:118-119 |
同上 |
| 位置 | 删除内容 |
|---|---|
compose.rs:128-129 |
match task_type { None => Ok(input.to_vec()) } 分支——必填后不可达 |
compose.rs:69-86 |
validate_embedding_request() 整体删除(TaskType 能力门控 + Option 校验的旧形态) |
mod.rs:190 |
对旧 validate_embedding_request 的调用(由新校验函数替代,见 §5) |
| 位置 | 删除内容 |
|---|---|
types.rs:59-63 |
EmbeddingCapability::TaskType 变体及其文档注释;枚举仅剩 OutputDimensions |
model_profiles.rs:79-86 |
EmbeddingModelProfile.capabilities 字段(唯一用途是自动勾选 TaskType),及 :19 的 EmbeddingCapability import |
model_profiles.rs:123,150-151 |
PROFILES 中 qwen3/gemma 档案的 capabilities: &[EmbeddingCapability::TaskType] |
model_openai_config.rs:213-214 |
embedding_capabilities multi_select 的 ("taskType", ...) 选项 |
| i18n 三语言 | model-entry.capabilities.task_type 键(zh-Hans.json:720 等) |
| 位置 | 删除内容 |
|---|---|
types.rs:216-219 |
ModelEmbeddingRequest.template_args 字段 |
mod.rs:191-197 |
template_args 与 input 数量一致性校验 |
mod.rs:212 |
Miss 结构体 args 字段 |
mod.rs:219-222,228 |
缓存键构造中的 args 拆分 |
mod.rs:260-263 |
无缓存路径 misses 构造中的 args |
mod.rs:277-282 |
子请求组装中的 template_args 重打包 |
compose.rs:118,121-126 |
compose_embedding_texts 的 template_args 参数与 args_for 闭包 |
compose.rs:21-63 |
render_template 的 args 参数、{1}+ 索引解析、越界报错——渲染器简化为仅替换 {0},其余原样输出 |
embedding_cache.rs:32 |
注释中 template_args 描述 |
| 位置 | 删除内容 |
|---|---|
model_openai_config.rs:224-270 |
task_templates array schema 整段(含 task 下拉 6 选项与 template 文本框的 object_items 定义,order 13 字段消失无碍,无需重排其余 order) |
| i18n 三语言 | model-entry.fields.task_templates 键组(zh-Hans.json:690-711,含 task/template 子键) |
| i18n 三语言 | model-entry.tasks 键组(zh-Hans.json:723-729)——仅被 task_templates 下拉引用,随之失去消费方 |
i18n 保留键:
capabilities.reasoning、capabilities.output_dimensions、similarities.*。
| 位置 | 用例 | 原因 |
|---|---|---|
compose.rs:234-249 |
user_template_overrides_builtin |
用户模板层已删除 |
compose.rs:251-268 |
template_placeholder_out_of_range_rejected |
template_args 已删除 |
compose.rs:271-287 |
multi_placeholder_template_renders_in_order |
同上 |
compose.rs:173-185 |
plain_text_task_gemma_fallback_passes_through |
兜底已删除(改写为"无档案模型所有任务裸传",见 §5.3) |
openai_compatible_provider.rs:393-407 |
embedding_rejects_task_type_without_capability |
TaskType 能力位已删除 |
model_profiles.rs:298-310 |
user_template_overrides_builtin |
同上 |
model_profiles.rs:321-365 |
apply_profile_fills... / apply_profile_keeps... / apply_profile_unknown_model_noop 三个用例 |
apply_profile_defaults 已删除 |
model_openai_config.rs:444-453 |
embedding enum 断言中 "taskType" |
改为 ["outputDimensions"](保留用例本体) |
task_type字段(:220-222):注释改为必填语义——"匹配模式,必填;序列化形状保留 Option 仅为容错,宿主入口对缺失/未知值返回 InvalidRequest"ModelEmbeddingResponse(:227):文档注释删除"归一化"承诺,改为"与 input 一一对应的向量(是否归一化取决于 provider)"EmbeddingCapability(:56-66):仅剩OutputDimensions,枚举注释同步更新
compose.rs:
- 新增
validate_task_type(task_type: Option<&str>) -> Result<SemanticTask, ModelError>:None/空串 → 必填报错(错误消息见 §3.1);未知 → 报错;命中 → 返回枚举。替代原validate_embedding_request compose_embedding_texts新签名:
pub(crate) fn compose_embedding_texts(
input: &[String],
task: SemanticTask,
model_id: &str,
) -> Result<Vec<String>, ModelError>实现:template_for(task, model_id) 命中 → 渲染(仅 {0} 替换);未命中(无档案或档案缺该任务)→ Ok(input.to_vec())
render_template简化:签名(template: &str, text: &str) -> String,仅识别{0};其余字符(含非数字占位符与{1})原样保留require_embedding_capability保留(OutputDimensions 门控仍在用)
model_profiles.rs:
template_for新签名:pub fn template_for(task: SemanticTask, model_id: &str) -> Option<&'static str>(返回档案模板;无档案/档案缺任务返回None)- 模块头注释重写为新规则(§3.2)
EmbeddingModelProfile仅剩id_prefix+task_templates两字段
mod.rs:
:190替换为let task = compose::validate_task_type(req.task_type.as_deref())?;,解析出的SemanticTask传入后续compose_embedding_texts:19import 行同步- 缓存单条拆分逻辑保留(
task_type已参与缓存键embedding_cache.rs:219-221,行为不变;template_args相关行按 §4.5 删除)
openai_compatible_provider.rs / ollama_provider.rs:
embedding()中compose_embedding_texts调用改为新签名(openai :342-353、ollama :290-301),删除 capabilities/task_templates/template_args 传参- 其余逻辑(dimensions 能力门控、请求构造)不变
| 位置 | 用例 |
|---|---|
compose.rs |
missing_task_type_rejected:None 与空串均返回 InvalidRequest |
compose.rs |
unknown_task_type_rejected(保留自现有用例) |
compose.rs |
unknown_model_all_tasks_pass_through:无档案模型 + retrieval_query/plain_text 均裸传(替代被删的 gemma fallback 用例) |
model_profiles.rs |
template_for_unknown_model_returns_none(改写自现有用例,断言 None) |
model_profiles.rs |
template_for_returns_none_when_profile_lacks_task(可选,构造缺任务场景) |
mod.rs |
embedding_requires_task_type:入口校验集成测试 |
host_proxy.rs:203、plugin_handle.rs:377的model_embedding文档注释补一句"task_type 必填"service.rs:31-35/provider.rs:41-45的 embedding 注释同步
mod.rs:368-374 的 cosine_similarity 当前为裸点积,契约注释(已删除的"归一化"承诺)与实现矛盾,且 Ollama/兼容网关的 embedding 输出普遍未归一化,默认 Cosine 配置下排序数学错误。修复为:
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() {
return 0.0;
}
let (dot, na, nb) = a.iter().zip(b).fold((0f32, 0f32, 0f32), |(d, x, y), (u, v)| {
(d + u * v, x + u * u, y + v * v)
});
let denom = na.sqrt() * nb.sqrt();
if denom <= f32::EPSILON || !denom.is_finite() {
return 0.0;
}
dot / denom
}mod.rs:618-655的 cosine 测试当前全用单位向量(与 bug 同源自证),补充非单位向量用例(如[3.0, 4.0]与[4.0, 3.0]应为24/25)ModelSimilarity::Cosine的文档注释(types.rs:71-74)"归一化向量与点积排序等价"删除
- 旧配置:已持久化设置 JSON 中的
task_templates数组与taskType能力值——serde 默认忽略未知字段,TaskTemplateItem/字段删除后静默丢弃,无需迁移代码 - 旧缓存:
cache_namespace包含 models 配置序列化(openai :218-221、ollama :146-149),配置结构变化使 namespace 变化,旧 L1/L2 缓存条目整体失效,属一次性失效,可接受 - 调用方:当前
task_type在全部非测试代码路径中均为None(宿主内部无 embedding 消费方、translator 不用 embedding、无第三方插件使用),必填化无实际破坏面
| 步骤 | 内容 | 说明 |
|---|---|---|
| 1 | 契约层 types.rs(§4.4、§4.5、§5.1) | 先定契约形状 |
| 2 | 宿主核心:settings → model_profiles → compose → mod → 两个 provider(§4.1-4.3、§5.2、§5.3) | 依赖契约层,逐层向内 |
| 3 | 配置组件 + i18n(§4.2 后两项、§4.6) | schema 驱动 UI,删字段即生效 |
| 4 | cosine 连带修复(§6) | 可独立 commit |
步骤 1-3 建议作为单个 commit(契约与实现不可分离地联动);步骤 4 独立 commit。
ModelManager提供embed_query()/embed_documents()语义化便捷方法,内部固定传RetrievalQuery/RetrievalDocument,进一步收敛调用方心智- 档案库补录更多模型(bge、nomic-embed 等)提升有档案覆盖率——无档案模型走裸传后,补录是提升检索质量的唯一路径
§3.1 的运行时白名单校验(Option<String> + validate_task_type)在首个第三方消费方(ai-search 插件)接入时被否——字符串让插件可以传任意值,宿主只能事后拒绝。改为类型系统强制:
SemanticTask枚举从宿主内部迁至plugin-api契约层(types.rs),带#[serde(rename)]序列化名ModelEmbeddingRequest.task_type由Option<String>改为非 Option 的SemanticTask;未知字符串在 serde 反序列化时直接失败(fail fast),不再进入宿主- 宿主删除
validate_task_type/parse_semantic_task(枚举已保证合法),入口直接消费req.task_type - 序列化 JSON 形状不变(
taskType: "retrieval_document"),旧插件若传合法值仍兼容;传非法值反序列化报错
| 文件 | 变更 |
|---|---|
plugin-api/types.rs |
新增 SemanticTask 枚举 + as_str();task_type: SemanticTask |
plugin-api/model/mod.rs、services/mod.rs |
re-export SemanticTask |
model_profiles.rs |
删本地枚举定义,use 契约枚举 |
compose.rs |
删 parse_semantic_task/validate_task_type 与相关测试 |
mod.rs、两个 provider |
删入口校验调用;测试构造改枚举 |
ai-search 插件 |
SemanticTask::RetrievalDocument / RetrievalQuery 枚举传参 |