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 пайплайны живут в отдельных пакетах режимов.
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"]
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.
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
app.Runподгружает.env, определяет locale и настраиваетslog.newCLIстроитurfave/cliдерево изcommandSpec.bind*InvocationсобираетcommandInvocationс нормализованными options.appRuntime.executeсоздаётcommandSession, progress plan и сигнал-совместимый context.commandSession.ensureInputOpenedоткрывает файл или материализуетstdinодин раз и держитinput.Handleживым до конца run/REPL.withChatInput/withChatAndEmbeddingInputсоздают клиентов и вызывают mode package;hybrid,tools --ragиragидут через ветку с embedder.self-updateне открывает input и не создаёт LLM-клиентов: runtime передаёт build-time version и progress meter вinternal/selfupdate.writeResultформатирует markdown-ответ и печатает его вstdout.- При
--interactionruntime открывает interaction I/O и крутит общий follow-up REPL поверх mode-specific interactive session.
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.
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 по ошибке.
internal/verbose задаёт stage-based progress contract:
commandSpec.templateописывает stages и slots;verbose.Planпревращает их вMeter;- конкретные режимы only report progress по ключам стадий, не зная о рендеринге.
Это позволяет тестировать progress отдельно от business logic.
internal/localizeсобирает зарегистрированные пакетами встроенные TOML-каталоги в общий bundle и даётT()для текстов CLI.internal/loggingконфигурируетslogи укорачивает source paths относительно корня проекта.
hybrid строится поверх тех же retrieval и tool-calling кирпичей, что уже есть в tools --rag и rag, но добавляет обязательный semantic seed до первого model turn.
Реальный pipeline:
- Через
internal/tools.PrepareSessionзаранее поднимается тот же tool registry, что и дляtools --rag. - До первого LLM turn
internal/aitools/seedвыполняет fused semantic seed: несколько deterministicsearch_ragвызовов по variants исходного вопроса. - Search phase и synthetic exact reads по strongest hit'ам добавляются в историю как два preloaded assistant/tool batch.
- Общий orchestration loop из
internal/toolsпозволяет модели оценить preloaded retrieval, дочитать контекст через file-tools и при необходимости повторитьsearch_ragс новым запросом. - В финале
internal/hybridдописываетSources:с provenance-aware секциямиVerifiedиRetrieved. - При
--interactiondirect-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.
По умолчанию 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:
- Создаётся tools session с
systemиuserсообщениями. - При
--ragзаранее выполняется build/load retrieval index черезinternal/ragcore. - В каждом turn отправляется chat request с tool definitions.
- Если модель запросила tool calls,
toolLoopStateвыполняет их локально. - Результаты возвращаются как
role=toolсообщения в едином JSON envelope с полямиresultиmeta. - Loop отслеживает duplicate calls, already-seen строки и уже перечисленные пути, а также no-progress runs.
- При зацикливании включаются защитные ветки: retry without tools, stop-calls prompt, forced finalization.
- Режим завершает работу, когда получает содержательный финальный text answer.
- При
--interactionпакет клонируетSession/toolLoopStateна каждый follow-up turn и commit-ит state только после успешного ответа.
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"]
Реальный pipeline:
- Вход спулится во временный файл и получает hash с учётом параметров индекса.
- Индекс грузится из кэша или строится заново.
- Выполняется fused semantic seed по нескольким deterministic variants исходного вопроса.
- Fused retrieval ранжирует кандидаты по сходству и lexical overlap.
- Выбираются
final-kevidence chunks. - LLM отвечает только по evidence.
- В ответ добавляется секция
Sources:. - При
--interactionподготовленныйPreparedSearchпереиспользуется для follow-up вопросов, а synthesis prompt получает предыдущий Q/A transcript как fallible context.
Файловые артефакты индекса:
manifest.jsonchunks.jsonlembeddings.jsonl
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"]
Реальный pipeline:
resolveChunkLengthберёт явный--lengthили пытается автоопределить контекст модели.SplitByApproxTokensрежет текст по строкам с approximate token budget.runMapParallelзапускает map-запросы к LLM параллельно.- Пустые/
SKIPответы отбрасываются, оставшиеся факты нормализуются. fanInReduceсхлопывает результаты батчами до одного блока фактов.- Генерируется финальный draft-answer.
selfRefineделает critique и, если нужно, refine проход.- При
--interactionfollow-up turn заново прогоняет тот же map pipeline, но вопрос заранее contextualize-ится предыдущими Q/A.
Когда режим полезен:
- embeddings endpoint или tool calling недоступны;
- длинный текст всё равно нужно прогнать чанками как fallback-сценарий.
Компромиссы:
- между чанками теряется часть глобального контекста;
- на слабых моделях качество critique/refine может быть неровным;
- на больших входах такой прогон легко оказывается самым медленным из доступных вариантов.
self-update — maintenance command, а не mode обработки текста. Он живёт в отдельном пакете, чтобы не смешивать CLI wiring и release-update policy.
Реальный pipeline:
- Пакет проверяет, что текущая версия — released semver, а не
dev. - Через GitHub Releases source выбирается последний stable release
borro/ragcli. - Для выбранного релиза ищется asset под текущие
GOOS/GOARCHи обязательныйchecksums.txt. --checkостанавливается после этапа discovery и возвращает статус без записи на диск.- Обычный
self-updateскачивает asset иchecksums.txt, валидирует checksum и только после этого атомарно заменяет текущий бинарь. - Ошибки checksum, отсутствующего asset-а и неполного rollback-а мапятся в user-facing сообщения без привязки к GitHub API деталям.
| Артефакт | Кто создаёт | Зачем | 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 |
Публичным поведением проекта считаются:
- 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.
- Новый режим добавляется через
internal/<mode>+ регистрацию вinternal/app/commandspec.go. - Новая общая AI-tool функциональность сначала оценивается на перенос в
internal/aitoolsили соответствующий доменный пакетinternal/aitools/<domain>, а не в отдельный режим. - Изменения CLI-описания в
internal/appдолжны сопровождаться обновлениемdoc/requirements.md,README.mdи package-level README соответствующих пакетов.