mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): route cheap ingest steps to a configurable light model
This commit is contained in:
parent
812daae0fc
commit
64b5587f56
@ -31,6 +31,16 @@ public class WikiKbConfig {
|
||||
*/
|
||||
private Long wikiDefaultModelId;
|
||||
|
||||
/**
|
||||
* KB-level lightweight chat model for the cheap, high-volume steps
|
||||
* (route / enrich / summary / entity extraction). When set, those steps
|
||||
* run on this cheaper model instead of the KB/system default, cutting token
|
||||
* spend without touching page generation quality. {@code null} falls back to
|
||||
* the system-level light model, then to normal routing. Strong steps
|
||||
* (create_page / merge_page) ignore this field.
|
||||
*/
|
||||
private Long wikiLightModelId;
|
||||
|
||||
/** Per-step model overrides: "heavy_ingest.create_page" → modelId */
|
||||
private Map<String, Long> stepModels;
|
||||
|
||||
|
||||
@ -52,6 +52,9 @@ public final class WikiKbConfigParser {
|
||||
} else if ("wikiDefaultModelId".equals(key)) {
|
||||
Long parsed = parseLong(value);
|
||||
if (parsed != null) config.setWikiDefaultModelId(parsed);
|
||||
} else if ("wikiLightModelId".equals(key)) {
|
||||
Long parsed = parseLong(value);
|
||||
if (parsed != null) config.setWikiLightModelId(parsed);
|
||||
} else if (key.startsWith("stepModels.")) {
|
||||
Long parsed = parseLong(value);
|
||||
if (parsed != null) {
|
||||
|
||||
@ -9,12 +9,13 @@ import vip.mate.wiki.job.model.WikiProcessingJobEntity;
|
||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
|
||||
/**
|
||||
* RFC-030: Final fallback strategy — uses the system default model.
|
||||
* Cheap steps (ROUTE, ENRICH, SUMMARY) prefer a lighter/cheaper model;
|
||||
* strong steps (CREATE_PAGE, MERGE_PAGE) use the default model.
|
||||
* Final fallback strategy — uses the system default model for every step.
|
||||
* Cheap steps that want a lighter model are handled earlier by
|
||||
* {@link WikiLightModelStrategy} (Order 2) when a light model is configured;
|
||||
* this strategy is the last resort and keeps all steps on the system default.
|
||||
*/
|
||||
@Component
|
||||
@Order(3)
|
||||
@Order(4)
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalDefaultStepModelStrategy implements WikiStepModelStrategy {
|
||||
|
||||
@ -25,9 +26,6 @@ public class GlobalDefaultStepModelStrategy implements WikiStepModelStrategy {
|
||||
|
||||
@Override
|
||||
public Long selectModelId(WikiProcessingJobEntity job, WikiKnowledgeBaseEntity kb, WikiJobStep step) {
|
||||
// RFC-030: cheap steps (ROUTE, ENRICH, SUMMARY) should ideally use a lighter model,
|
||||
// but ModelConfigService has no "cheapest chat model" concept yet.
|
||||
// When per-step pricing metadata is added, this switch can differentiate.
|
||||
return modelConfigService.getDefaultModel().getId();
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,14 +12,14 @@ import vip.mate.wiki.job.model.WikiProcessingJobEntity;
|
||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
|
||||
/**
|
||||
* RFC-051 PR-1a: middle-priority strategy that resolves the KB-level default
|
||||
* chat model ({@link WikiKbConfig#getWikiDefaultModelId()}). Sits between the
|
||||
* per-step override ({@link KbConfigStepModelStrategy}, Order 1) and the
|
||||
* system-wide default ({@link GlobalDefaultStepModelStrategy}, Order 3),
|
||||
* yielding the chain prescribed by RFC-051 §10.1:
|
||||
* RFC-051 PR-1a: resolves the KB-level default chat model
|
||||
* ({@link WikiKbConfig#getWikiDefaultModelId()}). Sits below the per-step
|
||||
* override ({@link KbConfigStepModelStrategy}, Order 1) and the cheap-step light
|
||||
* model ({@link WikiLightModelStrategy}, Order 2), and above the system-wide
|
||||
* default ({@link GlobalDefaultStepModelStrategy}, Order 4), yielding the chain:
|
||||
*
|
||||
* <pre>
|
||||
* stepModels[step] -> wikiDefaultModelId -> system default
|
||||
* stepModels[step] -> (light model for cheap steps) -> wikiDefaultModelId -> system default
|
||||
* </pre>
|
||||
*
|
||||
* The frontend has long written {@code wikiDefaultModelId} into the KB config
|
||||
@ -27,7 +27,7 @@ import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(2)
|
||||
@Order(3)
|
||||
@RequiredArgsConstructor
|
||||
public class KbDefaultModelStrategy implements WikiStepModelStrategy {
|
||||
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
package vip.mate.wiki.job.strategy;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.system.service.SystemSettingService;
|
||||
import vip.mate.wiki.job.WikiJobStep;
|
||||
import vip.mate.wiki.job.WikiKbConfig;
|
||||
import vip.mate.wiki.job.WikiKbConfigParser;
|
||||
import vip.mate.wiki.job.model.WikiProcessingJobEntity;
|
||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Routes the cheap, high-volume wiki steps (route / enrich / summary / entity
|
||||
* extraction) to a lightweight chat model so they don't bill at the premium
|
||||
* page-generation model's rate.
|
||||
*
|
||||
* <p>The light model is resolved as: per-KB {@code wikiLightModelId} →
|
||||
* system-level {@code wiki.lightModelId} setting. When neither is configured the
|
||||
* strategy returns {@code null} and routing falls through to the existing chain
|
||||
* ({@code wikiDefaultModelId} → system default), so behavior is unchanged until
|
||||
* an admin opts in.
|
||||
*
|
||||
* <p>Order is between the explicit per-step override
|
||||
* ({@link KbConfigStepModelStrategy}, Order 1) and the KB default
|
||||
* ({@link KbDefaultModelStrategy}, Order 3): once a light model is configured it
|
||||
* takes precedence over the KB default for the cheap steps, but a KB can still
|
||||
* pin a specific model on any step via {@code stepModels.<step>}. Strong steps
|
||||
* (create_page / merge_page) are not handled here.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(2)
|
||||
public class WikiLightModelStrategy implements WikiStepModelStrategy {
|
||||
|
||||
/** System-setting key for the global lightweight wiki model id. */
|
||||
static final String SETTING_KEY = "wiki.lightModelId";
|
||||
|
||||
/** Cheap, high-volume steps eligible for the lightweight model. */
|
||||
private static final Set<WikiJobStep> CHEAP_STEPS = EnumSet.of(
|
||||
WikiJobStep.ROUTE, WikiJobStep.ENRICH, WikiJobStep.SUMMARY, WikiJobStep.ENTITY_EXTRACTION);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SystemSettingService systemSettingService;
|
||||
|
||||
public WikiLightModelStrategy(ObjectMapper objectMapper, SystemSettingService systemSettingService) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.systemSettingService = systemSettingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(WikiJobStep step) {
|
||||
return CHEAP_STEPS.contains(step);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long selectModelId(WikiProcessingJobEntity job, WikiKnowledgeBaseEntity kb, WikiJobStep step) {
|
||||
if (!CHEAP_STEPS.contains(step)) {
|
||||
return null;
|
||||
}
|
||||
Long perKb = perKbLightModel(kb);
|
||||
if (perKb != null) {
|
||||
return perKb;
|
||||
}
|
||||
return systemLightModel();
|
||||
}
|
||||
|
||||
private Long perKbLightModel(WikiKnowledgeBaseEntity kb) {
|
||||
if (kb == null || kb.getConfigContent() == null) {
|
||||
return null;
|
||||
}
|
||||
WikiKbConfig config = WikiKbConfigParser.parse(objectMapper, kb.getConfigContent());
|
||||
return config != null ? config.getWikiLightModelId() : null;
|
||||
}
|
||||
|
||||
private Long systemLightModel() {
|
||||
String raw = systemSettingService.getString(SETTING_KEY, null);
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(raw.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("[WikiLightModel] Invalid {} setting (not a model id): {}", SETTING_KEY, raw);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,6 +95,17 @@ Eager ingest runs in two phases for an order-of-magnitude speedup:
|
||||
|
||||
**Resumable**: interrupted mid-import? Hit "Reprocess" and only the unfinished pages re-run; everything already produced stays put. Documents larger than the embedding model's context get mean-pool sub-segmented automatically.
|
||||
|
||||
#### Save tokens: a light model for the cheap steps
|
||||
|
||||
Digest runs several kinds of LLM step: route, merge generation, enrich, summary, entity extraction. The **route / enrich / summary / entity-extraction** steps are high-volume but lightweight — no need to run them on the same premium model as page merging.
|
||||
|
||||
Point them at a cheaper model to cut token spend without touching page-generation quality:
|
||||
|
||||
- **System-wide** — set `wiki.lightModelId` (a model id) in system settings; it applies to the cheap steps of every KB.
|
||||
- **Per-KB override** — set `wikiLightModelId` in the KB config to override the system-wide value.
|
||||
|
||||
Leave both unset and nothing changes (the cheap steps keep using the KB / system default). Precedence: `stepModels.<step>` (pin a step) → light model (cheap steps only) → `wikiDefaultModelId` → system default.
|
||||
|
||||
### Lazy: index now, compile later
|
||||
|
||||
The pipeline collapses to four steps:
|
||||
|
||||
@ -95,6 +95,17 @@ eager 模式分两阶段,速度提了一个数量级:
|
||||
|
||||
**可恢复**:中途断了?点"重新处理",只重跑未完成的页面,已生成的不动。超过模型上下文限制的文档,系统自动做 mean-pool 子段切分——你不用管。
|
||||
|
||||
#### 省 token:给廉价步骤配轻量模型
|
||||
|
||||
消化会跑好几类 LLM 步骤:路由、合并生成、富化、摘要、实体抽取。其中 **路由 / 富化 / 摘要 / 实体抽取** 是高频但轻量的活,没必要和「合并生成」用同一个高价模型。
|
||||
|
||||
给它们指定一个便宜模型即可显著省 token,页面生成质量不受影响:
|
||||
|
||||
- **系统级** —— 系统设置里配 `wiki.lightModelId`(一个模型 id),对所有知识库的廉价步骤生效;
|
||||
- **每库覆盖** —— 知识库配置里写 `wikiLightModelId`,覆盖系统级设置。
|
||||
|
||||
不配则一切照旧(廉价步骤仍走 KB 默认 / 系统默认模型)。优先级:`stepModels.<步骤>`(钉死某步)→ 轻量模型(仅廉价步骤)→ `wikiDefaultModelId` → 系统默认。
|
||||
|
||||
### Lazy 模式:先入索引,按需出页面
|
||||
|
||||
链路缩成四步:
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
package vip.mate.wiki.job.strategy;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import vip.mate.system.service.SystemSettingService;
|
||||
import vip.mate.wiki.job.WikiJobStep;
|
||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class WikiLightModelStrategyTest {
|
||||
|
||||
private SystemSettingService settings;
|
||||
private WikiLightModelStrategy strategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
settings = mock(SystemSettingService.class);
|
||||
strategy = new WikiLightModelStrategy(new ObjectMapper(), settings);
|
||||
}
|
||||
|
||||
private WikiKnowledgeBaseEntity kb(String configContent) {
|
||||
WikiKnowledgeBaseEntity kb = new WikiKnowledgeBaseEntity();
|
||||
kb.setConfigContent(configContent);
|
||||
return kb;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("supports() only the cheap, high-volume steps")
|
||||
void supportsOnlyCheapSteps() {
|
||||
assertTrue(strategy.supports(WikiJobStep.ROUTE));
|
||||
assertTrue(strategy.supports(WikiJobStep.ENRICH));
|
||||
assertTrue(strategy.supports(WikiJobStep.SUMMARY));
|
||||
assertTrue(strategy.supports(WikiJobStep.ENTITY_EXTRACTION));
|
||||
assertFalse(strategy.supports(WikiJobStep.CREATE_PAGE));
|
||||
assertFalse(strategy.supports(WikiJobStep.MERGE_PAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("No light model configured anywhere → null (behavior unchanged)")
|
||||
void noLightModelConfigured() {
|
||||
when(settings.getString(WikiLightModelStrategy.SETTING_KEY, null)).thenReturn(null);
|
||||
assertNull(strategy.selectModelId(null, null, WikiJobStep.ROUTE));
|
||||
assertNull(strategy.selectModelId(null, kb("{}"), WikiJobStep.SUMMARY));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("System light model applies to cheap steps")
|
||||
void systemLightModelApplies() {
|
||||
when(settings.getString(WikiLightModelStrategy.SETTING_KEY, null)).thenReturn("777");
|
||||
assertEquals(777L, strategy.selectModelId(null, null, WikiJobStep.ENRICH));
|
||||
// Strong steps are never routed here even if asked directly.
|
||||
assertNull(strategy.selectModelId(null, null, WikiJobStep.CREATE_PAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Per-KB light model overrides the system light model")
|
||||
void perKbOverridesSystem() {
|
||||
when(settings.getString(WikiLightModelStrategy.SETTING_KEY, null)).thenReturn("777");
|
||||
Long picked = strategy.selectModelId(null, kb("{\"wikiLightModelId\": 555}"), WikiJobStep.SUMMARY);
|
||||
assertEquals(555L, picked);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Invalid system setting is ignored (null, no crash)")
|
||||
void invalidSystemSetting() {
|
||||
when(settings.getString(WikiLightModelStrategy.SETTING_KEY, null)).thenReturn("not-a-number");
|
||||
assertNull(strategy.selectModelId(null, null, WikiJobStep.ROUTE));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user