package vip.mate.wiki; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Wiki 知识库配置 * * @author MateClaw Team */ @Data @ConfigurationProperties(prefix = "mate.wiki") public class WikiProperties { /** 是否启用 Wiki 知识库功能 */ private boolean enabled = true; /** * LLM 单次处理最大字符数(超过则分块)。 *
* RFC-012:默认从 30000 下调到 15000 —— 单 chunk 输出 tokens 砍半、并行饱和度更高、 * 质量也更稳定。中端模型(qwen-plus/claude-sonnet)建议 12000-20000, * 旗舰模型(qwen-max/claude-opus)可调大到 20000-30000。 */ private int maxChunkSize = 15000; /** * 同一次 processAllPending 下同时处理的原始材料数上限。 *
* RFC-012 Change 1:保护共享的 @Async 线程池(max=16)不被 wiki 长时间占满, * 同时给材料级并发定一个可控的上限,避免 LLM 提供方触发限流。 */ private int maxParallelRawMaterials = 3; /** * Max parallel chunks within a single raw material. *
* RFC-047 P3: Changed from 5 to 1. Parallel chunks all share the same existingPagesIndex * snapshot, so chunk N cannot see pages created by chunk N-1 — causing duplicate pages and * stale-index collisions. Serializing chunks eliminates this class of bug. Document-level * parallelism (maxParallelRawMaterials) is preserved, so overall throughput is unchanged * for multi-document batches. */ private int maxParallelChunks = 1; /** * 单个 chunk 内 phase B 阶段的 page 并行处理数上限。 *
* RFC-012 follow-up #3:原实现 phase B 的 create / merge 循环逐页串行调用 LLM, * 单 chunk N 个 page 就要串行 N 次 LLM 调用——一个卡超时整条流水线停摆。 * 改为受此 Semaphore 控制的并行,默认 3。结合 maxParallelRawMaterials × maxParallelChunks * × maxParallelPhaseBPages = 3 × 5 × 3 = 45 的理论最大并发,实际按 LLM 限流为准。 */ private int maxParallelPhaseBPages = 3; /** 注入 agent prompt 的最大字符数 */ private int maxContextChars = 10000; /** 单个原始材料最多生成的 Wiki 页面数 */ private int maxPagesPerRaw = 15; /** 上传后是否自动触发处理 */ private boolean autoProcessOnUpload = true; /** 上传文件存储目录 */ private String uploadDir = "./data/wiki-uploads"; /** 目录扫描最大文件数 */ private int maxScanFiles = 500; /** 扫描时跳过大于此大小的文件(字节),默认 50MB */ private long maxScanFileSize = 50 * 1024 * 1024; /** * Wiki LLM 重试最大尝试次数(含首次)。 *
* RFC-012 M1:旧实现无最大次数,遇到 nginx 504 这种"反复瞬时"错误会永远重试。 * 设为 5 后单 chunk 最多走 5 轮,配合 llmMaxTotalDurationMs 共同保证有界停止。 */ private int llmMaxAttempts = 5; /** * Wiki LLM 重试总耗时上限(毫秒),从首次调用开始计时。 *
* RFC-012 M1:单 chunk LLM 调用 + 重试的硬封顶,超过即放弃,让该 chunk 进入 failed 计数。 * 默认 4 分钟。 */ private long llmMaxTotalDurationMs = 240_000; /** * RFC-047 P1: Max pages per BatchCreate LLM call. * Pages planned by route are chunked into sub-batches of this size; * a local liveIndex is updated between sub-batches so later pages can * link to earlier ones created in the same chunk. * Default 2: keeps total output tokens well under typical provider caps * (~2k–3k completion tokens) so the FILE-block JSON doesn't get truncated * mid-object. Raising this risks unparseable JSON skips on long content. */ private int batchCreatePageSize = 2; /** * RFC-047: Minimum chunk length (chars) for the chunk-fallback mechanism. * If route returns 0 create+update entries and the chunk exceeds this threshold, * an overview page is auto-injected so no substantial content is silently dropped. * Chunks shorter than this (e.g. TOC lines, blank pages) are allowed to produce nothing. */ private int chunkFallbackMinChars = 200; /** * 是否启用两阶段消化(路由 → 逐页 merge)。 *
* RFC-012 M2:true 时单 chunk 的 LLM 输出量大幅缩减,避免 nginx 60s 网关超时。 * 默认 true(M2 上线);遇问题可在 application.yml 配 mate.wiki.use-two-phase-digest=false 回退到旧行为。 */ private boolean useTwoPhaseDigest = true; // ==================== RFC-011: Embedding ==================== /** 嵌入模型名称(DashScope) */ private String embeddingModel = "text-embedding-v3"; /** 嵌入批量大小(一次 API 调用处理多少 chunk) */ private int embeddingBatchSize = 16; /** * Embedding 模型单段最大字符数,超过则子段拆分 + 向量均值。 *
* 默认 6000(中文安全值,对应 ~4000 token,远小于 text-embedding-v3 的 8192 上限)。 * 纯英文场景可调大到 7500;其他 embedding 模型切换时按该模型的 token 限制调整。 */ private int embeddingMaxChars = 6000; /** * Expected embedding-input format version. The authoritative source is * the builder's {@code CURRENT_INPUT_VERSION} constant; this property * exists for staged rollouts and ops overrides. *
* Behavior on startup: *
* Set too low and a transient blip aborts a healthy pass; set too
* high and the user waits forever on a clearly-broken provider.
* Default 5 covers most real outages while tolerating a couple of
* isolated 5xx hiccups.
*/
private int embeddingConsecutiveFailureThreshold = 5;
/** 混合搜索默认模式:keyword / semantic / hybrid */
private String searchDefaultMode = "hybrid";
/**
* Minimum trimmed char length for a user message before per-turn wiki
* retrieval kicks in. Short messages like "继续" / "嗯" / "OK" carry no
* semantic signal and the retriever falls back to whichever pages happen
* to dominate the index, polluting the prompt with off-topic content.
*/
private int relevantContextMinQueryLength = 3;
/**
* Relative score floor for relevant-wiki hits. A hit is dropped when its
* score is below {@code topHit.score * this ratio}, so a single strong
* match does not drag in low-relevance tail pages alongside it. Set to
* 0 to disable.
*/
private double relevantContextMinRelativeScore = 0.5;
// ==================== RFC-031: Light processing tiers ====================
/** Whether to auto-dispatch a LIGHT_ENRICH job after heavy ingest completes */
private boolean lightEnrichEnabled = true;
/** Delay before light enrichment starts (ms) */
private long lightEnrichDelayMs = 2000;
/**
* Minimum ratio of enriched content length to original content length.
* If the LLM returns text shorter than this ratio, the enrichment is rejected.
*/
private double wikilinkMinContentRatio = 0.5;
/** Maximum characters for local repair single-page regeneration */
private int localRepairMaxChars = 8000;
/**
* Whether to run a document-level analysis pass before routing.
* When enabled, a single LLM call produces a concept map (topics + key_concepts)
* that is injected into every chunk's route prompt, giving the router global
* awareness of the document structure and reducing concept omissions.
* Adds ~1 LLM call and 10-20s per raw material.
*/
private boolean useDocumentAnalysis = true;
/**
* Max characters of document text fed to the analysis pass.
* Larger values improve coverage but increase prompt tokens.
* Default 15000 covers most documents while staying well within model limits.
*/
private int documentAnalysisSampleChars = 15000;
/**
* RFC-051 PR-6b: route-phase output binding. When {@code true}, the route
* LLM call uses a Spring AI {@code BeanOutputConverter
* Default {@code false} keeps existing behavior on first upgrade. Flip on
* once you've validated the route prompt against the models you actually
* run (DashScope / OpenAI / Anthropic / DeepSeek tend to be fine; Ollama
* and weaker models may need the fallback).
*/
private boolean useStructuredRoute = false;
/**
* RFC-051 follow-up: how many pages the enrich service packs into a single
* LLM call. {@code 1} (default) reproduces the legacy behavior of one
* call per page. {@code 5}–{@code 10} is reasonable for most chat models;
* weaker locally-served models may need to stay at 1.
*
* Larger batches reduce LLM cost roughly proportional to the batch size,
* but each batch's prompt grows linearly with the included page bodies,
* so very long pages still benefit from single-page mode. Pages exceeding
* {@link #enrichBatchPerPageMaxChars} are excluded from the batch and
* enriched individually.
*/
private int enrichBatchSize = 1;
/**
* RFC-051 follow-up: per-page content cap when packing pages into an
* enrich batch. Pages whose body exceeds this size fall through to a
* single-page enrich call so the batch prompt stays bounded. The cap
* applies only to the prompt; the applier always sees full content.
*/
private int enrichBatchPerPageMaxChars = 3000;
/**
* RFC-051 §9.4: replace the legacy flat 0.15 relation boost with a
* normalized score per query, scaled by {@link #relationBoostLambda}.
* Default {@code false} keeps the legacy ranking; flip on after
* validating against your retrieval test set.
*
* Why it matters: the flat 0.15 was bigger than typical RRF scores
* (~0.02–0.05), so boosted neighbors routinely leapfrogged real RRF
* hits. Normalization keeps boost on the same scale as fused scores.
*/
private boolean useNormalizedRelationBoost = false;
/**
* RFC-051 §9.4: maximum boost contribution from the relation pass when
* {@link #useNormalizedRelationBoost} is on. Each boosted candidate
* gets {@code (rawRelationScore / maxRawRelationScore) * lambda} added
* to its fused score. Default {@code 0.05} is roughly the size of a
* top-3 RRF score, so a max-relation neighbor competes evenly with a
* top-3 RRF hit but doesn't dominate it.
*/
private double relationBoostLambda = 0.05;
}