Skip to content

Latest commit

 

History

History
288 lines (220 loc) · 17.6 KB

File metadata and controls

288 lines (220 loc) · 17.6 KB

Архитектура ragcli

1. Сводка

ragcli построен как CLI-оболочка над набором режимов обработки текста. Архитектурно проект разделён на:

  • тонкую точку входа cmd/ragcli;
  • orchestration-слой internal/app;
  • mode-пакеты hybrid, tools, rag, map;
  • shared infrastructure: ragcore, llm, input, retrieval, aitools/*, selfupdate, verbose, localize, logging.

Главный принцип: internal/app связывает CLI, input lifecycle, создание клиентов и вывод результата, а domain-specific пайплайны живут в отдельных пакетах режимов.

2. Слои и зависимости

flowchart LR
    main["cmd/ragcli"] --> app["internal/app"]
    app --> input["internal/input"]
    app --> llm["internal/llm"]
    app --> verbose["internal/verbose"]
    app --> localize["internal/localize"]
    app --> logging["internal/logging"]
    app --> hybrid["internal/hybrid"]
    app --> tools["internal/tools"]
    app --> rag["internal/rag"]
    app --> ragcore["internal/ragcore"]
    app --> map["internal/map"]
    app --> selfupdate["internal/selfupdate"]
    hybrid --> tools
    hybrid --> ragcore
    tools --> ragcore
    rag --> ragcore
    ragcore --> retrieval["internal/retrieval"]
    tools --> aitools["internal/aitools"]
    tools --> aitoolsfiles["internal/aitools/files"]
    tools --> aitoolsrag["internal/aitools/rag"]
    hybrid --> aitoolsseed["internal/aitools/seed"]
    aitoolsseed --> aitoolsrag
    aitoolsseed --> ragcore
    llm --> goopenai["go-openai"]
Loading

Правила зависимостей

  • cmd/ragcli не содержит логики кроме вызова app.Run.
  • internal/app знает про все режимы и shared packages; режимы не знают про CLI.
  • internal/rag остаётся mode-пакетом standalone retrieval и использует internal/ragcore как shared search/index/fusion слой.
  • internal/ragcore использует internal/retrieval как общее ядро файлового retrieval и больше не знает о tool contracts.
  • internal/tools оркестрирует tool calling, а общий registry/API делегирует internal/aitools; файловый домен живёт в internal/aitools/files, retrieval-domain tool — в internal/aitools/rag.
  • internal/hybrid переиспользует shared tool session из internal/tools, а fused seed/history собирает через internal/aitools/seed.
  • internal/map не зависит от retrieval и tool calling.
  • internal/selfupdate изолирует GitHub Releases discovery, checksum validation и безопасную замену бинаря для ragcli self-update.

3. Жизненный цикл запуска

sequenceDiagram
    participant User
    participant Main as cmd/ragcli
    participant App as internal/app
    participant CLI as urfave/cli
    participant Runtime as commandSession
    participant Mode as mode package

    User->>Main: запуск команды
    Main->>App: Run(args, stdout, stderr, stdin, version)
    App->>App: load .env, detect locale, configure logging
    App->>CLI: build root command + subcommands
    CLI->>Runtime: bind commandInvocation
    Runtime->>Runtime: create context, reporter, plan
    Runtime->>Runtime: open input once, create LLM clients
    Runtime->>Mode: execute selected pipeline
    Mode-->>Runtime: result string or interactive session / error
    Runtime->>App: format output
    App-->>User: stdout result, stderr error/debug/verbose
    Runtime->>Runtime: optional follow-up REPL
Loading

Ключевые runtime-этапы

  1. app.Run подгружает .env, определяет locale и настраивает slog.
  2. newCLI строит urfave/cli дерево из commandSpec.
  3. bind*Invocation собирает commandInvocation с нормализованными options.
  4. appRuntime.execute создаёт commandSession, progress plan и сигнал-совместимый context.
  5. commandSession.ensureInputOpened открывает файл или материализует stdin один раз и держит input.Handle живым до конца run/REPL.
  6. withChatInput / withChatAndEmbeddingInput создают клиентов и вызывают mode package; hybrid, tools --rag и rag идут через ветку с embedder.
  7. self-update не открывает input и не создаёт LLM-клиентов: runtime передаёт build-time version и progress meter в internal/selfupdate.
  8. writeResult форматирует markdown-ответ и печатает его в stdout.
  9. При --interaction runtime открывает interaction I/O и крутит общий follow-up REPL поверх mode-specific interactive session.

4. Общие абстракции

4.1 input.Source

input.Source — общий wire-object между orchestration-слоем и режимами:

  • Descriptor — metadata входа: kind (stdin / file / directory), исходный path и список файлов корпуса.
  • SnapshotPath() — фактический path до materialized snapshot, который можно переоткрывать в downstream-пайплайнах.
  • Open() — повторно открывает snapshot как io.ReadCloser без хранения живого reader внутри Source.
  • Методы InputPath(), DisplayName(), BackingFiles() и IsMultiFile() скрывают различия между file, directory и stdin.
  • Поверх concrete type пакет даёт capability-интерфейсы SourceMeta, SnapshotSource и FileBackedSource для mode-пакетов.

Эта абстракция позволяет всем режимам одинаково работать с --path, compatibility alias --file и stdin.

4.2 LLM adapters

internal/llm даёт два интерфейса:

  • ChatAutoContextRequester — chat completion + auto context length resolve;
  • EmbeddingRequester — embeddings API.

Поверх go-openai пакет добавляет:

  • retry с exponential backoff;
  • request/embedding metrics;
  • proxy configuration;
  • auto-detect контекста модели через LM Studio models endpoint или probe по ошибке.

4.3 Progress model

internal/verbose задаёт stage-based progress contract:

  • commandSpec.template описывает stages и slots;
  • verbose.Plan превращает их в Meter;
  • конкретные режимы only report progress по ключам стадий, не зная о рендеринге.

Это позволяет тестировать progress отдельно от business logic.

4.4 Локализация и логирование

  • internal/localize собирает зарегистрированные пакетами встроенные TOML-каталоги в общий bundle и даёт T() для текстов CLI.
  • internal/logging конфигурирует slog и укорачивает source paths относительно корня проекта.

5. Пайплайны режимов

5.1 hybrid

hybrid строится поверх тех же retrieval и tool-calling кирпичей, что уже есть в tools --rag и rag, но добавляет обязательный semantic seed до первого model turn.

Реальный pipeline:

  1. Через internal/tools.PrepareSession заранее поднимается тот же tool registry, что и для tools --rag.
  2. До первого LLM turn internal/aitools/seed выполняет fused semantic seed: несколько deterministic search_rag вызовов по variants исходного вопроса.
  3. Search phase и synthetic exact reads по strongest hit'ам добавляются в историю как два preloaded assistant/tool batch.
  4. Общий orchestration loop из internal/tools позволяет модели оценить preloaded retrieval, дочитать контекст через file-tools и при необходимости повторить search_rag с новым запросом.
  5. В финале internal/hybrid дописывает Sources: с provenance-aware секциями Verified и Retrieved.
  6. При --interaction direct-context fast path остаётся history-based chat path, а retrieval/tool path переиспользует baseline tools conversation и умеет /reset.

Почему aitools/* и ragcore разведены:

  • общий AI-tool registry и контракты должны переиспользоваться между режимами;
  • файловый домен удобно развивать отдельно от orchestration loop и отдельно от будущих не-файловых tools;
  • retrieval-domain tools удобно держать в tool-layer рядом с другими AI tools, но поверх того же базового aitools.Registry;
  • каждый concrete tool можно подключать в разных сочетаниях через общий aitools.Registry.

5.2 tools

По умолчанию tools не строит retrieval index и не загружает файл в prompt целиком. Вместо этого режим оркестрирует диалог модели с локальными file-tools: list_files, search_file, read_lines, read_around.

Если передан --rag, режим до первого LLM turn строит или загружает тот же локальный embeddings index, что использует rag, и добавляет semantic retrieval tool search_rag.

Реальный pipeline:

  1. Создаётся tools session с system и user сообщениями.
  2. При --rag заранее выполняется build/load retrieval index через internal/ragcore.
  3. В каждом turn отправляется chat request с tool definitions.
  4. Если модель запросила tool calls, toolLoopState выполняет их локально.
  5. Результаты возвращаются как role=tool сообщения в едином JSON envelope с полями result и meta.
  6. Loop отслеживает duplicate calls, already-seen строки и уже перечисленные пути, а также no-progress runs.
  7. При зацикливании включаются защитные ветки: retry without tools, stop-calls prompt, forced finalization.
  8. Режим завершает работу, когда получает содержательный финальный text answer.
  9. При --interaction пакет клонирует Session/toolLoopState на каждый follow-up turn и commit-ит state только после успешного ответа.

5.3 rag

flowchart TD
    A["input.Source"] --> B["SpoolSource + hash"]
    B --> C{"load cached index?"}
    C -- yes --> D["manifest/chunks/embeddings"]
    C -- no --> E["chunk source"]
    E --> F["embed chunks"]
    F --> G["persist index"]
    D --> H["fused seed queries"]
    G --> H
    H --> I["fuse + rerank"]
    I --> J["select evidence"]
    J --> K["synthesize answer"]
    K --> L["append Sources"]
Loading

Реальный pipeline:

  1. Вход спулится во временный файл и получает hash с учётом параметров индекса.
  2. Индекс грузится из кэша или строится заново.
  3. Выполняется fused semantic seed по нескольким deterministic variants исходного вопроса.
  4. Fused retrieval ранжирует кандидаты по сходству и lexical overlap.
  5. Выбираются final-k evidence chunks.
  6. LLM отвечает только по evidence.
  7. В ответ добавляется секция Sources:.
  8. При --interaction подготовленный PreparedSearch переиспользуется для follow-up вопросов, а synthesis prompt получает предыдущий Q/A transcript как fallible context.

Файловые артефакты индекса:

  • manifest.json
  • chunks.jsonl
  • embeddings.jsonl

5.4 map

flowchart TD
    A["input.Source"] --> B["SplitByApproxTokens"]
    B --> C["parallel map calls"]
    C --> D["prepare / filter facts"]
    D --> E["fan-in reduce iterations"]
    E --> F["final answer generation"]
    F --> G["self critique"]
    G --> H["self refine or keep draft"]
    H --> I["stdout result"]
Loading

Реальный pipeline:

  1. resolveChunkLength берёт явный --length или пытается автоопределить контекст модели.
  2. SplitByApproxTokens режет текст по строкам с approximate token budget.
  3. runMapParallel запускает map-запросы к LLM параллельно.
  4. Пустые/SKIP ответы отбрасываются, оставшиеся факты нормализуются.
  5. fanInReduce схлопывает результаты батчами до одного блока фактов.
  6. Генерируется финальный draft-answer.
  7. selfRefine делает critique и, если нужно, refine проход.
  8. При --interaction follow-up turn заново прогоняет тот же map pipeline, но вопрос заранее contextualize-ится предыдущими Q/A.

Когда режим полезен:

  • embeddings endpoint или tool calling недоступны;
  • длинный текст всё равно нужно прогнать чанками как fallback-сценарий.

Компромиссы:

  • между чанками теряется часть глобального контекста;
  • на слабых моделях качество critique/refine может быть неровным;
  • на больших входах такой прогон легко оказывается самым медленным из доступных вариантов.

5.5 self-update

self-update — maintenance command, а не mode обработки текста. Он живёт в отдельном пакете, чтобы не смешивать CLI wiring и release-update policy.

Реальный pipeline:

  1. Пакет проверяет, что текущая версия — released semver, а не dev.
  2. Через GitHub Releases source выбирается последний stable release borro/ragcli.
  3. Для выбранного релиза ищется asset под текущие GOOS/GOARCH и обязательный checksums.txt.
  4. --check останавливается после этапа discovery и возвращает статус без записи на диск.
  5. Обычный self-update скачивает asset и checksums.txt, валидирует checksum и только после этого атомарно заменяет текущий бинарь.
  6. Ошибки checksum, отсутствующего asset-а и неполного rollback-а мапятся в user-facing сообщения без привязки к GitHub API деталям.

6. Файловые и временные артефакты

Артефакт Кто создаёт Зачем Lifecycle
temp file from stdin internal/input дать режимам path + random access удаляется в Handle.Close()
spooled source for rag internal/retrieval стабильный hash и повторное чтение удаляется после завершения run
index dir internal/ragcore кэш embeddings и metadata живёт до TTL cleanup

7. Что считать публичным поведением

Публичным поведением проекта считаются:

  • CLI-команды, флаги, env-переменные и defaults;
  • разделение stdout/stderr;
  • наличие локализации ru/en;
  • semantics режимов hybrid, tools, rag, map;
  • поведение self-update для binary installs из GitHub Releases;
  • формат источников в ответах rag.

Не считаются жёстким public API:

  • точные внутренние структуры commandInvocation, pipelineStats, toolLoopState;
  • конкретные progress labels;
  • конкретные названия внутренних helper functions.

8. Куда расширять систему

  • Новый режим добавляется через internal/<mode> + регистрацию в internal/app/commandspec.go.
  • Новая общая AI-tool функциональность сначала оценивается на перенос в internal/aitools или соответствующий доменный пакет internal/aitools/<domain>, а не в отдельный режим.
  • Изменения CLI-описания в internal/app должны сопровождаться обновлением doc/requirements.md, README.md и package-level README соответствующих пакетов.