Перейти к содержанию

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-372MSSQL_W_DEFAULT + GetMssqlW() + _mssqlWCache (60s TTL). Используется в inline DocsSearchMssql — путь 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
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 конкретного документа.

  1. Каждый файл должен иметь frontmatter (сейчас 98.5% coverage)
  2. Tags = ключевые слова для поиска (не категории, а поисковые термины)
  3. Не дублируй частые слова из общего лексикона (cache, auth, tokens) — они приводят к TAGS-bias
  4. 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/

Связанные