AI Search — Tuning Guide¶
Architecture: cascade-fts-spec.md | Runbook: runbook.md | Pipeline: ../ai/architecture/docs-pipeline-map.md | Last bias fix:
projects/1f-agent/initiatives/e2e-2026-05-07-fixes/ai-search-ranking-bias-handoff.md
Главное правило: меняй веса через SettingsCustom override, не через .cs¶
С 2026-05-07 веса cascade RRF — runtime-tunable через SettingsCustom["anfisa_docs_search_weights"]. Дефолты в .cs остаются production-стабильные, изменяются только через override. Никаких deploy не требуется — TTL кэша 60 секунд, новое значение подхватится автоматически.
Почему так: dropping веса hardcoded ломает быстрый rollback и tuning loop. Один SS-deploy ≈ 10 минут (compile + prewarm + verify). Один UPDATE через API form 72 = секунды + 60s TTL. До добавления override mechanism любой эксперимент с весами требовал deploy → нагрев кэша → ещё deploy для отката.
Где живёт mechanism¶
- Module:agent-tools-docs (SS 612) —
Module-agent-tools-docs.cs:330-372—MSSQL_W_DEFAULT+GetMssqlW()+_mssqlWCache(60s TTL). Используется в inlineDocsSearchMssql— путь Pipeline Prefetch + Анфисиных тулов. - Module:ai-search-docs-mssql (SS 589) —
Module-ai-search-docs-mssql.cs:21-62— instance-методыMssqlDocsEngine.GetMssqlW(). Используется черезModule:ai-search-docsобёртку (вторичный путь).
Обе точки читают тот же ключ SettingsCustom["anfisa_docs_search_weights"] — single source of truth.
Как изменить веса¶
Через 1f CLI (1f settings get/set) или прямой curl form 72 action. Partial JSON допустим — какие сигналы перечислены, те и переопределяются. Дефолты в .cs для остальных.
# Прочитать текущие
PAT=$(security find-generic-password -s '1forma-pat' -w)
curl -s -X POST -H "1F-Pat: $PAT" -H "Content-Type: application/json" \
"https://{host}/api-core/data-source-form/72/data" \
-d '{"select":["[ID]","[Value]"],"startRow":1,"endRow":1,"sortModel":[{"colId":"[ID]","sort":"desc"}],"filterModel":{"SettingsCustom.[Key]":{"filterType":"text","type":"equals","filter":"anfisa_docs_search_weights"}},"groupKeys":[],"rowGroupCols":[]}' \
| jq '.data.data[0].data."[Value]".value'
# Обновить (id из READ выше)
ID=3150
NEW_VALUE='{"FREETEXT_TAGS":2.5,"FREETEXT_BODY":2.5}' # partial — остальные дефолтные
PAYLOAD=$(python3 -c "
import json
v = json.dumps({'data': {'[ID]': {'value': $ID, 'colName': 'ID', 'isPk': True, 'lastValue': None}, '[Value]': {'value': '$NEW_VALUE', 'colName': 'Значение', 'lastValue': None}}})
print(json.dumps({'updateValue': v, 'action': 'update', 'entityIds': [$ID], 'formName': 'custom-settings'}))
")
curl -s -w 'HTTP:%{http_code}\n' -X POST -H "1F-Pat: $PAT" -H "Content-Type: application/json" \
"https://{host}/api-core/data-source-form/72/action" -d "$PAYLOAD"
TTL 60s — следующий probe после истечения cache MISS подхватит новое значение. Чтобы ускорить — redeploy SS 612 (compile сбросит кэш) или подождать минуту.
Rollback¶
Один шаг: DELETE SettingsCustom WHERE [Key]='anfisa_docs_search_weights' через form 72 → fallback на MSSQL_W_DEFAULT в коде. Производственные дефолты — последний known-good baseline до bias fix.
Production weights (2026-05-07+)¶
Defaults в коде (MSSQL_W_DEFAULT)¶
| Signal | Weight | Column | Что делает |
|---|---|---|---|
| CONTAINS_FM_OR | 3.0 | Frontmatter | Exact keyword в frontmatter (OR-join всех терминов) |
| CONTAINS_TAGS_OR | 3.0 | Tags | Exact keyword в extracted tags (OR-join) |
| CONTAINS_BODY | 1.0 | Body | Exact keyword в тексте (smart-AND, top-3 longest tokens) |
| FREETEXT_TAGS | 4.0 | Tags | Morphological match в tags |
| FREETEXT_BODY | 1.0 | Body | Morphological match в теле |
| PAGERANK | 2.0 | DocsPageRank | Авторитетность по ссылкам (top-N candidates) |
Соотношение TAGS:BODY = 10:2 = 5:1 (в дефолтах).
Production override (Variant A)¶
С 2026-05-07 на проде и dev активен override:
{
"CONTAINS_FM_OR": 1.5,
"CONTAINS_TAGS_OR": 1.5,
"CONTAINS_BODY": 2.0,
"FREETEXT_TAGS": 2.0,
"FREETEXT_BODY": 2.0,
"PAGERANK": 1.5
}
Соотношение TAGS:BODY = 5:4. Закрывает Bug #9 — localization/data-flow.md системно top-1 на CACHE/Redis/JWT-related queries (детали — research note).
Когда менять веса — decision tree¶
| Симптом | Корень | Действие |
|---|---|---|
| Один и тот же шумный документ top-1 на разных query (tag-rich, body не релевантный) | TAGS-bias: документ имеет tag из общего лексикона (cache, tokens, auth) — выигрывает через CONTAINS_TAGS_OR/FREETEXT_TAGS |
Снизить CONTAINS_TAGS_OR или FREETEXT_TAGS. Если уже на 1.5/2.0 — углубиться в case studies ниже |
Релевантный документ виден в docsearch query локально, но не в Анфисе |
Recall-проблема. Cascade FTS не возвращает документ в top-100 ни одного сигнала | Проверить tags/frontmatter документа. Возможно отсутствуют ключевые keywords. Re-index docs |
| Все top-N — из одной секции, нерелевантные | Phase 0 не покрывает шумную секцию ИЛИ Phase 1 box weights перекошены | Добавить секцию в MSSQL_SECTION_DEMOTE с multiplier 0.3-0.5 |
| top-1 «правильный» но top-2..top-5 шум | RRF k параметр слишком сглажен | Уменьшить RRF_K (60 → 40). Делать через code patch, не через override (k не в SettingsCustom). Eval перед deploy ОБЯЗАТЕЛЕН |
| Авторитетные docs (с большим PR) внизу | PAGERANK weight слишком низкий | Поднять PAGERANK (1.5 → 2.5) |
| Документ с одним совпадающим словом в body + 5 в tags выигрывает над тем где 5 в body | Term-coverage не учитывается | Не починить через rebalance. Нужен post-RRF term-coverage penalty. См. fix_b в research note (отброшен из-за nDCG регрессии); если нужно — вернуть с custom floor + gold dataset rejudgement |
Bias case studies — известные паттерны¶
Реальные случаи из production (research note ai-search-ranking-bias.md § «Системность bias»):
CACHE/Redis/SmartScript → localization/data-flow.md¶
До fix (5:1). На запрос «CACHE SmartScript in-memory Redis distributed» top-1 = localization/data-flow.md. Документ имеет tags: [..., cache, ...] — выигрывает через 3 TAGS-сигнала (CONTAINS_FM_OR + CONTAINS_TAGS_OR + FREETEXT_TAGS = 10.0 cumulative weight) против документов с full body match (BODY = 2.0). Один matching tag забивает 5 body-coverage hits.
После fix (Variant A 5:4). top-1 = meta-api/dev-spec.md, top-2 = docs-pipeline-map.md, top-3 = runbook-redis-connection-exception.md. Localization уходит из top-5.
OAuth/JWT → chart-color-guideline.md (frontend design tokens)¶
В DB всего 2 документа с тегом tokens — один про дизайн-токены, другой про count_tokens. Дизайн-документ выигрывает по Frontmatter+Tags+FREETEXT_TAGS на любой токен-related запрос.
Симптом для будущих случаев: если top-1 — документ из узкоспециализированной секции (frontend/design-system/, regulations/templates/) на запрос про общий технический термин — это TAGS-bias. Phase 0 demote не срабатывает потому что секция не в blacklist'е.
Lua/Roslyn/Delegate → tuned¶
Похожий паттерн на других query — research note показал 5/7 acceptance failures на дефолтах.
Acceptance set (7 system queries)¶
Используется как regression-test любого изменения весов. Файл: docs/projects/ai-search/initiative-mssql-native/eval/test_cascade_fix_2026-05-07.py. Ожидание — top-3 содержит expect_top3_any[], top-1 ≠ expect_top1_not.
Eval workflow — обязательно перед deploy¶
Не меняй веса без eval. Локальный тест занимает 30 секунд, ловит регрессии до production.
Acceptance test (быстрый, 7 queries)¶
cd ~/repo/docs/projects/ai-search/initiative-mssql-native
python3 eval/test_cascade_fix_2026-05-07.py --acceptance
Сравнивает три config'а: prod (5:1), fix_a (FT-only override), fix_b (term-coverage). Чтобы протестировать свою новую конфигурацию — отредактируй W_FIX_A или добавь W_VARIANT_X в скрипт.
Full eval (213 gold queries, ~3 минуты)¶
python3 eval/test_cascade_fix_2026-05-07.py --eval
Метрики: nDCG@5, Hit@1. Baseline дефолтов = 0.563. Variant A: nDCG@5 ≈ 0.553 (-2.1%), Hit@1 +1pp. Acceptable trade-off.
Где данные¶
| Файл | Что |
|---|---|
eval/eval_queries_combined.txt |
213 запросов |
eval/eval_relevance_matrix.json + eval_relevance_new113.json |
gold relevance labels |
eval/test_cascade_v5.py |
базовая cascade имитация (общий движок) |
eval/test_cascade_fix_2026-05-07.py |
wrapper с конкретными configs (W_PROD, W_FIX_A) + 7 acceptance queries |
Подключение к данным¶
Eval ходит в Production MSSQL напрямую через pymssql (creds в Keychain db-mssql-prod-*). Это значит результаты на актуальных production данных. Для тестирования на dev — нужно создать db-mssql-dev-* ключи в Keychain или модифицировать get_prod_conn в test_cascade_v5.py.
Важно: dev DocsChunks отстаёт от production на дни из-за асинхронной индексации. Eval против dev — диагностика, против production — production verify.
Diagnostics workflow — как искать root cause bias¶
Шаг 1: Voet probe в production через Анфису¶
/clear
Анфиса, найди в документации: <ваш bias query>
Анфиса использует Pipeline Prefetch (intent=search_docs) → top-1 в её ответе.
Шаг 2: Найти Pipeline Prefetch perfLog¶
SELECT TOP 5 ObjectKey, LEFT(AdditionalInfo, 400) AS Info, Date
FROM AutomationScriptsLog
WHERE Date >= DATEADD(MINUTE, -10, GETDATE())
AND AdditionalInfo LIKE '%Pipeline prefetch:%'
ORDER BY Id DESC
Покажет top1=<path> и количество docs в pre-fetch.
Шаг 3: Поднять signal-level breakdown¶
Прямой SQL для каждого сигнала на конкретной query:
-- Signal 1: CONTAINS_FM_OR
SELECT TOP 10 dc.Path, ft.[RANK]
FROM CONTAINSTABLE(dbo.DocsChunks, Frontmatter, N'"term1" OR "term2" OR ...') ft
JOIN dbo.DocsChunks dc ON dc.Id=ft.[KEY]
WHERE dc.Frontmatter IS NOT NULL AND dc.Section NOT IN (N'scenarios',N'clients')
ORDER BY ft.[RANK] DESC
-- Signal 2: CONTAINS_TAGS_OR (заменить Frontmatter → Tags)
-- Signal 3: CONTAINS_BODY (use AND вместо OR, top-3 longest)
-- Signal 4: FREETEXT_TAGS (FREETEXTTABLE, query as-is)
-- Signal 5: FREETEXT_BODY (TOP 100)
-- Signal 6: PAGERANK (SELECT Path, PageRank FROM DocsPageRank WHERE Path IN (...))
Position в каждом result-set = rank в RRF (0-based). RRF score = weight / (60 + position + 1).
Шаг 4: Воспроизвести cascade в Python¶
import sys; sys.path.insert(0, 'eval')
import importlib.util
spec = importlib.util.spec_from_file_location('tcf', 'eval/test_cascade_fix_2026-05-07.py')
tcf = importlib.util.module_from_spec(spec); spec.loader.exec_module(tcf)
W_TEST = {"CONTAINS_FM_OR":1.5, "CONTAINS_TAGS_OR":1.5, ...} # тестируемые веса
import pymssql
conn = tcf.get_hd_conn(); cur = conn.cursor()
scores = tcf.cascade_run(cur, "<your query>", W_TEST)
top = tcf.topk(scores, 10)
print(top)
Шаг 5: Если расхождение между production и eval — проверить override mechanism¶
Production использует GetMssqlW() с TTL 60s. Если override свежий (<60s после API update) и _mssqlWCache ещё не истёк — возможен stale-кэш scenario. Forced cache reset = redeploy SS 612 (compile сбросит assembly).
Стационарный observability (НЕ через DIAG)¶
PerfLog уже содержит Pipeline prefetch: ... top1=<path> и signal counts (docsSearchMssql: 5 results, CONTAINS_FM_OR=10,...). Для ad-hoc диагностики — добавить временный LOG.AddAutomationLog в GetMssqlW / DocsSearchMssql (примеры в handoff документе bias fix). После расследования обязательно убрать перед production deploy — DIAG-логи зашумляют AutomationScriptsLog.
RRF Merge (k parameter)¶
RRF_SCORE = weight / (k + rank + 1)
| k | Эффект |
|---|---|
| 30 | Sharper — top-1 сильнее доминирует |
| 60 (current) | Balanced |
| 120 | Smoother — разница между позициями меньше |
Когда менять: если top-1 «побеждает» с маленьким перевесом и нужно дифференцировать → уменьшить k. Code patch only (не в SettingsCustom override). Eval ОБЯЗАТЕЛЕН.
Section Exclude (pre-RRF filter)¶
Полное подавление целых секций docs/ на этапе SQL WHERE — до RRF merge'а, в каждом из 5 FTS-сигналов (CONTAINS_FM_OR/TAGS_OR, CONTAINS_BODY, FREETEXT_TAGS/BODY). По умолчанию отсекает scenarios/ (внутренние сценарии библиотеки) и clients/ (клиентские конфиги) из общего ai_search Анфисы — они засоряют ответы. На клиентских стендах ставится [], потому что всё содержимое стенда = clients/{client}/... или scenarios/, и поиск должен там искать.
| Где живёт | Ключ SC | Формат | Default | TTL |
|---|---|---|---|---|
Module-ai-search-docs-mssql.cs::GetSectionExclude() |
anfisa_search_section_exclude |
JSON array (["section1","section2"]) |
["scenarios","clients"] |
5 min |
Реализация. BuildSectionFilter() параметризует placeholder'ы — not in (@sx0, @sx1, ...) + dict параметров; пустой массив → sectionFilter = "" (filter не применяется, никаких сломанных not in ()). 5 SQL-сигналов используют MergeWith(sectionParams, "pred"|"q", value) — injection-safe Dictionary merge.
Изменение. Через 1f settings get/set (UPSERT anfisa_search_section_exclude в form 72). TTL 5 минут — новое значение подхватится без deploy.
PAT=$(security find-generic-password -s '1forma-pat' -w)
# Прочитать текущее
curl -s -X POST -H "1F-Pat: $PAT" -H "Content-Type: application/json" \
"https://{host}/api-core/data-source-form/72/data" \
-d '{"select":["[ID]","[Value]"],"startRow":1,"endRow":1,"sortModel":[{"colId":"[ID]","sort":"desc"}],"filterModel":{"SettingsCustom.[Key]":{"filterType":"text","type":"equals","filter":"anfisa_search_section_exclude"}},"groupKeys":[],"rowGroupCols":[]}' \
| jq '.data.data[0].data."[Value]".value'
Decision tree:
| Стенд | Значение | Почему |
|---|---|---|
| Production | ["scenarios","clients"] |
Default. Иначе общие docs-запросы засоряются клиентскими конфигами и сценариями библиотеки |
| dev / dev-pg | (ключ не нужен — fallback к default) | Тестовая копия Production, поведение совпадает |
| Клиентский стенд | [] |
Содержимое стенда = clients/{client}/... + scenarios/ |
| Клиентский стенд с дополнительными шумовыми секциями | напр. ["press","blog"] |
Можно расширить по месту |
Phase 0 — Global Section Demote¶
Секции с низким signal-to-noise ratio демотируются для ВСЕХ запросов. Применяется ПОСЛЕ RRF merge.
| Section | Multiplier | Почему |
|---|---|---|
| press | 0.3 | Пресс-релизы = шум |
| blog | 0.3 | Блог-контент = поверхностный |
| academy | 0.3 | Учебные материалы = общие |
| website | 0.5 | Сайт = маркетинговый |
| admin-manual | 0.0 | Полностью исключено (мигрировано в domains/{X}/admin.md) |
Добавить section: MSSQL_SECTION_DEMOTE в Module-agent-tools-docs.cs:374-381. Code patch + deploy.
Открытое: research note рекомендует добавить regulations/templates → 0.3 (template-bias на multi-term query).
Phase 1 — Box-Aware Section Weights¶
Контекст-зависимые multipliers. Применяются когда contextBox передан в DocsSearch(..., contextBox). Сейчас — Анфиса передаёт contextBox только если subcategory.box ∈ {dev, crm, servicedesk, pm, mgmt, impl, platform-user}. Иначе Phase 1 не применяется.
| Box | domains | platform | reference | regulations | website | projects | scenarios | boxes |
|---|---|---|---|---|---|---|---|---|
| dev | 1.5 | 1.5 | 1.3 | 0.5 | 0.3 | 0.7 | — | — |
| crm | 1.2 | — | — | 0.5 | — | 0.7 | 1.3 | 1.5 |
| servicedesk | 1.5 | — | 1.3 | 0.5 | — | 0.7 | — | 1.2 |
| pm | 1.2 | — | — | 0.5 | — | 1.3 | — | 1.5 |
| mgmt | 0.7 | 0.5 | — | 1.5 | — | 1.2 | — | 1.3 |
| impl | 1.3 | — | 1.5 | — | — | 0.7 | — | 1.2 |
| platform-user | 1.5 | 1.2 | 1.3 | — | — | 0.7 | — | — |
Source: Module-agent-tools-docs.cs:383-413.
Когда менять: если users из box X не находят docs из section Y → увеличить вес Y для box X. Code patch.
PageRank Damping¶
PR(A) = (1 - d) + d × Σ (PR(Ti) / C(Ti))
| Parameter | Current | Effect |
|---|---|---|
| Damping (d) | 0.85 | Standard. 0.9 = больше вес ссылок. 0.5 = более uniform |
| Iterations | 20 | Converges за ~10 на 7K docs. 20 = margin of safety |
Link Type Weights¶
| Type | Weight | Tuning |
|---|---|---|
| navigator | 1.5 | Навигаторы = структурные карты, высокий intent |
| crosslink | 1.3 | Cross-domain = ценные связи |
| readme | 1.2 | Структурные ссылки |
| inline | 1.0 | Стандартные ссылки |
| glossary | 0.8 | Словарные, не семантические |
Spec: docs-pagerank-spec.md. Calculator: build_pagerank.py (cron 06:03 MSK).
Frontmatter Optimization¶
Tags = высокий вес сигналов TAGS-группы. Оптимизация tags — самый эффективный способ улучшить ranking конкретного документа.
- Каждый файл должен иметь frontmatter (сейчас 98.5% coverage)
- Tags = ключевые слова для поиска (не категории, а поисковые термины)
- Не дублируй частые слова из общего лексикона (
cache,auth,tokens) — они приводят к TAGS-bias - Section определяет Phase 0 demote и Phase 1 box weights → правильный section critical
---
section: domains
domain: ai
tags: [анфиса, agent, mcp, llm, prompt, tools]
---
Embedding Model (zvec-search, не cascade)¶
zvec-search — отдельный path (через docsearch CLI / docs-srv:3005), не используется в Анфисином runtime. Для context см. zvec-search-spec.md.
| Parameter | Current | Alternatives |
|---|---|---|
| Model | Qwen3-Embedding-8B | Cohere embed-v3, OpenAI text-embedding-3-large |
| Dimensions | 4096 | 1024 (OpenAI truncated, used in MSSQL indexer для cosine), 768 (smaller) |
| Reranker | Qwen3-Reranker-8B (Fireworks, cross-encoder) | Cohere rerank-v3.5 (legacy server.py), RRF-only (без cross-encoder) |
| Threshold | default | Increase = fewer but more precise results |
Cascade Search (tasks, не docs)¶
Tasks search использует свой cascade в Module-agent-tools-search-v2.cs / SS 493. Это отдельная механика (не делит код с docs cascade). Tuning tasks search — cascade-fts-spec.md.
История изменений¶
| Дата | Что | Где |
|---|---|---|
| 2026-05-07 | Variant A applied (CONTAINS_FM_OR/CONTAINS_TAGS_OR 3.0→1.5, CONTAINS_BODY 1.0→2.0, FREETEXT_TAGS 4.0→2.0, FREETEXT_BODY 1.0→2.0, PAGERANK 2.0→1.5). Override mechanism через SettingsCustom. Закрывает Bug #9 — TAGS-bias на CACHE/JWT/Lua queries | handoff |
| 2026-04-... | MSSQL cascade v4 production (grid search 4500 combos, baseline nDCG@5=0.563, best 0.640) | toolsearchdocs-mssql-spec.md |
| 2026-04-... | C# port (Module-agent-tools-docs.cs SS 612, Module-ai-search-docs-mssql.cs SS 589) |
csharp-cutover-pilot/ |
Связанные¶
- cascade-fts-spec.md — cascade architecture (tasks search, отдельная от docs)
- docs-chunks-mssql-spec.md — DocsChunks + indexer (где живут tags/frontmatter/body)
- context-aware-search-spec.md — section/box weighting детально
- docs-pagerank-spec.md — PageRank formula + indexer
- toolsearchdocs-mssql-spec.md — историческая спека внедрения MSSQL cascade
- end-to-end-pipeline.md — pipeline overview (write+read, 4 канала)
- runbook.md — troubleshooting индексер / latency / зависший cron
projects/1f-agent/initiatives/e2e-2026-05-07-fixes/ai-search-ranking-bias.md— research note (закрыто)projects/1f-agent/initiatives/e2e-2026-05-07-fixes/ai-search-ranking-bias-handoff.md— handoff с lessons learned