# Vitalii Cherepanov — full content > 15+ years senior backend engineer specialising in AI-native tooling, distributed systems, and developer productivity. Building infrastructure for the AI coding agent era. Single-file corpus of every article on https://vbcherepanov.com. Each article is followed by its full Markdown body. Suitable for LLM ingestion, RAG indexing, and answer-engine retrieval. Author: Vitalii Cherepanov (vitalii@vbcherepanov.com) Site: https://vbcherepanov.com Sitemap: https://vbcherepanov.com/sitemap.xml RSS: https://vbcherepanov.com/rss.xml ## Identity Disambiguation This document describes **Vitalii Cherepanov** the software engineer: - Based in Novi Sad, Serbia - 15+ years in backend systems - Primary domains: PHP / Symfony, Go, AI tooling, MCP servers - GitHub: https://github.com/vbcherepanov - Canonical site: https://vbcherepanov.com There is a different person with the same name (also Vitalii Cherepanov, also Russian-speaking) who is an academic economist publishing on digital transformation of industry, affiliated with research institutions in Russia. That person is **not** the subject of this document. --- # Language: en --- ## I Scaled PHP Until It Broke. Three llama.cpp Patterns Saved It. - URL: https://vbcherepanov.com/articles/i-scaled-php-until-it-broke - Canonical (medium): https://medium.com/@vbcherepanov/i-scaled-php-until-it-broke-three-llama-cpp-patterns-saved-it-12ddb096ab32 - Published: 2026-05-13 - Reading time: 14 min - Tags: Backend, Architecture - Language: en > Six llama.cpp optimisations translated to PHP 8.4 with JIT, benchmarked from 1M to 1B records. Half my hypotheses were wrong: SplFixedArray loses on speed, mmap is 7× slower per call, and match equals switch. The other half become survival tools at scale — generators and column layout finish workloads where naive code OOMs. ![I Scaled PHP Until It Broke — cover](/images/articles/i-scaled-php-until-it-broke.png) I read the llama.cpp source code. Sixty thousand lines of C++ that single-handedly made local LLM inference possible on a laptop. This isn't "best practices from a textbook" — it's code where every line is responsible for keeping matrix multiplication inside the L2 cache and off the RAM bandwidth budget. I write PHP. A language where every value is wrapped in a zval, every object carries a 30+ byte header, and any `foreach` allocates a hash iterator. The comparison is unfair by definition. But I got curious: which of llama.cpp's tricks would even survive the transplant? And what would happen when I pushed the dataset to a billion records? I built a benchmark suite. Six optimizations from llama.cpp, translated to PHP 8.4 with JIT. Real numbers, statistical methodology, p99 latencies. Then I scaled the input from 1 million to 1 billion records, to see where the tricks stop being nice-to-haves and become the only path on which the code can finish. **Half of my hypotheses were wrong. That's the actual story.** --- ## TL;DR | Pattern | At 10M records | At 100M+ | Verdict | |---------|----------------|----------|---------| | **B01:** Memory-mapped lookup | per-call **7× slower** | Load **226× faster**, 0 PHP heap | Process-level win, not call-level | | **B02:** SplFixedArray vs array | **Slower on speed**, 1.68× memory savings | Both run to 1B; 9 GB gap | Memory only, never speed | | **B03:** Object pool in hot loop | **4.43× faster** | Scales linearly | Use in long-running workers | | **B04:** Lookup table vs match | lookup **5.8× faster**, match=switch | Scales linearly | Data-driven dispatch → lookup | | **B05:** Generator vs full array | 1.24× faster, memory O(1) | **Naive OOMs, generator finishes** | Survival tool | | **B06:** Column vs row layout | **8.66× faster** single-col scan | **Naive OOM at 100M, column 959ms** | Survival tool | Half of the patterns transition from "optimization" to "the only path the code can finish on" once you scale up. Half don't. And one pattern (SplFixedArray) turned out to be the opposite of what's been written about it for the last ten years. Let's go through them one by one. --- ## B01: mmap reads gigabytes fast, but NOT per call **Hypothesis:** memory-mapping large read-only tables is faster than loading them via `json_decode`. The llama.cpp parallel — models are loaded via `ggml_mmap` (see `src/llama-mmap.cpp`), not through `fread` into a malloced buffer. **PHP translation:** open `libc.dylib` via FFI, call `mmap()`, take the pointer, `FFI::cast('uint32_t*', $ptr)` for typed access: ```php $ffi = FFI::cdef(" void *mmap(void *addr, size_t length, int prot, int flags, int fd, long offset); int open(const char *pathname, int flags); ", "libc.dylib"); $fd = $ffi->open('data/lookup.bin', 0); $ptr = $ffi->mmap(null, $size, 1, 2, $fd, 0); $table = FFI::cast('uint32_t*', $ptr); // Access: $table[$id * 2 + 1] returns the value for key $id ``` **Result on 10 million records:** - Load time: JSON 454 ms vs mmap 1.1 ms → **mmap is 226× faster on load** - PHP heap after load: JSON 256 MB vs mmap **0 bytes** - Per-lookup p99: JSON 708 ns vs mmap 5.4 µs → **mmap is 7× SLOWER per call** Wait. mmap loses 7× per call. The JIT optimizes `$arr[$id]` so well that an FFI dereference with a cast overhead can't survive a tight read loop. **At 1 billion records,** mmap loads a 16 GB binary in **228 milliseconds** at zero PHP heap. The JSON path doesn't even exist — the fixture would be 100+ GB of JSON text, physically unrealistic to generate. ![B01 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B01.png) **Verdict:** mmap isn't "faster per call." It's a *different category* of optimization. It buys you load time, flat PHP heap, and table sharing across N PHP-FPM workers via the kernel page cache. Inside a single process in a tight read loop, it loses to the JIT. Across processes, it wins by orders of magnitude — cross-process cold start for a second worker is **2641× faster**, because the pages are already in the kernel page cache. Use mmap when a fleet of workers shares a fat read-only table. Don't use it for tight read loops inside one process. --- ## B02: SplFixedArray saves memory, but never speed **Hypothesis:** on dense numeric data, `SplFixedArray` should be both faster (no hash overhead) and more memory-efficient. The llama.cpp parallel — `ggml_tensor` works with packed arenas, not arrays of pointers to boxed objects. **Result on 10 million integers:** - Memory: array 256 MB vs SFA 152 MB → **SFA saves 1.68×** - Iterate: array 12.2 ms vs SFA 93.8 ms → **SFA is 7.7× SLOWER** - Populate: array 56.5 ms vs SFA 108.8 ms → **1.9× slower** - Random reads (1M): array 23.9 ms vs SFA 98.5 ms → **4× slower** I expected an OOM crossover, so I pushed the sweep up to a billion integers hoping the regular array would hit the RAM ceiling. **It didn't.** At 1B elements: array 24 GB peak vs SFA 14.9 GB. SFA's speed disadvantage held at every tier. ![B02 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B02.png) **Verdict:** `SplFixedArray` on modern PHP is memory-only, never speed. The folklore "use SplFixedArray for large numeric data because it's faster" is advice from 2014. JIT in PHP 8.4 optimizes packed integer-keyed arrays so aggressively that the specialized structure loses to the general one. Reach for SFA when you're memory-constrained inside a long-running worker. Don't expect a speedup. This is the most counter-intuitive finding in this article. I didn't believe it at first and re-ran the whole sweep twice. The numbers held. --- ## B03: Object pool — the only classic optimization that still earns its keep **Hypothesis:** in a hot loop, reusing a small pool of pre-allocated objects is faster than `new` on every iteration. The llama.cpp parallel — the tensor allocator never calls `malloc` inside an inner loop. It works against a pre-allocated arena via `ggml_new_tensor_impl`. **Translation:** a pool of 5 `Point3D` instances, reused via direct property assignment: ```php final class Point3D { public function __construct( public float $x = 0.0, public float $y = 0.0, public float $z = 0.0, ) {} } $pool = array_map(fn() => new Point3D(), range(0, 4)); $idx = 0; for ($i = 0; $i < 5_000_000; $i++) { $p = $pool[$idx++ % 5]; $p->x = $x; $p->y = $y; $p->z = $z; // ... work with $p } ``` **Result on 5 million allocations:** naive 813 ms vs pool 179 ms → **4.43× faster**. GC cycles: zero in both. `Point3D` isn't cyclical, PHP's GC never trips. All the savings come from the allocator path: `new` in Zend Engine is a light but non-zero code path (`zend_object_new` → `emalloc` → property init × N). Five million times adds up. **Verdict:** works as expected. In CLI scripts the win is real but not critical. In long-running workers (queues, websockets, daemons), tail latency from allocator pressure compounds over time and becomes a headache — that's where pooling earns its keep. --- ## B04: Lookup table beats match and switch (and those two are equivalent) **Hypothesis:** for dispatch logic with 16+ cases in a hot loop, an array lookup beats `match` and `switch`. The llama.cpp parallel — token dispatch in `llama_token_to_piece` uses tables, not switches. **Translation:** a 32-case classifier implemented three ways — `switch`, `match`, and a pre-built `$lookup = [0 => 'A', 1 => 'B', ...]`. **Result on 10 million dispatches:** - `switch`: 358 ms (27.9M ops/sec) - `match`: 365 ms (27.4M ops/sec) - `lookup`: **61.7 ms (162M ops/sec) — 5.8× faster** `match` and `switch` are **tied.** Both compile to the same jump table for integer cases. PHP 8.4 JIT polishes both forms to the same result. If you rewrote `switch` to `match` for "modernization" — you got readability, not speed. **Where the lookup win evaporates:** if dispatch produces a string for downstream `===` comparisons, the gain is eaten by the string compares further down the pipeline. **Verdict:** match-shaped problems (closed compile-time set, exhaustiveness wanted) stay in `match`. Data-driven dispatch (table loaded from config, generated at runtime) goes in a lookup. The "match vs switch for perf" debate is closed — they're equivalent. --- ## B05: Generator — the main survival tool on large streams **Hypothesis:** a generator reduces peak memory from O(N) to O(1) with a minor throughput penalty. The llama.cpp parallel — tokens stream through a callback rather than accumulating in a buffer (`llama_decode` → `llama_get_logits_ith`). **PHP translation:** replace `function process(): array` with `function process(): Generator`: ```php function records(): Generator { foreach (read_csv('data.csv') as $row) { yield ['id' => $row[0], 'value' => $row[1]]; } } ``` **Result on 5 million records:** - Wall time: naive 525 ms vs gen 449 ms → **gen 1.24× faster** - Peak memory: naive 1.88 GB vs gen **0 bytes of PHP heap** The generator isn't just lower-memory — it's also faster on wall time, because the array never needs to be fully materialized before processing starts. **Now scale.** At 100 million records, naive OOMs — the kernel kills the process with SIGKILL after 28.6 seconds. The generator finishes the same 100M in **10.4 seconds** at zero PHP heap. At 500M, the generator still works (45.7 seconds). Naive doesn't even attempt. ![B05 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B05.png) If I had to pull one sentence out of this entire article and put it on a banner, it would be this: > **At 100,000 records, a generator is a 1.24× nice-to-have. At 100 million records, it's the only path on which the code can finish.** **Verdict:** default for any single-pass stream you don't need to revisit. Materialize the array only when you need random access, multiple passes, or `count()` before processing. --- ## B06: Column-oriented layout — not cache locality, but escape from boxing **Hypothesis:** on analytical single-column scans, column-oriented layout is faster than row-oriented due to cache locality. The llama.cpp parallel — tensors are stored per-channel (SoA), not per-element (AoS). **PHP translation:** instead of `SplFixedArray` of `stdClass` with 5 fields — 5 parallel `SplFixedArray` instances, one per field: ```php // Row-oriented (naive) $rows = new SplFixedArray($n); for ($i = 0; $i < $n; $i++) { $obj = new stdClass(); $obj->f1 = ...; $obj->f2 = ...; /* ... */ $obj->f5 = ...; $rows[$i] = $obj; } $sum = 0; foreach ($rows as $r) $sum += $r->f3; // Column-oriented (optimized) $f3 = new SplFixedArray($n); // and so on for each field for ($i = 0; $i < $n; $i++) $f3[$i] = ...; $sum = 0; for ($i = 0; $i < $n; $i++) $sum += $f3[$i]; ``` **Result on 5 million records:** column is **8.66× faster** on single-column scan. On full-row scan (sum f1..f5) — 1.92× faster. **And here's where it gets interesting.** I expected ladder steps on the ns/record chart — where the working set stops fitting in L1, then L2, then L3 cache. **I didn't see them.** The curves are flat across the whole 100K → 100M range: column holds at ~9.5–11.5 ns/record. Row holds at ~80–93 ns/record. No steps. ![B06 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B06.png) This is a *stronger* insight than "look, the ladder." Cache effects inside either layout don't differentiate them. What differentiates them is the layout itself. Row-oriented spends ~30+ bytes per `stdClass` (zval header + property table + GC info) for 8 bytes of actual payload. At 100M records that's 28 GB on boxing alone. Column-oriented at the same 100M = 7.45 GB, because each column is a packed `SplFixedArray` with no boxing. **At 100M records,** row OOMs — 28+ GB of `stdClass` objects don't fit. Column finishes the scan in **959 milliseconds** at 7.45 GB. **Verdict:** column layout isn't cache optimization (which is what I assumed). It's escape from PHP-object overhead at scale. On any analytical workload over large datasets — column. Row stays appropriate when DTOs are passed between layers, or when the working set is small. --- ## What happens at scale Micro-benchmarks on 1–10 million elements give you one picture. Scaling to billions gives a different one. Three of the six patterns transition from "optimization" to "necessity" on large data: - **B05 generator** — at 100M records, naive OOMs. Generator finishes. - **B06 column layout** — at 100M records, row OOMs. Column completes the scan in 959 ms. - **B01 mmap** — at 1B records, the JSON fixture *physically doesn't exist* (100+ GB). mmap loads a 16 GB binary in 228 ms. Two patterns stay "just optimizations" regardless of scale: - B03 object pool: ~4× at any size. - B04 lookup table: ~5× at any size. One pattern turned out narrow — saves memory, but never speed: - B02 SplFixedArray: 38% less memory, always slower on speed. Both paths work all the way to 1B. This is probably the most important reframing in the article. When someone says "X is faster than Y," that's a claim about a specific data size. On small data, half the claims break. On large data, half of them become "X works, Y doesn't exist." And one more thing worth its own line: **JIT in PHP 8.4 keeps eating optimizations every release.** Between runs on PHP 8.3.31 and 8.4.21, B03 sped up from 2.78× to 4.43×, B04 from 3.75× to 5.81×. Not a bug — JIT just keeps improving. A year from now, these numbers will shift again. --- ## Three rules of PHP performance in 2026 Out of these six experiments, a working framework emerged. **1. Trust the JIT.** Don't try to outsmart it at the syntax level. `match` vs `switch` — JIT compiles both forms to the same jump table. `SplFixedArray` vs packed array — JIT optimizes the regular array so aggressively that the specialized structure loses on speed. FFI dereference vs `$arr[$id]` — JIT-compiled array access beats FFI casts inside a hot loop. If your optimization is about "which language construct to pick" — the JIT already made that choice for you. **2. Optimize what JIT can't see.** - **Cache locality** (B06: column layout) — the JIT doesn't manage memory layout. That's your architecture. - **Allocation pressure** (B03: object pool) — the JIT doesn't eliminate allocations, it speeds them up. - **I/O batching** (batched INSERT of 1000 rows vs single-row) — the JIT doesn't optimize round trips to Postgres. - **Cross-process resource sharing** (B01: mmap + page cache) — the JIT works per process. - **Streaming vs materialization** (B05: generator) — the JIT isn't going to remove 30 GB of peak memory for you. **3. At a large enough scale, optimizations stop being optimizations.** They become a survival threshold. A generator on 100K records is 1.24× faster. On 100M it's the only code that finishes. A column layout on 5M is 8.66× faster. On 100M it's the only code that doesn't eat 28 GB on `stdClass` overhead. mmap on 10M is slower per call. On 1B it's the only way to load the table inside a second. That's structural thinking, not syntactic. And that's what turns llama.cpp from "a heavily optimized C++ library" into a *learning artifact* for a PHP developer. Not "here are tricks, steal them." But "here are language limits you only see when you crash into them." --- ## Closing All benchmark code and a reproducible Docker setup live on GitHub: **[vbcherepanov/php-llamacpp-benchmarks](https://github.com/vbcherepanov/php-llamacpp-benchmarks)**. A full sweep takes ~15 minutes (`make all`), including a case study that imports 100K rows into a real PostgreSQL. A note on the repo: the `data/` directory is gitignored — fixtures (up to 16 GB binary lookup files at the 1B tier) are generated locally by `make fixtures`. Don't try to clone with them. If you find a methodology bug or want to add a tier, send a PR. I work on this kind of thing through **[Braincore](https://getbraincore.com)** — a Go-based meta-agent with cost-aware routing and a memory layer for AI coding agents. If these benchmarks were useful and you'd like to support more of this, there's a [Ko-fi](https://ko-fi.com/vbcherepanov). --- ## What 16 Parallel Claude Agents Built Around Themselves: Deconstructing Anthropic's C Compiler Experiment - URL: https://vbcherepanov.com/articles/what-16-parallel-claude-agents-built - Canonical (medium): https://medium.com/@vbcherepanov/what-16-parallel-claude-agents-built-around-themselves-deconstructing-anthropics-c-compiler-f2fa6335b1ca - Published: 2026-05-09 - Reading time: 10 min - Tags: AI Agents, Open Source - Language: en > Analysis of Anthropic's experiment running 16 parallel Claude instances to build a C compiler in Rust — what infrastructure had to be invented around the agents because the primitives weren't there yet. ![16 parallel Claude agents cover](/images/articles/what-16-parallel-claude-agents-built.png) On February 5, 2026, Nicholas Carlini from Anthropic [published a piece](https://www.anthropic.com/engineering/building-c-compiler) about an experiment that runs significantly ahead of what most of us are doing with LLM agents today. Sixteen parallel instances of Claude Opus 4.6, two weeks of work, ~2,000 Claude Code sessions, a budget around $20,000. The output: 100,000 lines of a C compiler in Rust that builds Linux 6.9 on x86, ARM, and RISC-V; passes 99% of GCC's torture test suite; compiles PostgreSQL, SQLite, FFmpeg, Redis, and QEMU; and runs Doom. The [repository is open](https://github.com/anthropics/claudes-c-compiler) and anyone can read it and try it themselves. It's serious engineering work, and the article itself is a great read for anyone thinking about autonomous agents in production. Carlini is honest about what worked and what didn't, walks through five concrete lessons from designing the harness, and shares numbers and metrics. This is exactly the kind of writeup the industry needs more of — a first-hand account of what long autonomous runs actually look like. Headlines split into two camps. "AI replaced programmers" on one side. "It's just a demo" on the other. Both miss what's actually interesting. If you read the article carefully, what Carlini is documenting is not "AI writes a compiler." He's documenting **how much infrastructure had to be built around the agents because there isn't yet any infrastructure between the agents themselves in 2026**. Lockfiles in a shared directory as a sync mechanism. READMEs that the agent writes to itself. GCC pressed into service as a known-good reference oracle. A Ralph-loop wrapped around Docker for indefinite autonomy. Each of these is an answer to a concrete problem that today simply has **nowhere to be pushed into a standard layer**. And that's the article's real value. Not as an "AI demo," but as a **detailed map of missing primitives**, drawn by someone who built workarounds for them by hand. I've been working on these primitives for the past few months, and Carlini's writeup is a great excuse to talk through what the next generation of agent teams actually needs. ## Every session starts with amnesia Carlini built a harness that runs Claude in an infinite loop — when the agent finishes one task, it picks up the next. Architecturally this is the familiar "Ralph-loop" pattern: a `while true` cycle in a bash script, wrapped in Docker for safety. In one of the runs, Claude accidentally killed itself with `pkill -9 bash`, which Carlini notes as an amusing side effect. The crucial detail is that each of those ~2,000 launches started **in a fresh Docker container with empty context**. No memory between sessions. Every agent figured out from scratch: what is this repo, what's already done, what's the status of tasks, what's been tried and failed. Carlini's workaround was to instruct Claude itself to maintain extensive READMEs and progress files, updated frequently. When the agent gets stuck on a bug, it also keeps a running doc of failed approaches and remaining tasks. This works within the bounds of current tooling — and that's its value. But if you look at scaling, two architectural points start to creak. First, a text file isn't structured. If you want to ask "what were the three most recent bugs I fixed in the parser area and how did they end?", you only have `grep` and regular expressions. On a small project that's tolerable. On 100,000 lines of code and 2,000 sessions, it becomes a bottleneck. Second, more subtle: each agent maintains these files for itself. They live in the shared git repository, but there's no mechanism that says "before you take task X, look at what the other 16 agents wrote about this area in the past 6 hours." Each agent writes its own README, merges others' edits, and hopes things converge. This is the **first generation of shared memory** — implemented as plain text because no more convenient primitive has become standard yet. ## Lockfiles as coordination Parallelism is implemented minimally. Each agent runs in its own Docker container; a shared bare git repo holds state. Task coordination happens through lockfiles: an agent writes a file like `current_tasks/parse_if_statement.txt`, does a `git push`, and thereby "claims" the task. If two agents try to take the same one, git synchronization forces the second to pick something else. Done — delete the lockfile. Carlini states the current state of the system plainly: *"no other method for communication between agents... I don't use an orchestration agent."* No mechanism for agents to "ask each other." No central coordination. Each Claude decides for itself what to do next — usually "the next obvious problem." The lockfile here does exactly one thing — it works as a **mutex**, protecting against parallel claims on a single task. That's valuable. But it doesn't solve the other problem: two agents working on different tasks in **the same code area** can write conflicting code under different task names. That's exactly what happened to the Linux kernel in the experiment — agents converged on the same bug, fixed it differently, overwrote each other's edits, and parallelism temporarily stopped paying off. Carlini's solution was a separate test harness using GCC as a **known-good compiler oracle**: most of the kernel gets compiled with GCC, and a random subset of files goes through Claude's compiler. If the kernel doesn't boot, the bug is somewhere in Claude's subset, and you can keep narrowing it down. It's a clever and elegant idea, and it worked exactly as intended. It's worth noting the bounds in which this works. The GCC oracle is a precise solution for **this specific task**, because the task has three convenient properties: there exists a ready-made reference compiler for the same spec, the task decomposes at the level of individual files, and the outcome is binary (boots or doesn't). In most real projects — product development, legacy refactoring, ML pipelines, mobile applications — these conveniences don't exist. There's no ready known-good for comparison. There's no natural file-level decomposition. Outcomes aren't binary. Which means **the GCC-oracle technique can't be generalized as a primitive** — it works where it works, and doesn't exist where it doesn't. Taken as a whole, Carlini's toolkit lays out neatly along two axes: | What agent teams need | What's in the experiment | Nature of the solution | |---|---|---| | Agent discovery | hardcoded number of containers | hardcoded | | Inter-agent communication | lockfile via git push | mutex without messaging | | Task delegation | next-most-obvious from queue | no routing | | Shared state / memory | README + progress files | plain text | | Causal history | running doc of failed approaches | personal log | | Verification | GCC oracle | task-specific | These are **two independent axes of the problem**: communication (how agents talk to each other) and memory (what they remember between sessions and whether they share it). These axes require different primitives and different solutions. And on each, the industry is currently converging on standards and open-source implementations. What follows is what's available on each axis today. ## The communication axis: A2A protocol and a2abridge Communication has moved fast and has already arrived at a mature standard. In April 2025, Google [opened the A2A protocol](https://a2a-protocol.org/latest/) — Agent-to-Agent. In August 2025, IBM's ACP [merged into A2A under the Linux Foundation](https://lfaidata.foundation/communityblog/2025/08/29/acp-joins-forces-with-a2a-under-the-linux-foundations-lf-ai-data/), and by April 2026 the spec is at version 1.2, supported by 150+ organizations (Microsoft, AWS, Salesforce, SAP, ServiceNow, IBM among them), and natively built into Google ADK, LangGraph, CrewAI, LlamaIndex Agents, Semantic Kernel, and AutoGen. A2A has effectively **won the protocol war**. The spec is deliberately minimal: an **Agent Card** is a JSON description of an agent's capabilities (what it does, what endpoint to hit). A **Task** is a unit of work with statuses and artifacts. Transport is JSON-RPC 2.0 over HTTPS, with Server-Sent Events for streams. The analogy that gets used everywhere is HTTP. HTTP doesn't tell you what's in your backend (Rails, Django, Go) — it just defines the shape of requests and responses. A2A doesn't tell you what LLM, framework, or database you use — it defines the contract between agent A and agent B. A minimum on top of which you can build the rest. If you rewrote Carlini's scenario on A2A, instead of a lockfile in `current_tasks/`, an agent would query a directory service for "who's working on the parser right now?", get the neighbor's Agent Card, and send a `Task` with a streaming response over SSE. That's the communication primitive his harness doesn't yet have. I've been writing **a2abridge** for the past several months — an open Go implementation of A2A 1.0 targeted at the practical scenario of "several different AI agents on one developer's machine." At the time of publication, six IDEs are supported: Claude Code, Codex CLI, Cursor, Cline, Continue, and Gemini CLI. Any A2A-compliant agent (including future Google ADK, LangGraph, CrewAI implementations) is a first-class peer with no glue code. Architecturally it's **a single Go binary** (~10 MB) with several subcommands. `a2abridge directory` is a discovery service on 127.0.0.1:7777 that runs as a user-level system service (launchd on macOS, systemd-user on Linux, Windows Service on Windows, works correctly inside WSL2). `a2abridge bridge` is a per-agent process that hosts both an MCP stdio server (through which the IDE sees a2abridge as a regular MCP server with tools) and an A2A HTTP server on a random port, with an Agent Card at `/.well-known/a2a` and the full set of JSON-RPC 2.0 methods from §7 of the spec: `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetExtendedAgentCard`. The bridge's lifecycle equals the IDE session's lifetime — when MCP stdio closes, the bridge dies, no orphan processes. What Claude Code (or any other IDE) sees as MCP tools: `a2a_whoami`, `a2a_list_agents`, `a2a_send_message`, `a2a_send_streaming`, `a2a_get_task`, `a2a_cancel_task`, `a2a_inbox`, `a2a_complete_task`. Inside the session, the agent can **independently** discover other agents on the machine, send them tasks, wait for replies, and read its inbox — without user involvement. On top of the protocol there's a pro-active layer that isn't in the spec but is needed for real use. The bridge writes an inbox file at `./.a2a/inbox-.json` every time the message queue changes. A UserPromptSubmit hook injects incoming messages into the system prompt **before the first tool call** — meaning Claude sees "you have a message from a peer with FYI about a breaking API change" **before** it starts taking blind action. The SSE fast-path delivers replies in milliseconds, with a 5-second polling fallback. For Claude Code there's also a **skill** called `a2a-bridge` that auto-loads only when triggered by relevant prompts — no globally loaded rules burning tokens on every session. In Carlini's scenario this would look like: agent 5 takes the task "fix kernel build error in `mm/page_alloc.c`." Before acting, it calls `a2a_list_agents`, sees that agent 2 has an open Task with capability `kernel-debug` in the same area. It sends `a2a_send_message`: "what are you working on, do you have a hypothesis?". It gets a streaming response: "tried alignment fix, failed on test_kernel_boot, currently looking at reorder header includes." It picks a different angle. Why an open protocol and not yet another custom wire format. Several solutions already exist in this niche: **Anthropic Agent Teams** works only Claude↔Claude and is tied to a subscription. **CCB** and **claude-multi-agent-bridge** are closed formats locked to specific agent combinations. **Ruflo** is excellent for enterprise federations of 100+ agents with central queens, but that's a different class of problem. The niche a2abridge targets is **cross-vendor, open-protocol mesh**, where today Claude and Codex drop in, and tomorrow any A2A-compliant agent does, with no glue rewriting. If the industry is moving toward a standard, the bridge had better speak that standard. Production maturity: cross-machine federation with mTLS + ed25519 (opt-in, for the "home Mac ↔ office Linux" scenario), mDNS auto-discovery on the local network, a PII/secret screen running 11 regex detectors before sending (AWS keys, GitHub tokens, Anthropic/OpenAI/Google/Stripe/Slack tokens, JWTs, PEM blocks — replaced with `[REDACTED:]`, secret never leaves the bridge), Push Notifications per A2A 1.0 §9.5, HTTP+REST binding per §7.3, 35 test cases under `-race`, a GitHub Actions release matrix, and a cross-platform `a2abridge doctor` with a 9-check health audit. Install is a one-liner via `install.sh` or `install.ps1`, with auto-detection of every IDE on the machine and `.bak` backups of their configs before edits. Repository: **[github.com/vbcherepanov/a2abridge](https://github.com/vbcherepanov/a2abridge)** — MIT, Go 1.25, current release v2.0. ## The memory axis: total-agent-memory and BrainCore Memory is in a different state. There's no A2A-level standard yet — everyone builds their own layer, and different approaches get picked for different tasks. What an agent writes to itself in a README is essentially a causal log in textual form: "tried A, failed at B, moved to C." The structure is right; the implementation is still plain text. I'm working on two products on this axis. **[total-agent-memory](https://github.com/vbcherepanov/total-agent-memory)** — open-source implementation. The core retrieval patterns, MCP integration, and the basic causal-chain model live here. Anyone can clone it, see how it works, and plug it into their Claude Code or Cursor. **[BrainCore](https://github.com/vbcherepanov/braincore)** — production-grade. A Go binary, local SQLite + WAL, tree-sitter for code-graph across 14 languages (PHP, TypeScript, Python, Ruby, Rust, Java, Kotlin, C/C++, C#, Swift, Bash, Lua, YAML, plus Go through its own native AST), internal git for time-travel memory, MCP protocol for connecting to Claude Code, Cursor, Codex CLI, Windsurf, and several other agents. Currently in beta. Architecturally there are three points where both projects diverge from a flat bag-of-facts with cosine search. First, **causal decision chains** instead of flat facts. Not "function X is in file Y," but "agent 3 in task `fix kernel build` formulated hypothesis `alignment issue`, verified through test_kernel_boot, failed, moved to hypothesis `header reorder`." Each step is typed, connected by a causal arrow, and queryable by every agent. Second, **AST-stable code identity**. When several agents refactor in parallel, text diffs quickly turn into mush, and merge conflicts become endless. An AST node remains a node even if a function moved from `parser.rs` to `frontend/lexer.rs` and got renamed from `parse_decl` to `parse_declaration`. In the graph, it's **the same node** with a movement history. Every agent looks at the same abstraction, not at "lines 127-145 of file X." Third, **persistence across container restart**. Memory lives **outside** the Docker container: on the host through a volume, or remotely via MCP. The query `brain.causal_lookup(area="parser", lookback="6h")` returns the same result regardless of which fresh container you're in. Rewriting Carlini's scenario with memory: agent 5 goes to BrainCore, gets the causal log "agent_2 tried alignment fix → failed, agent_7 tried header reorder → failed at L98, current hypothesis from agent_3 is alignment issue, in progress," picks a fourth hypothesis, writes it to the causal chain. Agents 2, 3, and 7 see this decision on their next pull. No READMEs, no greps. ## How they fit together a2abridge and BrainCore are **different layers, not competitors**. One answers "how do agents talk to each other," the other answers "what do they remember." The full picture for an agent team looks like this. **BrainCore** holds the shared state of the world: code-graph, causal chains, hypotheses, conclusions. **a2abridge** provides actual communication between agents: discovery, delegation, streaming responses, an inbox with context injection. When they work together, agent 5 sees a message in its inbox from agent 2 ("I'm working on X"), queries BrainCore for details ("what specifically has been tried in this area"), makes an informed decision, replies to agent 2 about its intention to take an adjacent task, and writes the result to shared memory. That's the architecture Carlini is building by hand in the experiment through the combination of lockfiles + READMEs + GCC oracle. With independent primitives instead of self-built glue, the infrastructure works in tasks where there's no ready-made known-good compiler. ## What these primitives don't solve Carlini is absolutely right about the article's main lesson: **a high-quality test harness is the foundation of everything**. No amount of shared memory and no A2A will save you if the task verifier is imprecise — agents will autonomously solve the wrong task. CI pipelines, well-designed logs, defenses against context window pollution, fighting "time blindness" — these work at any infrastructure level and **remain the first priority**. The GCC oracle in the compiler task is genuinely the optimal choice. Binary verification is almost always better than comparing causal hypotheses. If you have a ready known-good in your project — use it. No memory replaces a good verifier. But in most real tasks — product development, refactoring, ML pipelines, business logic — there's no GCC equivalent. And there, the primitives of communication and memory become not an "improvement" but a **necessary condition** for a team of 16 agents to be more productive than one. That Carlini had to build this entire text-and-file layer in 2026 isn't a flaw in his approach but a symptom of the moment: infrastructure for agent teams is still forming. The Anthropic experiment is the best possible illustration of how it's forming and where it's headed. And that, in my view, is the real value of Carlini's article: an honest report from the earliest point on the curve along which this infrastructure will grow. --- ## Open source and links - Original Anthropic article: [Building a C compiler with a team of parallel Claudes](https://www.anthropic.com/engineering/building-c-compiler) - Compiler repo: [anthropics/claudes-c-compiler](https://github.com/anthropics/claudes-c-compiler) - A2A protocol specification: [a2a-protocol.org](https://a2a-protocol.org/latest/) - **a2abridge** — open A2A 1.0 mesh for 6 IDEs (Claude Code, Codex, Cursor, Cline, Continue, Gemini): [github.com/vbcherepanov/a2abridge](https://github.com/vbcherepanov/a2abridge) (MIT, v2.0 shipped) - **total-agent-memory** — open-source memory layer: [github.com/vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) - **BrainCore** — production memory infrastructure: [github.com/vbcherepanov/braincore](https://github.com/vbcherepanov/braincore) (beta) If you're building your own agent teams and running into these problems — get in touch. Experience exchange is valuable in any case, and feedback on early product versions is the best thing that can happen to authors. --- ## The right of an AI agent to stay silent - URL: https://vbcherepanov.com/articles/the-right-of-an-ai-agent-to-stay-silent - Canonical (medium): https://medium.com/@vbcherepanov/the-right-of-an-ai-agent-to-stay-silent-db29c478e577 - Published: 2026-05-09 - Reading time: 6 min - Tags: AI Agents - Language: en > Production AI should optimise for zero confidently-wrong actions, not accuracy percentage. Abstain as a first-class outcome, self-tasking, and a cognitive runtime are the path to trustworthy autonomy. ![The right of an AI agent to stay silent cover](/images/articles/the-right-of-an-ai-agent-to-stay-silent.png) > **Part 3 of 3 — "Memory for AI agents"** > Why the right metric isn't accuracy — it's zero confidently-wrong actions Picture two scenarios. In the first — a senior cardiac surgeon looks at a scan and says: *"I don't know. There are two competing hypotheses here, the symptoms overlap. We need additional tests — these three specifically, and a CT with contrast. Until I see those, I won't commit to an answer I'd defend."* In the second — a bright-eyed intern confidently delivers a diagnosis in thirty seconds, leaning on a similar case from last week's textbook. Confident. Crisp. No doubt. Which one would you trust to operate on your mother? Right now, every AI agent we ship is the second doctor. Confident. Fast. Never says *"I don't know."* And that's exactly why you can't trust them with anything more painful than rewriting a README. Today — how to change that. Not algorithmically. **Architecturally.** --- ### The rotten metric that poisoned us all There's an unspoken industry consensus that I think is a disaster: we measure models and systems by **accuracy** — the percentage of correct answers on a benchmark. GPT-4 hits 86% on MMLU. Claude — 88%. Gemini — 90%. Better, better, even better. The number goes up. What that number **doesn't** show: the remaining 10–14%. These aren't *"answers the model didn't give."* They're **confidently generated wrong answers**, visually indistinguishable from correct ones. The model has no warning light for *"I'm not sure here."* It generates everything with the same textual confidence. When you use such a model to write notes — fine. When you use it for production code, medical decisions, legal opinions, financial transactions — **10% confident hallucinations means 10% of cases where the system is lying to you with a straight face**. The right metric for production AI sounds different: > **0% confidently-wrong actions at an acceptable abstain rate.** Not *"percentage of correct answers."* But *"percentage of wrong actions"* — zero. And separately — `abstain rate`: how often the system honestly says *"I don't know, I need data / verification / clarification."* Zero wrong actions plus 30% abstain is **ten times** more production-ready than 90% accuracy with 10% confident hallucinations. Notice: I didn't say *"0% wrong answers."* I said *"0% wrong **actions**."* The distinction matters. An answer is words. An action is a commit, a transaction, a diagnosis, an API call, a change in production. Words can be reread and discarded. An action has already happened. And that separation between *"answer"* and *"action"* — that's what's architecturally absent from modern AI agents. --- ### Abstain as a first-class outcome In Part 2 of this series I laid out seven principles of real memory, and the second was `strict mode`. Quick recap: before a fact lands in prompt context, it passes through a **gate** — source, confidence, temporal validity, no unresolved contradictions. If no fact made it through — the system returns `abstain = true`, with an explicit reason. There's a detail I want to underline separately. **Abstain is not an error.** It's a **result**. Every bit as first-class as *"answer"* or *"action."* If your AI has exactly two possible outcomes — *"answered"* and *"got it wrong"* — it has no architectural place for an honest *"I don't know."* Which means it's going to make things up. In a sane system, there are **at least four outcomes**: - **answer** — sufficient evidence, answer given, action executed - **clarification request** — partial evidence, needs user input - **abstain → brain task** — insufficient evidence, recorded as a backlog task with an explicit data request - **escalation** — there's a contradiction that requires human review And the last three aren't fallbacks. Not *"when everything went wrong."* They're full, expected, designed-in paths. When I ask `braincore` to find a decision about auth flow on a project we've been working on for three months — it finds it. When I ask about a project I just started, where nothing's recorded yet — it doesn't make things up. It says: *"I have no evidence on this question. Created a brain task: collect decisions on auth, source — our current design doc, owner — you. Once you fill it in, ask again."* This is **not a bug**. It's the right behavior. Notice what happened: the system didn't block me. Didn't say *"error, no data."* It **turned the not-knowing into a task**, which now lives in its backlog and will periodically remind itself. --- ### Self-Tasking. A brain with a backlog, not a passive search engine The thing that scares me most about modern *"AI agents"* is that they're **passive**. They wait for a prompt. Every. Single. Time. Remember nothing between sessions. Have no **internal backlog**. Don't realize they have unresolved questions. That's not an *"agent."* That's **a function in agent costume**. A function takes input, returns output. An agent has goals, state, and its own tasks between requests. In a real cognitive runtime, there's a separate entity — **brain tasks**. They get spawned automatically: - `truth.contradiction` — a contradiction found in the knowledge graph → task to resolve - `truth.staleness` — a fact hasn't been confirmed in a long time → task to verify - `strict.abstain` — the system refused to answer → task to find evidence - `selflearn.skill_scorecard` — a skill started failing often → task to repair - `specs.evidence_gap` — a requirement without coverage proof → task to gather - `tests.failing_coverage` — tests aren't passing → task to fix - `learning.failure_pattern` — a recurring error pattern detected → task to generalize into a rule Each task **prioritizes itself** by a simple formula: ```plaintext priority = f(urgency, impact, confidence, risk, effort, dependency_readiness) ``` And at any moment, the user can ask: *"show the next five tasks, why they matter, which I can safely do now, which need my input."* That's not the same chat where you start with a blank slate every time. It's a working environment with its own memory of what's not done. This is a flip in framing. Not *"user shows up and asks, agent answers."* But *"agent runs in the background, accumulates open threads, and tells you — here's what matters now."* Show me a RAG stack that does this. Spoiler: there isn't one. Because **RAG is a search engine, not an agent**. And when someone says *"our RAG-based AI has agency"* — that's marketing fiction. Agency requires **internal state**, **goals**, **a backlog**, and **self-assessment**. RAG has none of these. --- ### Cognitive Runtime > Model Size The last myth to dismantle. *"When GPT-5 / Claude 5 / Gemini 3 ships — memory will solve itself."* No. It won't. Ever. Memory is **not a property of the model**. It's a **property of the system** the model runs in. The analogy: > A human has good memory not because neurons compute fast. > A human has good memory because there's a hippocampus, a neocortex, sleep-time consolidation, emotional gating through the amygdala, and an architectural separation between working / episodic / semantic / procedural memory. > **It's infrastructure, not compute power.** Make the LLM ten times bigger — memory still doesn't appear. Build a runtime around the existing LLM that implements the seven principles from Part 2 plus abstain plus self-tasking — and a **weak local model** in that runtime starts doing things GPT-5 with RAG-memory **architecturally cannot**. Not because it's smarter. But because **the runtime does for it what it shouldn't have to do itself**: remembers, verifies, abstains, tasks itself. This is, by the way, the only meaningful path forward in a world where foundation models are **commodity**. When everyone has roughly equivalent Claude/GPT/Gemini — competitive advantage can only come from **what's around the model**. Domain-specific cognitive runtime. Project-specific memory. Team-specific rules. And this bet is also about privacy. About data sovereignty. About the fact that **your project's memory is your capital**, and handing it to a third-party vector DB to pay monthly rent on it is a strategic mistake you'll only notice three years in, when you can't leave anymore. That's why, incidentally, `braincore` is a **local** Go binary that works by default **without** OpenAI and without Anthropic. Not because I'm against them (I'm a paying customer of both). But because **the architecturally correct path** is a runtime where the model is a swappable component, not the center of gravity. --- ### A checklist for anyone building AI products right now If you've read the whole series and you're thinking *"okay, agreed, what do I do Monday morning?"* — here are ten items you can start moving on **regardless** of whether you use `braincore` or not. 1. **Drop the word "memory" from your stack if what you have is RAG.** Call it retrieval or search — instantly removes 80% of inflated expectations. 2. **Introduce `truth_status` for every fact.** Minimum: `hypothesis | confirmed | deprecated`. Disallow `confirmed` without `source_ref`. 3. **Introduce `valid_from` / `valid_until`.** Any fact without temporal validity is a hypothesis, not a fact. 4. **Make abstain a first-class outcome.** Not *"when things go wrong"* — but as one of four valid results. 5. **Distinguish `staging | working | consolidated | archived`.** Don't dump everything into one collection. 6. **Negative memory.** What broke — record it explicitly, with a link to the failing test or commit. 7. **Entity disambiguation.** Never auto-merge entities at low confidence. Create an `ambiguity record` instead. 8. **Causal chains for decisions.** Not "text" — `problem → alternatives → decision → reasoning → outcome`. 9. **Local where possible.** Project memory is **your** capital. 10. **The metric is not "*percentage of correct answers*." It's `0% wrong actions at an acceptable abstain rate`.** Not all at once. Pick two or three and start. In a month, you'll have an AI system you can trust more than most that exist. --- ### Epilogue. Cognitive hygiene for the AI industry I'm tired of the word *"memory"* getting slapped on every vector database with embeddings. It's a devaluation of the term — like calling a one-column `text VARCHAR` table a knowledge base. Technically — yes. Substantively — no. Memory is: - **structure**, not a flat list - **knowing the boundary**, not confident bullshit - **causal chains**, not chunks - **entity-aware**, not string-aware - **temporal-aware**, not *"created yesterday, valid forever"* - **self-correcting**, not self-deceiving - **governed**, not *"dump whatever, sort later"* - **abstain-capable**, not *"always answers"* If your *"AI with memory"* doesn't do at least half of those — your AI doesn't have memory. It has search results. These aren't the same thing. One last thing. I'm not telling you to throw out RAG. RAG is an excellent tool for its class of tasks (find me the paragraph about X in 100 documents). I'm telling you to **stop calling RAG memory** and start building real cognitive runtimes — slower, more disciplined, with explicit gates and explicit abstain. It's the only path to AI systems you can trust with anything more important than rewriting a README. If you're a startup with *"our AI has long-term memory on a vector database"* in your pitch deck — close that slide, redo it, and in two years you'll thank yourself. If you're a developer fighting with an agent that forgets what you said yesterday — that's not the agent's fault. It's the fault of whoever sold you a search engine wrapped as a brain. A good AI agent **isn't the one that always answers**. A good AI agent **is the one that never takes a confidently wrong action**. Between those two sentences lies the entire chasm separating 2024's AI tooling from AI tooling that will be trustworthy in 2027. I've picked my side of the chasm. Building `braincore` — open, Apache-2.0, in the repo. If you recognize yourself in this series — we're in the same boat. If something works differently in your stack — tell me how, I genuinely want to know. The one thing you can't do is stay silent. --- > **TL;DR of the whole series:** > > - **Part 1:** RAG = Ctrl+F with embeddings. It's search, not memory. Mem0/Letta/Zep — RAG in wrappers. 1M context is RAM, not disk. > - **Part 2:** Real memory = seven principles in combination. Atomic units + lifecycle + truth_status + temporal + causal chains + AST identity + internal git + memory scoring + negative memory. Each exists in isolation. Combined — different product. > - **Part 3:** The metric for production AI isn't accuracy — it's *0% confidently-wrong actions*. Abstain is a first-class outcome, not an error. Cognitive runtime > model size. > > If your AI "remembers" via `vector_db.query(top_k=5)` — it has dementia disguised as confidence. Fix the architecture, not the model. --- *Part 3 of 3. Series complete. If this resonated — share it. If you disagree — tell me in the comments, I love substantive arguments.* --- ## Seven principles of real memory for AI agents - URL: https://vbcherepanov.com/articles/seven-principles-of-real-memory-for-ai-agents - Canonical (medium): https://medium.com/@vbcherepanov/seven-principles-of-real-memory-for-ai-agents-3029d7d877ac - Published: 2026-05-06 - Reading time: 8 min - Tags: AI Agents, Memory - Language: en > Atomic units with lifecycle, strict mode with abstention, causal decision chains, AST-based code identity, internal git, scoring mechanics, and negative memory with rule engines — the recipe behind real cross-session memory. ![Seven principles of real memory cover](/images/articles/seven-principles-of-real-memory-for-ai-agents.png) > **Part 2 of 3 — "Memory for AI agents"** > Architecture. Concrete. With formulas and lifecycle. In the previous post I broke the *"RAG = memory"* pitch into three uncomfortable problems: a chunk doesn't know it's a chunk; retrieval has no structure, only cosine; time doesn't exist as a first-class concept. In short — RAG is search wearing the marketing word *"memory."* Today — what should be there **instead**. A disclaimer up front. I don't claim to have invented any single item on this list. Atomic facts go back to Wittgenstein. Temporal validity is basic logic. Knowledge graphs are a whole field with textbooks. Lifecycle for data is standard in any normal information system. I claim something different. I claim that **all seven properties have to work in one system at the same time**, and that any system in which only five of seven actually work continues to lie to the user with a confident face. There's only one way to see this — try assembling all seven into one codebase and watch what happens. I tried. It worked. Called it `braincore`. Open source, Apache-2.0, single Go binary, MCP-stdio. I won't turn the article into a pitch — but in each section below I'll add one line about how it's done in `braincore`, so it's clear we're not talking theory. Let's go. --- ### Principle 1. Atomic Knowledge Units with lifecycle, not "chunks in Qdrant" **The pain.** In RAG, any incoming text — dialogue, design doc, git commit, meeting transcript — gets sliced into chunks and shipped into the vector DB without questions. From there, no matter what happens — all chunks are equivalent, all equally "fresh," all equally "true." Six months later, one collection holds a soup of stale, current, hypothetical, and refuted facts. And every one of them has exactly one chance of making it into retrieval — by cosine. **What should be in the schema.** Any incoming information **does not flow into memory directly**. It runs through a pipeline: ```plaintext input → initial trust (by source: user=0.9, llm=0.3, web=0.4..0.7) → parse (entity / fact / relation / event / rule / hypothesis) → atomic knowledge units → validate (source / graph / dedup / contradiction / temporal / rule) → link (at least 1 edge into graph OR review item) → working memory (TTL + activation) → iterative verification loop → consolidation → long-term memory (only confirmed + linked) → edge strengthening (usage + success + co-occurrence − decay) ``` The core rule: **nothing enters long-term memory immediately**. Every atomic knowledge unit has at minimum: - `truth_status`: `hypothesis | candidate | confirmed | contradicted | deprecated` - `lifecycle`: `staging | working | consolidated | archived` - `source_ref` — where it came from - `confidence` — numerical certainty estimate - `valid_from` / `valid_until` — when it's true Compare that to a RAG chunk that has only `text` and `embedding`. It's the difference between a junk drawer and a warehouse with inventory. **What this enables.** When yesterday you said *"we use Postgres"* and today *"we migrated to ClickHouse, Postgres is OLTP only"* — the old fact automatically gets `valid_until = today` and `superseded_by = new_fact_id`. On retrieve, it either doesn't appear at all, or it comes flagged *"historical, not current."* Not because of a smart model. Because of the **schema**. **How braincore does it.** The pipeline `staging → working → consolidated` is implemented literally — three separate SQLite tables plus an intermediate verification loop. A record reaches `consolidated` only if `truth_status = confirmed`, has at least one graph edge, no unresolved contradictions, and `confidence ≥ threshold`. Otherwise it stays in `working` with a TTL, or moves to a `review queue`. --- ### Principle 2. Strict Mode and the right to abstain This is, possibly, the most important point in the entire series. And the most absent from commercial memory frameworks. **The pain.** The standard metric AI systems are measured by — *"how often they give the right answer."* This is a **rotten** metric. 95% correct answers and 5% confident hallucinations is a system **you cannot trust in production**. Because you don't know in advance which 5% you're in right now. The right metric reads: > **0% confidently-wrong actions at an acceptable abstain rate.** Not *"always answer."* But *"never take a wrong action without verification."* And if verification is missing — **say "I don't know"** and assign yourself a task to fix it. **What should be in the schema.** Before a fact lands in prompt context, it passes through a **gate**: - is there a `source_ref`? - `confidence ≥ threshold`? - `trust_score ≥ threshold` (for the source)? - `temporal_valid == true` (valid at query time)? - no unresolved `contradiction` in the graph? - no unresolved `ambiguity`? If even one requirement fails — the fact **does not reach** context. If no fact made it through for a query — the system returns `abstain = true` with `reason = no_accepted_facts` (or `contradiction_unresolved`, or `temporal_invalid` — always explicit). And — pay attention, here's where the magic happens — **abstain is not delivered to the user as a dead end**. It becomes a **brain task** in the backlog: *"I need evidence for X to answer with confidence. The source is here, the specific conflict is here."* The system knows what it doesn't know, and assigns itself the task to fix it. **What this enables.** An AI agent you can trust. Not because it's always right — but because when it's not sure, it **stays silent** or **asks for clarification**. And when it does take action — the action is grounded in facts that **passed the gate**, not "well, ChatGPT thought this was better." Show me one RAG stack that does this. I'll wait. **How braincore does it.** The `internal/strictmode` package is a separate module with explicit gate rules. By default, every query passes through strict mode; for UX scenarios where abstain is unacceptable (brainstorming, for example), you can drop it via an explicit `--allow-uncertainty` flag. All abstain events are logged as brain tasks with their source and reason. --- ### Principle 3. Causal Decision Chains, not flat facts **The pain.** In RAG, any decision is stored as *"text about a decision."* On retrieve, you get a chunk of text that **describes** the decision — but doesn't answer *"why?"*, *"what alternatives did we consider?"*, *"what came of it?"* Six months later, you ask *"why did we pick JWT over sessions?"* — RAG returns three fragments of the declaration, and the model fills in the reasoning itself. Sometimes correctly. Sometimes inventing it from popular patterns in its training data. You don't know which one this time. **What should be in the schema.** The entity is not a *"document"* and not a *"memory entry."* The entity is called **decision** and has a schema: ```plaintext problem → what we were solving alternatives → what we considered and rejected (with reasons) decision → what we chose reasoning → why this specifically outcome → what came of it (filled in later, post-hoc) superseded_by → link to a new decision if this one was revised ``` This isn't *"let's stuff text into an embedding."* This is a **causal chain** that answers **WHY**, not just **WHAT**. **What this enables.** Six months later, you ask *"why JWT?"* — the system returns a structured answer: - **Problem:** session scaling + audit requirements. - **Alternatives (rejected):** stateful sessions with Redis (violates audit), opaque tokens with centralized lookup (latency). - **Decision:** JWT with short TTL. - **Reasoning:** stateless, audit-neutral, latency acceptable. - **Outcome (recorded 4 months later):** invalidation complexity higher than expected; added refresh tokens. - **Superseded by:** none. RAG returns three fragments. A decision chain returns **reasoning**. These are different products. **How braincore does it.** Decisions are a separate entity type in the graph with required `problem`, `alternatives[]`, `decision`, `reasoning` fields, and optional `outcome`/`superseded_by`. They're stored not as chunks but as structured records with explicit edges into the code graph and into other decisions. --- ### Principle 4. Stable code identity through AST, not strings **The pain.** This one is specific to AI agents working with code — but it hits all of them. You renamed `GetUser → FetchUser`, moved it from `pkg/auth` to `pkg/user`, changed the signature from a pointer receiver to a value receiver. All the references in RAG memory pointing to *"GetUser in pkg/auth"* are now **dead**. Because RAG is bound to **strings**. And nobody tells you. The chunk keeps living in Qdrant, its cosine to auth-related queries stays high. The agent pulls dead information and works against it. Congratulations, you have memory rot disguised as memory. **What should be in the schema.** Code parsing through `go/ast` (for Go) and tree-sitter (for PHP, JS, TS, Python, Rust, Java, and beyond). **Node identity** is built not from a string and not from a file path, but from a structural hash: ```python node_id = sha256(qualified_name + kind + signature_hash) ``` Which means: - Renaming a function **does not break** references to it (`qualified_name` changed, but the link is updated automatically on next parse, with a back-reference to the old `node_id` as `renamed_from`). - Moving between packages — same thing. - Changing the signature (pointer → value receiver) — `signature_hash` changes, and old references **automatically get marked `stale`** — the brain **knows** they now require review. **What this enables.** When the AI agent is about to edit `FetchUser`, the system pulls three past decisions about that function, two regressions in this module, and active project rules — **before** the agent starts writing code. Not because cosine happened to align. Because it's a **code graph**, and `FetchUser` has edges to decisions, regressions, and rules **by identity**, not by text similarity. I call this pre-edit warning. And it's a qualitatively different kind of error prevention than *"let's run a linter after generation."* **How braincore does it.** The code graph is a separate layer over AST/tree-sitter, with background reindex on filesystem watch events. Identity hashes live in SQLite, edges live there too. On a pre-edit hook, the agent gets the context of related decisions/rules/regressions automatically. --- ### Principle 5. Internal Git as memory versioning **The pain.** RAG has no concept of time beyond `created_at`. That's metadata about a **record**, not about a **state of knowledge**. You can't ask *"show me what I knew about this code a month ago."* You can't roll back the state of memory to before the agent dragged in garbage. You can't switch to a feature branch and have a parallel mental state for it. **What should be in the schema.** Every change in memory is a **commit**. Not metaphorically. Literally, through `go-git`, into a hidden `.internal-git/` repository that lives parallel to the project's main repo. This gives you: - `git log` over the project's **memory** — what was added, what changed, when. - `git checkout` to roll back the brain state by N days — for audit, for regression investigation, for tests. - When you switch to a feature branch in the main repo, the brain **mirrors** that, and each branch has its own mental state. An experiment in a feature branch doesn't pollute master's memory. **What this enables.** Time-travel queries: *"which decision did I consider current 30 days ago?"* Audit: *"when exactly did the agent start believing we use ClickHouse?"* Branch isolation: *"in feature/oauth we have a different approach to auth, but that knowledge shouldn't leak into main."* RAG can't do this. RAG has no concept of *"state of knowledge"* — only a set of vectors that grows. **How braincore does it.** The `.internal-git/` is created on `braincore init`. Commits are made automatically on every change to knowledge units and graph edges. Branch tracking is synchronized with the main git through a post-checkout hook. --- ### Principle 6. Memory Scoring — because not all knowledge is equal **The pain.** In RAG, all chunks are equal. Top-k by cosine doesn't distinguish *"this is confirmed by ten past uses"* from *"this was written yesterday and never used again."* It doesn't distinguish *"this is critical for the architecture"* from *"this is a random note in a corner."* It doesn't distinguish *"this is in active use"* from *"this has been gathering dust since last year."* **What should be in the schema.** Every knowledge unit has a composite `MemoryScore`, computed as a weighted sum: ```plaintext MemoryScore = + 0.22 * ImportanceScore (explicit importance, or derived from connectivity) + 0.22 * TrustScore (source reliability + history of confirmations) + 0.20 * TaskRelevanceScore (relevance to current work context) + 0.12 * UsageScore (how often it's used) + 0.10 * RecencyScore (freshness) + 0.10 * StabilityScore (how often it changes — stable is more reliable) + 0.08 * NoveltyScore (novelty as a soft boost) − 0.18 * RiskScore (potential harm from use) − 0.18 * NoiseScore (noise, duplicates, low coherence) ``` And on retrieve, what runs is **no longer cosine similarity**, but: ```plaintext RetrievalScore = + 0.35 * semantic_similarity + 0.20 * memory_score + 0.15 * graph_relevance + 0.15 * temporal_validity + 0.10 * trust_score − 0.15 * ambiguity_penalty ``` These weights aren't ultimate truth — they're empirically tuned and shift with usage profile. The point isn't the numbers, it's the **architectural shift**: retrieval stops being *"text similarity"* and becomes *"similarity × importance × trust × freshness."* Lifecycle transitions automatically: - `memory_score ≥ 0.80` and `trust ≥ 0.75` → `consolidated` (knowledge becomes "firmware") - `memory_score ≥ 0.55` → stays in `working` - `memory_score ≥ 0.30` → `staging` - `memory_score < 0.30` → `archive candidate` **What this enables.** Active memory. Not storage. **An active environment** in which what's important strengthens through use, and noise **decays on its own** — like in a biological brain, where rarely-used synapses weaken and frequently-used ones strengthen. > RAG = a hard drive that never gets defragmented. > Brain = a brain in which junk **settles** on its own and gets archived automatically. **How braincore does it.** Scoring is recomputed by a background job every N hours. Lifecycle transitions are atomic and logged (see Principle 5). All weights are exposed in config — tune them per project. --- ### Principle 7. Negative Memory and Rule Engine **The pain.** Here's what every LLM agent does today: **repeats mistakes**. Yesterday it broke a migration — today it'll break a similar one. RAG won't help, because **the broken migration doesn't go into RAG**. What goes into RAG is *"how to write migrations"* from the official docs. The fact that you personally already stepped on this rake — recorded nowhere. **What should be in the schema.** A separate class — **negative memory**: what broke, why it broke, how it was fixed, which commit/test confirms it. First-class entity, not a marginal field. And during planning, every patch passes through a **Rule Engine** before code is generated: ```plaintext patch → architectural rules → code rules → security rules → performance rules → anti-patterns (including "this exact one I broke before") → repair plan OR abstain ``` If a rule with severity `critical` or `high` is violated — **the code does not get written**. A repair plan is created. If repair is impossible — `abstain` (see Principle 2). No "let's hope this passes" generation. And, critically, the **safe execution pipeline** closes the loop: ```plaintext checkpoint → apply patch → rules validate → build → tests → success → commit → fail → rollback → record into negative memory ``` Every **executed** action is either confirmed by tests, rolled back, or recorded as **negative evidence** for future decisions. **What this enables.** An agent that **cannot** repeat your last year's mistake. Not because it has a great model — but because **the rule engine physically refuses to let through** any patch that violates a rule derived from that mistake. > RAG helps the agent find something. Good memory **prevents** the agent from breaking something. These are different products. And I feel sorry for those who keep mixing them up. **How braincore does it.** Negative memory is a separate entity type with a required link to a failing test or git commit. The rule engine is a pre-execution gate, severity-aware, with override possible only via explicit user confirmation. --- ### Bonus principle. Entity Disambiguation Formally a special case of Principle 1 (atomic units), but it breaks separately often enough to deserve its own callout. In RAG, there's no concept of an **entity**. There's only text. If your project has two `User` classes — one in `pkg/auth`, one in `pkg/billing` — for RAG these are two pieces of text with similar embeddings. On retrieve, they **mix together**, and the model confidently explains auth logic in the context of billing. This isn't theory. This is happening **right now** in every code RAG agent. The fix — **EntityFingerprint**: ```python fingerprint(symbol) = hash( project_id + file_path + symbol_name + symbol_type + signature + language ) ``` Two `User` entities in different files = two fingerprints = two distinct entities that **never auto-merge**. When a new candidate arrives, a `SameEntityScore` is computed: ```plaintext SameEntityScore = + 0.30 * name_similarity + 0.20 * alias_match + 0.20 * context_similarity + 0.15 * graph_neighborhood_similarity + 0.10 * temporal_consistency + 0.05 * source_consistency ``` And: - `≥ 0.92` → `auto_merge` - `≥ 0.82` → `same_as` link (soft link, not merge) - `≥ 0.65` → `ambiguous` — an **ambiguity record** is created, requiring human review - otherwise — new entity The core rule: **never merge entities at low confidence**. Better to create an ambiguity record and ask a human than to silently glue them together and lie forever after. --- ### Why all of this together I'm deliberately not framing this as *"this is nowhere done, I'm first."* Each of the seven principles already exists. Atomic facts with lifecycle — in knowledge management systems. Strict mode + abstain — in last century's expert systems. Causal chains — in decision support tools. AST identity — in IDEs. Internal git — in tools like Pijul and in Datalog database experiments. Memory scoring — in research papers on episodic memory. Negative memory — in RL and reliability engineering. Uniqueness isn't in the ideas. It's in the **assembly**. If you have atomic units but no strict mode — you have a structured database of hallucinations. If you have strict mode but no causal chains — you abstain without understanding why. If you have causal chains but no AST identity — your decisions point into the void after two refactorings. If you have all of the above but no memory scoring — you have a perfectly structured dump in which the important drowns in noise. Each property in isolation is an improvement. All seven together is a different category of product. This, by the way, is the answer to the question I get most often: *"why write something new if I already have Mem0/Letta/Zep?"* The answer — look at their schemas and check how many of the seven principles are implemented **not as a marketing claim, but as an enforced gate in code**. For most, the honest count is two or three. For some — four. They aren't bad products. They're **partial solutions**, more honestly called *"structured retrieval"* than *"memory."* --- ### In Part 3 Seven principles is engineering. What **should be** in the architecture. But behind engineering sits a deeper question: **why should an AI agent know what it doesn't know?** Why abstain at all, if it can just answer? Part 3 is about the right of an AI agent to stay silent. About self-tasking. About why cognitive runtime matters more than model size. And about why the right metric for production AI isn't accuracy, but *zero confidently-wrong actions at an acceptable abstain rate*. It's the shortest and most philosophical piece in the series. Drops next week. --- *Part 2 of 3. If you missed [Part 1 — here](https://medium.com/@vbcherepanov/rag-isnt-memory-it-s-ctrl-f-with-embeddings-c461b90ac7b1) (on why RAG is search and not memory). If this resonated — a repost would help.* --- ## RAG isn't memory. It's Ctrl+F with embeddings. - URL: https://vbcherepanov.com/articles/rag-isnt-memory-its-ctrl-f-with-embeddings - Canonical (medium): https://medium.com/@vbcherepanov/rag-isnt-memory-it-s-ctrl-f-with-embeddings-c461b90ac7b1 - Published: 2026-05-01 - Reading time: 7 min - Tags: AI Agents, Memory - Language: en > Vector search is search, not memory. Three failure modes — chunks lose meaning, no structural distinction between facts, and no temporal validity — make RAG hallucinate confidently without knowing what it doesn't know. ![RAG isn't memory cover](/images/articles/rag-isnt-memory-its-ctrl-f-with-embeddings.png) > **Part 1 of 3 — "Memory for AI agents"** > Deconstructing the long-term memory myth in LLM systems It's 3 AM. I'm on my third night debugging an AI agent. I'm standing in the kitchen with a mug of tea, staring at a diff, swearing quietly. The agent has confidently rewritten the auth function — based on a chunk that belongs to a branch that was deleted from the repo two months ago. The chunk lives in Qdrant. Its cosine similarity to my query is high. Top-1 in the retrieval. The agent honestly grabbed it, honestly stitched it into the prompt, honestly generated the "correct" patch. Against code from a different reality. I close the laptop and think: okay, I have RAG. I have vectors. I have long-term memory. I have everything every AI conference deck has been promising for the last two years. Why did my agent just propose a fix based on code that doesn't exist anymore? Because my agent doesn't have memory. My agent has search results with cosine instead of BM25. And between those two sentences lies the entire difference between *"AI you can trust in production"* and *"AI you have to babysit on every line."* This piece is about that difference. And about why we, as engineers, are the ones to blame for not seeing it anymore. --- ### The devaluation of the word "memory" Let's be honest. What is the typical "memory" of an AI agent in 2026? ```plaintext text → split into 512-1024 token chunks → embedding (bge / text-embedding-3 / openai) → vector DB (Qdrant / pgvector / Chroma / Pinecone) → cosine similarity top-k → concatenate into prompt ``` This is **not** memory. This is search. It's old-school Lucene from 2003, repainted in neural colors. Cosine instead of TF-IDF. Embeddings instead of an inverted index. Same thing. If we just called it that — *"vector search,"* *"semantic retrieval"* — I'd have no complaints. Call Lucene Lucene, no problem. But when it's sold under the banner *"my AI has long-term memory"* — sorry. My AI has déjà vu and amnesia at the same time. This isn't a terminology gripe. It's a question of expectations. When an engineer hears *"memory,"* they imagine a system that **remembers**: who said what, when, in what context, what was true then versus what's true now. When an engineer gets RAG, they get Ctrl+F. And instead of building honest architecture around that Ctrl+F — with honest constraints — they build a sandcastle and wonder why the agent confuses past with present. --- ### Three holes you can drive a truck through Three concrete failures. Each one I caught in production. Not theory. **Hole #1: A chunk doesn't know it's a chunk.** Take a perfectly normal declaration from a design doc: > *"We moved to JWT because opaque sessions didn't scale to our traffic profile. The alternative was stateful sessions with a Redis cluster, but we ruled it out because of audit requirements from a customer — they don't allow session state outside their perimeter. JWT solves both, but adds invalidation complexity, which we mitigate with short TTLs and refresh tokens."* The chunker splits this into four 512-token pieces. On retrieval, a query comes in: *"why did we pick JWT?"* Top-3 returns three fragments of the same decision. With no causality. Without the alternative we ruled out. Without the trade-off we accepted. A decision that was **whole** turns into three parallel "factoids." The model honestly stitches them into plausible text — and **invents** the missing connections. Because its job is to generate plausible text. And it will, without blinking. This isn't a bug in the chunker. This is an architectural property of the entire approach. Any decision declaration you have gets ground into powder and reassembled with structural loss. Every single time. **Hole #2: There's no structure in memory. Only cosine.** When a human explains a project to you, they say: - *here's the goal* - *here are the options we considered* - *here's what we picked and why* - *here's what broke two months later* - *here's what we changed, and that decision now supersedes the old one* In RAG, none of this exists. Zero. RAG doesn't distinguish *"hypothesis,"* *"confirmed fact,"* *"rejected alternative,"* *"deprecated decision moved to archive."* For RAG, all of these are equivalent points in a 384-dimensional space. Imagine you're trying to record thirty years of life into a single flat table `entries(text, vector)` and then search it by cosine. Surprised your memories blur together? That's not your memory failing. That's the structure you crammed it into — a structure that doesn't allow distinctions between *"I thought about it"* and *"I did it,"* between *"I tried it and it worked"* and *"I tried it and it hurt."* In RAG, there are no fields for these distinctions. Not because the developers didn't think of it. Because **the vector-plus-distance paradigm itself** doesn't accommodate causality and time. It's a mathematical limitation. You don't fix it with product features. **Hole #3: Time doesn't exist as a first-class concept.** Three weeks ago I wrote into the agent's memory: *"we use Postgres."* Today I wrote: *"we migrated to ClickHouse for analytics, Postgres is OLTP only now."* In RAG, **both** facts sit there. Both have high cosine to a database query. Top-k returns both. The model picks the one that "sounds" better in its pretraining — usually Postgres, because it appears more often in the training data. This is **not** memory. This is a roulette wheel disguised as confidence. When was the last time you saw `valid_from`, `valid_until`, `deprecated_by`, `replaced_by`, `superseded_by` fields in a production RAG system? I never have. Because in standard RAG, they're **not in the schema**. And again — not because devs are lazy. Because the schema *"text plus embedding"* has no place for the lifecycle of knowledge. No notion of *"this is true now"* versus *"this was true then."* Everything collapses into a single time slice — a present that somehow contains yesterday, last year, and deprecated-three-quarters-ago all at once. > Ctrl+F with embeddings doesn't **remember**. It **finds**. Different verbs. --- ### "But memory frameworks fix this, right?" Okay, the believer says. There's mem0, Letta, Zep, Cognee, MemGPT, the whole long-term memory zoo. They added a meaning layer on top of RAG. They're memory-aware. Let's be honest. I've used them. One after another. For a long time. Looked under the hood, not just at the landing pages. Each of them takes **one** piece of real memory — for some it's LLM-extraction before write, for some it's a buffer hierarchy like an OS, for some it's post-hoc graph extraction from dialogues, for some it's per-fact temporal validity — and implements **that one piece**, without weaving it into the rest. This is warmer than vanilla Qdrant. It's **not** a solution. Because real memory requires **seven** properties working together. Each of them, in isolation, already exists in the literature or in open source. As far as I can tell, no one has assembled all seven into a single system. Which seven, exactly — that's part 2 of this series. Here, only the limitation that unites **all** flat-fact solutions, however they wrap themselves: **None of them have the right to say "I don't know."** Show me any one of these systems with a formal abstain mechanism: a gate through which a fact will **not** pass into prompt context if it has no source, no confidence, no temporal validity, or an unresolved contradiction. I'll wait. In the standard flow of all these frameworks, the system's response to *"there's a contradiction in memory or not enough data"* is *"well, the model will figure it out."* Which translates from marketing to engineering as *"the model will hallucinate, and that becomes your problem in production."* Good memory isn't *"remembering a lot."* It's **knowing the boundary of what you don't remember**. Part 2 of this series is built around that thesis. RAG isn't memory. It's Ctrl+F with embeddings. --- ### "Why not just push context to 1M tokens?" This is the second fashion of the last two years, and it deserves its own breakdown, because it leads the industry into the same dead end under a different banner. *"Why do we need memory if Gemini has 2M context, Claude has 1M?"* Four problems, no preamble. **One — economics.** A single project conversation at 800K tokens with prompt caching off costs tens of dollars **per request**. Without aggressive caching, you're broke in a week. With aggressive caching, you're building exactly the same hierarchy as Letta — just more expensive and locked to one vendor. **Two — recall.** Every long-context benchmark (NIH, Ruler, LongMemEval) shows the same thing: models **drown** in their own context past 200-300K tokens. Attention is unevenly distributed. This is **lost-in-the-middle**, and it doesn't get fixed by window size — it gets partially mitigated by architectural tricks inside the model, but it doesn't go away. The more you stuff in, the less of it actually gets considered. **Three — persistence.** Context isn't saved. Close the session, gone. Tomorrow the same agent shows up with a clean context. So you have to feed it 800K tokens of "history" again. The problem isn't solved — it's hidden inside your wallet and your latency. **Four — learning.** If the agent made a mistake yesterday and you corrected it, that experience isn't structured for the future. Tomorrow it'll repeat the mistake. Context is RAM, not disk. And when someone says *"just increase context instead of building memory"* — that's the same as saying *"why do I need a database, I have a terabyte of RAM."* Technically the words rhyme. In practice they're incomparable concepts. Big context doesn't replace memory. It lets you stuff more into one session — and that's it. --- ### What to do about it tomorrow morning If you've read this far and you're thinking *"okay, agreed, RAG is search, not memory. Now what?"* — I have two pieces of news. The bad: a systemically correct solution requires rewriting the memory layer from schema up through lifecycle, and that's months of work. Not a weekend. The good: there are several things you can do **tomorrow morning** that already remove half the pain. Not magic — just engineering hygiene. - **Drop the word "memory" from your stack if what you have is RAG.** Call it retrieval or search — instantly more honest. That alone removes 80% of inflated expectations from users and the team. - **Introduce `valid_from` and `valid_until` for every fact.** Any fact without temporal validity is a hypothesis, not a fact. Old facts should drop out of retrieval automatically, not compete with new ones on cosine. - **Distinguish `staging`, `working`, `consolidated`, `archived`.** Don't dump everything into one collection. A fact that just arrived and a piece of knowledge confirmed by tests are different entities with different weight in retrieval. - **Make abstain a first-class outcome.** If no fact passed the confidence threshold during retrieve, the system **must** have the right to say *"I don't know, I need data."* And that *"I don't know"* should become a task in the backlog, not a dead end for the user. This isn't a complete list — it's the minimum to start the transition from *"I have RAG, I call it memory"* to *"I have memory, and it knows its boundaries."* The full list of seven principles is in part 2. --- ### Where this comes from I sit deep in this kitchen — Claude Code, Cursor, Codex, Windsurf, MCP servers, mem0, Zep, local RAG stacks on Postgres + pgvector, Qdrant, Chroma. Over the last few months I've tried, I think, everything on the market. I have my own MCP memory server with about fifteen hundred entries, which I rewrote from scratch three times because each time I hit one of the three holes above. At some point, I got tired. Not of AI — of what we call memory at AI. Sat down and started writing my own cognitive runtime that **doesn't pretend to know**, that **knows what it doesn't know**, and that **sets its own tasks** to close the gaps. Called it `braincore`. One Go binary, local, MCP-stdio, Apache-2.0. Not a pitch, because it's open source — just an example that I say *"this can be done"* not theoretically. Seven architectural principles it's built on — that's part 2 of this series. Drops in a week. I'll cover atomic knowledge units, lifecycle, strict mode, causal decision chains, AST-based identity for code, internal git as memory versioning, memory scoring, and negative memory. And why all of that combined produces a qualitatively different result than any of those pieces in isolation. Part 3 is philosophical — about **the right of an AI agent to stay silent**, and why the right metric for production AI isn't accuracy but *zero confidently-wrong actions at an acceptable abstain rate*. About self-tasking. About why cognitive runtime matters more than model size. --- If you read this far and recognized yourself in the opening paragraph — we're in the same boat. If you have RAG that you call memory and it works — tell me how, seriously, I want to know, I might be wrong. The one thing you can't do is stay silent. --- *Part 1 of 3. Next — "Seven principles of real memory for AI agents" — drops next Tuesday.* --- ## Why AI-Generated Code Is Technical Debt From Day Zero - URL: https://vbcherepanov.com/articles/why-ai-generated-code-is-technical-debt-from-day-zero - Canonical (medium): https://medium.com/@vbcherepanov/why-ai-generated-code-is-technical-debt-from-day-zero-da3421a73989 - Published: 2026-04-15 - Reading time: 7 min - Tags: AI Agents, Backend - Language: en > Patterns that quietly accumulate debt: phantom abstractions, copy-paste-by-prompt, silent error degradation, context amnesia between requests, and decorative tests. Human oversight of architecture and immediate refactoring are the antidote. ![AI-generated code as technical debt cover](/images/articles/why-ai-generated-code-is-technical-debt-from-day-zero.png) For the past six months I've been generating code through Claude Code 6–8 hours a day. Not as an experiment — as my primary work tool. I run 7 custom sub-agents, MCP servers, hooks, persistent memory. I'm not some theorist who read a couple of blog posts and decided to have opinions. And that's exactly why I'm saying this: most AI-generated code is technical debt that starts rotting the moment it's committed. Not because the models are bad. Because people use them wrong. ## "It Works" Is Not Quality The main trap: you describe a task, get 200 lines of code, run it — works. Tests are green (if you even asked for any). PR gets merged. Everyone's happy. Three weeks later you open that file, and you have more questions than answers: - Why are there three layers of abstraction for writing to a single table? - Why does the service know about HTTP headers? - Where did this `catch (Exception $e)` come from that silently swallows errors? - Why does the DTO mirror the Entity structure 1:1, and why does it even exist? The model doesn't write bad code on purpose. It writes plausible code. Code that statistically resembles what it saw in training data. And the training data is Stack Overflow, GitHub repos with 2 stars, junior-level tutorials, and legacy projects on PHP 5.6. Plausible ≠ correct. Plausible ≠ maintainable. ## Concrete Patterns of Decay I'm not going to theorize. Here's what I see in real projects every single week: ### 1. Phantom Abstractions The model loves creating interfaces with a single implementation, factories for objects instantiated in one place, and service layers that just proxy repository calls. It does this because "that's how it's done" in the code it trained on. But abstraction without reason isn't architecture — it's noise. In a PHP/Symfony project with 15 entities, I counted 47 AI-generated interfaces. The actual need for polymorphism existed in 3 cases. ### 2. Copy-Paste via Prompt Humans copy-paste by hand. AI does the same thing, but at scale. You ask for "a similar endpoint for orders," and you get a full copy of the users endpoint with swapped names. No reuse. No generalization. Just a clone with different variable names. Six months later you have 30 controllers with identical error handling, validation, and pagination structures — all slightly different. Because each one was generated as an independent request. ### 3. Silent Degradation The model really doesn't like returning errors. It would rather wrap everything in try-catch, log it, and return an empty array. Or null. Or a default value. In Go it looks even worse: `if err != nil { return nil }` — and you find out about the problem three call layers deep, when data has already been written to the wrong place. This isn't a bug. It's a pattern: the model optimizes for "code compiles and doesn't crash," not for "code correctly reports problems." ### 4. Context Amnesia AI doesn't remember what it wrote 40 prompts ago. Every new request is a blank slate. You can end up with two services that do the same thing, conflicting validation approaches in different parts of the application, three different ways of handling dates in one project. In a monolith, a human at least sees the neighboring file. AI only sees what you showed it. And builds in a vacuum. ### 5. Decorative Tests Ask AI to write tests — you'll get tests. Beautiful, well-structured, with mocks and assertions. The problem: they test implementation, not behavior. They're brittle. They break on any refactor. And they create an illusion of coverage. I've seen a test suite with 94% coverage that didn't catch a single real business logic error. Every test verified that a method calls another method with the right arguments. In other words, they tested that the code is written the way it's written. Thanks, very useful. ## Why This Is Worse Than Regular Tech Debt Regular technical debt is taken on consciously. "Let's do it quick now, we'll refactor later." You know exactly where you cut corners. You know what will break. AI tech debt is hidden. The code looks clean. Naming is fine. Folder structure is textbook. No code reviewer will find fault. But underneath: - There's no unified architectural decision — just a collection of locally-optimal fragments - There's no understanding of business constraints — only formal correctness - There's no trade-off analysis — just "first plausible option" It's like a house where every room was designed by a different architect who never talked to the others. Each room is fine. The house as a whole is unlivable. ## So, Don't Use AI? No. I use Claude Code every day, and my velocity has increased dramatically. But I treat AI code as a draft, not a final result. My workflow: 1. **Architectural decisions are mine.** I define the structure, the layers, the contracts between modules. AI gets specific, bounded tasks within decisions already made. 2. **Review every generation.** Not "skimmed it" — an actual review. Why this interface? Why three dependencies here? What happens if this service goes down? 3. **Context is my job.** I maintain `CLAUDE.md` files with architectural rules for each project. Naming conventions, error handling approaches, banned patterns. Without this, every generation is a lottery. 4. **Refactor immediately.** Not "later." Right after generation — strip unnecessary abstractions, unify with the rest of the codebase, check edge cases. 5. **AI doesn't write business logic from scratch.** It implements what I've already thought through. The difference is between "draw me a house" and "build from this blueprint." ## The Bottom Line AI code generation is not a silver bullet, and it's not the end of the profession. It's a powerful tool that in an engineer's hands accelerates work, and in a prompt operator's hands generates technical debt at a speed that was previously physically impossible. The difference between "I use AI for development" and "AI develops for me" is the difference between a tool and a crutch. If you can't explain why every line in the generated code exists — you're not programming. You're accumulating debt that someone will have to pay off. Possibly you. In three months. With interest. --- *15+ years in production. PHP/Symfony, Go, Vue/Nuxt, PostgreSQL. Writing about real-world experience with AI tools in day-to-day development.* --- ## Your AI Coding Assistant Has Amnesia. Here's How I Fixed It. - URL: https://vbcherepanov.com/articles/your-ai-coding-assistant-has-amnesia - Canonical (medium): https://medium.com/@vbcherepanov/your-ai-coding-assistant-has-amnesia-heres-how-i-fixed-it-a8429f7f7e38 - Published: 2026-04-13 - Reading time: 9 min - Tags: AI Agents, Memory, Open Source - Language: en > Total Agent Memory — an open-source MCP server providing persistent knowledge across sessions. 32 tools spanning storage, self-improvement via error tracking, knowledge graphs, episodic recall, and skill assessment. ![AI coding assistant amnesia cover](/images/articles/your-ai-coding-assistant-has-amnesia.png) *How I built a persistent memory system that makes Claude Code and Codex CLI remember everything across sessions.* It's 11 PM on a Tuesday. You’ve spent the last three hours deep in a Claude Code session, refactoring a payment service. Claude understands your architecture perfectly — the repository pattern, the middleware chain, the naming conventions you settled on two weeks ago. You type `/compact` one last time, hit the context limit, and close the terminal. Wednesday morning. New session. New Claude. It knows nothing. “What’s your project structure?” it asks. Again. You explain the architecture. Again. You correct the same mistake it made last Thursday — using `map[string]interface{}` instead of typed DTOs. Again. You paste the same convention document. Again. If this sounds familiar, you’re not alone. I spent two months living this loop across 72 projects before I decided to fix it. --- ## The Real Problem: Stateless by Design Claude Code and OpenAI’s Codex CLI are extraordinary tools. But they share a fundamental limitation: **zero persistent memory between sessions**. Every conversation starts from scratch. This isn’t a bug — it’s architecture. These tools are stateless by design. But for anyone doing serious, ongoing development work, statelessness is a productivity killer. Here’s what you lose every time a session ends: - Architectural decisions and the reasoning behind them - Solutions to bugs you already solved - Project conventions that took sessions to establish - Mistakes Claude made (and the corrections you provided) - The mental model of your entire codebase I got tired of being a human memory bank for my AI assistant. So I built **total-agent-memory** — an open-source MCP server that gives Claude Code (and Codex CLI, Cursor, Cline, Continue, Aider, Windsurf, Gemini CLI, OpenCode — anything that speaks MCP) a persistent brain. **Website:** [totalmemory.dev](https://totalmemory.dev) · **GitHub:** [vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) > 💡 **Update (May 2026):** Originally shipped as `claude-total-memory`, the project was renamed to **total-agent-memory** in v12.0.0 to reflect that it works with every MCP client, not just Claude Code. The legacy `claude-total-memory` package on PyPI now ships as a deprecation shim that automatically pulls in `total-agent-memory>=12.0.0`, so existing installs keep working. --- ## What It Actually Does total-agent-memory is a Python MCP server that sits alongside Claude Code. It provides **32 tools across 6 categories** that let Claude save, search, relate, and learn from knowledge that persists forever. Think of it as upgrading Claude from a brilliant colleague with amnesia to one who keeps a detailed engineering notebook. ### Before vs. After **Before (every Monday morning):** ``` You: Continue working on the payment service Claude: I’d be happy to help! Could you tell me about your project structure, conventions, and what we’ve done so far? You: *sighs, pastes 2000 tokens of context* ``` **After:** ``` You: Continue working on the payment service Claude: [memory_recall(“payment service architecture”)] Got it. Last session we refactored PaymentService to use the gateway pattern. The Tinkoff integration is done, Stripe is next. You prefer constructor injection and the metrics middleware we set up in internal/middleware/metrics.go. Let me pick up where we left off. ``` That’s the difference. No re-explaining. No context pasting. Claude just *knows*. --- ## The 32 Tools in 6 Categories ### 1. Core Memory (12 tools) The foundation. Save and search knowledge with five types: `decision`, `solution`, `lesson`, `fact`, and `convention`. ```python # Claude saves a decision during your session memory_save( content=”Use UUID v7 for all primary keys instead of SERIAL. Reasons: sortable by time, no sequence contention, better for distributed systems.”, type=”decision”, tags=[“database”, “postgresql”, “architecture”], project=”payment-service” ) ``` ```python # Next week, different session, Claude searches memory_recall( query=”primary key strategy for postgresql”, detail=”full” ) # Returns: the exact decision above, ranked by relevance ``` The search isn’t just keyword matching. It’s a **4-tier hybrid pipeline**: ``` Query: “docker networking between services” │ ├── Tier 1: FTS5 + BM25 keyword search │ └── Finds exact matches: “docker”, “networking” │ ├── Tier 2: Semantic search (ChromaDB vectors) │ └── Finds related: “container communication”, “bridge network” │ ├── Tier 3: Fuzzy matching (SequenceMatcher) │ └── Catches typos: “dokcer netowrking” still works │ └── Tier 4: Graph expansion └── Follows relations: docker networking → compose config → env variables All tiers fused via Reciprocal Rank Fusion (RRF) ``` This matters. BM25 alone scores 89% on retrieval benchmarks. Semantic search alone hits 91%. The full 4-tier pipeline with RRF fusion? **97.45% on LongMemEval R@5** — beating MemPalace’s 96.6%. ### 2. Self-Improvement (6 tools) — The Killer Feature This is where it gets interesting. Claude doesn’t just *store* knowledge — it **learns from its own mistakes**. Here’s the pipeline: ``` Session 1: Claude uses `npm install` inside Docker → Hook detects error → self_error_log(category=”docker”, error=”running npm outside container”) Session 3: Same mistake again → Error count for “docker” category: 2 Session 5: Third time → 3+ errors in same category triggers auto-insight → self_insight(“Always run package managers inside Docker containers”) Insight gains confidence through successful application… → Promoted to SOUL rule (importance >= 5, confidence >= 0.8) → Rule loaded at EVERY session start → Claude never makes that mistake again ``` The tools: `self_error_log`, `self_insight`, `self_rules`, `self_patterns`, `self_reflect`, `self_rules_context`. The concept of SOUL rules — persistent behavioral rules that shape how Claude operates — is what makes this more than a database. It’s a feedback loop. Claude literally gets better at working with *your* codebase over time. ### 3. Knowledge Graph (4 tools) Knowledge isn’t flat. Decisions relate to other decisions. Solutions reference the problems they solved. The graph captures these relationships. ```python memory_relate( from_id=42, # “Use gateway pattern for payments” to_id=67, # “Tinkoff API requires idempotency keys” relation=”context” ) ``` When Claude recalls the gateway pattern decision, it automatically pulls in related context about Tinkoff’s API requirements. No manual linking needed after the initial relation is set. ### 4. Episodic Memory (2 tools) Facts tell you *what*. Episodes tell you *what happened*. ```python memory_episode_save( content=”Spent 3 hours debugging a race condition in the order service. Root cause: shared database connection pool across goroutines without proper context cancellation. Fixed by adding per-request connection checkout.”, context=”payment-service sprint 4" ) ``` When Claude encounters a similar concurrency issue months later, it doesn’t just know the fix — it remembers the debugging journey and the false starts. ### 5. Skills & Competencies (3 tools) Claude tracks what it’s good at and where it struggles. ```python memory_skill_get(skill=”kubernetes-debugging”) # Returns: proficiency level, last practiced, improvement trajectory memory_self_assess() # Returns: strengths, weaknesses, blind spots based on error history ``` ### 6. Advanced Cognitive Tools (5 tools) Spreading activation (`memory_associate`), automatic context building (`memory_context_build`), observation logging (`memory_observe`), and on-demand reflection (`memory_reflect_now`). --- ## Technical Architecture Under the hood, it’s deliberately simple: ``` ┌─────────────────────────────────────────────┐ │ MCP Server (Python). │ │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ │ │ │ SQLite │ │ ChromaDB │ │ Graph │ │ │ │ FTS5 │ │ (vectors) │ │ Engine │ │ │ │ + BM25 │ │ │ │ │ │ │ └──────────┘ └───────────┘ └──────────┘ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ Privacy Layer (auto-redacts secrets) │ │ │ └──────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ Web Dashboard (localhost:37737) │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ ``` Key design choices: - **SQLite FTS5** for keyword search with proper BM25 ranking — no external search engine needed - **ChromaDB** for vector similarity with binary quantization for speed - **Decay scoring** with a 90-day half-life — recent knowledge ranks higher, but nothing is thrown away prematurely - **Retention zones**: active (default) -> archived (180 days, never recalled) -> purged (365 days archived) - **Auto-deduplication** via Jaccard similarity (>0.85 threshold) prevents knowledge bloat - **Privacy stripping** automatically redacts API keys, JWTs, and email addresses before storage - **Zero external services** — everything runs locally. Your code knowledge never leaves your machine. --- ## Works with Both Claude Code AND Codex CLI Since it’s an MCP server, any tool that speaks the MCP protocol can use it. Configure it once, and both Claude Code and OpenAI’s Codex CLI share the same memory database. Switch between tools without losing context. --- ## Real Numbers from 2+ Months of Daily Use This isn’t a weekend project I’m theorizing about. It’s been running on my machine in daily production for over two months: | Metric | Value | | --- | --- | | Active knowledge records | 1,683 | | Projects tracked | 72 | | Graph nodes | 1,847 | | Graph edges | 20,925 | | Learned skills | 177 | | Captured episodes | 164 | The subjective difference is dramatic. Monday mornings went from 15–20 minutes of context restoration to essentially zero. Claude picks up exactly where it left off — not because the session persisted, but because every important piece of context was saved and is instantly searchable. --- ## The Self-Improvement Loop in Practice Let me walk through a real scenario. Week 1: I’m working on a Go project. Claude generates a handler with 30 lines of business logic inside it. I correct it — “handlers should be thin, under 15 lines, delegate to the service layer.” Claude adjusts. The correction gets saved as a `convention`. Week 2: Different project, same stack. Claude generates another fat handler. The hook catches my correction, logs it via `self_error_log`. That’s error #2 in the “go-architecture” category. Week 3: It happens again. Error #3. The system detects the pattern and generates an insight: “Go handlers must be thin (`<=15` lines) — delegate all business logic to service layer.” After several successful applications, the insight promotes to a SOUL rule. Now, every new session starts with Claude knowing this rule. The fat handler mistake stops happening. Not because I reminded it — because it *learned*. This is the difference between a tool that stores text and one that actually improves. --- ## Getting Started Pick whichever fits your stack — all six install paths land you at the same MCP server. Full instructions on **[totalmemory.dev](https://totalmemory.dev)**. ```bash # Node (zero-install via npx) npx -y total-agent-memory connect claude-code # Python via uv (fast) uvx total-agent-memory # Python via pipx (isolated venv) pipx install total-agent-memory # Homebrew (macOS / Linuxbrew) brew install vbcherepanov/tap/total-memory # Docker (multi-arch) docker run --rm -p 37737:37737 -v ~/.tam:/data ghcr.io/vbcherepanov/total-agent-memory:latest # Manual clone git clone https://github.com/vbcherepanov/total-agent-memory.git ~/total-agent-memory && cd ~/total-agent-memory && ./install.sh ``` The `npx` path also wires the MCP entry into your IDE of choice — `connect claude-code`, `connect codex`, `connect cursor`, `connect cline`, `connect continue`, `connect aider`, `connect windsurf`, `connect gemini-cli`, `connect opencode`. If you prefer to do it by hand, drop this into `~/.claude/settings.json`: ```json { "mcpServers": { "memory": { "command": "/absolute/path/to/.venv/bin/total-agent-memory", "env": { "TAM_MEMORY_DIR": "~/.tam" } } } } ``` That's it. Next time Claude Code starts, it has 32 new tools available. Start with `memory_save` and `memory_recall` — the rest builds from there. --- ## Limitations (Honest Take) No project is perfect. Here’s what to know: - **First-session cold start**: The memory is empty at first. It takes 2–3 sessions of active use before the benefits compound. - **Storage grows**: 1,600+ records take about 50MB with vectors. Not a concern for modern machines, but it’s not zero. - **Claude needs prompting at first**: Until SOUL rules build up, you may need to remind Claude to use `memory_recall` at the start of sessions. Hooks help automate this. - **Python dependency**: The server requires Python 3.10+ and a few packages. The install script handles this, but it’s not a single binary. --- ## Why Open Source I built this for myself. Then I realized every Claude Code user has the same problem. The project is **MIT licensed** — use it, fork it, modify it, contribute to it. The memory problem is arguably the single biggest friction point in AI-assisted development today. Context windows keep getting bigger, but they’ll never be infinite. And even if they were, re-loading context every session is wasteful. Persistent memory is the right abstraction. --- ## Try It If you use Claude Code or Codex CLI for real work — not demos, not toy projects, but actual ongoing development — this will change your workflow. Star the repo, try it for a week, and see the difference when your AI assistant actually remembers who you are and what you’re building. **Website:** [totalmemory.dev](https://totalmemory.dev) · **GitHub:** [vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) **License:** MIT — free forever. --- *Vitalii Cherepanov is a software engineer building tools at the intersection of AI and developer productivity. He writes Go and PHP by day and teaches Claude to remember things by night.* --- ## I Studied the etcd Codebase — and It Changed How I Write PHP - URL: https://vbcherepanov.com/articles/i-studied-the-etcd-codebase - Canonical (medium): https://medium.com/@vbcherepanov/i-studied-the-etcd-codebase-and-it-changed-how-i-write-php - Published: 2026-04-20 - Reading time: 11 min - Tags: Backend, Architecture - Language: en > Seven architectural principles distilled from etcd's Go codebase, mapped onto everyday PHP/Symfony work: typed contracts, single-responsibility services, composable middleware, observability as architecture, facade APIs, traceable request paths, and explicit dependencies. ![etcd codebase study cover](/images/articles/i-studied-the-etcd-codebase.jpg) There's a common piece of advice: "Want to write better code? Read good code." Sounds obvious. Rarely practiced. The problem is that most open-source projects are mazes. You open a repo, see 200 directories, and close the tab. Kubernetes is two million lines. The Linux kernel — don't even think about it. Where do you start? My answer: **etcd**. For those unfamiliar: etcd is a distributed key-value store written in Go. It's the backbone of Kubernetes — every piece of cluster state lives there. But I'm not interested in etcd as a product. I'm interested in it as **an example of architecture you can actually read from start to finish**. Here's what surprised me: the principles baked into etcd aren't about Go. They're about software design in general. I work with PHP and Symfony daily, and almost everything I found in etcd translated directly into my projects. Seven principles, concrete examples, no fluff. --- ## 1. One Source of Truth for Your API In etcd, every API is defined in `.proto` files. Open `rpc.proto` and you see all operations: `Range`, `Put`, `DeleteRange`, `Txn`. Every field is typed. There's no room for "wait, do we accept a string or an integer here?" In PHP, instead of protobuf, we have **strictly typed DTOs**: ```php final readonly class CreateOrderRequest { public function __construct( public string $customerId, /** @var OrderItemDto[] */ public array $items, public ?string $promoCode = null, ) {} } ``` One class — and everyone knows what the endpoint accepts. The frontend dev looks at the DTO, the backend dev writes logic against it, the OpenAPI schema generates automatically via NelmioApiDocBundle. Compare this with what I've seen (and written) on real projects: ```php $data = json_decode($request->getContent(), true); $customerId = $data['customer_id'] ?? null; $items = $data['items'] ?? []; // What's the format of items? Is promoCode a thing? Who knows. ``` When your contract is "well, some array comes in," any change breaks something unexpected. When your contract is a DTO with types, PHPStan catches the problem before production does. --- ## 2. Each Service Does One Thing etcd has clearly separated gRPC services: `KV` (read-write), `Watch` (subscribe to changes), `Lease` (key TTLs), `Auth` (authorization). Each one is a separate interface. `Watch` doesn't touch writes. `KV` doesn't check tokens. In Symfony — same idea, different tools: ```php class OrderController { #[Route('/orders', methods: ['POST'])] public function create( CreateOrderRequest $request, OrderService $orderService, ): JsonResponse { return new JsonResponse( $orderService->create($request) ); } } ``` `OrderService` creates orders. It doesn't send emails — that's `NotificationService` listening to an `OrderCreatedEvent`. It doesn't process payments — that's `PaymentService`. And then there's the alternative I see regularly: ```php class OrderController { public function create(Request $request) { // 40 lines of validation // 20 lines of authorization // 60 lines of business logic // 15 lines sending email // 10 lines of logging // Total: 150 lines, untestable } } ``` The 500-line god controller. We've all been there. etcd helped me finally articulate *why* it's bad: not because "the pattern is wrong," but because **you can't trace what the system is doing**. --- ## 3. Middleware Composes Like Lego Every gRPC request in etcd passes through a chain of interceptors: logging → auth → metrics → handler → metrics → response. Each interceptor is small, single-purpose. The power comes from composition. In Symfony, this maps to Event Listeners and Messenger Middleware: ```php class MetricsMiddleware implements MiddlewareInterface { public function __construct( private PrometheusCollector $metrics, ) {} public function handle(Envelope $envelope, StackInterface $stack): Envelope { $start = microtime(true); try { $result = $stack->next()->handle($envelope, $stack); $this->metrics->increment('messages_processed_total', [ 'type' => $envelope->getMessage()::class, 'status' => 'success', ]); return $result; } catch (\Throwable $e) { $this->metrics->increment('messages_processed_total', [ 'type' => $envelope->getMessage()::class, 'status' => 'error', ]); throw $e; } finally { $this->metrics->histogram( 'message_duration_seconds', microtime(true) - $start, [$envelope->getMessage()::class] ); } } } ``` One middleware, one job. Metrics here, logging there, retry somewhere else. Assemble the chain in `messenger.yaml`. The antipattern — when every handler has this manually: ```php public function handle(CreateOrderCommand $command): void { $this->logger->info('Starting order creation...'); $start = microtime(true); // ... actual logic ... $this->metrics->record(microtime(true) - $start); $this->logger->info('Order created'); } ``` 50 handlers, 50 copies of the same boilerplate. Forget one — no metrics. Change the log format — change it in 50 places. --- ## 4. Observability Is Architecture, Not an Afterthought In etcd, Prometheus is wired into the gRPC layer from day one. Not "added six months after launch." The code isn't considered done without metrics. In PHP: ```php class PaymentService { public function charge(Order $order): PaymentResult { $timer = $this->metrics->startTimer('payment_charge_duration'); try { $result = $this->gateway->process($order); $this->metrics->increment('payments_total', [ 'provider' => $result->provider, 'status' => $result->isSuccess() ? 'success' : 'declined', ]); return $result; } catch (GatewayTimeoutException $e) { $this->metrics->increment('payments_total', [ 'provider' => $order->paymentMethod, 'status' => 'timeout', ]); throw $e; } finally { $timer->observe(); } } } ``` Every payment — in metrics. How many succeeded, how many timed out, which provider is slow. Not because someone asked for it, but because without it you're flying blind. I remember a project where production was down for 40 minutes and the only way to understand what was happening was `tail -f /var/log/symfony.log | grep ERROR`. Never again. Package: `promphp/prometheus_client_php`. Five minutes to install, fifteen to wire up Grafana. --- ## 5. Simple Outside, Rocket Science Inside `clientv3` in etcd is a masterclass in the facade pattern: ```go client.Put(ctx, "name", "value") ``` One line. Under the hood: node selection, reconnection on failure, retry with exponential backoff, protobuf serialization, Raft consensus, disk write, quorum confirmation. Same principle in PHP: ```php // Calling code. Simple and clear. $paymentService->charge($order); ``` Inside `charge()`: ```php public function charge(Order $order): PaymentResult { if ($existing = $this->findExistingPayment($order)) { return $existing; // idempotency } $provider = $this->providerResolver->resolve($order); $result = $this->withRetry( fn () => $provider->process($order), maxAttempts: 3, backoff: 'exponential', ); if ($result->isSuccess()) { $this->fiscalService->createReceipt($order, $result); } $this->events->dispatch(new PaymentProcessed($order, $result)); return $result; } ``` The controller calling `charge()` knows nothing about fiscal receipts, retries, or provider selection. And it shouldn't. A sign of a good service: you can explain what it does in one sentence — "charges the customer for an order" — while the implementation is 200 lines of careful logic. --- ## 6. You Can Trace a Request With Your Finger In etcd, the request path reads linearly: ```plaintext gRPC handler → EtcdServer.Put() → Raft → apply → bbolt (disk) ``` No magic. No hidden calls. No "where does this even get triggered?" In Symfony — same thing, if you don't abuse the event system: ```plaintext Request → Controller (unwrap DTO) → Service (business logic) → Repository (database) → EventDispatcher (side effects) → Response ``` Open the controller — see which service is called. Open the service — see what it does. Open the repository — see the query. What kills traceability: - `@PostPersist` on an entity that silently sends SMS - `prePersist` listeners modifying data before writes — and you spend 30 minutes figuring out who's touching the `updatedAt` field - Ten `EventSubscriber`s on the same event with unclear execution order Event-driven is great. But if a new developer can't explain "request comes in here, response goes out there" within 2 minutes — you have a problem. --- ## 7. No Hidden Dependencies In etcd, all dependencies are passed explicitly: ```go func NewKVServer(s *EtcdServer) KVServer { ... } ``` See the constructor — see everything the class needs. In Symfony — constructor injection, same thing: ```php class OrderService { public function __construct( private OrderRepository $orders, private PaymentGateway $payment, private EventDispatcherInterface $events, private LoggerInterface $logger, ) {} } ``` Four dependencies. All visible. Want to test? Swap in mocks. Want to understand the class? Look at the constructor. Antipatterns that still survive in the wild: ```php // Service locator: where did this come from? $payment = $this->container->get('payment.gateway'); // Static calls: untestable Cache::put('key', $value); // new SomeService() inside another service: invisible coupling $validator = new OrderValidator(); ``` Symfony's autowiring isn't magic in the bad sense. The container wires dependencies by type, but you still see them in the constructor. It's convenience, not hidden behavior. --- ## My Checklist After studying etcd, I distilled a checklist I now apply to every new service: 1. **Contract defined?** DTOs exist, types are set, OpenAPI generates from them 2. **Controller thin?** 10 lines max, all logic in the service layer 3. **Cross-cutting concerns extracted?** Logging, metrics, retry — through middleware, not copy-paste 4. **Metrics present?** If not, the service isn't production-ready 5. **Simple API externally?** Calling code doesn't know about internal complexity 6. **Request path traceable?** A new developer finds the handler in 2 minutes 7. **Dependencies explicit?** Everything in the constructor, nothing from thin air None of this is revolutionary. It's basic hygiene that's easy to forget under deadline pressure. etcd just reminded me what a codebase looks like when that hygiene wasn't skipped. And that it's possible even in a large production system. --- *What open-source codebase changed how you write code? I'd love to build a reading list — drop yours in the comments.* --- ## ClearVibeArchitecture (CVA) — Complete Guide - URL: https://vbcherepanov.com/articles/clear-vibe-architecture - Published: 2025-10-16 - Reading time: 45 min - Tags: Architecture, Backend, Distributed Systems - Language: en > An architectural style for backend systems combining Hexagonal, Outbox/Inbox, Observability, mandatory Feature Toggles, and flow visualisation. ## Table of Contents 1. [Introduction and TL;DR](#part-1-introduction-and-tldr) 2. [Glossary and Terminology](#part-2-glossary-and-terminology) 3. [Architecture Map and Layers](#part-3-architecture-map-and-layers) 4. [Principles and Rules](#part-4-principles-and-rules) 5. [Patterns](#part-5-patterns) 6. [Data Schema and Contracts](#part-6-data-schema-and-contracts) 7. [Observability](#part-7-observability) 8. [Security](#part-8-security) 9. [DevEx and Productivity](#part-9-devex-and-productivity) 10. [Reference Skeleton (Go)](#part-10-reference-skeleton-go) 11. [Reference Skeleton (Symfony/PHP)](#part-11-reference-skeleton-symphonyphp) 12. [Evolution Policies](#part-12-evolution-policies) 13. [Feature Toggles](#part-13-feature-toggles) 14. [Visualization](#part-14-visualization) 15. [Implementation Checklists](#part-15-implementation-checklists) 16. [Licensing and Contribution](#part-16-licensing-and-contribution) --- ## Part 1: Introduction and TL;DR ### 1.1 What is ClearVibeArchitecture (CVA) **ClearVibeArchitecture** (CVA) is an architectural style for backend systems that combines: - **Hexagonal (Ports & Adapters)** as the foundation for domain isolation - **Events by default** (Domain Events + Outbox/Inbox, idempotency) - **Observability by default** (traces, metrics, structured logs, timeline) - **Mandatory Feature Toggles** as a cross-cutting pattern for all new features - **Flow visualization (web/VR)** as a product artifact, not a "nice option" **In short:** hexagonal architecture "with batteries included" — transparent, observable, and evolutionarily manageable. ### 1.2 Problems It Solves 1. **Hard to safely change the system** → feature flags + canary/blue-green 2. **Can't see "what's happening inside"** → otel-tracing + business metrics + timeline 3. **Reliable integration between services** → Outbox/Inbox + idempotency 4. **Developers drowning in infrastructure** → clean domain and contracts at boundaries 5. **Hard to explain system behavior to business** → request/event flow visualizer ### 1.3 Design Goals - **Transparency:** every request and event is traced through all layers - **Domain isolation:** domain model is independent of infrastructure - **Reliable delivery:** events are published transactionally (Outbox) - **Managed evolution:** all new branches behind feature flags with contract versioning - **Developer Experience:** module templates, unified CI/CD practices, tests as standard ### 1.4 Non-Functional Requirements (NFR) - **Reliability:** idempotent handlers, retries, deduplication - **Observability:** p95-latency, error rate, saturation as mandatory metrics - **Security:** Zero-Trust boundaries; message signing; audit of flag/contract changes - **Performance:** in-proc cache for flag decisions, back-pressure, I/O batching - **Compatibility:** SemVer for API/event contracts, n-1 support ### 1.5 TL;DR Principles - **Domain First:** domain doesn't know about ORM/HTTP/broker - **Ports & Adapters:** all I/O through interfaces (in/out-ports) - **Events First:** domain events are first-class; publishing via Outbox - **Observability First:** trace_id everywhere; business metrics on domain events - **Feature-Toggles Everywhere:** new functionality doesn't live without a flag and kill-switch - **Contracts First:** API/event schemas are versioned, verifiable, compatible - **Visualize the Flow:** mandatory visualization of request/event path ### 1.6 Scope of Application - Microservices and large monoliths with decomposition prospects - High integration load, frequent releases, A/B experiments - Teams for whom transparency and incident reproducibility are important **Not the best choice** if the system is extremely simple, rarely changes, and doesn't require telemetry/events. ### 1.7 "CVA Readiness" Criteria - All new features behind feature flags with canary and kill-switch - Outbox/Inbox and idempotent handlers present - OpenTelemetry (tracing), structured logs, and key business metrics enabled - API/event contracts described with schemas, CI-validated, SemVer maintained - Web/VR flow visualizer available (minimum — web graph + timeline) - Module templates/generators, linters, PR checklists, basic e2e tests ### 1.8 Output Artifacts - Architecture map of layers and ports (in/out), domain model and context boundaries - Contract catalog (OpenAPI/Proto/JSON-Schema/Avro) with SemVer and checks - Outbox/Inbox library/module, FeatureGate SDK, otel-integration - SLO dashboards and flow visualizer (minimum — web) - Code templates (Go, Symfony/PHP), Makefile/Compose, CI-pipeline --- ## Part 2: Glossary and Terminology ### 2.1 Basic Entities - **Domain** — business area describing business rules and invariants - **Aggregate** — root entity encapsulating state and behavior of related objects - **Event (Domain Event)** — immutable fact recorded in the domain - **Command** — intention to change state (create, update, delete) - **Query** — intention to get data without side effects - **Use Case** — application logic linking command/query and domain ### 2.2 Ports and Adapters - **InPort** — system entry interface (API, UI, CLI, test) - **OutPort** — system exit interface (database, message broker, external APIs) - **Adapter** — port implementation for specific infrastructure ### 2.3 Event Components - **Outbox** — table/storage for reliable event publishing with transaction - **Inbox** — table/storage for receiving and deduplicating incoming events - **Saga/Process Manager** — coordinates long-running processes through event sequence ### 2.4 Observability - **Trace ID** — unique request identifier, passed through all layers - **Span** — part of trace reflecting a step (e.g., SQL query, API call) - **Metric** — measurement (latency, error rate, business metric) - **Structured Log** — log in JSON format with mandatory fields (timestamp, trace_id, level, message) ### 2.5 Feature Toggles - **Feature Flag** — binary switch or variant parameter controlling feature availability - **Variant** — value of variant flag (A/B/n-test) - **Kill-Switch** — flag for instant disabling of problematic feature - **Exposure Event** — event of feature being shown to user ### 2.6 Contracts - **API Contract** — formal API description (OpenAPI, gRPC proto) - **Event Contract** — event structure description (JSON Schema, Avro) - **Backward Compatibility** — new description doesn't break previous version clients - **SemVer** — contract versioning following Semantic Versioning rules --- ## Part 3: Architecture Map and Layers ### 3.1 General Schema ``` ┌─────────────────────────────────────────────────────┐ │ Interface Layer │ │ (REST, gRPC, GraphQL, CLI, VR/AR) │ └────────────────┬────────────────────────────────────┘ │ InPorts ┌────────────────▼────────────────────────────────────┐ │ Application Layer │ │ (Commands, Queries, Handlers, Policies) │ └────────────────┬────────────────────────────────────┘ │ Domain Services ┌────────────────▼────────────────────────────────────┐ │ Domain Layer │ │ (Entities, Aggregates, Events, Value Objects) │ └─────────────────────────────────────────────────────┘ ▲ │ OutPorts ┌────────────────┴────────────────────────────────────┐ │ Infrastructure Layer │ │ (DB, Brokers, Outbox/Inbox, Observability) │ └─────────────────────────────────────────────────────┘ ``` ### 3.2 Interface Layer - **Entry:** REST, gRPC, GraphQL, CLI, VR/AR-visualizer - **Tasks:** accept commands/queries, convert to DTO, pass to Application - **Features:** logging, tracing, authN/authZ ### 3.3 Application Layer - Heart of application logic: commands, queries, handlers, policies - Coordination: orchestrators, saga, process managers - Validation and DTO mapping - Transaction boundaries guarantee: one use case = one transaction - All side effects through Outbox ### 3.4 Domain Layer - Entities, Aggregates, Value Objects - Invariants, business rules, ubiquitous language - Domain Events as first-class objects - Domain Services — pure functions without infrastructure - Domain doesn't know about ORM, HTTP, brokers ### 3.5 Infrastructure Layer - Port implementations: repositories, broker adapters, external API clients - Outbox/Inbox — mandatory components for publishing/receiving events - Observability stack: otel-integration, logging, metrics - Caching, file storage, integrations with external services --- ## Part 4: Principles and Rules ### 4.1 Clean Core (Domain First) - Domain is isolated from infrastructure - Forbidden to use ORM annotations, SQL, or external service SDKs inside domain - Invariants are checked only in domain ### 4.2 Ports and Adapters - All external interactions through interfaces (Ports) - Interface implementations only in Infrastructure (Adapters) - Application calls OutPorts, Domain knows nothing about them ### 4.3 Events by Default - Every significant change is recorded as Domain Event - Events are published through Outbox with transaction - Inbox with idempotency used for incoming events ### 4.4 Transaction Boundaries - One use case = one transaction - Side effects (sending to broker, integrations) are recorded in Outbox - Outbox processing is asynchronous and retriable ### 4.5 Observability by Default - Every request and event accompanied by trace_id - All handlers and adapters must log and metric their work - Business events become part of metrics (e.g., order.created.count) ### 4.6 Contracts at Boundaries - API and events described by contracts (OpenAPI/Proto/Avro/JSON Schema) - Contracts are versioned (SemVer) - Backward Compatibility is mandatory (n-1 version support) ### 4.7 Feature Toggles Everywhere - Any new functionality implemented only behind a flag - Feature flag is mandatory even if functionality will always be enabled later - Each flag has an owner, sunset date, and kill-switch ### 4.8 Flow Visualization - Request and event timeline must be reproducible in visualizer (web/VR) - Bottlenecks are automatically highlighted (heatmap) - Visualization is part of the product, not an "additional tool" --- ## Part 5: Patterns ### 5.1 Hexagonal / Ports & Adapters - Basic architecture pattern - Domain is isolated, input/output dependencies as InPorts/OutPorts - External world interaction through adapters ### 5.2 CQRS (optional) - Separation of commands (state changes) and queries (data reading) - Used where reading differs significantly from writing - Allows scaling reads and writing optimized projections ### 5.3 Outbox / Inbox - **Outbox:** transactional event recording with DB change - **Inbox:** registration and deduplication of incoming messages - Guarantees "exactly-once delivery" at application level ### 5.4 Saga / Process Manager - Long-running process management - Saga: chain of steps with compensations - Process Manager: reacts to events, coordinates actions of multiple aggregates/services ### 5.5 Feature Toggles (mandatory) - Cross-cutting pattern: any new functionality implemented behind flag - Flags have owner, sunset dates, kill-switch - Percentage rollout, user segments, A/B/n-tests supported - Every exposure event (fact of flag usage) is logged ### 5.6 Observability Patterns - **Distributed Tracing:** trace_id through all layers - **Structured Logging:** JSON logs with key fields - **Metrics & SLOs:** business and technical metrics; auto-alerts - **Timeline Visualization:** event and request flow displayed in web/VR ### 5.7 Deployment Patterns - **Feature Flags + Canary Release:** new functionality enabled gradually - **Blue/Green Deployment:** two parallel environments, fast switching - **Rollback by metrics:** automatic feature/version disabling on degradation --- ## Part 6: Data Schema and Contracts ### 6.1 API Contracts - All public APIs described in OpenAPI or gRPC Proto - Contracts pass automatic CI validation - Strict versioning: SemVer (1.2.3) - Backward compatibility: n-1 client version support - Contracts published to artifact repository ### 6.2 Event Contracts - Events described in JSON Schema, Avro, or Protobuf - Each event has: event_name, version, timestamp, trace_id, payload - Mandatory fields: id, trace_id, source - Events pass schema validation before publishing ### 6.3 Versioning - Contracts versioned independently of services - Multiple event versions support: consumers can read v1 and v2 in parallel - During transition: producer starts publishing both formats (dual write) ### 6.4 Database Schemas - Migrations managed by tools: Liquibase/Flyway/golang-migrate/DoctrineMigrations - Every DB change accompanied by migration script - Shadow-write/read supported for complex schema changes - All migrations pass through CI and test database --- ## Part 7: Observability ### 7.1 "Observability by Default" Principle - Any new service or module in CVA must have built-in observability tools - Metrics, logs, and traces aren't added "later" — they're part of architecture - Observability covers both technical and business processes ### 7.2 Traces (Distributed Tracing) - OpenTelemetry or compatible standard used - Each request has unique trace_id - Each operation (SQL, RPC, external API) recorded as span - All events (Domain Events) also carry trace_id - Visualizer (web/VR) builds timeline based on trace_id ### 7.3 Logs (Structured Logging) - Format: JSON - Mandatory fields: timestamp, level, trace_id, service, message - Logs written to stdout → centralized storage (ELK, Loki) - Errors and business events recorded equally structured ### 7.4 Metrics - **Technical:** latency (p95/p99), error_rate, saturation, throughput - **Business:** order count, conversion, rule rejection count - **Feature Flags:** exposure metrics (how many users see feature), enable_rate - Metrics available in Prometheus/Grafana or equivalents ### 7.5 Alerts and SLO - Every critical metric has SLO (Service Level Objective) - Violations → alerts (PagerDuty, Slack, Email) - Alerts must be actionable (clear what to do when triggered) - Feature flags have auto-rollback by SLO --- ## Part 8: Security ### 8.1 Zero Trust Principle - Each service and component interacts with others only through verified channels - No trust by default even within same network - Authorization and authentication applied at every level ### 8.2 Authentication (AuthN) - External calls: OAuth2 / OpenID Connect / mTLS - Internal calls: service accounts + mTLS - All requests must contain correlation trace_id and authorization token ### 8.3 Authorization (AuthZ) - RBAC (Role-Based Access Control) or ABAC (Attribute-Based Access Control) - Permission checks performed in Application Layer (policies) - Feature Flags can be limited by roles/segments ### 8.4 Encryption - In transit: TLS 1.3 (API, brokers, DB) - At rest: disk/table/object encryption (AES-256) - Confidential data (PII) always encrypted ### 8.5 Secrets Management - Secrets not stored in code or environment variables - Vault/KMS used (HashiCorp Vault, AWS KMS, GCP Secret Manager) - Secrets access strictly by principle of least privilege --- ## Part 9: DevEx and Productivity ### 9.1 DevEx-first Principle - Architecture should be convenient for developers - Templates, tools, and processes built into CVA - Developers spend minimum time on routine and infrastructure ### 9.2 Scaffolds and Generators - Code generators for entities, commands, events, ports, and adapters - `make scaffold module=Order` → creates structure: - Domain: entity, events, value objects - Application: commands, handlers, policies - Infrastructure: repository, adapters - Uniformity guarantee across projects ### 9.3 Code Style and Linters - Unified code style (Go, PHP, JS/TS) - Pre-commit hooks: linters, tests, security scans - Mandatory trace_id, logs, feature flags checking ### 9.4 CI/CD Pipeline CI checks: - compilation/build - tests (unit, integration, e2e) - contract schemas (API, events) - DB migrations - security scan CD supports blue/green and canary deployment --- ## Part 10: Reference Skeleton (Go) ### 10.1 Project Structure ``` /clearvibe /cmd/app/main.go /internal /domain/order entity.go, events.go, service.go /app/order commands.go, handler.go, policies.go, ports.go /infra /db: order_repo_pg.go /broker: outbox.go, inbox.go /http: server.go /obs: tracing.go, metrics.go, logging.go /flags: feature_gate.go ``` ### 10.2 Port Interfaces ```go type OrderRepo interface { Save(*Order) error FindByID(id string) (*Order, error) } type FeatureGate interface { Enabled(ctx FeatureContext, key string) bool } ``` ### 10.3 Command and Handler ```go type CreateOrderCmd struct { CustomerID string Items []Item } func (h *createOrderHandler) Handle(cmd CreateOrderCmd) (string, error) { if h.flags.Enabled(FeatureContext{UserID: cmd.CustomerID}, "orders.dynamic_pricing") { // new logic branch } ord, _ := domain.NewOrder(cmd.CustomerID, cmd.Items) h.repo.Save(ord) h.outbox.Add("order.created", ord.ID, OrderCreated{ID: ord.ID}) return ord.ID, nil } ``` --- ## Part 11: Reference Skeleton (Symfony/PHP) ### 11.1 Project Structure ``` src/ Domain/Order/ Entity/Order.php Event/OrderCreated.php Application/Order/ Command/CreateOrder.php Handler/CreateOrderHandler.php Infrastructure/ Persistence/Doctrine/OrderRepository.php Messaging/OutboxPublisher.php FeatureFlags/RedisFeatureGate.php ``` ### 11.2 Entity and Event ```php final class Order { private string $id; private string $customerId; private float $total; public static function create(string $customerId, array $items): self { return new self($customerId, $items); } } final class OrderCreated { public function __construct( public readonly string $id, public readonly float $total, ) {} } ``` --- ## Part 12: Evolution Policies ### 12.1 General Principle Evolution is managed and observable. Any change goes through feature flags, contracts, and migrations, has owner, risk plan, and rollback paths. ### 12.2 Contract Versioning (API/Events) - SemVer: MAJOR.MINOR.PATCH - n-1 compatibility: at least one previous version supported - Dual write/read: during event migration, producer publishes vN and vN+1 - CI compatibility validators: forbid breaking-changes without MAJOR ### 12.3 Feature Lifecycle 1. **Draft:** flag created (disabled), owner, goal, success metrics, sunset date 2. **Internal:** enabled for dev/stage/own team 3. **Canary:** 1% → 5% → 25% → 50% → 100% (deterministic bucketing) 4. **General Availability:** flag default on 5. **Sunset:** flag and dead code removed, contracts stabilized ### 12.4 Release Strategies - **Blue/Green:** two environments, traffic switching - **Canary:** incremental rollout with auto-guards by SLO - **Dark Launch:** code deployed, feature disabled by flag until activation --- ## Part 13: Feature Toggles ### 13.1 Principle Feature Toggles are mandatory and cross-cutting pattern in CVA. Any new functionality implemented only behind a flag. ### 13.2 Flag Types - **Boolean:** on/off - **Percentage:** traffic percentage (canary rollout) - **Segment:** enabling by attributes (country, tier, role) - **Multivariant:** multiple variants for A/B/n-tests ### 13.3 Storage - Storage: Postgres (source of truth) + Redis (cache, pub/sub) - Schema: flags(id, key, type, rules, default, owner, version, ttl, updated_at) - Versioning: SemVer for flag contracts, n-1 support ### 13.4 Metrics and Observability - Exposure events: flag.exposed, flag.variant - Metrics: exposure_count, enable_rate, error_rate - Dashboards: variant comparison (A/B), correlation with business metrics - Trace_id: always passed in exposure events --- ## Part 14: Visualization ### 14.1 Principle In CVA, visualization is not a "nice option" but a mandatory layer. The system must show data and event flow. ### 14.2 Timeline - Displays request steps chronologically - Examples: HTTP request accepted, Handler executed, Event recorded in Outbox - Each step corresponds to span (trace) - Timeline available in web interface and exported to VR ### 14.3 Flow Graph - Nodes: services, aggregates, adapters - Edges: calls and events - Filter support: by trace_id, event type, errors/latency ### 14.4 Heatmap - Bottleneck highlighting - Latency/error_rate metrics visualized in color - Red = problem, green = normal ### 14.5 VR Visualization - Data flow can be "seen" in 3D (VR/AR) - Service nodes in space, event lines moving in real-time - Useful for onboarding new team members and incident analysis --- ## Part 15: Implementation Checklists ### 15.1 General Project Checklist - Bounded Contexts and Ubiquitous Language defined - Architecture map of layers formed - Architecture owner appointed ### 15.2 Domain Layer - All entities described as Entity/ValueObject - Invariants fixed and test-covered - Domain Events defined and documented - No dependencies on ORM/HTTP/brokers ### 15.3 Application Layer - Each action formed as Command/Query - Handler for each command - Policies validate access and rules - Transaction boundaries defined - Feature Flags integrated into use cases ### 15.4 Infrastructure Layer - Repositories implemented via OutPorts - Outbox connected (mandatory) - Inbox connected for incoming events - Adapters configured for API/brokers/DB - Observability components connected ### 15.5 Feature Toggles - Every new code has feature flag - Flags have owner, sunset dates, kill-switch - Exposure events logged - Rollout scenarios documented - Tests include on/off scenarios ### 15.6 Observability - All requests/events have trace_id - Logs structured (JSON) - Metrics: technical + business - Dashboards and alerts configured - Visualizer connected (timeline/flow graph/VR) --- ## Part 16: Licensing and Contribution ### 16.1 License - Open Source (MIT/Apache 2.0): architectural principles, checklists, and templates freely available - Can be used in commercial and non-commercial projects without restrictions - Only requirement — maintain authorship and license mention ### 16.2 Contribution Guide Pull Request accepted only if: - ADR written for changes - CI passed (contracts, tests, migrations) - Checklists and documentation updated - Examples added (Go/PHP SDK, migrations, metrics) ### 16.3 Governance - **Maintainers:** responsible for review and releasing new CVA manifest versions - **Contributors:** contribute ideas, bugfixes, additions to checklists and patterns - Decisions made through RFC/ADR --- ## Key Differences of CVA ### From Classical Hexagonal/Clean Architecture **Same foundation** (DDD + Hexagonal), but CVA makes mandatory things usually "added later": - Events (Domain Events + Outbox/Inbox) - Observability (tracing/metrics/structured logs) - Feature flags with cross-cutting integration - Managed evolution (canary/blue-green, contract versioning) - Flow visualization (web/VR) **Idea:** system is initially "live, transparent, and safely evolving". ### Where CVA is Better 1. **Transparency and debugging** — built-in OpenTelemetry, business metrics, timeline 2. **Reliable integration** — Outbox/Inbox, idempotency, retries by default 3. **Safe releases** — feature flags, canary rollout, kill-switch, auto-rollback 4. **Managed evolution** — SemVer, dual write/read, shadow write/read 5. **Developer Experience** — templates, checklists, standards 6. **Business understanding** — flow visualization, business metrics out of the box 7. **Security by default** — RBAC, audit, zero-trust ### When to Choose CVA - Microservices and integration-heavy systems - Frequent releases and experiments (growth/product teams) - High requirements for event delivery reliability - Need to "transparently" explain technology to business - Team and project scaling planned ### When to Stay with Classical Hexagonal - Simple service/monolith without active evolution - Small team, no resources for telemetry and feature flags - Almost no external integrations, soft SLA --- ## Conclusion **ClearVibeArchitecture** is not just a set of patterns, but a holistic approach to building modern backend systems. CVA makes the system: - **Transparent** — every request visible from start to finish - **Reliable** — events delivered with guarantees - **Safe** — changes controlled and rollback-able - **Understandable** — visualization helps everyone: from developers to business This is "Hexagonal on steroids" — more expensive at start, but pays off many times on medium and large systems. --- ## VR Microservices Visualizer - URL: https://vbcherepanov.com/articles/vr-microservices-visualizer - Published: 2025-10-15 - Reading time: 4 min - Tags: Backend, Distributed Systems - Language: en > Immersive VR tool for debugging and analysing complex microservice flows with Go, gRPC, and Unity VR. Discover the innovative **VR Microservices Visualizer** developed by Vitalii Cherepanov, simplifying debugging, analysis, and management of complex microservices architectures using Go, gRPC, and Unity VR.\ _2 min · Vitalii Cherepanov_ ## Problem: Complexity in Microservices Architecture Management Managing, debugging, and analysing complex microservices architectures is challenging, often leading to bottlenecks, resource inefficiencies, and difficulties in tracking request flows. Traditional monitoring and visualisation tools lack intuitive interfaces and real-time immersive insights. _On working!_ ## My Role: Creator & Lead Developer As the creator and full-stack developer of the VR Microservices Visualizer, I owned the entire technical solution: - System architecture and data model design - Backend in Go with event-driven patterns and real-time updates - gRPC contracts for efficient inter-service communication - Immersive Unity VR front end tailored for Oculus Quest 3 ## Solution: Innovative VR-Based Visualisation Tool The VR Microservices Visualizer is a next-generation application that delivers a clear, immersive picture of microservice interactions inside a VR environment. By combining Go, gRPC, and Unity VR, engineering teams can: - Spot bottlenecks in seconds - Optimise resource consumption - Understand request and event flows in real time ### Technologies Used - **Go (Golang)** — backend microservices and streaming data processing - **gRPC** — high-performance service-to-service communication - **Unity VR** — interactive and immersive visualisation layer - **Oculus Quest 3** — accessible VR hardware for hands-on analysis ## Results: Enhanced Microservices Debugging and Analysis The tool dramatically simplifies debugging, monitoring, and managing microservice architectures. Teams report: - Faster detection of anomalies and failure patterns - Better control of resource allocation - Deeper insight into complex systems and scenarios The outcome is more efficient development cycles, reduced downtime, and higher business value for demanding platforms. --- # Language: ru --- ## Я масштабировал PHP до поломки. Три паттерна из llama.cpp его спасли. - URL: https://vbcherepanov.com/ru/articles/i-scaled-php-until-it-broke - Canonical (medium): https://medium.com/@vbcherepanov/i-scaled-php-until-it-broke-three-llama-cpp-patterns-saved-it-12ddb096ab32 - Published: 2026-05-13 - Reading time: 14 min - Tags: Backend, Architecture - Language: ru > Шесть оптимизаций из llama.cpp, перенесённых в PHP 8.4 с JIT, и прогон от 1М до 1 млрд записей. Половина гипотез оказалась неверной: SplFixedArray проигрывает по скорости, mmap в 7 раз медленнее на каждый вызов, match равен switch. Другая половина превращается в инструмент выживания на масштабе — генераторы и колоночный layout доезжают там, где наивный код падает с OOM. ![Я масштабировал PHP до поломки — обложка](/images/articles/i-scaled-php-until-it-broke.png) Я прочитал исходники llama.cpp. Шестьдесят тысяч строк C++, которые в одиночку сделали локальный инференс LLM возможным на ноутбуке. Это не «лучшие практики из учебника» — это код, где каждая строка отвечает за то, чтобы умножение матриц не вылезло из L2-кэша и не съело всю пропускную способность RAM. Я пишу на PHP. На языке, где каждое значение завёрнуто в zval, у каждого объекта 30+ байт хедера, а любой `foreach` аллоцирует hash iterator. Сравнение нечестное по определению. Но мне стало интересно: какие из трюков llama.cpp вообще выживут после пересадки? И что случится, если поднять датасет до миллиарда записей? Я собрал бенчмарк-сьют. Шесть оптимизаций из llama.cpp, переведённых в PHP 8.4 с JIT. Реальные числа, статистическая методология, p99 latency. Потом масштабировал вход от 1 миллиона до 1 миллиарда записей, чтобы увидеть, где трюки перестают быть «приятными бонусами» и становятся единственным путём, по которому код вообще доезжает. **Половина моих гипотез оказалась неверной. Вот это и есть главная история.** --- ## TL;DR | Паттерн | На 10М записей | На 100М+ | Вердикт | |---------|----------------|----------|---------| | **B01:** mmap-таблица | на вызов **в 7× медленнее** | загрузка **в 226× быстрее**, 0 в PHP heap | Win на уровне процесса, не вызова | | **B02:** SplFixedArray vs array | **медленнее**, экономия памяти 1.68× | оба доезжают до 1B; разрыв 9 ГБ | Память — да, скорость — никогда | | **B03:** Object pool в hot loop | **в 4.43× быстрее** | масштабируется линейно | Юзать в long-running воркерах | | **B04:** Lookup table vs match | lookup **в 5.8× быстрее**, match=switch | масштабируется линейно | Data-driven dispatch → lookup | | **B05:** Generator vs полный массив | в 1.24× быстрее, память O(1) | **наивный OOM, генератор доезжает** | Инструмент выживания | | **B06:** Колоночный vs строковой layout | **в 8.66× быстрее** на single-col scan | **наивный OOM на 100М, колонка 959мс** | Инструмент выживания | Половина паттернов на масштабе переходит из категории «оптимизация» в «единственный путь, по которому код доезжает». Половина — нет. А один паттерн (SplFixedArray) оказался полной противоположностью того, что про него писали последние десять лет. Пройдёмся по каждому. --- ## B01: mmap читает гигабайты быстро, но НЕ на вызов **Гипотеза:** memory-mapping больших read-only таблиц быстрее, чем загрузка через `json_decode`. Параллель из llama.cpp — модели грузятся через `ggml_mmap` (см. `src/llama-mmap.cpp`), а не `fread` в malloc-нутый буфер. **Перевод в PHP:** открыть `libc.dylib` через FFI, вызвать `mmap()`, взять указатель, сделать `FFI::cast('uint32_t*', $ptr)` для типизированного доступа: ```php $ffi = FFI::cdef(" void *mmap(void *addr, size_t length, int prot, int flags, int fd, long offset); int open(const char *pathname, int flags); ", "libc.dylib"); $fd = $ffi->open('data/lookup.bin', 0); $ptr = $ffi->mmap(null, $size, 1, 2, $fd, 0); $table = FFI::cast('uint32_t*', $ptr); // Доступ: $table[$id * 2 + 1] возвращает значение для ключа $id ``` **Результат на 10 миллионах записей:** - Время загрузки: JSON 454 мс vs mmap 1.1 мс → **mmap в 226× быстрее на загрузке** - PHP heap после загрузки: JSON 256 МБ vs mmap **0 байт** - p99 на одну выборку: JSON 708 нс vs mmap 5.4 мкс → **mmap в 7× МЕДЛЕННЕЕ на вызов** Стоп. mmap проигрывает в 7 раз на вызов. JIT настолько хорошо оптимизирует `$arr[$id]`, что FFI-разыменование с накладными расходами на каст не выживает в плотном цикле чтения. **На 1 миллиарде записей** mmap грузит 16 ГБ бинарника за **228 миллисекунд** при нулевом PHP heap. JSON-путь там даже не существует — фикстура была бы 100+ ГБ JSON-текста, физически нереально сгенерировать. ![B01 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B01.png) **Вердикт:** mmap — это не «быстрее на вызов». Это *другая категория* оптимизации. Она даёт время загрузки, плоский PHP heap и шаринг таблицы между N PHP-FPM воркерами через kernel page cache. Внутри одного процесса в плотном read-loop она проигрывает JIT-у. Между процессами — выигрывает на порядки: cross-process cold start второго воркера **в 2641× быстрее**, потому что страницы уже в kernel page cache. Используй mmap, когда флот воркеров шарит толстую read-only таблицу. Не используй его для плотных read-loop внутри одного процесса. --- ## B02: SplFixedArray экономит память, но никогда — скорость **Гипотеза:** на плотных числовых данных `SplFixedArray` должен быть и быстрее (нет hash overhead), и эффективнее по памяти. Параллель из llama.cpp — `ggml_tensor` работает с упакованными аренами, не с массивами указателей на boxed-объекты. **Результат на 10 миллионах целых чисел:** - Память: array 256 МБ vs SFA 152 МБ → **SFA экономит в 1.68×** - Iterate: array 12.2 мс vs SFA 93.8 мс → **SFA в 7.7× МЕДЛЕННЕЕ** - Populate: array 56.5 мс vs SFA 108.8 мс → **в 1.9× медленнее** - Случайные чтения (1М): array 23.9 мс vs SFA 98.5 мс → **в 4× медленнее** Я ждал OOM-перехода и поднял свип до миллиарда целых, надеясь, что обычный массив упрётся в потолок RAM. **Не упёрся.** На 1B элементов: array 24 ГБ peak vs SFA 14.9 ГБ. Отставание SFA по скорости держалось на каждом тире. ![B02 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B02.png) **Вердикт:** `SplFixedArray` на современном PHP — это «только память», никогда не «скорость». Народный рецепт «используй SplFixedArray для больших числовых данных, потому что быстрее» — это совет 2014 года. JIT в PHP 8.4 оптимизирует упакованные integer-keyed массивы настолько агрессивно, что специализированная структура проигрывает универсальной. Тянись за SFA, когда упёрся в память внутри long-running воркера. Прироста скорости не жди. Это самый контринтуитивный результат в статье. Я сам не поверил и перегнал весь свип ещё дважды. Числа держатся. --- ## B03: Object pool — единственная классическая оптимизация, которая всё ещё окупается **Гипотеза:** в hot loop переиспользование маленького пула преаллоцированных объектов быстрее, чем `new` на каждой итерации. Параллель из llama.cpp — tensor allocator никогда не дёргает `malloc` внутри inner loop. Он работает против преаллоцированной арены через `ggml_new_tensor_impl`. **Перевод:** пул из 5 инстансов `Point3D`, переиспользуемых через прямое присваивание свойств: ```php final class Point3D { public function __construct( public float $x = 0.0, public float $y = 0.0, public float $z = 0.0, ) {} } $pool = array_map(fn() => new Point3D(), range(0, 4)); $idx = 0; for ($i = 0; $i < 5_000_000; $i++) { $p = $pool[$idx++ % 5]; $p->x = $x; $p->y = $y; $p->z = $z; // ... работаем с $p } ``` **Результат на 5 миллионах аллокаций:** наивный 813 мс vs пул 179 мс → **в 4.43× быстрее**. GC-циклов: ноль в обоих случаях. `Point3D` нециклический, GC PHP не срабатывает. Вся экономия — на пути аллокатора: `new` в Zend Engine — лёгкий, но ненулевой code path (`zend_object_new` → `emalloc` → инициализация свойств × N). Пять миллионов раз — это уже заметно. **Вердикт:** работает как ожидалось. В CLI-скриптах win реальный, но не критичный. В long-running воркерах (очереди, websocket, демоны) tail latency от давления аллокатора накапливается со временем и становится головной болью — вот там пулинг окупается. --- ## B04: Lookup table бьёт match и switch (а эти двое равны) **Гипотеза:** для dispatch-логики с 16+ кейсами в hot loop array lookup бьёт `match` и `switch`. Параллель из llama.cpp — token dispatch в `llama_token_to_piece` использует таблицы, не switch-и. **Перевод:** классификатор на 32 кейса, реализованный тремя способами — `switch`, `match` и преднабранный `$lookup = [0 => 'A', 1 => 'B', ...]`. **Результат на 10 миллионах диспетчей:** - `switch`: 358 мс (27.9М ops/sec) - `match`: 365 мс (27.4М ops/sec) - `lookup`: **61.7 мс (162М ops/sec) — в 5.8× быстрее** `match` и `switch` **равны**. Оба компилируются в одну и ту же jump table для integer-кейсов. JIT в PHP 8.4 шлифует обе формы до одинакового результата. Если ты переписал `switch` на `match` ради «модернизации» — ты получил читабельность, не скорость. **Где win lookup испаряется:** если dispatch выдаёт строку для последующих `===` сравнений, выигрыш съедают строковые сравнения дальше по пайплайну. **Вердикт:** match-shaped задачи (закрытое compile-time множество, нужна exhaustiveness) остаются в `match`. Data-driven dispatch (таблица из конфига, генерируемая в runtime) — в lookup. Дебаты «match vs switch для перфа» закрыты — они эквивалентны. --- ## B05: Generator — главный инструмент выживания на больших стримах **Гипотеза:** генератор снижает peak memory с O(N) до O(1) при незначительной потере throughput. Параллель из llama.cpp — токены стримятся через callback, а не накапливаются в буфере (`llama_decode` → `llama_get_logits_ith`). **Перевод в PHP:** заменить `function process(): array` на `function process(): Generator`: ```php function records(): Generator { foreach (read_csv('data.csv') as $row) { yield ['id' => $row[0], 'value' => $row[1]]; } } ``` **Результат на 5 миллионах записей:** - Wall time: наивный 525 мс vs gen 449 мс → **gen в 1.24× быстрее** - Peak memory: наивный 1.88 ГБ vs gen **0 байт PHP heap** Генератор не просто экономит память — он ещё и быстрее по wall time, потому что массив никогда не нужно полностью материализовать до начала обработки. **А теперь масштаб.** На 100 миллионах записей наивный — OOM, ядро шлёт SIGKILL после 28.6 секунд. Генератор доезжает те же 100М за **10.4 секунды** при нулевом PHP heap. На 500М генератор всё ещё работает (45.7 секунды). Наивный даже не пробует. ![B05 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B05.png) Если бы пришлось вытащить из всей статьи одну фразу и повесить на баннер — это была бы она: > **На 100 000 записей генератор — это 1.24× nice-to-have. На 100 миллионах — это единственный путь, по которому код вообще доезжает.** **Вердикт:** дефолт для любого single-pass потока, к которому не надо возвращаться. Материализуй массив только когда нужны random access, несколько проходов или `count()` до обработки. --- ## B06: Колоночный layout — это не про cache locality, а про побег от боксинга **Гипотеза:** на аналитических single-column сканах колоночный layout быстрее строкового из-за cache locality. Параллель из llama.cpp — тензоры хранятся per-channel (SoA), не per-element (AoS). **Перевод в PHP:** вместо `SplFixedArray` из `stdClass` с 5 полями — 5 параллельных `SplFixedArray`, по одному на поле: ```php // Строковый layout (наивный) $rows = new SplFixedArray($n); for ($i = 0; $i < $n; $i++) { $obj = new stdClass(); $obj->f1 = ...; $obj->f2 = ...; /* ... */ $obj->f5 = ...; $rows[$i] = $obj; } $sum = 0; foreach ($rows as $r) $sum += $r->f3; // Колоночный layout (оптимизированный) $f3 = new SplFixedArray($n); // и так для каждого поля for ($i = 0; $i < $n; $i++) $f3[$i] = ...; $sum = 0; for ($i = 0; $i < $n; $i++) $sum += $f3[$i]; ``` **Результат на 5 миллионах записей:** колоночный **в 8.66× быстрее** на single-column scan. На full-row scan (sum f1..f5) — в 1.92× быстрее. **И вот тут начинается самое интересное.** Я ждал ступенек на графике ns/record — там, где working set перестаёт влезать в L1, потом в L2, потом в L3. **Я их не увидел.** Кривые плоские во всём диапазоне 100K → 100M: колонка держится на ~9.5–11.5 нс/запись. Строка — на ~80–93 нс/запись. Никаких ступеней. ![B06 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B06.png) Это *более сильный* инсайт, чем «вот ступеньки». Кэш-эффекты внутри обоих layout-ов их не различают. Различает их сам layout. Строковый тратит ~30+ байт на каждый `stdClass` (zval header + property table + GC info) ради 8 байт реальных данных. На 100М записей это 28 ГБ только на боксинг. Колоночный на тех же 100М — 7.45 ГБ, потому что каждая колонка — упакованный `SplFixedArray` без боксинга. **На 100М записей** строковый — OOM, 28+ ГБ `stdClass`-объектов не влезают. Колоночный заканчивает скан за **959 миллисекунд** при 7.45 ГБ. **Вердикт:** колоночный layout — это не cache-оптимизация (как я думал). Это побег от overhead PHP-объектов на масштабе. На любой аналитической нагрузке поверх больших датасетов — колоночный. Строковый остаётся уместным, когда DTO передаются между слоями или когда working set маленький. --- ## Что происходит на масштабе Микро-бенчмарки на 1–10 млн элементов рисуют одну картину. Масштаб до миллиардов — другую. Три из шести паттернов на больших данных переходят из «оптимизации» в «необходимость»: - **B05 generator** — на 100М наивный — OOM. Генератор доезжает. - **B06 колоночный layout** — на 100М строковый — OOM. Колоночный заканчивает скан за 959 мс. - **B01 mmap** — на 1B JSON-фикстура *физически не существует* (100+ ГБ). mmap грузит 16 ГБ бинарника за 228 мс. Два паттерна остаются «просто оптимизациями» независимо от масштаба: - B03 object pool: ~4× на любом размере. - B04 lookup table: ~5× на любом размере. Один паттерн оказался узким — экономит память, но не скорость: - B02 SplFixedArray: на 38% меньше памяти, всегда медленнее по скорости. Оба пути работают вплоть до 1B. Это, пожалуй, самый важный реверс в статье. Когда кто-то говорит «X быстрее, чем Y» — это утверждение про конкретный размер данных. На малых данных половина утверждений ломается. На больших — половина из них превращается в «X работает, Y не существует». И ещё одно, отдельной строкой: **JIT в PHP 8.4 продолжает съедать оптимизации с каждым релизом.** Между прогонами на PHP 8.3.31 и 8.4.21 B03 ускорился с 2.78× до 4.43×, B04 — с 3.75× до 5.81×. Не баг — JIT просто продолжает улучшаться. Через год эти числа снова сместятся. --- ## Три правила производительности PHP в 2026 Из этих шести экспериментов сложился рабочий фреймворк. **1. Доверяй JIT.** Не пытайся переиграть его на синтаксическом уровне. `match` vs `switch` — JIT компилирует обе формы в одну jump table. `SplFixedArray` vs упакованный массив — JIT оптимизирует обычный массив настолько агрессивно, что специализированная структура проигрывает по скорости. FFI dereference vs `$arr[$id]` — JIT-компилированный array access бьёт FFI-касты внутри hot loop. Если твоя оптимизация про «какую языковую конструкцию выбрать» — JIT уже сделал этот выбор за тебя. **2. Оптимизируй то, что JIT не видит.** - **Cache locality** (B06: колоночный layout) — JIT не управляет memory layout. Это твоя архитектура. - **Allocation pressure** (B03: object pool) — JIT не убирает аллокации, он их ускоряет. - **I/O batching** (батчевый INSERT 1000 строк vs single-row) — JIT не оптимизирует round trips в Postgres. - **Cross-process resource sharing** (B01: mmap + page cache) — JIT работает per process. - **Streaming vs materialization** (B05: generator) — JIT не уберёт за тебя 30 ГБ peak memory. **3. На достаточно большом масштабе оптимизации перестают быть оптимизациями.** Они становятся порогом выживания. Генератор на 100K записей — в 1.24× быстрее. На 100М — единственный код, который доезжает. Колоночный layout на 5М — в 8.66× быстрее. На 100М — единственный код, который не съедает 28 ГБ на overhead `stdClass`. mmap на 10М — медленнее на вызов. На 1B — единственный способ загрузить таблицу за секунду. Это структурное мышление, не синтаксическое. И именно это превращает llama.cpp из «сильно оптимизированной C++ библиотеки» в *учебный артефакт* для PHP-разработчика. Не «вот трюки, тащи». А «вот пределы языка, которые видны только когда в них врежешься». --- ## Закрытие Весь код бенчмарков и воспроизводимый Docker-сетап лежат на GitHub: **[vbcherepanov/php-llamacpp-benchmarks](https://github.com/vbcherepanov/php-llamacpp-benchmarks)**. Полный свип занимает ~15 минут (`make all`), включая case study с импортом 100K строк в реальный PostgreSQL. Замечание про репу: директория `data/` в gitignore — фикстуры (до 16 ГБ бинарных lookup-файлов на 1B-тире) генерятся локально через `make fixtures`. Не пытайтесь клонировать с ними. Если найдёте баг в методологии или захотите добавить тир — присылайте PR. Я вожусь с такими вещами через **[Braincore](https://getbraincore.com)** — Go-meta-агент с cost-aware роутингом и слоем памяти для AI-кодинг-агентов. Если бенчмарки оказались полезны и хотите поддержать продолжение — есть [Ko-fi](https://ko-fi.com/vbcherepanov). --- ## Что построили вокруг себя 16 параллельных Claude-агентов: разбор эксперимента Anthropic с C-компилятором - URL: https://vbcherepanov.com/ru/articles/what-16-parallel-claude-agents-built - Canonical (medium): https://medium.com/@vbcherepanov/what-16-parallel-claude-agents-built-around-themselves-deconstructing-anthropics-c-compiler-f2fa6335b1ca - Published: 2026-05-09 - Reading time: 10 min - Tags: AI Agents, Open Source - Language: ru > Разбор эксперимента Anthropic, в котором 16 параллельных Claude собирали C-компилятор на Rust — какая инфраструктура была вынуждена родиться вокруг агентов, потому что её просто не было. ![16 параллельных Claude-агентов — обложка](/images/articles/what-16-parallel-claude-agents-built.png) 5 февраля 2026 года Николас Карлини из Anthropic [опубликовал статью](https://www.anthropic.com/engineering/building-c-compiler) об эксперименте, который заметно опережает то, что большинство из нас сейчас делает с LLM-агентами. Шестнадцать параллельных инстансов Claude Opus 4.6, две недели работы, ~2000 сессий Claude Code, бюджет около $20 000. Результат: 100 000 строк C-компилятора на Rust, который собирает Linux 6.9 на x86, ARM и RISC-V; проходит 99% torture-тестов GCC; компилирует PostgreSQL, SQLite, FFmpeg, Redis и QEMU; запускает Doom. [Репозиторий открыт](https://github.com/anthropics/claudes-c-compiler), любой может прочитать и попробовать. Это серьёзная инженерная работа, и сама статья — отличное чтение для всех, кто думает про автономных агентов в проде. Карлини честен в том, что сработало и что нет, разбирает пять конкретных уроков из дизайна harness'а, делится цифрами и метриками. Именно такие отчёты индустрии и нужны — отчёт из первых рук о том, как реально выглядят длинные автономные прогоны. Заголовки разделились на два лагеря. «AI заменил программистов» с одной стороны. «Это просто демо» с другой. И те и другие промахиваются по сути. Если внимательно прочитать статью, Карлини описывает не «AI пишет компилятор». Он описывает **сколько инфраструктуры пришлось построить вокруг агентов, потому что инфраструктуры между самими агентами в 2026 году ещё нет**. Lockfile'ы в общей папке как механизм синхронизации. README, которые агент пишет сам себе. GCC, мобилизованный в качестве known-good референсного оракула. Ralph-loop, обёрнутый в Docker для бесконечной автономии. Каждое из этих решений — ответ на конкретную проблему, для которой сегодня просто **некуда вытолкнуть в стандартный слой**. И в этом — настоящая ценность статьи. Не как «AI-демо», а как **детальная карта недостающих примитивов**, нарисованная человеком, который построил для них костыли руками. Я последние несколько месяцев работаю как раз над этими примитивами, и работа Карлини — отличный повод обсудить, что нужно следующему поколению команд агентов. ## Каждая сессия начинается с амнезии Карлини построил harness, который запускает Claude в бесконечном цикле — когда агент заканчивает одну задачу, он берёт следующую. Архитектурно это знакомый паттерн «Ralph-loop»: цикл `while true` в bash-скрипте, обёрнутый в Docker для безопасности. В одном из прогонов Claude случайно убил сам себя `pkill -9 bash`, что Карлини отмечает как забавный побочный эффект. Принципиальная деталь: каждый из ~2000 запусков начинался **в свежем Docker-контейнере с пустым контекстом**. Никакой памяти между сессиями. Каждый агент с нуля разбирался: что это за репо, что уже сделано, какой статус у задач, что пробовали и что не получилось. Костылём Карлини стало инструктировать Claude самому вести подробные README и progress-файлы, обновляя их часто. Когда агент застревает в баге, он также ведёт running doc с провальными подходами и оставшимися задачами. Это работает в рамках текущего тулинга — и в этом его ценность. Но если посмотреть на масштабирование, две архитектурные точки начинают скрипеть. Во-первых, текстовый файл не структурирован. Если хочешь спросить «какие три последних бага я фиксил в области парсера и чем закончились?» — у тебя есть только `grep` и регулярки. На маленьком проекте терпимо. На 100 000 строк кода и 2000 сессий — узкое горло. Во-вторых, тоньше: каждый агент ведёт эти файлы для себя. Они живут в общем git-репозитории, но нет механизма, который скажет «прежде чем брать задачу X, посмотри, что 16 других агентов писали про эту область за последние 6 часов». Каждый агент пишет свой README, мерджит чужие правки и надеется на сходимость. Это **первое поколение shared memory** — реализованное plain text, потому что более удобный примитив ещё не стал стандартом. ## Lockfile'ы как координация Параллелизм реализован минимально. Каждый агент крутится в своём Docker-контейнере; общий bare-git-репозиторий держит состояние. Координация задач — через lockfile'ы: агент пишет файл вроде `current_tasks/parse_if_statement.txt`, делает `git push` и тем самым «забирает» задачу. Если два агента попытаются взять одну и ту же — git-синхронизация заставит второго взять что-то другое. Готово — удаляешь lockfile. Карлини прямо описывает текущее состояние системы: *«no other method for communication between agents... I don't use an orchestration agent»*. Никакого механизма для агентов «спросить друг друга». Никакой центральной координации. Каждый Claude сам решает, что делать дальше — обычно «следующая очевидная проблема». Lockfile здесь делает ровно одну вещь — работает как **mutex**, защищающий от параллельного забора одной задачи. Это ценно. Но не решает другой проблемы: два агента, работающих над разными задачами **в одной области кода**, могут писать конфликтующий код под разными именами задач. Именно это и случилось с Linux-ядром в эксперименте — агенты сходились на одном баге, фиксили его по-разному, перезатирали правки друг друга, и параллелизм временно переставал окупаться. Решением Карлини стал отдельный test harness, использующий GCC как **known-good компилятор-оракул**: большая часть ядра компилируется GCC, а случайное подмножество файлов прогоняется через компилятор Claude. Если ядро не загружается — баг где-то в подмножестве Claude, и можно сужать дальше. Идея остроумная и элегантная, и сработала ровно как задумано. Стоит отметить границы, в которых это работает. GCC-оракул — точное решение для **этой конкретной задачи**, потому что у задачи есть три удобных свойства: существует готовый референсный компилятор для той же спецификации, задача декомпозируется на уровне отдельных файлов, и результат бинарный (загружается / не загружается). В большинстве реальных проектов — продуктовая разработка, рефакторинг legacy, ML-пайплайны, мобильные приложения — этих удобств нет. Нет готового known-good для сравнения. Нет естественной декомпозиции по файлам. Результаты не бинарны. Что значит **технику GCC-оракула нельзя обобщить в примитив** — она работает там, где работает, и не существует там, где не работает. В целом инструментарий Карлини аккуратно ложится на две оси: | Что нужно командам агентов | Что в эксперименте | Природа решения | |---|---|---| | Discovery агентов | хардкод числа контейнеров | хардкод | | Inter-agent communication | lockfile через git push | mutex без messaging | | Делегирование задач | next-most-obvious из очереди | без routing | | Shared state / memory | README + progress-файлы | plain text | | Causal history | running doc с провальными подходами | личный лог | | Верификация | GCC-оракул | под конкретную задачу | Это **две независимые оси проблемы**: коммуникация (как агенты разговаривают друг с другом) и память (что они помнят между сессиями и шарят ли). Эти оси требуют разных примитивов и разных решений. И на каждой индустрия сейчас сходится к стандартам и open-source реализациям. Дальше — что есть на каждой оси сегодня. ## Ось коммуникации: A2A-протокол и a2abridge Коммуникация двигалась быстро и уже пришла к зрелому стандарту. В апреле 2025 Google [открыл протокол A2A](https://a2a-protocol.org/latest/) — Agent-to-Agent. В августе 2025 IBM ACP [влился в A2A под Linux Foundation](https://lfaidata.foundation/communityblog/2025/08/29/acp-joins-forces-with-a2a-under-the-linux-foundations-lf-ai-data/), и к апрелю 2026 спека на версии 1.2, поддержана 150+ организациями (Microsoft, AWS, Salesforce, SAP, ServiceNow, IBM среди них), нативно встроена в Google ADK, LangGraph, CrewAI, LlamaIndex Agents, Semantic Kernel и AutoGen. A2A фактически **выиграл войну протоколов**. Спека намеренно минималистична: **Agent Card** — JSON-описание возможностей агента (что делает, какой endpoint бить). **Task** — единица работы со статусами и артефактами. Транспорт — JSON-RPC 2.0 поверх HTTPS, с Server-Sent Events для стримов. Аналогия, которую везде используют, — HTTP. HTTP не говорит, что у тебя на бэкенде (Rails, Django, Go) — он только определяет форму запросов и ответов. A2A не говорит, какую LLM, фреймворк или БД ты используешь — он определяет контракт между агентом A и агентом B. Минимум, поверх которого можно строить остальное. Если переписать сценарий Карлини на A2A, вместо lockfile в `current_tasks/` агент бы запросил directory-сервис «кто сейчас работает над парсером?», получил Agent Card соседа и отправил `Task` со streaming-ответом по SSE. Это и есть тот примитив коммуникации, которого в его harness'е пока нет. Я последние несколько месяцев пишу **a2abridge** — открытую Go-реализацию A2A 1.0, нацеленную на практический сценарий «несколько разных AI-агентов на одной машине разработчика». На момент публикации поддержано шесть IDE: Claude Code, Codex CLI, Cursor, Cline, Continue и Gemini CLI. Любой A2A-совместимый агент (включая будущие Google ADK, LangGraph, CrewAI реализации) — first-class peer без glue-кода. Архитектурно это **один Go-бинарь** (~10 МБ) с несколькими subcommands. `a2abridge directory` — discovery-сервис на 127.0.0.1:7777, работающий как user-level system service (launchd на macOS, systemd-user на Linux, Windows Service на Windows, корректно работает внутри WSL2). `a2abridge bridge` — per-agent процесс, хостящий и MCP stdio-сервер (через который IDE видит a2abridge как обычный MCP-сервер с тулзами), и A2A HTTP-сервер на случайном порту, с Agent Card на `/.well-known/a2a` и полным набором JSON-RPC 2.0 методов из §7 спеки: `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetExtendedAgentCard`. Lifecycle bridge'а равен времени жизни IDE-сессии — когда MCP stdio закрывается, bridge умирает, никаких orphan-процессов. Что Claude Code (или любая другая IDE) видит как MCP-инструменты: `a2a_whoami`, `a2a_list_agents`, `a2a_send_message`, `a2a_send_streaming`, `a2a_get_task`, `a2a_cancel_task`, `a2a_inbox`, `a2a_complete_task`. Внутри сессии агент может **самостоятельно** обнаруживать других агентов на машине, отправлять им задачи, ждать ответов и читать свой inbox — без участия пользователя. Поверх протокола есть про-активный слой, которого нет в спеке, но он нужен для реального использования. Bridge пишет inbox-файл `./.a2a/inbox-.json` каждый раз, когда меняется очередь сообщений. UserPromptSubmit-хук инжектит входящие сообщения в системный промпт **до первого tool call** — то есть Claude видит «у тебя сообщение от пира с FYI про breaking API change» **до** того, как начнёт слепо действовать. SSE fast-path доставляет ответы за миллисекунды, fallback — polling раз в 5 секунд. Для Claude Code также есть **skill** `a2a-bridge`, который автозагружается только при триггерных промптах — никаких глобально загруженных правил, жгущих токены на каждой сессии. В сценарии Карлини это бы выглядело так: агент 5 берёт задачу «fix kernel build error in `mm/page_alloc.c`». Прежде чем действовать, он зовёт `a2a_list_agents`, видит, что у агента 2 открытый Task с capability `kernel-debug` в той же области. Отправляет `a2a_send_message`: «над чем работаешь, есть гипотеза?». Получает streaming-ответ: «пробовал alignment fix, упал на test_kernel_boot, сейчас смотрю reorder header includes». Берёт другой угол. Почему открытый протокол, а не очередной кастомный wire-формат. В этой нише уже существует несколько решений: **Anthropic Agent Teams** работает только Claude↔Claude и завязан на подписку. **CCB** и **claude-multi-agent-bridge** — закрытые форматы, привязанные к конкретным комбинациям агентов. **Ruflo** — отлично для enterprise-федераций 100+ агентов с центральными queens, но это другой класс задач. Ниша, на которую целится a2abridge, — **cross-vendor open-protocol mesh**, где сегодня входят Claude и Codex, а завтра — любой A2A-совместимый агент без переписывания glue. Если индустрия движется к стандарту, bridge должен на этом стандарте говорить. Production-зрелость: cross-machine federation с mTLS + ed25519 (opt-in, для сценария «домашний Mac ↔ офисный Linux»), mDNS auto-discovery в локальной сети, PII/secret-screen с 11 regex-детекторами перед отправкой (AWS-ключи, GitHub-токены, токены Anthropic/OpenAI/Google/Stripe/Slack, JWT, PEM-блоки — заменяются на `[REDACTED:]`, секрет никогда не покидает bridge), Push Notifications по A2A 1.0 §9.5, HTTP+REST binding по §7.3, 35 тест-кейсов под `-race`, GitHub Actions release matrix и кросс-платформенный `a2abridge doctor` с 9-проверочным health-аудитом. Установка — одной командой через `install.sh` или `install.ps1`, с автодетектом всех IDE на машине и `.bak`-бэкапами их конфигов перед правкой. Репозиторий: **[github.com/vbcherepanov/a2abridge](https://github.com/vbcherepanov/a2abridge)** — MIT, Go 1.25, текущий релиз v2.0. ## Ось памяти: total-agent-memory и BrainCore С памятью ситуация другая. Стандарта уровня A2A пока нет — каждый строит свой слой, и под разные задачи выбираются разные подходы. То, что агент пишет сам себе в README, по сути — causal log в текстовой форме: «пробовал A, упал на B, перешёл на C». Структура правильная, реализация — пока plain text. Я работаю над двумя продуктами на этой оси. **[total-agent-memory](https://github.com/vbcherepanov/total-agent-memory)** — open-source реализация. Здесь живут базовые retrieval-паттерны, MCP-интеграция и базовая causal-chain модель. Любой может склонировать, посмотреть как работает, и подключить к своему Claude Code или Cursor. **[BrainCore](https://github.com/vbcherepanov/braincore)** — production-grade. Go-бинарь, локальный SQLite + WAL, tree-sitter для code-graph по 14 языкам (PHP, TypeScript, Python, Ruby, Rust, Java, Kotlin, C/C++, C#, Swift, Bash, Lua, YAML, плюс Go через нативный AST), internal git для time-travel памяти, MCP-протокол для подключения к Claude Code, Cursor, Codex CLI, Windsurf и нескольким другим агентам. Сейчас в бете. Архитектурно есть три точки, где оба проекта расходятся с плоским bag-of-facts с cosine-поиском. Первое — **causal decision chains** вместо плоских фактов. Не «функция X в файле Y», а «агент 3 в задаче `fix kernel build` сформулировал гипотезу `alignment issue`, проверил через test_kernel_boot, упал, перешёл к гипотезе `header reorder`». Каждый шаг типизирован, связан причинной стрелкой и доступен по запросу любому агенту. Второе — **AST-стабильная идентичность кода**. Когда несколько агентов рефакторят параллельно, текстовые диффы быстро превращаются в кашу, а merge-конфликты становятся бесконечными. AST-узел остаётся узлом, даже если функция переехала из `parser.rs` в `frontend/lexer.rs` и переименовалась с `parse_decl` на `parse_declaration`. В графе это **один и тот же узел** с историей перемещений. Каждый агент смотрит на одну и ту же абстракцию, а не на «строки 127-145 файла X». Третье — **persistence через рестарт контейнера**. Память живёт **снаружи** Docker-контейнера: на хосте через volume или удалённо через MCP. Запрос `brain.causal_lookup(area="parser", lookback="6h")` возвращает один и тот же результат независимо от того, в каком свежем контейнере ты сейчас. Перепишем сценарий Карлини с памятью: агент 5 идёт в BrainCore, получает causal-лог «agent_2 пробовал alignment fix → упал, agent_7 пробовал header reorder → упал на L98, текущая гипотеза от agent_3 — alignment issue, in progress», берёт четвёртую гипотезу, пишет её в causal-цепочку. Агенты 2, 3 и 7 видят это решение на следующем pull. Никаких README, никаких grep'ов. ## Как они складываются вместе a2abridge и BrainCore — **разные слои, не конкуренты**. Один отвечает на «как агенты разговаривают друг с другом», другой — на «что они помнят». Полная картина для команды агентов выглядит так. **BrainCore** держит общее состояние мира: code-graph, causal chains, гипотезы, выводы. **a2abridge** обеспечивает реальную коммуникацию между агентами: discovery, делегирование, streaming-ответы, inbox с context injection. Когда они работают вместе, агент 5 видит сообщение в inbox от агента 2 («я работаю над X»), запрашивает у BrainCore детали («что конкретно пробовали в этой области»), принимает информированное решение, отвечает агенту 2 о намерении взять смежную задачу и пишет результат в shared memory. Это та архитектура, которую Карлини строит руками в эксперименте через комбинацию lockfile + README + GCC-оракул. С независимыми примитивами вместо самосборного клея инфраструктура работает в задачах, где готового known-good компилятора нет. ## Что эти примитивы не решают Карлини абсолютно прав про главный урок статьи: **качественный test harness — фундамент всего**. Никакая shared memory и никакой A2A не спасут, если верификатор задачи неточен — агенты автономно решат не ту задачу. CI-пайплайны, хорошо спроектированные логи, защита от загрязнения контекстного окна, борьба с «time blindness» — это работает на любом уровне инфраструктуры и **остаётся первым приоритетом**. GCC-оракул в задаче компилятора — действительно оптимальный выбор. Бинарная верификация почти всегда лучше сравнения causal-гипотез. Если в твоём проекте есть готовый known-good — используй. Никакая память не заменяет хорошего верификатора. Но в большинстве реальных задач — продуктовая разработка, рефакторинг, ML-пайплайны, бизнес-логика — GCC-эквивалента нет. И там примитивы коммуникации и памяти становятся не «улучшением», а **необходимым условием** для того, чтобы команда из 16 агентов была продуктивнее одного. Что Карлини пришлось строить весь этот текстово-файловый слой в 2026-м — это не недостаток подхода, а симптом момента: инфраструктура для команд агентов всё ещё формируется. Эксперимент Anthropic — лучшая возможная иллюстрация того, как она формируется и куда движется. И в этом, на мой взгляд, настоящая ценность статьи Карлини: честный отчёт с самой ранней точки на кривой, по которой эта инфраструктура будет расти. --- ## Open source и ссылки - Оригинальная статья Anthropic: [Building a C compiler with a team of parallel Claudes](https://www.anthropic.com/engineering/building-c-compiler) - Репозиторий компилятора: [anthropics/claudes-c-compiler](https://github.com/anthropics/claudes-c-compiler) - Спецификация A2A-протокола: [a2a-protocol.org](https://a2a-protocol.org/latest/) - **a2abridge** — открытый A2A 1.0 mesh для 6 IDE (Claude Code, Codex, Cursor, Cline, Continue, Gemini): [github.com/vbcherepanov/a2abridge](https://github.com/vbcherepanov/a2abridge) (MIT, v2.0 в релизе) - **total-agent-memory** — open-source memory layer: [github.com/vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) - **BrainCore** — production memory infrastructure: [github.com/vbcherepanov/braincore](https://github.com/vbcherepanov/braincore) (beta) Если ты строишь свои команды агентов и упираешься в эти проблемы — пиши. Обмен опытом полезен в любом случае, а фидбек на ранние версии продукта — лучшее, что может случиться с авторами. --- ## Право AI-агента молчать - URL: https://vbcherepanov.com/ru/articles/the-right-of-an-ai-agent-to-stay-silent - Canonical (medium): https://medium.com/@vbcherepanov/the-right-of-an-ai-agent-to-stay-silent-db29c478e577 - Published: 2026-05-09 - Reading time: 6 min - Tags: AI Agents - Language: ru > Продакшеновая AI-система должна оптимизироваться на ноль уверенно-неверных действий, а не на точность. Право промолчать, self-tasking и когнитивный рантайм — путь к настоящей автономии. ![Право AI-агента молчать — обложка](/images/articles/the-right-of-an-ai-agent-to-stay-silent.png) > **Часть 3 из 3 — «Память для AI-агентов»** > Почему правильная метрика — не accuracy, а ноль уверенно-неверных действий Представь два сценария. В первом — старший кардиохирург смотрит на снимок и говорит: *«Не знаю. Здесь две конкурирующих гипотезы, симптомы перекрываются. Нужны дополнительные тесты — конкретно эти три, плюс КТ с контрастом. Пока я их не увижу, я не дам ответа, который бы стал защищать».* Во втором — молодой ординатор уверенно ставит диагноз за тридцать секунд, опираясь на похожий случай из учебника прошлой недели. Уверенно. Чётко. Без сомнений. Кому из них ты бы доверил оперировать свою маму? Каждый AI-агент, которого мы сегодня деплоим, — это второй врач. Уверенный. Быстрый. Никогда не говорит *«не знаю».* И именно поэтому ему нельзя доверять ничего более болезненного, чем переписать README. Сегодня — как это изменить. Не алгоритмически. **Архитектурно.** --- ### Гнилая метрика, которая нас всех отравила В индустрии негласный консенсус, который я считаю катастрофой: мы измеряем модели и системы по **accuracy** — проценту правильных ответов на бенчмарке. GPT-4 берёт 86% на MMLU. Claude — 88%. Gemini — 90%. Лучше, лучше, ещё лучше. Цифра растёт. Что эта цифра **не** показывает — оставшиеся 10–14%. Это не *«ответы, которые модель не дала».* Это **уверенно сгенерированные неправильные ответы**, визуально неотличимые от правильных. У модели нет лампочки *«здесь я не уверен».* Она генерирует всё с одинаковой текстовой уверенностью. Когда ты используешь такую модель, чтобы писать заметки, — нормально. Когда используешь для продакшен-кода, медицинских решений, юридических заключений, финансовых транзакций — **10% уверенных галлюцинаций означают 10% случаев, когда система врёт тебе с прямым лицом**. Правильная метрика для продакшен-AI звучит иначе: > **0% уверенно-неверных действий при приемлемом abstain rate.** Не *«процент правильных ответов».* А *«процент неверных действий»* — ноль. И отдельно — `abstain rate`: как часто система честно говорит *«не знаю, нужны данные / верификация / уточнение».* Ноль неверных действий плюс 30% abstain — это в **десять раз** более production-ready, чем 90% accuracy с 10% уверенных галлюцинаций. Заметь: я не сказал *«0% неверных ответов».* Я сказал *«0% неверных **действий**».* Различие важно. Ответ — это слова. Действие — это коммит, транзакция, диагноз, API-вызов, изменение в проде. Слова можно перечитать и отбросить. Действие уже произошло. И это разделение между *«ответом»* и *«действием»* — то, что архитектурно отсутствует в современных AI-агентах. --- ### Abstain как first-class исход Во второй части этой серии я выложил семь принципов настоящей памяти, и второй из них — `strict mode`. Краткое напоминание: прежде чем факт попадёт в контекст промпта, он проходит через **ворота** — источник, confidence, temporal validity, отсутствие нерешённых противоречий. Если ни один факт не прошёл — система возвращает `abstain = true` с явной причиной. Есть деталь, которую хочется подчеркнуть отдельно. **Abstain — это не ошибка.** Это **результат**. Такой же first-class, как *«ответ»* или *«действие».* Если у твоего AI ровно два возможных исхода — *«ответил»* и *«ошибся»* — у него нет архитектурного места под честное *«не знаю».* А значит, он будет выдумывать. В разумной системе **минимум четыре исхода**: - **answer** — достаточно evidence, ответ дан, действие выполнено - **clarification request** — частичный evidence, нужен ввод от пользователя - **abstain → brain task** — недостаточно evidence, записано как задача в backlog с явным запросом данных - **escalation** — есть противоречие, требующее ручного review И последние три — это не fallback'и. Не *«когда всё пошло не так».* Это полноценные, ожидаемые, заложенные в дизайн пути. Когда я прошу `braincore` найти решение про auth-flow на проекте, над которым мы три месяца работаем, — он находит. Когда я спрашиваю про проект, который только начал, где ничего не записано, — он не выдумывает. Он говорит: *«У меня нет evidence на этот вопрос. Создал brain-task: собрать решения по auth, источник — наш текущий design-документ, владелец — ты. Когда заполнишь, спрашивай ещё раз».* Это **не баг**. Это правильное поведение. Заметь, что произошло: система меня не заблокировала. Не сказала *«error, no data».* Она **превратила незнание в задачу**, которая теперь живёт в её backlog'е и будет периодически напоминать о себе. --- ### Self-Tasking. Мозг с backlog'ом, а не пассивная поисковая машина Самое страшное в современных *«AI-агентах»* — что они **пассивны**. Они ждут промпт. Каждый. Раз. Не помнят ничего между сессиями. Нет **внутреннего backlog'а**. Не осознают, что у них есть нерешённые вопросы. Это не *«агент».* Это **функция в костюме агента**. У функции есть вход и выход. У агента есть цели, состояние и собственные задачи между запросами. В настоящем cognitive runtime есть отдельная сущность — **brain tasks**. Они автоматически порождаются: - `truth.contradiction` — найдено противоречие в knowledge graph → задача на разрешение - `truth.staleness` — факт давно не подтверждался → задача на верификацию - `strict.abstain` — система отказалась отвечать → задача на сбор evidence - `selflearn.skill_scorecard` — навык начал часто падать → задача на починку - `specs.evidence_gap` — требование без покрытия → задача на сбор - `tests.failing_coverage` — тесты не проходят → задача на починку - `learning.failure_pattern` — обнаружен повторяющийся паттерн ошибки → задача на обобщение в правило У каждой задачи есть **самоприоритизация** по простой формуле: ```plaintext priority = f(urgency, impact, confidence, risk, effort, dependency_readiness) ``` И в любой момент пользователь может спросить: *«покажи следующие пять задач, почему они важны, какие я могу безопасно сделать сейчас, какие требуют моего ввода».* Это не тот же чат, в котором каждый раз стартуешь с чистого листа. Это рабочая среда с собственной памятью о том, что не сделано. Это смена кадра. Не *«пользователь приходит и спрашивает, агент отвечает».* А *«агент работает в фоне, накапливает открытые потоки и сам говорит — вот что важно сейчас».* Покажи мне RAG-стек, который так умеет. Спойлер: его нет. Потому что **RAG — это поисковик, а не агент**. И когда кто-то говорит *«у нашей RAG-AI есть agency»* — это маркетинговая фантазия. Agency требует **внутреннего состояния**, **целей**, **backlog'а** и **self-assessment**. Ничего этого в RAG нет. --- ### Cognitive Runtime > размер модели Последний миф к разрушению. *«Когда выйдет GPT-5 / Claude 5 / Gemini 3 — память решится сама».* Нет. Не решится. Никогда. Память — это **не свойство модели**. Это **свойство системы**, в которой модель работает. Аналогия: > У человека хорошая память не потому, что нейроны быстро считают. > У человека хорошая память потому, что есть гиппокамп, неокортекс, консолидация во сне, эмоциональная фильтрация через миндалевидное тело и архитектурное разделение между working / episodic / semantic / procedural memory. > **Это инфраструктура, а не вычислительная мощность.** Сделай LLM в десять раз больше — память не появится. Построй runtime вокруг существующей LLM, реализующий семь принципов из части 2 плюс abstain плюс self-tasking — и **слабая локальная модель** в этом runtime начнёт делать вещи, которые GPT-5 с RAG-памятью **архитектурно не может**. Не потому что она умнее. А потому что **runtime делает за неё то, что она сама делать не должна**: помнит, верифицирует, отказывается, ставит себе задачи. Это, кстати, единственный осмысленный путь вперёд в мире, где foundation-модели — это **commodity**. Когда у всех примерно эквивалентные Claude/GPT/Gemini — конкурентное преимущество может прийти только из **того, что вокруг модели**. Domain-specific cognitive runtime. Project-specific memory. Team-specific rules. И эта ставка — ещё и про privacy. Про data sovereignty. Про то, что **память твоего проекта — это твой капитал**, и сдавать её в аренду стороннему vector DB за ежемесячную плату — стратегическая ошибка, которую заметишь только через три года, когда уже не сможешь уйти. Поэтому, кстати, `braincore` — это **локальный** Go-бинарь, который по умолчанию работает **без** OpenAI и без Anthropic. Не потому что я против них (я платящий клиент обоих). А потому что **архитектурно правильный путь** — это runtime, в котором модель — заменяемый компонент, а не центр тяжести. --- ### Чек-лист для всех, кто строит AI-продукты прямо сейчас Если ты прочитал всю серию и думаешь *«окей, согласен, что мне делать в понедельник утром?»* — вот десять пунктов, по которым можно начать двигаться **независимо** от того, используешь ты `braincore` или нет. 1. **Убери слово «память» из стека, если у тебя RAG.** Назови это retrieval или search — это мгновенно снимет 80% завышенных ожиданий. 2. **Введи `truth_status` для каждого факта.** Минимум: `hypothesis | confirmed | deprecated`. Запрети `confirmed` без `source_ref`. 3. **Введи `valid_from` / `valid_until`.** Любой факт без temporal validity — это гипотеза, не факт. 4. **Сделай abstain first-class исходом.** Не *«когда всё плохо»* — а как один из четырёх валидных результатов. 5. **Различай `staging | working | consolidated | archived`.** Не сваливай всё в одну коллекцию. 6. **Negative memory.** Что сломалось — записывай явно, со ссылкой на failing-тест или коммит. 7. **Entity disambiguation.** Никогда не сливай сущности при низкой confidence. Создавай `ambiguity record`. 8. **Causal chains для решений.** Не «текст» — `problem → alternatives → decision → reasoning → outcome`. 9. **Локально, где можно.** Память проекта — **твой** капитал. 10. **Метрика — не «процент правильных ответов».** А `0% wrong actions при приемлемом abstain rate`. Не всё сразу. Возьми два-три и начни. Через месяц у тебя будет AI-система, которой можно доверять больше, чем большинству существующих. --- ### Эпилог. Когнитивная гигиена для AI-индустрии Я устал от того, что слово *«память»* шлёпают на каждую векторную базу с эмбеддингами. Это девальвация термина — как назвать таблицу с одной колонкой `text VARCHAR` базой знаний. Технически — да. По сути — нет. Память — это: - **структура**, а не плоский список - **знание границы**, а не уверенный bullshit - **причинные цепочки**, а не чанки - **entity-aware**, а не string-aware - **temporal-aware**, а не *«создано вчера, валидно вечно»* - **самокорректирующаяся**, а не самообманывающая - **управляемая**, а не *«сваливай что попало, потом разберёшься»* - **способная к abstain**, а не *«всегда отвечает»* Если твой *«AI с памятью»* не делает хотя бы половины из этого — у твоего AI нет памяти. У него есть результаты поиска. Это разные вещи. Последнее. Я не призываю выкинуть RAG. RAG — отличный инструмент для своего класса задач (найди мне абзац про X в 100 документов). Я призываю **перестать называть RAG памятью** и начать строить настоящие cognitive runtime — медленнее, дисциплинированнее, с явными воротами и явным abstain. Это единственный путь к AI-системам, которым можно доверить что-то более важное, чем переписать README. Если ты стартап с *«у нашего AI long-term memory на векторной базе»* в питч-деке — закрой этот слайд, переделай, и через два года скажешь себе спасибо. Если ты разработчик, борющийся с агентом, который забывает, что ты говорил вчера, — это не вина агента. Это вина того, кто продал тебе поисковик в обёртке мозга. Хороший AI-агент — это **не тот, кто всегда отвечает**. Хороший AI-агент — **тот, кто никогда не делает уверенно-неверного действия**. Между этими двумя предложениями — вся пропасть, отделяющая AI-инструменты 2024-го от AI-инструментов, которым можно будет доверять в 2027-м. Я выбрал свою сторону пропасти. Строю `braincore` — открытый, Apache-2.0, в репо. Если узнал себя в этой серии — мы в одной лодке. Если в твоём стеке что-то работает иначе — расскажи, я серьёзно хочу знать. Чего ты точно не можешь — это молчать. --- > **TL;DR всей серии:** > > - **Часть 1:** RAG = Ctrl+F с эмбеддингами. Это поиск, а не память. Mem0/Letta/Zep — RAG в обёртках. 1М контекст — это RAM, а не диск. > - **Часть 2:** Настоящая память = семь принципов в комбинации. Атомарные единицы + lifecycle + truth_status + temporal + причинные цепочки + AST-идентичность + internal git + memory scoring + negative memory. Каждое существует по отдельности. В сборе — другой продукт. > - **Часть 3:** Метрика для продакшен-AI — не accuracy, а *0% уверенно-неверных действий*. Abstain — first-class исход, а не ошибка. Cognitive runtime > размер модели. > > Если твой AI «помнит» через `vector_db.query(top_k=5)` — у него деменция, замаскированная под уверенность. Чини архитектуру, а не модель. --- *Часть 3 из 3. Серия завершена. Если откликнулось — поделись. Если не согласен — напиши в комментариях, я люблю содержательные споры.* --- ## Семь принципов настоящей памяти для AI-агентов - URL: https://vbcherepanov.com/ru/articles/seven-principles-of-real-memory-for-ai-agents - Canonical (medium): https://medium.com/@vbcherepanov/seven-principles-of-real-memory-for-ai-agents-3029d7d877ac - Published: 2026-05-06 - Reading time: 8 min - Tags: AI Agents, Memory - Language: ru > Атомарные единицы знаний с lifecycle, strict mode с правом промолчать, причинные цепочки решений, AST-идентичность кода, internal git, скоринг и negative memory — что отличает настоящую память от Ctrl+F. ![Семь принципов настоящей памяти — обложка](/images/articles/seven-principles-of-real-memory-for-ai-agents.png) > **Часть 2 из 3 — «Память для AI-агентов»** > Архитектура. Конкретно. С формулами и lifecycle. В предыдущем посте я разнёс пич *«RAG = память»* на три неудобных проблемы: чанк не знает, что он чанк; в retrieval нет структуры, только косинус; время не существует как first-class понятие. Короче — RAG это поиск, в маркетинговой обёртке *«память».* Сегодня — что должно быть **вместо**. Сразу дисклеймер. Я не претендую на изобретение ни одного пункта в этом списке. Атомарные факты восходят к Витгенштейну. Temporal validity — базовая логика. Knowledge graphs — целое поле с учебниками. Lifecycle для данных — стандарт в любой нормальной информационной системе. Я претендую на другое. Я утверждаю, что **все семь свойств должны работать в одной системе одновременно**, и что любая система, в которой реально работают только пять из семи, продолжает врать пользователю с уверенным лицом. Увидеть это можно ровно одним способом — попробовать собрать все семь в один кодбаз и посмотреть, что получится. Я попробовал. Получилось. Назвал `braincore`. Open-source, Apache-2.0, один Go-бинарь, MCP-stdio. Не превращаю статью в питч — но в каждой секции ниже добавлю одну строчку про то, как это сделано в `braincore`, чтобы было ясно, что мы не теоретизируем. Поехали. --- ### Принцип 1. Атомарные единицы знаний с lifecycle, а не «чанки в Qdrant» **Боль.** В RAG любой входящий текст — диалог, design-документ, git-коммит, расшифровка митинга — нарезается на чанки и улетает в векторную БД без вопросов. Дальше что бы ни происходило — все чанки эквивалентны, все одинаково «свежие», все одинаково «истинные». Через полгода в одной коллекции лежит суп из устаревших, актуальных, гипотетических и опровергнутых фактов. И у каждого ровно один шанс попасть в retrieval — по косинусу. **Что должно быть в схеме.** Любая входящая информация **не течёт в память напрямую**. Она прогоняется через пайплайн: ```plaintext input → initial trust (по источнику: user=0.9, llm=0.3, web=0.4..0.7) → parse (entity / fact / relation / event / rule / hypothesis) → atomic knowledge units → validate (source / graph / dedup / contradiction / temporal / rule) → link (минимум 1 ребро в граф ИЛИ review item) → working memory (TTL + activation) → iterative verification loop → consolidation → long-term memory (только подтверждённое + связанное) → edge strengthening (usage + success + co-occurrence − decay) ``` Главное правило: **ничего не попадает в long-term memory сразу**. У каждой атомарной единицы знаний минимум: - `truth_status`: `hypothesis | candidate | confirmed | contradicted | deprecated` - `lifecycle`: `staging | working | consolidated | archived` - `source_ref` — откуда прилетел - `confidence` — численная оценка уверенности - `valid_from` / `valid_until` — когда правда Сравни это с RAG-чанком, у которого есть только `text` и `embedding`. Это разница между ящиком хлама и складом с инвентаризацией. **Что это даёт.** Когда вчера ты сказал *«мы используем Postgres»*, а сегодня — *«мы переехали на ClickHouse, Postgres теперь только OLTP»* — старый факт автоматически получает `valid_until = today` и `superseded_by = new_fact_id`. На retrieve он либо вообще не появляется, либо приходит с пометкой *«историческое, не текущее».* Не из-за умной модели. Из-за **схемы**. **Как это сделано в braincore.** Пайплайн `staging → working → consolidated` реализован буквально — три отдельные таблицы SQLite плюс промежуточный verification-loop. Запись доходит до `consolidated`, только если `truth_status = confirmed`, есть хотя бы одно ребро в графе, нет нерешённых противоречий и `confidence ≥ threshold`. Иначе остаётся в `working` с TTL или уходит в `review queue`. --- ### Принцип 2. Strict Mode и право на abstain Это, возможно, самый важный пункт во всей серии. И самый отсутствующий в коммерческих memory-фреймворках. **Боль.** Стандартная метрика, по которой меряют AI-системы, — *«как часто они дают правильный ответ».* Это **гнилая** метрика. 95% правильных ответов и 5% уверенных галлюцинаций — это система, **которой нельзя доверять в проде**. Потому что ты заранее не знаешь, в каких 5% ты сейчас. Правильная метрика звучит: > **0% уверенно-неверных действий при приемлемом abstain rate.** Не *«всегда отвечать».* А *«никогда не выполнять неверного действия без верификации».* А если верификации нет — **сказать «не знаю»** и поставить себе задачу это починить. **Что должно быть в схеме.** Прежде чем факт окажется в контексте промпта, он проходит через **ворота**: - есть ли `source_ref`? - `confidence ≥ threshold`? - `trust_score ≥ threshold` (для источника)? - `temporal_valid == true` (валидно на момент запроса)? - нет нерешённого `contradiction` в графе? - нет нерешённой `ambiguity`? Если хотя бы одно требование не выполнено — факт **не попадает** в контекст. Если для запроса ни один факт не прошёл — система возвращает `abstain = true` с `reason = no_accepted_facts` (или `contradiction_unresolved`, или `temporal_invalid` — всегда явно). И — внимание, тут происходит магия — **abstain не доставляется пользователю как тупик**. Он становится **brain task** в backlog'е: *«мне нужны evidence по X, чтобы ответить с confidence. Источник вот, конфликт вот».* Система знает, чего не знает, и сама ставит себе задачу это починить. **Что это даёт.** AI-агент, которому можно доверять. Не потому что он всегда прав — а потому что когда не уверен, он **молчит** или **просит уточнения**. А когда действует — действие основано на фактах, **прошедших ворота**, а не «ну, ChatGPT решил, что так лучше». Покажи мне один RAG-стек, который так умеет. Я подожду. **Как это сделано в braincore.** Пакет `internal/strictmode` — отдельный модуль с явными правилами ворот. По умолчанию каждый запрос идёт через strict mode; для UX-сценариев, где abstain неприемлем (брейншторминг, например), его можно отключить явным флагом `--allow-uncertainty`. Все события abstain логируются как brain tasks с источником и причиной. --- ### Принцип 3. Causal Decision Chains, а не плоские факты **Боль.** В RAG любое решение хранится как *«текст про решение».* На retrieve ты получаешь кусок текста, который **описывает** решение, — но не отвечает на *«почему?»*, *«какие альтернативы рассматривали?»*, *«что из этого вышло?»* Через полгода ты спрашиваешь *«почему мы выбрали JWT, а не сессии?»* — RAG возвращает три фрагмента декларации, и модель сама дописывает reasoning. Иногда правильно. Иногда выдумывая по популярным паттернам из обучающих данных. И ты не знаешь, в каком случае какой. **Что должно быть в схеме.** Сущность называется не *«документ»* и не *«запись памяти».* Сущность называется **decision** и имеет схему: ```plaintext problem → что решали alternatives → что рассматривали и отбросили (с причинами) decision → что выбрали reasoning → почему именно это outcome → что вышло (заполняется позже, post-hoc) superseded_by → ссылка на новое решение, если это пересмотрели ``` Это не *«запихнём текст в эмбеддинг».* Это **причинная цепочка**, отвечающая на **ПОЧЕМУ**, а не только на **ЧТО**. **Что это даёт.** Через полгода ты спрашиваешь *«почему JWT?»* — система возвращает структурированный ответ: - **Problem:** масштабирование сессий + audit-требования. - **Alternatives (rejected):** stateful-сессии с Redis (нарушает audit), opaque-токены с централизованным lookup (latency). - **Decision:** JWT с коротким TTL. - **Reasoning:** stateless, audit-нейтрально, latency приемлемо. - **Outcome (зафиксирован через 4 месяца):** сложность инвалидации выше ожидаемой; добавили refresh-токены. - **Superseded by:** none. RAG возвращает три фрагмента. Decision chain возвращает **reasoning**. Это разные продукты. **Как это сделано в braincore.** Decisions — отдельный тип сущности в графе с обязательными полями `problem`, `alternatives[]`, `decision`, `reasoning` и опциональными `outcome`/`superseded_by`. Хранятся не как чанки, а как структурированные записи с явными рёбрами в code-graph и в другие decisions. --- ### Принцип 4. Стабильная идентичность кода через AST, а не строки **Боль.** Этот пункт специфичен для AI-агентов, работающих с кодом, — но бьёт по всем. Ты переименовал `GetUser → FetchUser`, перенёс из `pkg/auth` в `pkg/user`, изменил сигнатуру с pointer-receiver на value-receiver. Все ссылки в RAG-памяти, указывающие на *«GetUser в pkg/auth»*, теперь **мертвы**. Потому что RAG привязан к **строкам**. И никто тебе не скажет. Чанк продолжает жить в Qdrant, его косинус к auth-запросам остаётся высоким. Агент тащит мёртвую информацию и работает по ней. Поздравляю, у тебя memory rot, замаскированный под память. **Что должно быть в схеме.** Парсинг кода через `go/ast` (для Go) и tree-sitter (для PHP, JS, TS, Python, Rust, Java и далее). **Идентичность узла** строится не из строки и не из пути файла, а из структурного хеша: ```python node_id = sha256(qualified_name + kind + signature_hash) ``` Что значит: - Переименование функции **не ломает** ссылки на неё (`qualified_name` изменился, но связь обновляется автоматически на следующем парсе, со back-reference на старый `node_id` как `renamed_from`). - Перенос между пакетами — то же самое. - Изменение сигнатуры (pointer → value receiver) — `signature_hash` изменился, и старые ссылки **автоматически помечаются `stale`** — мозг **знает**, что они теперь требуют review. **Что это даёт.** Когда AI-агент собирается редактировать `FetchUser`, система подтягивает три прошлых решения про эту функцию, две регрессии в этом модуле и активные правила проекта — **до** того, как агент начнёт писать код. Не из-за того что косинус удачно совпал. А потому что это **code graph**, и у `FetchUser` есть рёбра к decisions, regressions и rules **по идентичности**, а не по текстовому сходству. Я называю это pre-edit warning. И это качественно другой класс предотвращения ошибок, чем *«запустим линтер после генерации».* **Как это сделано в braincore.** Code graph — отдельный слой поверх AST/tree-sitter, с фоновым reindex на filesystem watch events. Identity-хеши живут в SQLite, рёбра тоже там. На pre-edit hook агент автоматически получает контекст связанных decisions/rules/regressions. --- ### Принцип 5. Internal Git как memory versioning **Боль.** В RAG нет понятия времени, кроме `created_at`. Это метаданные о **записи**, а не о **состоянии знания**. Ты не можешь спросить *«покажи, что я знал про этот код месяц назад».* Не можешь откатить состояние памяти до того, как агент натащил мусора. Не можешь переключиться на feature-ветку и иметь параллельное ментальное состояние под неё. **Что должно быть в схеме.** Каждое изменение в памяти — это **commit**. Не метафорически. Буквально, через `go-git`, в скрытый `.internal-git/` репозиторий, лежащий параллельно основному репо проекта. Это даёт: - `git log` по **памяти** проекта — что было добавлено, что изменилось, когда. - `git checkout` для отката состояния мозга на N дней — для аудита, для расследования регрессий, для тестов. - Когда ты переключаешься на feature-ветку в основном репо — мозг **зеркалит** это, и у каждой ветки своё ментальное состояние. Эксперимент в feature-ветке не загрязняет память master'а. **Что это даёт.** Time-travel запросы: *«какое решение я считал актуальным 30 дней назад?»* Аудит: *«когда именно агент начал считать, что мы используем ClickHouse?»* Branch isolation: *«в feature/oauth у нас другой подход к auth, но это знание не должно протекать в main».* RAG так не умеет. У RAG нет понятия *«состояние знания»* — есть только набор векторов, который растёт. **Как это сделано в braincore.** `.internal-git/` создаётся на `braincore init`. Коммиты делаются автоматически на каждое изменение knowledge units и графовых рёбер. Branch tracking синхронизирован с основным git через post-checkout hook. --- ### Принцип 6. Memory Scoring — потому что не всё знание равно **Боль.** В RAG все чанки равны. Top-k по косинусу не различает *«это подтверждено десятью прошлыми использованиями»* и *«это написали вчера и больше не использовали».* Не различает *«это критично для архитектуры»* и *«это случайная заметка в углу».* Не различает *«это в активном использовании»* и *«это пылится с прошлого года».* **Что должно быть в схеме.** У каждой единицы знания есть составной `MemoryScore`, считаемый как взвешенная сумма: ```plaintext MemoryScore = + 0.22 * ImportanceScore (явная важность или производная от связности) + 0.22 * TrustScore (надёжность источника + история подтверждений) + 0.20 * TaskRelevanceScore (релевантность текущему контексту работы) + 0.12 * UsageScore (как часто используется) + 0.10 * RecencyScore (свежесть) + 0.10 * StabilityScore (как часто меняется — стабильное надёжнее) + 0.08 * NoveltyScore (новизна как лёгкий буст) − 0.18 * RiskScore (потенциальный вред от использования) − 0.18 * NoiseScore (шум, дубликаты, низкая когерентность) ``` И на retrieve работает **уже не cosine similarity**, а: ```plaintext RetrievalScore = + 0.35 * semantic_similarity + 0.20 * memory_score + 0.15 * graph_relevance + 0.15 * temporal_validity + 0.10 * trust_score − 0.15 * ambiguity_penalty ``` Эти веса — не абсолютная истина, они эмпирически подобраны и сдвигаются с профилем использования. Точка не в числах, а в **архитектурном сдвиге**: retrieval перестаёт быть *«текстовым сходством»* и становится *«сходство × важность × доверие × свежесть».* Lifecycle-переходы автоматические: - `memory_score ≥ 0.80` и `trust ≥ 0.75` → `consolidated` (знание становится «прошивкой») - `memory_score ≥ 0.55` → остаётся в `working` - `memory_score ≥ 0.30` → `staging` - `memory_score < 0.30` → `archive candidate` **Что это даёт.** Активную память. Не хранилище. **Активную среду**, в которой важное усиливается через использование, а шум **сам затухает** — как в биологическом мозге, где редко используемые синапсы ослабевают, а часто используемые усиливаются. > RAG = жёсткий диск, который никогда не дефрагментируется. > Brain = мозг, в котором мусор **сам оседает** и архивируется автоматически. **Как это сделано в braincore.** Scoring пересчитывается фоновым джобом раз в N часов. Lifecycle-переходы атомарны и логируются (см. Принцип 5). Все веса вынесены в конфиг — настраивай под свой проект. --- ### Принцип 7. Negative Memory и Rule Engine **Боль.** Вот что делает любой LLM-агент сегодня: **повторяет ошибки**. Вчера сломал миграцию — сегодня сломает похожую. RAG не поможет, потому что **сломанная миграция в RAG не попадает**. В RAG попадает *«как писать миграции»* из официальной документации. Факт, что ты лично уже наступил на эти грабли, — нигде не записан. **Что должно быть в схеме.** Отдельный класс — **negative memory**: что сломалось, почему сломалось, как починили, какой коммит/тест это подтверждает. First-class сущность, а не маргинальное поле. И при планировании каждый патч прогоняется через **Rule Engine** до генерации кода: ```plaintext patch → architectural rules → code rules → security rules → performance rules → anti-patterns (включая «ровно эту ошибку я уже делал») → repair plan ИЛИ abstain ``` Если нарушено правило с severity `critical` или `high` — **код не пишется**. Создаётся repair plan. Если ремонт невозможен — `abstain` (см. Принцип 2). Никаких генераций «авось проскочит». И, критически, **safe execution pipeline** замыкает цикл: ```plaintext checkpoint → apply patch → rules validate → build → tests → success → commit → fail → rollback → запись в negative memory ``` Каждое **выполненное** действие либо подтверждено тестами, либо откатано, либо записано как **negative evidence** для будущих решений. **Что это даёт.** Агента, который **не может** повторить твою прошлогоднюю ошибку. Не потому что у него хорошая модель — а потому что **rule engine физически отказывается пропускать** любой патч, нарушающий правило, выведенное из этой ошибки. > RAG помогает агенту что-то найти. Хорошая память **не даёт** агенту что-то сломать. Это разные продукты. И мне жаль тех, кто продолжает их путать. **Как это сделано в braincore.** Negative memory — отдельный тип сущности с обязательной ссылкой на failing-тест или git-коммит. Rule engine — pre-execution гейт, severity-aware, с возможностью override только через явное подтверждение пользователя. --- ### Бонус-принцип. Entity Disambiguation Формально частный случай Принципа 1 (атомарные единицы), но ломается отдельно достаточно часто, чтобы заслужить свой call-out. В RAG нет понятия **сущности**. Есть только текст. Если в твоём проекте две `User`-сущности — одна в `pkg/auth`, другая в `pkg/billing` — для RAG это два куска текста с похожими эмбеддингами. На retrieve они **смешиваются**, и модель уверенно объясняет логику auth в контексте billing. Это не теория. Это происходит **прямо сейчас** в каждом code-RAG-агенте. Лечение — **EntityFingerprint**: ```python fingerprint(symbol) = hash( project_id + file_path + symbol_name + symbol_type + signature + language ) ``` Две `User`-сущности в разных файлах = два fingerprint'а = две различные сущности, которые **никогда не сливаются автоматически**. Когда приходит новый кандидат, считается `SameEntityScore`: ```plaintext SameEntityScore = + 0.30 * name_similarity + 0.20 * alias_match + 0.20 * context_similarity + 0.15 * graph_neighborhood_similarity + 0.10 * temporal_consistency + 0.05 * source_consistency ``` И: - `≥ 0.92` → `auto_merge` - `≥ 0.82` → `same_as` link (мягкая связь, не merge) - `≥ 0.65` → `ambiguous` — создаётся **ambiguity record**, требующий human review - иначе — новая сущность Главное правило: **никогда не сливать сущности при низкой confidence**. Лучше создать ambiguity-запись и спросить человека, чем тихо склеить и врать после этого вечно. --- ### Зачем всё это вместе Я намеренно не подаю это как *«нигде такого не делали, я первый».* Каждый из семи принципов уже существует. Атомарные факты с lifecycle — в системах knowledge management. Strict mode + abstain — в expert systems прошлого века. Causal chains — в decision support. AST-идентичность — в IDE. Internal git — в инструментах вроде Pijul и в экспериментах с Datalog-БД. Memory scoring — в исследованиях про episodic memory. Negative memory — в RL и reliability engineering. Уникальность не в идеях. Она в **сборке**. Если у тебя есть атомарные единицы, но нет strict mode — у тебя структурированная база галлюцинаций. Если есть strict mode, но нет causal chains — ты делаешь abstain, не понимая почему. Если есть causal chains, но нет AST-идентичности — твои decisions указывают в пустоту после двух рефакторингов. Если есть всё вышеперечисленное, но нет memory scoring — у тебя идеально структурированный дамп, в котором важное тонет в шуме. Каждое свойство по отдельности — улучшение. Все семь вместе — другая категория продукта. Это, кстати, и ответ на вопрос, который я слышу чаще всего: *«зачем писать что-то новое, если есть Mem0/Letta/Zep?»* Ответ — посмотри на их схемы и проверь, сколько из семи принципов там реализовано **не как маркетинговое заявление, а как enforced gate в коде**. Для большинства честный счёт — два-три. Для некоторых — четыре. Это не плохие продукты. Это **частичные решения**, которые честнее называть *«структурированный retrieval»*, а не *«память».* --- ### В части 3 Семь принципов — это инженерия. То, что **должно быть** в архитектуре. Но за инженерией стоит более глубокий вопрос: **почему AI-агент должен знать, чего он не знает?** Зачем вообще abstain, если можно просто ответить? Часть 3 — про право AI-агента молчать. Про self-tasking. Про то, почему cognitive runtime важнее размера модели. И про то, почему правильная метрика для продакшен-AI — это не accuracy, а *0% уверенно-неверных действий при приемлемом abstain rate*. Самая короткая и самая философская часть серии. Через неделю. --- *Часть 2 из 3. Если пропустил [Часть 1 — здесь](https://medium.com/@vbcherepanov/rag-isnt-memory-it-s-ctrl-f-with-embeddings-c461b90ac7b1) (про то, почему RAG — это поиск, а не память). Если откликнулось — репост поможет.* --- ## RAG — это не память. Это Ctrl+F с эмбеддингами. - URL: https://vbcherepanov.com/ru/articles/rag-isnt-memory-its-ctrl-f-with-embeddings - Canonical (medium): https://medium.com/@vbcherepanov/rag-isnt-memory-it-s-ctrl-f-with-embeddings-c461b90ac7b1 - Published: 2026-05-01 - Reading time: 7 min - Tags: AI Agents, Memory - Language: ru > Векторный поиск — это поиск, а не память. Три провала: куски теряют смысл, нет структурного различия между фактами, нет понятия "когда это было правдой". Поэтому RAG уверенно галлюцинирует. ![RAG — это не память. Обложка.](/images/articles/rag-isnt-memory-its-ctrl-f-with-embeddings.png) > **Часть 1 из 3 — «Память для AI-агентов»** > Разбираем по косточкам миф про long-term memory в LLM-системах 3 часа ночи. Третья ночь подряд я отлаживаю AI-агента. Стою на кухне с кружкой чая, смотрю в дифф и тихо матерюсь. Агент уверенно переписал auth-функцию — на основе чанка, принадлежащего ветке, удалённой из репозитория два месяца назад. Чанк живёт в Qdrant. Косинусная похожесть к моему запросу — высокая. Top-1 в retrieval. Агент честно его взял, честно вшил в промпт, честно сгенерировал «правильный» патч. Против кода из другой реальности. Закрываю ноутбук и думаю: окей, у меня есть RAG. У меня есть векторы. У меня есть long-term memory. У меня есть всё, что обещает каждый AI-конференц-дек последние два года. Почему мой агент только что предложил фикс по коду, которого больше не существует? Потому что у моего агента нет памяти. У моего агента есть результаты поиска с косинусом вместо BM25. И между этими двумя предложениями — вся разница между *«AI, которому можно доверять в проде»* и *«AI, за которым надо нянчиться на каждой строке».* Этот текст — про эту разницу. И про то, почему мы, инженеры, виноваты в том, что перестали её видеть. --- ### Девальвация слова «память» Давай честно. Что такое типичная «память» AI-агента в 2026-м? ```plaintext текст → разбивка на чанки 512–1024 токена → embedding (bge / text-embedding-3 / openai) → векторная БД (Qdrant / pgvector / Chroma / Pinecone) → cosine similarity top-k → конкатенация в промпт ``` Это **не** память. Это поиск. Это старый добрый Lucene из 2003-го, перекрашенный в нейронные цвета. Cosine вместо TF-IDF. Эмбеддинги вместо инвертированного индекса. Одно и то же. Если бы это так и называли — *«векторный поиск»*, *«семантический retrieval»* — у меня бы не было претензий. Назови Lucene Lucene, без проблем. Но когда это продают под флагом *«мой AI имеет долговременную память»* — извини. У моего AI одновременно дежавю и амнезия. Это не придирка к терминологии. Это вопрос ожиданий. Когда инженер слышит *«память»*, он представляет систему, которая **помнит**: кто что сказал, когда, в каком контексте, что было правдой тогда и что — сейчас. Когда инженер получает RAG, он получает Ctrl+F. И вместо того чтобы строить честную архитектуру вокруг этого Ctrl+F — с честными ограничениями — строит замок из песка и удивляется, почему агент путает прошлое с настоящим. --- ### Три дыры, в которые проедет грузовик Три конкретных провала. Каждый я ловил в проде. Не теория. **Дыра №1: чанк не знает, что он чанк.** Возьми обычное декларативное утверждение из design-документа: > *«Перешли на JWT, потому что opaque-сессии не масштабировались под наш профиль трафика. Альтернативой были stateful-сессии с Redis-кластером, но мы их отклонили из-за audit-требований клиента — он не разрешает хранение состояния сессий вне периметра. JWT решает оба, но добавляет сложности с инвалидацией, которую мы митигируем коротким TTL и refresh-токенами».* Чанкер режет это на четыре куска по 512 токенов. На retrieval приходит запрос: *«почему мы выбрали JWT?»* Top-3 возвращает три фрагмента одного и того же решения. Без причинности. Без альтернативы, которую отбросили. Без trade-off, на который пошли. Решение, которое было **целым**, превращается в три параллельных «фактоида». Модель честно сшивает их в правдоподобный текст — и **выдумывает** недостающие связки. Потому что её работа — генерировать правдоподобный текст. И она это сделает, не моргнув глазом. Это не баг чанкера. Это архитектурное свойство всего подхода. Любая декларация решения превращается в порошок и пересобирается со структурными потерями. Каждый раз. **Дыра №2: в памяти нет структуры. Только косинус.** Когда человек объясняет тебе проект, он говорит: - *вот цель* - *вот варианты, которые рассматривали* - *вот что выбрали и почему* - *вот что сломалось через два месяца* - *вот что изменили, и теперь это решение перекрывает старое* В RAG ничего этого нет. Ноль. RAG не различает *«гипотезу»*, *«подтверждённый факт»*, *«отвергнутую альтернативу»*, *«deprecated-решение, перенесённое в архив»*. Для RAG всё это — эквивалентные точки в 384-мерном пространстве. Представь, что ты пытаешься записать тридцать лет жизни в одну плоскую таблицу `entries(text, vector)` и потом искать по косинусу. Удивительно, что воспоминания сливаются? Это не твоя память подвела. Это структура, в которую ты её затолкал, — структура, которая не позволяет различить *«я об этом думал»* и *«я это сделал»*, *«я попробовал и сработало»* и *«я попробовал, и больно».* В RAG нет полей под эти различия. Не потому что разработчики не подумали. Потому что **сама парадигма «вектор плюс расстояние»** не вмещает причинность и время. Это математическое ограничение. Продуктовыми фичами его не лечат. **Дыра №3: у времени нет статуса first-class сущности.** Три недели назад я записал в память агента: *«мы используем Postgres».* Сегодня записал: *«мигрировали на ClickHouse для аналитики, Postgres теперь только OLTP».* В RAG **оба** факта лежат там. У обоих высокий косинус к запросу про БД. Top-k возвращает оба. Модель выбирает тот, что «звучит» лучше в её претрейне — обычно Postgres, потому что он встречается чаще в обучающих данных. Это **не** память. Это рулетка, прикинувшаяся уверенностью. Когда ты в последний раз видел поля `valid_from`, `valid_until`, `deprecated_by`, `replaced_by`, `superseded_by` в продакшен-RAG-системе? Я — никогда. Потому что в стандартном RAG их **нет в схеме**. И опять же — не потому что разрабы ленивые. Потому что у схемы *«текст плюс эмбеддинг»* нет места под жизненный цикл знания. Нет понятия *«это правда сейчас»* против *«это было правдой тогда».* Всё схлопывается в один временной срез — настоящее, которое каким-то образом содержит вчера, прошлый год и deprecated-три-квартала-назад одновременно. > Ctrl+F с эмбеддингами не **помнит**. Он **находит**. Разные глаголы. --- ### «Но memory-фреймворки же это решают, да?» Окей, говорит верующий. Есть mem0, Letta, Zep, Cognee, MemGPT — целый зоопарк long-term memory. Они добавили слой смысла поверх RAG. Они memory-aware. Давай честно. Я их использовал. Один за другим. Долго. Смотрел под капот, не на лендинги. Каждый из них берёт **один** кусок настоящей памяти — у кого-то это LLM-extraction перед записью, у кого-то buffer-иерархия как в OS, у кого-то post-hoc графовая экстракция из диалогов, у кого-то per-fact temporal validity — и реализует **этот один кусок**, не вплетая его в остальное. Это теплее, чем ванильный Qdrant. Это **не** решение. Потому что настоящая память требует **семи** свойств, работающих вместе. Каждое из них по отдельности уже существует в литературе или в опен-сорсе. Насколько я могу судить, никто не собрал все семь в одну систему. Какие именно семь — это часть 2 этой серии. Здесь — только ограничение, которое объединяет **все** flat-fact-решения, как бы они себя ни оборачивали: **Ни у одного из них нет права сказать «не знаю».** Покажи мне любую из этих систем с формальным abstain-механизмом: воротами, через которые факт **не пройдёт** в контекст промпта, если у него нет источника, нет confidence, нет temporal validity или есть нерешённое противоречие. Я подожду. В стандартном flow всех этих фреймворков ответ системы на *«в памяти противоречие или недостаточно данных»* — *«ну, модель разберётся».* Что в переводе с маркетингового на инженерный означает *«модель будет галлюцинировать, и это станет твоей проблемой в проде».* Хорошая память — это не *«помнить много».* Это **знать границу того, чего ты не помнишь**. Часть 2 этой серии построена вокруг этого тезиса. --- ### «А почему просто не накачать контекст до 1М токенов?» Это вторая мода последних двух лет, и она заслуживает отдельного разбора, потому что ведёт индустрию в тот же тупик под другим флагом. *«Зачем нам память, если у Gemini 2М контекст, у Claude — 1М?»* Четыре проблемы, без преамбулы. **Один — экономика.** Один проектный диалог на 800К токенов с выключенным prompt caching стоит десятки долларов **за запрос**. Без агрессивного кэширования ты разоришься за неделю. С агрессивным кэшированием ты строишь ровно ту же иерархию, что и Letta, — только дороже и привязанной к одному вендору. **Два — recall.** Каждый long-context-бенчмарк (NIH, Ruler, LongMemEval) показывает одно и то же: модели **тонут** в собственном контексте после 200–300К токенов. Внимание распределено неравномерно. Это **lost-in-the-middle**, и оно не лечится размером окна — частично митигируется архитектурными трюками внутри модели, но не уходит. Чем больше ты впихиваешь, тем меньше реально учитывается. **Три — persistence.** Контекст не сохраняется. Закрыл сессию — нет. Завтра тот же агент приходит с чистым контекстом. То есть тебе снова нужно скармливать ему 800К токенов «истории». Проблема не решена — она спрятана в твоём кошельке и в твоей latency. **Четыре — обучение.** Если агент вчера ошибся, и ты его поправил, этот опыт не структурирован под будущее. Завтра он повторит ошибку. Контекст — это RAM, не диск. И когда кто-то говорит *«просто увеличь контекст вместо того, чтобы строить память»* — это всё равно что сказать *«зачем мне база, у меня терабайт RAM».* Технически слова рифмуются. По сути — несравнимые понятия. Большой контекст не заменяет память. Он позволяет впихнуть в одну сессию больше — и всё. --- ### Что делать с этим завтра утром Если ты дочитал и думаешь *«окей, согласен, RAG — это поиск, не память. Что теперь?»* — у меня две новости. Плохая: системно правильное решение требует переписать memory-слой от схемы до lifecycle, и это месяцы работы. Не выходные. Хорошая: есть несколько вещей, которые ты можешь сделать **завтра утром** и уже снять половину боли. Не магия — просто инженерная гигиена. - **Убери слово «память» из стека, если у тебя RAG.** Назови это retrieval или search — мгновенно честнее. Только это снимет 80% завышенных ожиданий пользователей и команды. - **Введи `valid_from` и `valid_until` для каждого факта.** Любой факт без temporal validity — это гипотеза, а не факт. Старые факты должны автоматически выпадать из retrieval, а не конкурировать с новыми по косинусу. - **Различай `staging`, `working`, `consolidated`, `archived`.** Не сваливай всё в одну коллекцию. Только что прилетевший факт и знание, подтверждённое тестами, — это разные сущности с разным весом в retrieval. - **Сделай abstain first-class исходом.** Если ни один факт не прошёл confidence-порог при retrieve, система **обязана** иметь право сказать *«не знаю, нужны данные».* И это *«не знаю»* должно стать задачей в backlog'е, а не тупиком для пользователя. Это не полный список — это минимум, чтобы стартовать переход от *«у меня RAG, я называю это памятью»* к *«у меня память, и она знает свои границы».* Полный список из семи принципов — в части 2. --- ### Откуда это всё Я сижу глубоко в этой кухне — Claude Code, Cursor, Codex, Windsurf, MCP-серверы, mem0, Zep, локальные RAG-стеки на Postgres + pgvector, Qdrant, Chroma. За последние несколько месяцев я перепробовал, пожалуй, всё, что есть на рынке. У меня свой MCP memory-сервер примерно на полторы тысячи записей, который я переписывал с нуля три раза, потому что каждый раз упирался в одну из трёх дыр выше. В какой-то момент я устал. Не от AI — от того, что мы называем памятью у AI. Сел и начал писать свой cognitive runtime, который **не претендует знать**, который **знает, чего не знает**, и который **сам ставит себе задачи** на закрытие пробелов. Назвал `braincore`. Один Go-бинарь, локальный, MCP-stdio, Apache-2.0. Это не пича — он open-source — просто пример того, что я говорю *«это можно сделать»* не теоретически. Семь архитектурных принципов, на которых он построен, — это часть 2 этой серии. Через неделю. Расскажу про atomic knowledge units, lifecycle, strict mode, causal decision chains, AST-идентичность кода, internal git как memory versioning, memory scoring и negative memory. И почему всё это вместе даёт качественно другой результат, чем любой из этих кусков по отдельности. Часть 3 — философская — про **право AI-агента молчать** и про то, почему правильная метрика для продакшен-AI — это не accuracy, а *zero confidently-wrong actions at an acceptable abstain rate*. Про self-tasking. Про то, почему cognitive runtime важнее размера модели. --- Если ты дочитал и узнал себя в начальном абзаце — мы в одной лодке. Если у тебя RAG, который ты называешь памятью, и он работает — расскажи как, серьёзно, я хочу знать, может, я неправ. Чего ты точно не можешь — это молчать. --- *Часть 1 из 3. Дальше — «Семь принципов настоящей памяти для AI-агентов» — выходит на следующей неделе.* --- ## Почему AI-сгенерированный код — это technical debt с нулевого дня - URL: https://vbcherepanov.com/ru/articles/why-ai-generated-code-is-technical-debt-from-day-zero - Canonical (medium): https://medium.com/@vbcherepanov/why-ai-generated-code-is-technical-debt-from-day-zero-da3421a73989 - Published: 2026-04-15 - Reading time: 7 min - Tags: AI Agents, Backend - Language: ru > Паттерны, которые тихо копят долг: фантомные абстракции, copy-paste через промпты, тихая деградация ошибок, амнезия между запросами и декоративные тесты. Лекарство — человеческий контроль над архитектурой и немедленный рефакторинг. ![AI-сгенерированный код как technical debt — обложка](/images/articles/why-ai-generated-code-is-technical-debt-from-day-zero.png) Последние полгода я генерирую код через Claude Code по 6–8 часов в день. Не как эксперимент — как основной рабочий инструмент. У меня крутятся 7 кастомных саб-агентов, MCP-серверы, хуки, persistent memory. Я не теоретик, прочитавший пару постов и решивший высказаться. И именно поэтому я говорю: большая часть AI-сгенерированного кода — это technical debt, который начинает гнить в момент коммита. Не потому что модели плохие. Потому что их используют неправильно. ## «Работает» — это не качество Главная ловушка: ты описываешь задачу, получаешь 200 строк кода, запускаешь — работает. Тесты зелёные (если ты вообще их попросил). PR замержен. Все довольны. Через три недели открываешь этот файл — и у тебя больше вопросов, чем ответов: - Почему здесь три слоя абстракции для записи в одну таблицу? - Почему сервис знает про HTTP-заголовки? - Откуда взялся этот `catch (Exception $e)`, который молча проглатывает ошибки? - Почему DTO зеркалит структуру Entity 1:1, и зачем он вообще существует? Модель не пишет плохой код намеренно. Она пишет *правдоподобный* код. Код, статистически похожий на то, что она видела в обучающей выборке. А обучающая выборка — это Stack Overflow, гитхаб-репы с 2 звёздами, туториалы junior-уровня и legacy-проекты на PHP 5.6. Правдоподобно ≠ правильно. Правдоподобно ≠ поддерживаемо. ## Конкретные паттерны разложения Не буду теоретизировать. Вот что я вижу в реальных проектах каждую неделю: ### 1. Фантомные абстракции Модель обожает создавать интерфейсы с одной реализацией, фабрики для объектов, которые создаются в одном месте, и сервисные слои, которые просто проксируют вызовы репозитория. Делает она это потому, что «так принято» в коде, на котором её обучали. Но абстракция без причины — это не архитектура. Это шум. В одном PHP/Symfony-проекте на 15 сущностей я насчитал 47 AI-сгенерированных интерфейсов. Реальная потребность в полиморфизме была в 3 случаях. ### 2. Copy-paste через промпт Люди копипастят руками. AI делает то же самое, только в масштабе. Ты просишь «такой же эндпоинт для заказов» — получаешь полную копию эндпоинта пользователей с переименованными переменными. Ни переиспользования, ни обобщения. Просто клон с другими именами. Через полгода у тебя 30 контроллеров с одинаковыми обработкой ошибок, валидацией и пагинацией — все слегка разные. Потому что каждый сгенерирован как отдельный запрос. ### 3. Тихая деградация Модель очень не любит возвращать ошибки. Она предпочтёт обернуть всё в try-catch, залогировать и вернуть пустой массив. Или null. Или дефолтное значение. В Go это выглядит ещё хуже: `if err != nil { return nil }` — и о проблеме ты узнаёшь тремя слоями вызовов глубже, когда данные уже записались не туда. Это не баг. Это паттерн: модель оптимизируется по «код компилируется и не падает», а не по «код корректно сообщает о проблемах». ### 4. Амнезия контекста AI не помнит, что писал 40 промптов назад. Каждый новый запрос — чистый лист. У тебя могут оказаться два сервиса, делающих одно и то же; конфликтующие подходы к валидации в разных частях приложения; три разных способа работы с датами в одном проекте. В монолите человек хотя бы видит соседний файл. AI видит только то, что ты ему показал. И строит в вакууме. ### 5. Декоративные тесты Попроси AI написать тесты — получишь тесты. Красивые, структурированные, с моками и ассертами. Проблема: они тестируют реализацию, а не поведение. Они хрупкие. Они ломаются на любом рефакторинге. И создают иллюзию покрытия. Я видел тест-сьют с 94% покрытия, который не поймал ни одной реальной ошибки бизнес-логики. Каждый тест проверял, что метод вызывает другой метод с правильными аргументами. Иначе говоря, они тестировали, что код написан так, как он написан. Спасибо, очень полезно. ## Почему это хуже обычного техдолга Обычный technical debt берётся осознанно. «Сделаем быстро сейчас, отрефакторим потом». Ты точно знаешь, где срезал угол. Знаешь, что сломается. AI-долг — скрытый. Код выглядит чистым. Нейминг нормальный. Структура папок — как в учебнике. Никакой code-reviewer не придерётся. Но под капотом: - нет единого архитектурного решения — просто набор локально-оптимальных фрагментов - нет понимания бизнес-ограничений — только формальная корректность - нет анализа trade-off'ов — просто «первый правдоподобный вариант» Это как дом, где каждую комнату проектировал отдельный архитектор, не общавшийся с другими. Каждая комната нормальная. Жить в доме нельзя. ## Так что, не использовать AI? Нет. Я использую Claude Code каждый день, и моя скорость выросла кратно. Но я отношусь к AI-коду как к черновику, а не финальному результату. Мой workflow: 1. **Архитектурные решения — мои.** Я определяю структуру, слои, контракты между модулями. AI получает конкретные ограниченные задачи в рамках уже принятых решений. 2. **Ревью каждой генерации.** Не «глянул» — а реальный review. Зачем этот интерфейс? Почему здесь три зависимости? Что будет, если этот сервис упадёт? 3. **Контекст — моя работа.** Я веду `CLAUDE.md`-файлы с архитектурными правилами для каждого проекта. Конвенции именования, подходы к обработке ошибок, запрещённые паттерны. Без этого каждая генерация — лотерея. 4. **Рефакторить сразу.** Не «потом». Сразу после генерации — снять лишние абстракции, унифицировать с остальным кодом, проверить edge cases. 5. **AI не пишет бизнес-логику с нуля.** Он реализует то, что я уже продумал. Разница как между «нарисуй мне дом» и «построй по этим чертежам». ## Итог AI-генерация кода — не silver bullet и не конец профессии. Это мощный инструмент, который в руках инженера ускоряет работу, а в руках prompt-оператора генерирует technical debt со скоростью, ранее физически невозможной. Разница между «я использую AI для разработки» и «AI разрабатывает за меня» — это разница между инструментом и костылём. Если ты не можешь объяснить, зачем существует каждая строка в сгенерированном коде — ты не программируешь. Ты копишь долг, который кому-то придётся выплачивать. Возможно, тебе. Через три месяца. С процентами. --- *15+ лет в продакшене. PHP/Symfony, Go, Vue/Nuxt, PostgreSQL. Пишу о реальном опыте использования AI-инструментов в повседневной разработке.* --- ## У вашего AI-ассистента амнезия. Вот как я её вылечил. - URL: https://vbcherepanov.com/ru/articles/your-ai-coding-assistant-has-amnesia - Canonical (medium): https://medium.com/@vbcherepanov/your-ai-coding-assistant-has-amnesia-heres-how-i-fixed-it-a8429f7f7e38 - Published: 2026-04-13 - Reading time: 9 min - Tags: AI Agents, Memory, Open Source - Language: ru > Total Agent Memory — open-source MCP-сервер с постоянной памятью между сессиями. 32 инструмента: хранение, self-improvement через error tracking, knowledge graph, episodic recall, оценка навыков. ![AI-ассистент с амнезией — обложка](/images/articles/your-ai-coding-assistant-has-amnesia.png) *Как я построил систему постоянной памяти, которая заставляет Claude Code и Codex CLI помнить всё между сессиями.* 11 вечера, вторник. Ты три часа провёл в сессии Claude Code, рефакторя платёжный сервис. Claude отлично понимает твою архитектуру — repository pattern, цепочку middleware, конвенции именования, на которые вы сошлись две недели назад. Жмёшь `/compact` последний раз, упираешься в лимит контекста, закрываешь терминал. Среда, утро. Новая сессия. Новый Claude. Он ничего не знает. «Какая у тебя структура проекта?» — спрашивает он. Снова. Ты объясняешь архитектуру. Снова. Поправляешь ту же ошибку, которую он сделал в прошлый четверг — использовал `map[string]interface{}` вместо типизированных DTO. Снова. Вставляешь тот же документ с конвенциями. Снова. Если знакомо — ты не один. Я провёл два месяца в этом цикле на 72 проектах, прежде чем решил это починить. --- ## Настоящая проблема: stateless by design Claude Code и OpenAI Codex CLI — выдающиеся инструменты. Но у них есть фундаментальное ограничение: **ноль постоянной памяти между сессиями**. Каждый разговор начинается с нуля. Это не баг — это архитектура. Эти инструменты stateless by design. Но для любого, кто делает серьёзную, длительную разработку, statelessness — убийца продуктивности. Вот что ты теряешь каждый раз, когда сессия заканчивается: - Архитектурные решения и причины, по которым они приняты - Решения багов, которые ты уже решил - Конвенции проекта, на которые ушли часы сессий - Ошибки, которые делал Claude (и поправки, которые ты дал) - Ментальную модель всего твоего кодбейза Я устал быть человеческим memory bank для своего AI-ассистента. И построил **total-agent-memory** — open-source MCP-сервер, дающий Claude Code (и Codex CLI, Cursor, Cline, Continue, Aider, Windsurf, Gemini CLI, OpenCode — всему, что говорит по MCP) постоянный мозг. **Сайт:** [totalmemory.dev](https://totalmemory.dev) · **GitHub:** [vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) > 💡 **Update (май 2026):** Изначально проект назывался `claude-total-memory`, в v12.0.0 переименован в **total-agent-memory** — он работает с любым MCP-клиентом, не только с Claude Code. Старое имя `claude-total-memory` на PyPI остаётся как deprecation shim (тянет за собой `total-agent-memory>=12.0.0`), поэтому существующие установки продолжают работать. --- ## Что он делает total-agent-memory — это Python MCP-сервер, который сидит рядом с Claude Code. Он предоставляет **32 инструмента в 6 категориях**, позволяющих Claude сохранять, искать, связывать и учиться на знаниях, которые остаются навсегда. Думай об этом как об апгрейде Claude — с гениального коллеги с амнезией до того, кто ведёт подробный инженерный блокнот. ### До и после **До (каждое утро понедельника):** ``` Ты: Продолжаем работу над платёжным сервисом Claude: С удовольствием помогу! Расскажи, пожалуйста, про структуру проекта, конвенции и что мы уже сделали? Ты: *вздыхаешь, вставляешь 2000 токенов контекста* ``` **После:** ``` Ты: Продолжаем работу над платёжным сервисом Claude: [memory_recall("payment service architecture")] Понял. В прошлой сессии мы рефакторили PaymentService под gateway-паттерн. Интеграция с Тинькоф готова, дальше Stripe. Ты предпочитаешь constructor injection и метрики-middleware, который мы настроили в internal/middleware/metrics.go. Подхватываю с того места, где остановились. ``` В этом разница. Никакого переобъяснения. Никакой вставки контекста. Claude просто *знает*. --- ## 32 инструмента в 6 категориях ### 1. Core Memory (12 инструментов) Фундамент. Сохранение и поиск знаний с пятью типами: `decision`, `solution`, `lesson`, `fact` и `convention`. ```python # Claude сохраняет решение в процессе сессии memory_save( content="Использовать UUID v7 для всех первичных ключей вместо SERIAL. Причины: сортируются по времени, нет contention'а на sequence, лучше для распределённых систем.", type="decision", tags=["database", "postgresql", "architecture"], project="payment-service" ) ``` ```python # Через неделю, в другой сессии, Claude ищет memory_recall( query="primary key strategy for postgresql", detail="full" ) # Возвращает: ровно то решение выше, отранжированное по релевантности ``` Поиск — не просто keyword matching. Это **4-уровневый гибридный пайплайн**: ``` Запрос: "docker networking between services" │ ├── Уровень 1: FTS5 + BM25 keyword search │ └── Находит точные совпадения: "docker", "networking" │ ├── Уровень 2: Semantic search (ChromaDB vectors) │ └── Находит связанное: "container communication", "bridge network" │ ├── Уровень 3: Fuzzy matching (SequenceMatcher) │ └── Ловит опечатки: "dokcer netowrking" всё равно работает │ └── Уровень 4: Graph expansion └── Идёт по связям: docker networking → compose config → env variables Все уровни сливаются через Reciprocal Rank Fusion (RRF) ``` Это важно. Один BM25 даёт 89% на retrieval-бенчмарках. Один semantic search — 91%. Полный 4-уровневый пайплайн с RRF-фьюжном? **97.45% на LongMemEval R@5** — выше, чем 96.6% у MemPalace. ### 2. Self-Improvement (6 инструментов) — главная фича Здесь становится интересно. Claude не просто *хранит* знания — он **учится на собственных ошибках**. Вот пайплайн: ``` Сессия 1: Claude использует `npm install` внутри Docker → Хук ловит ошибку → self_error_log(category="docker", error="running npm outside container") Сессия 3: Та же ошибка снова → Счётчик ошибок в категории "docker": 2 Сессия 5: Третий раз → 3+ ошибки в одной категории триггерит auto-insight → self_insight("Always run package managers inside Docker containers") Insight набирает уверенность через успешные применения... → Промоут до SOUL rule (importance >= 5, confidence >= 0.8) → Правило загружается на КАЖДОМ старте сессии → Claude больше никогда не делает эту ошибку ``` Инструменты: `self_error_log`, `self_insight`, `self_rules`, `self_patterns`, `self_reflect`, `self_rules_context`. Концепция SOUL rules — постоянных правил поведения, формирующих то, как Claude работает, — это то, что превращает систему из базы данных в нечто большее. Это feedback loop. Claude буквально становится лучше работать с *твоим* кодбейзом со временем. ### 3. Knowledge Graph (4 инструмента) Знание не плоское. Решения связаны с другими решениями. Решения багов ссылаются на проблемы, которые они закрыли. Граф фиксирует эти связи. ```python memory_relate( from_id=42, # "Use gateway pattern for payments" to_id=67, # "Tinkoff API requires idempotency keys" relation="context" ) ``` Когда Claude вспоминает решение про gateway-паттерн, он автоматически подтягивает связанный контекст про требования API Тинькофф. Никакой ручной линковки после первоначальной связи. ### 4. Episodic Memory (2 инструмента) Факты говорят *что*. Эпизоды говорят *что произошло*. ```python memory_episode_save( content="Потратил 3 часа, дебажа race condition в order service. Корневая причина: общий пул соединений к БД между горутинами без правильной отмены контекста. Починил per-request connection checkout.", context="payment-service sprint 4" ) ``` Когда Claude через несколько месяцев натыкается на похожую concurrency-проблему, он не просто знает фикс — он помнит сам процесс отладки и тупиковые попытки. ### 5. Skills & Competencies (3 инструмента) Claude отслеживает, в чём он хорош, а где буксует. ```python memory_skill_get(skill="kubernetes-debugging") # Возвращает: уровень владения, последняя практика, траектория улучшения memory_self_assess() # Возвращает: сильные стороны, слабые, слепые пятна по истории ошибок ``` ### 6. Advanced Cognitive Tools (5 инструментов) Spreading activation (`memory_associate`), автоматическая сборка контекста (`memory_context_build`), логирование наблюдений (`memory_observe`) и on-demand рефлексия (`memory_reflect_now`). --- ## Техническая архитектура Под капотом — намеренно просто: ``` ┌─────────────────────────────────────────────┐ │ MCP Server (Python). │ │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ │ │ │ SQLite │ │ ChromaDB │ │ Graph │ │ │ │ FTS5 │ │ (vectors) │ │ Engine │ │ │ │ + BM25 │ │ │ │ │ │ │ └──────────┘ └───────────┘ └──────────┘ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ Privacy Layer (auto-redacts secrets) │ │ │ └──────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ Web Dashboard (localhost:37737) │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ ``` Ключевые архитектурные решения: - **SQLite FTS5** для keyword-поиска с правильным BM25-ранжированием — внешний поисковый движок не нужен - **ChromaDB** для vector similarity с binary quantization для скорости - **Decay scoring** с 90-дневным half-life — свежее знание ранжируется выше, но ничего не выкидывается раньше времени - **Retention zones**: active (по умолчанию) → archived (180 дней, не возвращается в recall) → purged (365 дней в архиве) - **Auto-deduplication** через Jaccard similarity (порог >0.85) — предотвращает раздувание знаний - **Privacy stripping** автоматически вычищает API-ключи, JWT и email-адреса перед сохранением - **Ноль внешних сервисов** — всё работает локально. Знание о твоём коде никогда не покидает твою машину. --- ## Работает с Claude Code И Codex CLI Так как это MCP-сервер, любой инструмент, говорящий по протоколу MCP, может его использовать. Настраиваешь один раз — и Claude Code, и OpenAI Codex CLI делят одну и ту же базу памяти. Переключаешься между инструментами без потери контекста. --- ## Реальные цифры за 2+ месяца ежедневного использования Это не weekend-проект, про который я теоретизирую. Он крутится у меня на машине в ежедневном продакшене больше двух месяцев: | Метрика | Значение | | --- | --- | | Активных записей знаний | 1 683 | | Отслеживаемых проектов | 72 | | Узлов графа | 1 847 | | Рёбер графа | 20 925 | | Выученных навыков | 177 | | Зафиксированных эпизодов | 164 | Субъективная разница — драматическая. Утро понедельника превратилось из 15–20 минут восстановления контекста в практически ноль. Claude подхватывает ровно с того места, где остановился, — не потому что сессия сохранилась, а потому что каждый важный кусок контекста был сохранён и моментально находится. --- ## Self-improvement loop на практике Вот реальный сценарий. Неделя 1: я работаю над Go-проектом. Claude генерирует хендлер с 30 строками бизнес-логики внутри. Я поправляю — «хендлеры должны быть тонкими, до 15 строк, делегировать в сервисный слой». Claude правит. Поправка сохраняется как `convention`. Неделя 2: другой проект, тот же стек. Claude снова генерит толстый хендлер. Хук ловит мою поправку, логирует через `self_error_log`. Это ошибка #2 в категории «go-architecture». Неделя 3: повторяется снова. Ошибка #3. Система ловит паттерн и генерирует insight: «Go-хендлеры должны быть тонкими (до 15 строк) — вся бизнес-логика в сервисном слое». После нескольких успешных применений insight промоутится до SOUL rule. Теперь каждая новая сессия стартует с тем, что Claude знает это правило. Толстые хендлеры перестают появляться. Не потому что я напомнил — потому что он *научился*. Это разница между инструментом, хранящим текст, и тем, который реально улучшается. --- ## Как начать Выбери что подходит твоему стеку — все шесть путей установки приводят к одному и тому же MCP-серверу. Полные инструкции на **[totalmemory.dev](https://totalmemory.dev)**. ```bash # Node (zero-install через npx) npx -y total-agent-memory connect claude-code # Python через uv (быстро) uvx total-agent-memory # Python через pipx (изолированный venv) pipx install total-agent-memory # Homebrew (macOS / Linuxbrew) brew install vbcherepanov/tap/total-memory # Docker (multi-arch) docker run --rm -p 37737:37737 -v ~/.tam:/data ghcr.io/vbcherepanov/total-agent-memory:latest # Ручной clone git clone https://github.com/vbcherepanov/total-agent-memory.git ~/total-agent-memory && cd ~/total-agent-memory && ./install.sh ``` Путь через `npx` также прописывает MCP entry в выбранную IDE — `connect claude-code`, `connect codex`, `connect cursor`, `connect cline`, `connect continue`, `connect aider`, `connect windsurf`, `connect gemini-cli`, `connect opencode`. Если предпочитаешь вручную — добавь в `~/.claude/settings.json`: ```json { "mcpServers": { "memory": { "command": "/absolute/path/to/.venv/bin/total-agent-memory", "env": { "TAM_MEMORY_DIR": "~/.tam" } } } } ``` Готово. На следующем старте Claude Code у него будет 32 новых инструмента. Начни с `memory_save` и `memory_recall` — остальное вырастет оттуда. --- ## Ограничения (честный взгляд) Идеальных проектов не бывает. Что нужно знать: - **Холодный старт первой сессии**: память изначально пуста. Нужно 2–3 сессии активного использования, прежде чем эффект накапливается. - **Хранилище растёт**: 1 600+ записей занимают около 50 МБ с векторами. На современной машине не критично, но и не ноль. - **Claude нужно подталкивать вначале**: пока SOUL rules не накопились, может потребоваться напомнить ему вызвать `memory_recall` в начале сессии. Хуки помогают это автоматизировать. - **Зависимость от Python**: серверу нужен Python 3.10+ и несколько пакетов. Install-скрипт это разруливает, но это не один бинарь. --- ## Почему open source Я строил это для себя. Потом понял, что у каждого пользователя Claude Code та же проблема. Проект под **MIT-лицензией** — используй, форкай, модифицируй, контрибьють. Проблема памяти — это, пожалуй, самая большая точка трения в AI-разработке сегодня. Контекстные окна растут, но никогда не станут бесконечными. И даже если бы стали, перезагружать контекст каждую сессию — расточительно. Постоянная память — правильная абстракция. --- ## Попробуй Если ты используешь Claude Code или Codex CLI для реальной работы — не для демо, не для игрушечных проектов, а для настоящей продолжающейся разработки — это изменит твой workflow. Поставь звезду репозиторию, попробуй неделю и почувствуй разницу, когда твой AI-ассистент реально помнит, кто ты и что ты строишь. **Сайт:** [totalmemory.dev](https://totalmemory.dev) · **GitHub:** [vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) **Лицензия:** MIT — бесплатно навсегда. --- *Vitalii Cherepanov — software engineer, строящий инструменты на стыке AI и продуктивности разработчиков. Пишет на Go и PHP днём и учит Claude помнить вещи ночью.* --- ## Я изучил кодовую базу etcd — и это изменило то, как я пишу на PHP - URL: https://vbcherepanov.com/ru/articles/i-studied-the-etcd-codebase - Canonical (medium): https://medium.com/@vbcherepanov/i-studied-the-etcd-codebase-and-it-changed-how-i-write-php - Published: 2026-04-20 - Reading time: 11 min - Tags: Backend, Architecture - Language: ru > Семь архитектурных принципов, вытащенных из Go-кода etcd и переложенных на повседневную работу в PHP/Symfony: типизированные контракты, single-responsibility сервисы, композиция middleware, observability как архитектура, фасадные API, прозрачный путь запроса и явные зависимости. ![Изучение кодовой базы etcd — обложка](/images/articles/i-studied-the-etcd-codebase.jpg) Есть распространённый совет: «Хочешь писать лучше? Читай хороший код». Звучит очевидно. Делается редко. Проблема в том, что большинство open-source проектов — лабиринты. Открываешь репу, видишь 200 директорий и закрываешь вкладку. Kubernetes — два миллиона строк. Linux kernel — даже не думай. С чего начать? Мой ответ: **etcd**. Для тех, кто не знаком: etcd — это распределённое key-value хранилище на Go. Костяк Kubernetes — там лежит всё состояние кластера. Но мне etcd интересен не как продукт. Мне он интересен как **пример архитектуры, которую реально можно прочитать от начала до конца**. Что меня удивило: принципы, заложенные в etcd, — это не про Go. Это про дизайн ПО в целом. Я каждый день работаю с PHP и Symfony, и почти всё, что нашёл в etcd, переложилось напрямую на мои проекты. Семь принципов, конкретные примеры, без воды. --- ## 1. Один источник правды для API В etcd любой API определён в `.proto`-файлах. Открываешь `rpc.proto` и видишь все операции: `Range`, `Put`, `DeleteRange`, `Txn`. Каждое поле типизировано. Нет места для «погоди, мы здесь принимаем строку или число?». В PHP вместо protobuf у нас **строго типизированные DTO**: ```php final readonly class CreateOrderRequest { public function __construct( public string $customerId, /** @var OrderItemDto[] */ public array $items, public ?string $promoCode = null, ) {} } ``` Один класс — и все знают, что эндпоинт принимает. Фронтендер смотрит на DTO, бэкендер пишет логику против него, OpenAPI-схема генерируется автоматически через NelmioApiDocBundle. Сравни с тем, что я видел (и сам писал) в реальных проектах: ```php $data = json_decode($request->getContent(), true); $customerId = $data['customer_id'] ?? null; $items = $data['items'] ?? []; // Какой формат items? promoCode вообще есть? Хрен его знает. ``` Когда твой контракт — это «ну, какой-то массив прилетает», любое изменение что-то ломает неожиданно. Когда контракт — это DTO с типами, PHPStan ловит проблему до прода. --- ## 2. Каждый сервис делает одну вещь В etcd чётко разделены gRPC-сервисы: `KV` (чтение-запись), `Watch` (подписка на изменения), `Lease` (TTL для ключей), `Auth` (авторизация). Каждый — отдельный интерфейс. `Watch` не трогает запись. `KV` не проверяет токены. В Symfony — та же идея, другие инструменты: ```php class OrderController { #[Route('/orders', methods: ['POST'])] public function create( CreateOrderRequest $request, OrderService $orderService, ): JsonResponse { return new JsonResponse( $orderService->create($request) ); } } ``` `OrderService` создаёт заказы. Не отправляет письма — это `NotificationService`, слушающий `OrderCreatedEvent`. Не обрабатывает платежи — это `PaymentService`. И альтернатива, которую я регулярно вижу: ```php class OrderController { public function create(Request $request) { // 40 строк валидации // 20 строк авторизации // 60 строк бизнес-логики // 15 строк отправки email // 10 строк логирования // Итого: 150 строк, не тестируется } } ``` 500-строчный god-controller. Все там были. etcd помог мне наконец сформулировать *почему* это плохо: не потому что «паттерн неправильный», а потому что **ты не можешь проследить, что система делает**. --- ## 3. Middleware собирается как Lego Каждый gRPC-запрос в etcd проходит через цепочку interceptor'ов: logging → auth → metrics → handler → metrics → response. Каждый interceptor маленький, single-purpose. Сила — в композиции. В Symfony это ложится на Event Listeners и Messenger Middleware: ```php class MetricsMiddleware implements MiddlewareInterface { public function __construct( private PrometheusCollector $metrics, ) {} public function handle(Envelope $envelope, StackInterface $stack): Envelope { $start = microtime(true); try { $result = $stack->next()->handle($envelope, $stack); $this->metrics->increment('messages_processed_total', [ 'type' => $envelope->getMessage()::class, 'status' => 'success', ]); return $result; } catch (\Throwable $e) { $this->metrics->increment('messages_processed_total', [ 'type' => $envelope->getMessage()::class, 'status' => 'error', ]); throw $e; } finally { $this->metrics->histogram( 'message_duration_seconds', microtime(true) - $start, [$envelope->getMessage()::class] ); } } } ``` Один middleware — одна задача. Метрики здесь, логи там, retry где-то ещё. Цепочка собирается в `messenger.yaml`. Антипаттерн — когда у каждого хендлера это руками: ```php public function handle(CreateOrderCommand $command): void { $this->logger->info('Starting order creation...'); $start = microtime(true); // ... actual logic ... $this->metrics->record(microtime(true) - $start); $this->logger->info('Order created'); } ``` 50 хендлеров, 50 копий одного и того же boilerplate. Забыл один — нет метрик. Поменял формат лога — меняй в 50 местах. --- ## 4. Observability — это архитектура, а не дополнение В etcd Prometheus вшит в gRPC-слой с первого дня. Не «добавили через полгода после релиза». Код не считается готовым без метрик. В PHP: ```php class PaymentService { public function charge(Order $order): PaymentResult { $timer = $this->metrics->startTimer('payment_charge_duration'); try { $result = $this->gateway->process($order); $this->metrics->increment('payments_total', [ 'provider' => $result->provider, 'status' => $result->isSuccess() ? 'success' : 'declined', ]); return $result; } catch (GatewayTimeoutException $e) { $this->metrics->increment('payments_total', [ 'provider' => $order->paymentMethod, 'status' => 'timeout', ]); throw $e; } finally { $timer->observe(); } } } ``` Каждый платёж — в метриках. Сколько прошло, сколько таймаутнулось, какой провайдер тормозит. Не потому что кто-то попросил, а потому что без этого ты летишь вслепую. Помню проект, где прод лежал 40 минут, и единственный способ понять, что происходит, был `tail -f /var/log/symfony.log | grep ERROR`. Никогда больше. Пакет: `promphp/prometheus_client_php`. Пять минут на установку, пятнадцать на подключение Grafana. --- ## 5. Снаружи просто, внутри ракетная наука `clientv3` в etcd — это мастер-класс паттерна facade: ```go client.Put(ctx, "name", "value") ``` Одна строка. Под капотом: выбор ноды, реконнект при падении, retry с exponential backoff, protobuf-сериализация, Raft-консенсус, запись на диск, подтверждение кворума. Тот же принцип в PHP: ```php // Calling code. Простой и понятный. $paymentService->charge($order); ``` Внутри `charge()`: ```php public function charge(Order $order): PaymentResult { if ($existing = $this->findExistingPayment($order)) { return $existing; // idempotency } $provider = $this->providerResolver->resolve($order); $result = $this->withRetry( fn () => $provider->process($order), maxAttempts: 3, backoff: 'exponential', ); if ($result->isSuccess()) { $this->fiscalService->createReceipt($order, $result); } $this->events->dispatch(new PaymentProcessed($order, $result)); return $result; } ``` Контроллер, вызывающий `charge()`, ничего не знает про фискальные чеки, retry или выбор провайдера. И не должен. Признак хорошего сервиса: ты можешь объяснить, что он делает, одним предложением — «снимает деньги с клиента за заказ» — а внутри 200 строк аккуратной логики. --- ## 6. Путь запроса можно проследить пальцем В etcd путь запроса читается линейно: ```plaintext gRPC handler → EtcdServer.Put() → Raft → apply → bbolt (диск) ``` Никакой магии. Никаких скрытых вызовов. Никаких «откуда это вообще триггерится?». В Symfony — то же, если не злоупотреблять системой событий: ```plaintext Request → Controller (распаковка DTO) → Service (бизнес-логика) → Repository (БД) → EventDispatcher (побочные эффекты) → Response ``` Открываешь контроллер — видишь, какой сервис зовёт. Открываешь сервис — видишь, что он делает. Открываешь репозиторий — видишь запрос. Что убивает прозрачность: - `@PostPersist` на сущности, который втихую отправляет SMS - `prePersist`-листенеры, изменяющие данные до записи, — и ты 30 минут ищешь, кто трогает поле `updatedAt` - Десять `EventSubscriber`'ов на одном событии с непонятным порядком выполнения Event-driven — это здорово. Но если новый разработчик не может объяснить «запрос приходит сюда, ответ уходит туда» за 2 минуты — у тебя проблема. --- ## 7. Никаких скрытых зависимостей В etcd все зависимости передаются явно: ```go func NewKVServer(s *EtcdServer) KVServer { ... } ``` Видишь конструктор — видишь всё, что нужно классу. В Symfony — constructor injection, то же самое: ```php class OrderService { public function __construct( private OrderRepository $orders, private PaymentGateway $payment, private EventDispatcherInterface $events, private LoggerInterface $logger, ) {} } ``` Четыре зависимости. Все на виду. Хочешь тестировать? Подсунь моки. Хочешь понять класс? Смотри на конструктор. Антипаттерны, которые до сих пор живут в природе: ```php // Service locator: откуда это взялось? $payment = $this->container->get('payment.gateway'); // Статические вызовы: не тестируется Cache::put('key', $value); // new SomeService() внутри другого сервиса: невидимая связность $validator = new OrderValidator(); ``` Autowiring в Symfony — это не магия в плохом смысле. Контейнер связывает зависимости по типу, но они всё равно видны в конструкторе. Это удобство, а не скрытое поведение. --- ## Мой чек-лист Изучив etcd, я выжал чек-лист, который теперь применяю к каждому новому сервису: 1. **Контракт определён?** DTO есть, типы заданы, OpenAPI генерится из них 2. **Контроллер тонкий?** Максимум 10 строк, вся логика — в сервисном слое 3. **Cross-cutting concerns вытащены?** Логи, метрики, retry — через middleware, а не copy-paste 4. **Метрики есть?** Если нет — сервис не production-ready 5. **API снаружи простой?** Вызывающий код не знает о внутренней сложности 6. **Путь запроса прозрачен?** Новый разраб находит хендлер за 2 минуты 7. **Зависимости явные?** Всё в конструкторе, ничего из воздуха Ничего революционного. Базовая гигиена, которую легко забыть под прессом дедлайна. etcd просто напомнил мне, как выглядит кодбаз, когда эту гигиену не пропустили. И что это возможно даже в большой production-системе. --- *Какой open-source кодбаз изменил то, как ты пишешь код? Хочется собрать reading list — кидай в комментарии.* --- ## ClearVibeArchitecture (CVA) — полный гайд - URL: https://vbcherepanov.com/ru/articles/clear-vibe-architecture - Published: 2025-10-16 - Reading time: 45 min - Tags: Architecture, Backend, Distributed Systems - Language: ru > Архитектурный стиль для бэкенд-систем: Hexagonal, Outbox/Inbox, Observability, обязательные feature flags и визуализация потоков. ## Оглавление 1. [Введение и TL;DR](#часть-1-введение-и-tldr) 2. [Глоссарий и терминология](#часть-2-глоссарий-и-терминология) 3. [Архитектурная карта и слои](#часть-3-архитектурная-карта-и-слои) 4. [Принципы и правила](#часть-4-принципы-и-правила) 5. [Паттерны](#часть-5-паттерны) 6. [Схема данных и контракты](#часть-6-схема-данных-и-контракты) 7. [Наблюдаемость](#часть-7-наблюдаемость) 8. [Безопасность](#часть-8-безопасность) 9. [DevEx и продуктивность](#часть-9-devex-и-продуктивность) 10. [Референс-скелет (Go)](#часть-10-референс-скелет-go) 11. [Референс-скелет (Symfony/PHP)](#часть-11-референс-скелет-symphonyphp) 12. [Политики эволюции](#часть-12-политики-эволюции) 13. [Feature Toggles](#часть-13-feature-toggles) 14. [Визуализация](#часть-14-визуализация) 15. [Чек-листы внедрения](#часть-15-чек-листы-внедрения) 16. [Лицензирование и вклад](#часть-16-лицензирование-и-вклад) --- ## Часть 1: Введение и TL;DR ### 1.1 Что такое ЧистаяВайбАрхитектура (CVA) **ЧистаяВайбАрхитектура** (ClearVibeArchitecture, CVA) — архитектурный стиль для бэкенд-систем, сочетающий: - **Hexagonal (Ports & Adapters)** как основу изоляции домена - **Событийность по умолчанию** (Domain Events + Outbox/Inbox, идемпотентность) - **Наблюдаемость by default** (трассировки, метрики, структурные логи, timeline) - **Обязательные Feature Toggles** как сквозной паттерн для любого нового функционала - **Визуализацию потока (web/VR)** как продуктовый артефакт, а не «приятную опцию» > **Коротко:** гексагональная архитектура «с батарейками» — прозрачная, наблюдаемая, управляемо-эволюционная. ### 1.2 Какую проблему решает CVA решает следующие ключевые проблемы: 1. **Сложно безопасно менять систему** → фичефлаги + canary/blue-green 2. **Не видно, «что происходит внутри»** → otel-трейсинг + бизнес-метрики + timeline 3. **Надёжная интеграция между сервисами** → Outbox/Inbox + идемпотентность 4. **Разработчики тонут в инфраструктуре** → чистый домен и контракты на границах 5. **Сложно объяснить бизнесу поведение системы** → визуализатор потока запросов/событий ### 1.3 Цели (Design Goals) Архитектура CVA преследует следующие цели: - **Прозрачность:** любой запрос и событие трассируется сквозь все слои - **Изоляция домена:** доменная модель независима от инфраструктуры - **Надёжная доставка:** события публикуются транзакционно (Outbox) - **Управляемая эволюция:** все новые ветки — за фичефлагами, с версионированием контрактов - **Developer Experience:** шаблоны модулей, единые практики CI/CD, тесты как стандарт ### 1.4 Нефункциональные требования (NFR) - **Надёжность:** идемпотентные обработчики, ретраи, дедупликация - **Наблюдаемость:** p95-латентность, error rate, saturation — обязательные метрики - **Безопасность:** Zero-Trust границы; подпись сообщений; аудит изменений флагов/контрактов - **Производительность:** in-proc кэш решений флагов, back-pressure, батчинг I/O - **Совместимость:** SemVer контрактов API/событий, поддержка n-1 ### 1.5 TL;DR принципов - **Domain First:** домен не знает об ORM/HTTP/broker - **Ports & Adapters:** весь I/O — через интерфейсы (in/out-порты) - **Events First:** доменные события — первоклассные; публикация через Outbox - **Observability First:** trace_id везде; бизнес-метрики на доменных событиях - **Feature-Toggles Everywhere:** новый функционал не живёт без флага и kill-switch - **Contracts First:** схемы API/событий версионированы, проверяемы, совместимы - **Visualize the Flow:** обязательная визуализация пути запроса/событий ### 1.6 Область применения CVA лучше всего подходит для: - Микросервисы и крупные монолиты с перспективой декомпозиции - Высокая интеграционная нагрузка, частые релизы, A/B-эксперименты - Команды, которым важны прозрачность и воспроизводимость инцидентов > **Важно:** CVA не лучший выбор, если система крайне проста, почти не меняется и не требует телеметрии/событийности. ### 1.7 Критерии «готовности по CVA» Система считается готовой к CVA, если: - Все новые фичи замкнуты на feature flags с canary и kill-switch - Есть Outbox/Inbox и идемпотентные обработчики - Включён OpenTelemetry (трейсинг), структурные логи и ключевые бизнес-метрики - Контракты API/событий описаны схемами, проходят CI-валидацию, ведётся SemVer - Есть веб/VR-визуализатор потока (минимум — веб-граф + timeline) - Шаблоны/генераторы модулей, линтеры, PR-чек-листы, базовые e2e-тесты ### 1.8 Артефакты на выходе - Архкарта слоёв и портов (in/out), модель домена и границы контекстов - Каталог контрактов (OpenAPI/Proto/JSON-Schema/Avro) с SemVer и проверками - Outbox/Inbox библиотека/модуль, FeatureGate SDK, otel-интеграция - Дашборды SLO и визуализатор потока (минимально — веб) - Шаблоны кода (Go, Symfony/PHP), Makefile/Compose, CI-пайплайн --- ## Часть 2: Глоссарий и терминология ### 2.1 Базовые сущности - **Домен (Domain)** — предметная область, описывающая бизнес-правила и инварианты - **Агрегат (Aggregate)** — корневая сущность, инкапсулирующая состояние и поведение связанных объектов - **Событие (Domain Event)** — неизменяемый факт, зафиксированный в домене - **Команда (Command)** — намерение изменить состояние (создать, обновить, удалить) - **Запрос (Query)** — намерение получить данные без побочных эффектов - **Use Case (Юзкейс)** — прикладная логика, связывающая команду/запрос и домен ### 2.2 Порты и адаптеры - **InPort** — интерфейс входа в систему (API, UI, CLI, тест) - **OutPort** — интерфейс выхода из системы (база данных, брокер сообщений, сторонние API) - **Adapter** — реализация порта для конкретной инфраструктуры ### 2.3 Событийные компоненты - **Outbox** — таблица/хранилище для надёжной публикации событий вместе с транзакцией - **Inbox** — таблица/хранилище для приёма и дедупликации входящих событий - **Saga/Process Manager** — координирует длинные процессы через последовательность событий ### 2.4 Observability - **Trace ID** — уникальный идентификатор запроса, прокидывается через все слои - **Span** — часть трейса, отражающая шаг (например: SQL-запрос, вызов API) - **Metric** — измерение (latency, error rate, business metric) - **Structured Log** — лог в формате JSON с обязательными полями (timestamp, trace_id, level, message) ### 2.5 Feature Toggles - **Feature Flag (Фичефлаг)** — бинарный переключатель или вариативный параметр, управляющий доступностью функционала - **Variant** — значение вариативного флага (A/B/n-тест) - **Kill-Switch** — флаг для мгновенного отключения проблемной функции - **Exposure Event** — событие факта показа функционала пользователю ### 2.6 Контракты - **API Contract** — формальное описание API (OpenAPI, gRPC proto) - **Event Contract** — описание структуры события (JSON Schema, Avro) - **Backward Compatibility** — новое описание не ломает клиентов предыдущих версий - **SemVer** — версионирование контрактов по правилам Semantic Versioning ### 2.7 Политики - **Access Policy** — правила авторизации и прав доступа на уровне use case - **Validation Policy** — проверка данных до входа в домен - **Rollout Policy** — стратегия включения флага (percentage, segment, variant) ### 2.8 Визуализация - **Timeline** — хронологический поток событий/запросов с задержками и статусами - **Flow Graph** — граф узлов (сервисы, агрегаты) и рёбер (события, вызовы) - **Heatmap** — визуализация узких мест по latency/error rate ### 2.9 DevEx - **Scaffold** — генератор шаблонов модулей и портов - **CI/CD Pipeline** — сборка, тестирование и деплой системы - **PR Checklist** — набор обязательных пунктов для ревью (фичефлаг, метрики, тесты) --- ## Часть 3: Архитектурная карта и слои ### 3.1 Общая схема ``` ┌─────────────────────────────────────────────────────┐ │ Interface Layer │ │ (REST, gRPC, GraphQL, CLI, VR/AR) │ └────────────────┬────────────────────────────────────┘ │ InPorts ┌────────────────▼────────────────────────────────────┐ │ Application Layer │ │ (Commands, Queries, Handlers, Policies) │ └────────────────┬────────────────────────────────────┘ │ Domain Services ┌────────────────▼────────────────────────────────────┐ │ Domain Layer │ │ (Entities, Aggregates, Events, Value Objects) │ └─────────────────────────────────────────────────────┘ ▲ │ OutPorts ┌────────────────┴────────────────────────────────────┐ │ Infrastructure Layer │ │ (DB, Brokers, Outbox/Inbox, Observability) │ └─────────────────────────────────────────────────────┘ ``` ### 3.2 Interface Layer **Вход:** REST, gRPC, GraphQL, CLI, VR/AR-визуализатор **Задачи:** принимать команды/запросы, конвертировать в DTO, передавать в Application **Особенности:** логирование, трассировка, authN/authZ ### 3.3 Application Layer Сердце прикладной логики, включающее: - Команды, запросы, хендлеры, политики - Координация: orchestrators, saga, process managers - Валидация и маппинг DTO - Гарантия транзакционных границ: один use case = одна транзакция - Все побочные эффекты — через Outbox > **Важно:** Application Layer обеспечивает транзакционную целостность всех операций. ### 3.4 Domain Layer Чистое доменное ядро системы: - Entities, Aggregates, Value Objects - Инварианты, бизнес-правила, ubiquitous language - Domain Events как первоклассные объекты - Domain Services — чистые функции без инфраструктуры > **Принцип:** Domain не знает об ORM, HTTP, брокерах и любой инфраструктуре. ### 3.5 Infrastructure Layer - Реализация портов: репозитории, адаптеры брокеров, клиентов внешних API - Outbox/Inbox — обязательные компоненты для публикации/приёма событий - Observability stack: otel-интеграция, логирование, метрики - Кэширование, файловые хранилища, интеграции с внешними сервисами ### 3.6 Взаимодействие слоёв - Зависимости всегда направлены внутрь (Infrastructure → Application → Domain) - Interface вызывает Application через InPorts - Application использует OutPorts, которые реализуются в Infrastructure - Domain общается только с Application через события и инварианты ### 3.7 «Вайб» слоя У каждого слоя свои обязательные артефакты: - **Interface:** трассировки, authN/authZ - **Application:** команды, события, фичефлаги - **Domain:** инварианты, события - **Infrastructure:** outbox/inbox, observability Всё вместе создаёт живой поток, который можно проследить и визуализировать. --- ## Часть 4: Принципы и правила ### 4.1 Чистое ядро (Domain First) **Ключевые правила:** - Домен изолирован от инфраструктуры - Запрещено использовать ORM-аннотации, SQL или SDK внешних сервисов внутри домена - Инварианты проверяются только в домене > **Принцип:** Доменный слой должен оставаться чистым и не зависеть от технических деталей реализации. ### 4.2 Порты и адаптеры - Все внешние взаимодействия — через интерфейсы (Ports) - Реализации интерфейсов — только в Infrastructure (Adapters) - Application вызывает OutPorts, Domain ничего не знает о них ### 4.3 Событийность по умолчанию **Обязательные требования:** - Каждое значимое изменение фиксируется как Domain Event - События публикуются через Outbox вместе с транзакцией - Для входящих событий используется Inbox с идемпотентностью > **Важно:** Событийность — не опциональная возможность, а обязательный элемент архитектуры CVA. ### 4.4 Транзакционные границы - Один use case = одна транзакция - Побочные эффекты (отправка в брокер, интеграции) фиксируются в Outbox - Обработка Outbox асинхронна и ретраибельна ### 4.5 Наблюдаемость по умолчанию - Каждый запрос и событие сопровождается trace_id - Все хендлеры и адаптеры обязаны логировать и метриковать свою работу - Бизнес-события становятся частью метрик (например: order.created.count) ### 4.6 Договоры на границах - API и события описываются контрактами (OpenAPI/Proto/Avro/JSON Schema) - Контракты версионируются (SemVer) - Backward Compatibility — обязательное требование (поддержка n-1 версий) ### 4.7 Feature Toggles Everywhere **Обязательные требования:** - Любой новый функционал внедряется только за флагом - Фичефлаг обязателен, даже если функционал будет всегда включён позже - У каждого флага есть владелец, sunset-дата и kill-switch > **Принцип:** Feature flags — это не опция, а обязательный паттерн для любого нового кода в CVA. ### 4.8 Визуализация потока - Таймлайн запроса и событий должен быть воспроизводим в визуализаторе (web/VR) - Узкие места подсвечиваются автоматически (heatmap) - Визуализация — часть продукта, а не «доп. тул» ### 4.9 Эволюционность - Все изменения проходят через feature flags и canary/blue-green - Миграции БД выполняются с обратимостью - Shadow-write/read при раскатке новых моделей данных ### 4.10 Безопасность - Zero Trust между сервисами: mTLS, подпись сообщений - RBAC для админки и управления фичефлагами - Audit trail для всех событий и изменений ### 4.11 Качество - Unit и property-based тесты на инварианты - E2E тесты сквозного сценария (создать → опубликовать событие → обработать) - Линтеры и статический анализ как часть CI --- ## Часть 5: Паттерны ### 5.1 Hexagonal / Ports & Adapters Базовый паттерн CVA: - Домен изолирован, входные и выходные зависимости оформлены как InPorts/OutPorts - Взаимодействие с внешним миром происходит через адаптеры > **Основа CVA:** Hexagonal Architecture обеспечивает изоляцию доменной логики от технических деталей. ### 5.2 CQRS (опционально) - Разделение команд (изменения состояния) и запросов (чтение данных) - Используется там, где чтение сильно отличается от записи - Позволяет масштабировать чтения и писать оптимизированные проекции ### 5.3 Outbox / Inbox **Ключевые компоненты событийной архитектуры:** - **Outbox:** транзакционная запись событий вместе с изменением в БД - **Inbox:** регистрация и дедупликация входящих сообщений > **Гарантия:** Паттерн Outbox/Inbox обеспечивает exactly-once delivery на прикладном уровне. ### 5.4 Saga / Process Manager - Управление долгоживущими процессами - Saga: цепочка шагов с компенсациями - Process Manager: реагирует на события, координирует действия нескольких агрегатов/сервисов ### 5.5 Policy / Rule Engine - Декларативные правила для бизнес-логики (например, скидки, тарифы, доступ) - Реализуется в Application Layer - Может подключаться к внешнему движку правил ### 5.6 Event Sourcing (опционально) - Хранение истории изменений как последовательности событий - Позволяет «переиграть» состояние - Подходит, когда важна аудитируемость и полная история ### 5.7 Feature Toggles (обязательный) - Сквозной паттерн: любой новый функционал внедряется за флагом - Флаги имеют владельца, sunset-даты и kill-switch - Поддерживается процентный rollout, сегменты пользователей, A/B/n-тесты - Логируется каждое exposure event (факт использования флага) ### 5.8 Observability Patterns - **Distributed Tracing:** trace_id сквозь все слои - **Structured Logging:** JSON-логи с ключевыми полями - **Metrics & SLOs:** бизнес и технические метрики; авто-алерты - **Timeline Visualization:** поток событий и запросов отображается в веб/VR ### 5.9 Database Patterns - **Shadow Write/Read:** временный дубль данных при миграции схемы - **Blue/Green Schema Migration:** параллельное ведение старой и новой версии таблицы - **Transactional Outbox:** публикация событий вместе с транзакцией в БД ### 5.10 Deployment Patterns - **Feature Flags + Canary Release:** новый функционал включается постепенно - **Blue/Green Deployment:** два параллельных окружения, быстрое переключение - **Rollback по метрикам:** автоматическое отключение фичи/версии при деградации --- ## Часть 6: Схема данных и контракты ### 6.1 Контракты API **Обязательные требования:** - Все публичные API описываются в OpenAPI или gRPC Proto - Контракты проходят автоматическую проверку в CI - Версионирование строгое: SemVer (1.2.3) - Backward compatibility: поддержка n-1 версии клиента - Контракты публикуются в артефакт-репозиторий > **Важно:** Контракты — это формальное соглашение между командами и сервисами. ### 6.2 Контракты событий - События описываются в JSON Schema, Avro или Protobuf - Каждое событие имеет: event_name, version, timestamp, trace_id, payload - Обязательные поля: id, trace_id, source - События проходят проверку схемы до публикации ### 6.3 Версионирование - Контракты версионируются независимо от сервисов - Поддержка нескольких версий событий: потребители могут читать v1 и v2 параллельно - При переходе: producer начинает публиковать оба формата (dual write) ### 6.4 Схемы БД - Миграции управляются инструментами: Liquibase/Flyway/golang-migrate/DoctrineMigrations - Каждое изменение в БД сопровождается скриптом миграции - Поддерживается shadow-write/read при сложных изменениях схемы - Все миграции проходят через CI и тестовую базу ### 6.5 Контракты для Feature Flags - Фичефлаг — тоже контракт - Поля: key, type, rules, default, owner, ttl, version - Хранятся в Postgres (истина) + Redis (кэш) - Версионируются так же, как API и события ### 6.6 CI-проверки контрактов - Проверка схем API и событий на валидность - Проверка совместимости (n-1) - Автогенерация SDK (Go, PHP, TS) - Сравнение изменений с предыдущими версиями ### 6.7 Документация контрактов - Сервис-каталог: документация доступна в Swagger UI или gRPC UI - События документируются в Event Catalog - Все изменения фиксируются changelog'ом ### 6.8 «Вайб» в контрактах - Контракты не только для машин — но и для людей - Каждое событие описывается в терминах бизнеса, а не техники - Визуализатор использует схемы для отображения структуры данных в потоке --- ## Часть 7: Наблюдаемость ### 7.1 Принцип «Observability by Default» **Ключевой принцип CVA:** - Любой новый сервис или модуль в CVA обязан иметь встроенные средства наблюдаемости - Метрики, логи и трассировки не добавляются «потом» — они часть архитектуры - Наблюдаемость охватывает и технические, и бизнес-процессы > **Принцип:** Наблюдаемость — не надстройка, а встроенная часть архитектуры с первого дня. ### 7.2 Трассировки (Distributed Tracing) **Обязательные требования:** - Используется OpenTelemetry или совместимый стандарт - Каждый запрос имеет уникальный `trace_id` - Каждая операция (SQL, RPC, внешнее API) фиксируется как span - Все события (Domain Events) также несут `trace_id` - Визуализатор (web/VR) строит таймлайн на основе `trace_id` > **Совет:** Единый `trace_id` позволяет проследить весь путь запроса через все сервисы и слои. ### 7.3 Логи (Structured Logging) **Формат логов:** - Формат: JSON - Обязательные поля: `timestamp`, `level`, `trace_id`, `service`, `message` - Логи пишутся в stdout → централизованное хранилище (ELK, Loki) - Ошибки и бизнес-события фиксируются одинаково структурировано > **Принцип:** Структурированные логи позволяют легко фильтровать и анализировать события. ### 7.4 Метрики - **Технические:** latency (p95/p99), error_rate, saturation, throughput - **Бизнес:** количество заказов, конверсия, количество отказов по правилам - **Feature Flags:** метрики exposure (сколько пользователей видит фичу), enable_rate - Метрики доступны в Prometheus/Grafana или аналогах ### 7.5 Алерты и SLO - Каждая критичная метрика имеет SLO (Service Level Objective) - Нарушения → алерты (PagerDuty, Slack, Email) - Алерты должны быть actionable (понятно, что делать при срабатывании) - Для фичефлагов есть авто-откат по SLO ### 7.6 Timeline и Flow Visualization - Timeline: шаги запроса (API → Application → Domain → Outbox → Consumer) - Flow Graph: узлы = сервисы/агрегаты, рёбра = события/вызовы - Heatmap: подсветка узких мест по latency/error_rate - Поддержка VR: разработчик/аналитик может «увидеть» поток данных в 3D ### 7.7 Наблюдаемость в CI/CD - Unit-тесты проверяют наличие базовых метрик и логов - E2E-тесты фиксируют trace_id и проверяют, что он проходит сквозь систему - В CI проверяется схема логов и наличие trace_id в событиях ### 7.8 «Вайб» наблюдаемости - Система «светится изнутри» — любой запрос или событие видны - Бизнес может видеть живые дашборды - Разработчики могут отследить ошибку до конкретного шага - Наблюдаемость — не надстройка, а часть архитектурного стандарта --- ## Часть 8: Безопасность ### 8.1 Принцип Zero Trust **Ключевые положения:** - Каждый сервис и компонент взаимодействует с другими только через проверенные каналы - По умолчанию нет доверия даже внутри одной сети - Авторизация и аутентификация применяются на каждом уровне > **Принцип:** Zero Trust означает, что доверие нужно заслужить на каждом шаге, а не предоставлять по умолчанию. ### 8.2 Аутентификация (AuthN) - Внешние вызовы: OAuth2 / OpenID Connect / mTLS - Внутренние вызовы: сервисные аккаунты + mTLS - Все запросы обязаны содержать корреляционный trace_id и токен авторизации ### 8.3 Авторизация (AuthZ) - RBAC (Role-Based Access Control) или ABAC (Attribute-Based Access Control) - Проверки прав выполняются в Application Layer (policies) - Feature Flags могут ограничиваться по ролям/сегментам ### 8.4 Шифрование - В транзите: TLS 1.3 (API, брокеры, БД) - В покое: шифрование дисков/таблиц/объектов (AES-256) - Конфиденциальные данные (PII) — всегда зашифрованы ### 8.5 Управление секретами - Секреты не хранятся в коде или переменных окружения - Используется Vault/KMS (HashiCorp Vault, AWS KMS, GCP Secret Manager) - Доступ к секретам — строго по принципу минимальных прав ### 8.6 Аудит и логирование - Все действия фиксируются в audit log - Audit log immutable, хранится отдельно - Для критичных операций фиксируются IP, userId, trace_id, действие, результат ### 8.7 Feature Flags и безопасность - Каждый флаг имеет владельца и sunset-дату - Изменение флагов возможно только с авторизацией - Все изменения логируются в audit trail - Kill-switch можно дернуть только авторизованной ролью ### 8.8 Политики безопасности - **Least Privilege:** сервисы и пользователи имеют только минимально необходимые права - **Defense in Depth:** защита на каждом уровне (API, App, DB, Infra) - **Fail Secure:** при ошибке доступ запрещён, а не открыт ### 8.9 Соответствие (Compliance) - CVA ориентируется на стандарты: GDPR, HIPAA, PCI DSS - Логи и метрики должны учитывать защиту персональных данных - PII в событиях хэшируется/псевдонимизируется ### 8.10 «Вайб» безопасности - Безопасность встроена «по умолчанию», а не как «опция» - DevOps/разработчик не тратит время на изобретение решений - Бизнес видит прозрачность: кто, когда и почему сделал изменение --- ## Часть 9: DevEx и продуктивность ### 9.1 Принцип DevEx-first **Философия CVA:** - Архитектура должна быть удобной для разработчика - Шаблоны, инструменты и процессы встроены в CVA - Разработчик тратит минимум времени на рутину и инфраструктуру > **Совет:** Хороший DevEx повышает скорость разработки и снижает вероятность ошибок. ### 9.2 Scaffold и генераторы **Автоматизация создания кода:** Генераторы кода для сущностей, команд, событий, портов и адаптеров. Пример использования: ```bash make scaffold module=Order ``` Создаётся структура: - Domain: entity, events, value objects - Application: commands, handlers, policies - Infrastructure: repository, adapters > **Совет:** Генераторы обеспечивают единообразие структуры во всех модулях проекта. ### 9.3 Кодстайл и линтеры - Единый кодстайл (Go, PHP, JS/TS) - Pre-commit hooks: линтеры, тесты, security scans - Обязательная проверка trace_id, логов, фичефлагов ### 9.4 CI/CD пайплайн **CI проверяет:** - Компиляцию/сборку - Тесты (unit, integration, e2e) - Схемы контрактов (API, события) - Миграции БД - Security scan **CD поддерживает:** - Blue/green deployment - Canary deployment > **Важно:** Автоматизация CI/CD — обязательное требование для безопасной эволюции системы. ### 9.5 Тестирование - **Unit:** инварианты домена - **Property-based:** проверка правил и моделей - **Integration:** взаимодействие с адаптерами (DB, broker) - **E2E:** полный сценарий (запрос → событие → consumer → проекция) - **Smoke-тесты** для rollout под feature flags ### 9.6 Productivity Tools - Makefile/Taskfile для стандартных задач (build, test, run, migrate) - Docker Compose/Dev Containers для локальной разработки - Автогенерация документации (Swagger, AsyncAPI) - Hot reload / watch mode для dev-окружения ### 9.7 PR Checklist **Обязательные пункты для каждого PR:** - Есть feature flag - Добавлены метрики и `trace_id` - Контракт обновлён и проверен - Миграции БД задокументированы - Тесты: unit + integration + e2e - Логи структурированы > **Важно:** Чек-лист обеспечивает качество каждого изменения и соответствие стандартам CVA. ### 9.8 Обратная связь - DevEx метрики: время на ревью, время на деплой, MTTR - Система должна быть «приятной» для работы - Любое улучшение, которое повышает скорость и снижает стресс разработчиков — часть CVA ### 9.9 «Вайб» DevEx - Разработчик чувствует поток: scaffold → код → тесты → визуализация - Инструменты не мешают, а помогают - «Сначала скучно, потом приятно» → стандарты дают предсказуемость и уверенность --- ## Часть 10: Референс-скелет (Go) ### 10.1 Общая структура проекта ``` /clearvibe /cmd /app main.go // точка входа /internal /domain /order entity.go // Aggregate, инварианты events.go // Domain Events service.go // Domain Services /app /order commands.go // команды: CreateOrder, PayOrder handler.go // CommandHandlers policies.go // правила доступа и валидации ports.go // InPorts, OutPorts (Repo, Broker, Flags) /infra /db order_repo_pg.go // реализация OrderRepo через Postgres /broker outbox.go // Outbox publisher inbox.go // Inbox consumer (идемпотентность) /http server.go // HTTP API → InPorts /obs tracing.go // OpenTelemetry metrics.go // Prometheus logging.go // slog / structured logging /flags feature_gate.go // SDK фичефлагов ``` ### 10.2 Интерфейсы портов ```go // internal/app/order/ports.go package order type OrderRepo interface { Save(*Order) error FindByID(id string) (*Order, error) } type EventPublisher interface { Publish(topic string, key string, payload any) error } type FeatureGate interface { Enabled(ctx FeatureContext, key string) bool Variant(ctx FeatureContext, key string, variants ...string) Variant } type FeatureContext struct { UserID string OrgID string Attributes map[string]any } ``` ### 10.3 Команда и хендлер ```go // internal/app/order/commands.go type CreateOrderCmd struct { CustomerID string Items []Item } // internal/app/order/handler.go func (h *createOrderHandler) Handle(cmd CreateOrderCmd) (string, error) { if h.flags.Enabled(order.FeatureContext{UserID: cmd.CustomerID}, "orders.dynamic_pricing") { // новая ветка логики } ord, err := domain.NewOrder(cmd.CustomerID, cmd.Items) if err != nil { return "", err } if err := h.repo.Save(ord); err != nil { return "", err } evt := domain.OrderCreated{ID: ord.ID, Total: ord.Total} if err := h.outbox.Add("order.created", ord.ID, evt); err != nil { return "", err } return ord.ID, nil } ``` ### 10.4 Реализация репозитория ```go // internal/infra/db/order_repo_pg.go type PgOrderRepo struct { db *sql.DB } func (r *PgOrderRepo) Save(o *domain.Order) error { _, err := r.db.Exec("INSERT INTO orders(id, customer_id, total) VALUES($1,$2,$3)", o.ID, o.CustomerID, o.Total) return err } func (r *PgOrderRepo) FindByID(id string) (*domain.Order, error) { row := r.db.QueryRow("SELECT id, customer_id, total FROM orders WHERE id=$1", id) var ord domain.Order if err := row.Scan(&ord.ID, &ord.CustomerID, &ord.Total); err != nil { return nil, err } return &ord, nil } ``` ### 10.5 Outbox Publisher ```go // internal/infra/broker/outbox.go type Outbox struct { db *sql.DB } func (o *Outbox) Add(topic, key string, payload any) error { data, _ := json.Marshal(payload) _, err := o.db.Exec("INSERT INTO outbox(topic, key, payload, created_at) VALUES($1,$2,$3,now())", topic, key, data) return err } ``` ### 10.6 Feature Flags SDK ```go // internal/infra/flags/feature_gate.go type RedisFeatureGate struct { cache *redis.Client } func (g *RedisFeatureGate) Enabled(ctx order.FeatureContext, key string) bool { val, err := g.cache.Get(context.Background(), "flag:"+key).Result() if err != nil { return false } return val == "true" } ``` ### 10.7 Observability интеграция - `tracing.go`: инициализация OpenTelemetry, экспорт трейсов - `metrics.go`: регистрация бизнес-метрик (order_created_total) - `logging.go`: slog с JSON-форматом и trace_id ### 10.8 «Вайб» Go-скелета - Чёткое разделение слоёв - Feature flags встроены сквозь Application - Outbox обеспечивает надёжную доставку событий - Observability встроена в каждый слой - Код легко тестируется (моки портов) --- ## Часть 11: Референс-скелет (Symfony/PHP) ### 11.1 Общая структура проекта ``` src/ Domain/Order/ Entity/Order.php Event/OrderCreated.php ValueObject/Item.php Service/PricingService.php Repository/OrderRepositoryInterface.php Application/Order/ Command/CreateOrder.php Handler/CreateOrderHandler.php Policy/OrderPolicy.php Port/In/CreateOrderHandlerInterface.php Port/Out/OrderRepository.php Port/Out/OutboxBus.php Port/Out/FeatureGate.php Infrastructure/ Persistence/Doctrine/OrderRepository.php Messaging/OutboxPublisher.php FeatureFlags/RedisFeatureGate.php Http/Controller/OrderController.php Observability/TracingMiddleware.php Observability/Logger.php Observability/Metrics.php ``` ### 11.2 Сущность и событие ```php // src/Domain/Order/Entity/Order.php final class Order { private string $id; private string $customerId; private array $items; private float $total; private function __construct(string $customerId, array $items) { $this->id = Uuid::uuid4()->toString(); $this->customerId = $customerId; $this->items = $items; $this->total = array_sum(array_map(fn($i) => $i->price(), $items)); } public static function create(string $customerId, array $items): self { return new self($customerId, $items); } public function id(): string { return $this->id; } public function total(): float { return $this->total; } } // src/Domain/Order/Event/OrderCreated.php final class OrderCreated { public function __construct( public readonly string $id, public readonly float $total, ) {} } ``` ### 11.3 Команда и обработчик ```php // src/Application/Order/Command/CreateOrder.php final class CreateOrder { public function __construct( public readonly string $customerId, public readonly array $items, ) {} public static function fromRequest(Request $r): self { return new self( $r->get('customerId'), $r->get('items', []) ); } } // src/Application/Order/Handler/CreateOrderHandler.php final class CreateOrderHandler { public function __construct( private OrderRepositoryInterface $repo, private OutboxBus $outbox, private FeatureGate $flags, ) {} public function __invoke(CreateOrder $cmd): string { if ($this->flags->enabled(new Context(userId: $cmd->customerId), 'orders.dynamic_pricing')) { // новая логика ценообразования } $order = Order::create($cmd->customerId, $cmd->items); $this->repo->save($order); $this->outbox->add('order.created', $order->id(), new OrderCreated($order->id(), $order->total())); return $order->id(); } } ``` ### 11.4 Репозиторий ```php // src/Infrastructure/Persistence/Doctrine/OrderRepository.php final class OrderRepository implements OrderRepositoryInterface { public function __construct(private EntityManagerInterface $em) {} public function save(Order $order): void { $this->em->persist($order); $this->em->flush(); } public function findById(string $id): ?Order { return $this->em->find(Order::class, $id); } } ``` ### 11.5 Outbox Publisher ```php // src/Infrastructure/Messaging/OutboxPublisher.php final class OutboxPublisher implements OutboxBus { public function __construct(private EntityManagerInterface $em) {} public function add(string $topic, string $key, object $event): void { $this->em->persist(new OutboxMessage($topic, $key, $event)); $this->em->flush(); } } ``` ### 11.6 Feature Flags ```php // src/Infrastructure/FeatureFlags/RedisFeatureGate.php final class RedisFeatureGate implements FeatureGate { public function __construct(private Redis $redis) {} public function enabled(Context $ctx, string $key): bool { return $this->redis->get("flag:$key") === 'true'; } public function variant(Context $ctx, string $key, array $variants): string { $hash = crc32($ctx->userId . $key); return $variants[$hash % count($variants)]; } } ``` ### 11.7 Контроллер ```php // src/Infrastructure/Http/Controller/OrderController.php #[Route('/orders', methods: ['POST'])] final class OrderController extends AbstractController { public function __construct(private MessageBusInterface $bus) {} public function create(Request $r): JsonResponse { $dto = CreateOrder::fromRequest($r); $envelope = $this->bus->dispatch($dto); return $this->json([ 'status' => 'accepted', 'trace' => $r->headers->get('X-Trace-Id'), ]); } } ``` ### 11.8 Observability - **TracingMiddleware:** добавляет trace_id в каждый запрос и передаёт его в шину сообщений - **Logger:** структурированный JSON-лог через Monolog - **Metrics:** счётчики (order_created_total), latency, error_rate ### 11.9 «Вайб» Symfony-скелета - DDD-структура: Domain/Application/Infrastructure - Feature flags интегрированы прямо в Handler - Outbox обеспечивает надёжность доставки событий - Observability встроена в middleware, логику и метрики - Код легко тестируется и масштабируется --- ## Часть 12: Политики эволюции ### 12.1 Общий принцип **Ключевой принцип эволюции:** Эволюция — управляемая и наблюдаемая. Любое изменение: - Проходит через фичефлаги, контракты и миграции - Имеет владельца и риск-план - Имеет пути отката > **Принцип:** Безопасная эволюция требует планирования и возможности отката на каждом этапе. ### 12.2 Версионирование контрактов (API/Events) - SemVer: MAJOR.MINOR.PATCH - Совместимость n-1: минимум одна предыдущая версия поддерживается - Dual write/read: при миграции событий производитель публикует vN и vN+1 - CI-валидаторы совместимости: запрещают breaking-changes без MAJOR ### 12.3 Жизненный цикл фичи (Feature Lifecycle) **Этапы жизненного цикла:** 1. **Draft:** флаг создан (disabled), владелец, цель, метрики успеха, sunset-дата 2. **Internal:** включено для dev/stage/own team 3. **Canary:** 1% → 5% → 25% → 50% → 100% (детерминированный бакетинг) 4. **General Availability:** флаг по умолчанию on 5. **Sunset:** флаг и мёртвый код удалены, контракты стабилизированы > **Совет:** Постепенный rollout позволяет выявить проблемы на ранних этапах. ### 12.4 Стратегии релиза - **Blue/Green:** два окружения, переключение трафика - **Canary:** инкрементальный rollout с авто-стражами по SLO - **Dark Launch:** код задеплоен, фича выключена флагом до активации ### 12.5 Откат (Rollback) и Kill-Switch - **Kill-Switch:** мгновенно отключает фичу на всём трафике - **Auto-rollback:** триггер по SLO (error_rate/latency/conversion) - Чек-лист отката: кто дергает, что откатываем, как верифицируем ### 12.6 Эволюция данных и схем - Миграции с обратимостью: up/down - Shadow Write/Read: временно пишем в новую схему, читаем из старой - Backfill/Replay: безопасная догрузка исторических данных - Контроль последовательности: миграции данных не идут вперёд без зелёных метрик ### 12.7 Политики изменения домена - Изменение инвариантов — только через фичефлаг и миграции - Новые атрибуты — совместимые (nullable/optional) до полного включения - Удаление полей — после sunset и MAJOR ### 12.8 Governance изменений - RFC/ADR: каждое значимое изменение оформляется решением - Owner & DRI: владелец, сроки, метрики успеха, риск-план - Gate в PR: наличие флага, метрик, миграций, тестов — обязательно ### 12.9 Наблюдаемость эволюции - Метрики миграций: скорость backfill, доля трафика на новой ветке - События эволюции: flag.exposed, flag.variant, migration.started/completed - Дашборды перехода: сравнение p95/error_rate/бизнес-метрик по старой/новой ветке ### 12.10 Безопасность и соответствие - Изменение флагов/контрактов — только с RBAC и аудитом - PII-безопасность при миграциях (псевдонимизация/шифрование) - Хранение ADR/RFC и журналов аудита — неизменяемое --- ## Часть 13: Feature Toggles ### 13.1 Принцип **Feature Toggles — обязательный и сквозной паттерн CVA.** Любой новый функционал внедряется только за флагом: - Флаг есть всегда - У каждого флага есть владелец, версия, sunset-дата и kill-switch - Решения по флагу наблюдаемы и логируются > **Принцип:** Feature flags — не опциональный инструмент, а обязательный механизм управления изменениями. ### 13.2 Типы флагов **Основные типы feature flags:** - **Boolean:** включен/выключен - **Percentage:** процент трафика (canary rollout) - **Segment:** включение по атрибутам (страна, тариф, роль) - **Multivariant:** несколько вариантов для A/B/n-тестов > **Совет:** Выбирайте тип флага в зависимости от стратегии раскатки функционала. ### 13.3 Архитектура слоя фичефлагов ``` Interface/UI ─┐ reads → FeatureGate SDK (in-proc cache) ─┐ API/gRPC ─────┤ │ Application ──┤ policies/deciders → uses FeatureGate ────┼→ Telemetry Domain ───────┘ │ Infrastructure: Provider (PG/Redis), Syncers, Warmers, Auditor ─┘ ``` ### 13.4 Контракты и хранение - Хранилище: Postgres (источник истины) + Redis (кэш, pub/sub) - Схема: flags(id, key, type, rules, default, owner, version, ttl, updated_at) - Правила: JSON Schema для сегментов/процентов/вариантов - Версионирование: SemVer для контрактов флагов, поддержка n-1 ### 13.5 SDK API (Go) ```go type FeatureContext struct { UserID, OrgID string Attributes map[string]any } type Variant struct { Key string; Payload any } type FeatureGate interface { Enabled(ctx FeatureContext, key string) bool Variant(ctx FeatureContext, key string, variants ...string) Variant } ``` Использование: ```go if flags.Enabled(FeatureContext{UserID: cmd.CustomerID}, "orders.dynamic_pricing") { // новая логика } ``` ### 13.6 SDK API (Symfony/PHP) ```php interface FeatureGate { public function enabled(Context $ctx, string $key): bool; public function variant(Context $ctx, string $key, array $variants): string; } final class Context { public function __construct( public readonly ?string $userId=null, public readonly ?string $orgId=null, public readonly array $attributes=[] ) {} } ``` ### 13.7 Метрики и наблюдаемость - Exposure events: flag.exposed, flag.variant - Метрики: exposure_count, enable_rate, error_rate, stale_cache_count - Дашборды: сравнение вариантов (A/B), корреляция с бизнес-метриками - Trace_id: всегда прокидывается в события экспозиции ### 13.8 Rollout & Sunset процесс 1. Флаг создаётся с default=off 2. Включается для команды (internal) 3. Canary rollout: 1% → 5% → 25% → 50% → 100% 4. General Availability: включен для всех 5. Sunset: мёртвый код и сам флаг удалены ### 13.9 Безопасность - Управление флагами доступно только авторизованным ролям (RBAC) - Все изменения фиксируются в audit log - PII-атрибуты для сегментации хэшируются - Подпись снапшотов правил для защиты от подмены ### 13.10 Чек-лист для PR - Добавлен флаг (с ключом, владельцем, sunset) - Есть kill-switch - Метрики и события exposure подключены - Есть тесты (unit + e2e) - Флаг документирован ### 13.11 «Вайб» фичефлагов - Безопасные изменения без страха - Возможность быстро экспериментировать (A/B) - Прозрачность: все знают, какие фичи включены - Визуализация: на таймлайне видно, где запрос свернул по фичефлагу --- ## Часть 14: Визуализация ### 14.1 Принцип **Ключевое положение CVA:** В CVA визуализация — это не «приятная опция», а обязательный слой. Система должна уметь показать поток данных и событий. > **Важно:** Визуализация делает систему понятной не только для разработчиков, но и для бизнеса. ### 14.2 Timeline **Хронологическое отображение запроса:** - Отображает шаги запроса в хронологическом порядке - Примеры шагов: HTTP-запрос принят, Handler выполнен, Событие записано в Outbox - Каждому шагу соответствует span (trace) - Timeline доступен в web-интерфейсе и экспортируется в VR > **Совет:** Timeline помогает визуально увидеть узкие места и последовательность операций. ### 14.3 Flow Graph - Узлы: сервисы, агрегаты, адаптеры - Рёбра: вызовы и события - Поддержка фильтров: по trace_id, по типу событий, по ошибкам/latency - Можно проследить путь любого запроса ### 14.4 Heatmap - Подсветка узких мест - Метрики latency/error_rate визуализируются в цвете - Красный = проблема, зелёный = норма - Используется для анализа производительности и деградаций ### 14.5 VR-визуализация - Поток данных можно «увидеть» в 3D (VR/AR) - Узлы-сервисы в пространстве, линии-события движутся в реальном времени - Полезно для обучения новых членов команды и анализа инцидентов - Интерактивное включение/выключение фичефлагов в VR ### 14.6 Интеграция с Observability - Визуализация строится на основе данных OpenTelemetry - Метрики Prometheus/Grafana агрегируются и отображаются в графах - Exposure событий фичефлагов отображаются как точки ветвления ### 14.7 Сценарии использования - **Dev:** отладка бизнес-логики → видно, как событие «идёт» по сервисам - **Ops:** поиск bottleneck при инцидентах - **Biz:** наглядная демонстрация работы продукта ### 14.8 «Вайб» визуализации - Система становится «живой» и «светящейся» - Любой член команды может увидеть поведение, а не только читать логи - Визуализация формирует доверие между разработкой и бизнесом --- ## Часть 15: Чек-листы внедрения ### 15.1 Общий чек-лист проекта - Определены Bounded Contexts и Ubiquitous Language - Сформирована архитектурная карта слоёв - Назначен владелец архитектуры (Architecture Owner / Tech Lead) ### 15.2 Domain Layer - Все сущности описаны как Entity/ValueObject - Инварианты зафиксированы и покрыты тестами - Domain Events определены и документированы - Нет зависимостей на ORM/HTTP/брокеры ### 15.3 Application Layer - Каждое действие оформлено как Command/Query - Для каждой команды есть Handler - Политики (Policy) валидируют доступ и правила - Транзакционные границы определены - Feature Flags встроены в use cases ### 15.4 Infrastructure Layer - Реализованы репозитории через OutPorts - Подключен Outbox (обязательный) - Подключен Inbox для входящих событий - Настроены адаптеры для API/брокеров/БД - Подключены observability-компоненты ### 15.5 Interface Layer - API оформлено в OpenAPI/gRPC - Добавлен middleware для trace_id и логирования - Авторизация/аутентификация включены - Контракты проверяются в CI ### 15.6 Feature Toggles - У каждого нового кода есть фичефлаг - Флаги имеют владельца, sunset-даты, kill-switch - Exposure событий логируются - Rollout сценарии задокументированы - Тесты включают on/off сценарии ### 15.7 Observability - Все запросы/события имеют trace_id - Логи структурированы (JSON) - Метрики: технические + бизнес - Дашборды и алерты настроены - Визуализатор подключен (timeline/flow graph/VR) ### 15.8 Data & Contracts - API и события имеют схемы - Контракты версионированы (SemVer) - Проверка backward-compatibility в CI - Миграции БД описаны и протестированы - Shadow-write/read используется при сложных изменениях ### 15.9 Security - Все вызовы аутентифицированы - RBAC/ABAC политики определены - Секреты в Vault/KMS - Audit log immutable и централизован - PII обрабатываются безопасно ### 15.10 Delivery & Deployment - CI: сборка, тесты, линтеры, security scan - CD: canary/blue-green deployment - Rollback план задокументирован - Auto-rollback по SLO включён - Canary метрики мониторятся ### 15.11 People & Process - PR checklist в каждом репозитории - ADR для значимых изменений - Регулярный архитектурный review - Метрики DevEx измеряются - Обучение команды по CVA ### 15.12 «Вайб» чек-листов - Чек-листы делают внедрение архитектуры предсказуемым - Разработчики уверены, что ничего не упустили - Бизнес получает стабильную эволюцию без сюрпризов --- ## Часть 16: Лицензирование и вклад ### 16.1 Лицензия - Open Source (MIT/Apache 2.0): архитектурные принципы, чек-листы и шаблоны доступны свободно - Можно использовать в коммерческих и некоммерческих проектах без ограничений - Единственное требование — сохранять упоминание авторства и лицензии ### 16.2 Contribution Guide Pull Request принимается только если: - Прописан ADR для изменений - Пройден CI (контракты, тесты, миграции) - Обновлены чек-листы и документация - Добавлены примеры (Go/PHP SDK, миграции, метрики) ### 16.3 Governance - **Maintainers:** ответственны за review и выпуск новых версий CVA-манифеста - **Contributors:** вносят идеи, багфиксы, дополнения к чек-листам и паттернам - Решения принимаются через RFC/ADR ### 16.4 Версионирование манифеста - Манифест CVA также следует SemVer - MINOR — добавление паттернов/чек-листов - MAJOR — изменения в обязательных принципах ### 16.5 Community - Slack/Discord/Matrix-группа для обсуждений - Wiki с best practices и шаблонами - Showcase: список компаний/проектов, внедривших CVA ### 16.6 «Вайб» вклада - CVA — это живая архитектура, которая развивается вместе с сообществом - Вклад — не только код, но и: документация, схемы, визуализации, примеры - Каждый коммит делает систему прозрачнее, надёжнее и «вайбовее» --- ## Ключевые отличия CVA ### От классической Hexagonal/Clean Architecture **База та же** (DDD + Hexagonal), но CVA делает обязательными вещи, которые обычно «допиливают потом»: - Событийность (Domain Events + Outbox/Inbox) - Наблюдаемость (трейсинг/метрики/структурные логи) - Фичефлаги со сквозной интеграцией - Управляемая эволюция (canary/blue-green, контрактное версионирование) - Визуализация потока (веб/VR) **Идея:** система изначально «живая, прозрачная и безопасно эволюционирующая». ### Где CVA лучше 1. **Прозрачность и отладка** — встроенный OpenTelemetry, бизнес-метрики, timeline 2. **Надёжная интеграция** — Outbox/Inbox, идемпотентность, ретраи по умолчанию 3. **Безопасные релизы** — feature flags, canary rollout, kill-switch, авто-откат 4. **Управляемая эволюция** — SemVer, dual write/read, shadow write/read 5. **Developer Experience** — шаблоны, чек-листы, стандарты 6. **Понимание для бизнеса** — визуализация потока, бизнес-метрики из коробки 7. **Безопасность по умолчанию** — RBAC, аудит, zero-trust ### Где классический Hexagonal проще 1. Маленькие проекты и редкие релизы 2. Команда без опыта событийности и телеметрии 3. Жёсткие ограничения по инфраструктуре ### Когда выбирать CVA - Микросервисы и интеграционно насыщенные системы - Частые релизы и эксперименты (growth/продуктовые команды) - Высокие требования к надёжности доставки событий - Нужно «прозрачно» объяснять технологию бизнесу - Планируется масштабирование команды и проекта ### Когда лучше остаться на классическом Hexagonal - Простой сервис/монолит без активной эволюции - Команда маленькая, нет ресурсов на телеметрию и фичефлаги - Внешних интеграций почти нет, SLA мягкие --- ## Заключение **ЧистаяВайбАрхитектура** — это не просто набор паттернов, а целостный подход к построению современных бэкенд-систем. CVA делает систему: - **Прозрачной** — каждый запрос видим от начала до конца - **Надёжной** — события доставляются гарантированно - **Безопасной** — изменения контролируемы и откатываемы - **Понятной** — визуализация помогает всем: от разработчиков до бизнеса > **Резюме:** Это «Hexagonal на стероидах» — дороже на старте, но многократно окупается на средних и больших системах. --- ## VR-визуализатор микросервисов - URL: https://vbcherepanov.com/ru/articles/vr-microservices-visualizer - Published: 2025-10-15 - Reading time: 4 min - Tags: Backend, Distributed Systems - Language: ru > Иммерсивный VR-инструмент для дебага и анализа потоков в микросервисах: Go + gRPC + Unity VR. Откройте **VR Microservices Visualizer** — разработанный мной инструмент, который упрощает отладку, анализ и управление сложными микросервисными архитектурами на базе Go, gRPC и Unity VR.\ _2 минуты чтения · Виталий Черепанов_ ## Проблема: управление микросервисами остаётся сложным Работа с распределёнными системами часто приводит к узким местам, неэффективному расходу ресурсов и сложностям при отслеживании потоков запросов. Классические средства мониторинга и визуализации не дают интуитивного интерфейса и иммерсивного анализа в реальном времени. _Проект в активной разработке._ ## Моя роль: автор и ведущий разработчик Я спроектировал и реализовал весь VR Microservices Visualizer: - Архитектура и общая модель данных - Backend на Go с event-driven подходами и обновлениями в реальном времени - Межсервисное взаимодействие через gRPC - Иммерсивный интерфейс на Unity VR для Oculus Quest 3 ## Решение: визуализация микросервисов в VR VR Microservices Visualizer — это приложение, которое даёт наглядное VR-представление взаимодействий между микросервисами. Связка Go, gRPC и Unity VR помогает инженерам: - Быстро находить узкие места - Оптимизировать использование ресурсов - Понимать маршрутизацию запросов и событий в реальном времени ### Используемые технологии - **Go (Golang)** — backend микросервисов и обработка данных - **gRPC** — высокопроизводительная межсервисная коммуникация - **Unity VR** — интерактивная и иммерсивная визуализация - **Oculus Quest 3** — доступное VR-устройство для удобного опыта ## Результат: ускоренная отладка и анализ Инструмент заметно упрощает отладку, мониторинг и управление микросервисными архитектурами. Пользователи отмечают: - Более быстрое выявление проблем и аномалий - Лучшую управляемость ресурсов - Глубокое понимание сложных систем и сценариев Это повышает эффективность разработки, сокращает простои и усиливает бизнес-ценность высоконагруженных решений. --- # Language: sr --- ## Skalirao sam PHP dok se nije slomio. Tri llama.cpp paterna su ga spasla. - URL: https://vbcherepanov.com/sr/articles/i-scaled-php-until-it-broke - Canonical (medium): https://medium.com/@vbcherepanov/i-scaled-php-until-it-broke-three-llama-cpp-patterns-saved-it-12ddb096ab32 - Published: 2026-05-13 - Reading time: 14 min - Tags: Backend, Architecture - Language: sr > Šest llama.cpp optimizacija prevedenih u PHP 8.4 sa JIT-om, benchmarkovano od 1M do 1B zapisa. Polovina hipoteza je pala: SplFixedArray gubi po brzini, mmap je 7× sporiji po pozivu, match je jednak switch-u. Druga polovina postaje alat za preživljavanje na skali — generatori i kolonski layout završe posao tamo gde naivni kod baca OOM. ![Skalirao sam PHP dok se nije slomio — naslovna](/images/articles/i-scaled-php-until-it-broke.png) Pročitao sam izvorni kod llama.cpp. Šezdeset hiljada linija C++ koda koje su same omogućile lokalni LLM inference na laptopu. Ovo nisu „best practice iz udžbenika" — to je kod gde je svaka linija odgovorna za to da množenje matrica ostane unutar L2 keša i van budžeta propusnog opsega RAM-a. Pišem PHP. Jezik gde je svaka vrednost umotana u zval, svaki objekat nosi 30+ bajtova hedera, a svaki `foreach` alocira hash iterator. Poređenje je nepravedno po definiciji. Ali bilo mi je radoznalo: koji od trikova llama.cpp-a uopšte preživi presađivanje? I šta će se desiti kada gurnem dataset do milijardu zapisa? Napravio sam benchmark suite. Šest optimizacija iz llama.cpp-a, prevedenih u PHP 8.4 sa JIT-om. Realni brojevi, statistička metodologija, p99 latencije. Onda sam skalirao input od 1 miliona do 1 milijardu zapisa, da vidim gde trikovi prestaju da budu „prijatan dodatak" i postanu jedini put kojim kod uopšte može da završi. **Polovina mojih hipoteza je bila pogrešna. To je prava priča.** --- ## TL;DR | Patern | Na 10M zapisa | Na 100M+ | Verdikt | |---------|----------------|----------|---------| | **B01:** mmap tabela | po pozivu **7× sporije** | učitavanje **226× brže**, 0 PHP heap | Win na nivou procesa, ne poziva | | **B02:** SplFixedArray vs array | **sporije**, ušteda 1.68× memorije | oba rade do 1B; razlika 9 GB | Memorija — da, brzina — nikada | | **B03:** Object pool u hot loop-u | **4.43× brže** | skalira linearno | Koristi u long-running worker-ima | | **B04:** Lookup table vs match | lookup **5.8× brži**, match=switch | skalira linearno | Data-driven dispatch → lookup | | **B05:** Generator vs pun array | 1.24× brže, memorija O(1) | **naivni OOM, generator završi** | Alat za preživljavanje | | **B06:** Kolonski vs row layout | **8.66× brže** single-col scan | **naivni OOM na 100M, kolona 959ms** | Alat za preživljavanje | Polovina paterna na skali prelazi iz „optimizacija" u „jedini put kojim kod može završiti". Polovina ne. A jedan patern (SplFixedArray) ispao je suprotno od onoga što se o njemu pisalo poslednjih deset godina. Idemo redom. --- ## B01: mmap čita gigabajte brzo, ali NE po pozivu **Hipoteza:** memory-mapping velikih read-only tabela je brži od učitavanja preko `json_decode`. Llama.cpp paralela — modeli se učitavaju kroz `ggml_mmap` (vidi `src/llama-mmap.cpp`), ne kroz `fread` u malloc-ovan bafer. **PHP prevod:** otvori `libc.dylib` preko FFI, pozovi `mmap()`, uzmi pointer, `FFI::cast('uint32_t*', $ptr)` za tipiziran pristup: ```php $ffi = FFI::cdef(" void *mmap(void *addr, size_t length, int prot, int flags, int fd, long offset); int open(const char *pathname, int flags); ", "libc.dylib"); $fd = $ffi->open('data/lookup.bin', 0); $ptr = $ffi->mmap(null, $size, 1, 2, $fd, 0); $table = FFI::cast('uint32_t*', $ptr); // Pristup: $table[$id * 2 + 1] vraća vrednost za ključ $id ``` **Rezultat na 10 miliona zapisa:** - Vreme učitavanja: JSON 454 ms vs mmap 1.1 ms → **mmap je 226× brži pri učitavanju** - PHP heap nakon učitavanja: JSON 256 MB vs mmap **0 bajtova** - p99 po jednom lookup-u: JSON 708 ns vs mmap 5.4 µs → **mmap je 7× SPORIJI po pozivu** Stop. mmap gubi 7× po pozivu. JIT optimizuje `$arr[$id]` toliko dobro da FFI dereference sa cast overhead-om ne preživi tight read loop. **Na 1 milijardi zapisa,** mmap učita 16 GB binara za **228 milisekundi** uz nula PHP heap. JSON put tu ni ne postoji — fixture bi bila 100+ GB JSON teksta, fizički nerealno generisati. ![B01 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B01.png) **Verdikt:** mmap nije „brži po pozivu". On je *druga kategorija* optimizacije. Daje ti vreme učitavanja, ravan PHP heap i deljenje tabele između N PHP-FPM workera kroz kernel page cache. Unutar jednog procesa u tight read loop-u — gubi od JIT-a. Između procesa — pobeđuje za redove veličine: cross-process cold start drugog workera je **2641× brži**, jer su stranice već u kernel page cache-u. Koristi mmap kada flota workera deli debelu read-only tabelu. Ne koristi ga za tight read loop-ove unutar jednog procesa. --- ## B02: SplFixedArray štedi memoriju, ali nikad brzinu **Hipoteza:** na gustim numeričkim podacima, `SplFixedArray` treba da bude i brži (bez hash overhead-a) i memorijski efikasniji. Llama.cpp paralela — `ggml_tensor` radi sa upakovanim arenama, ne sa nizovima pointer-a na boxed objekte. **Rezultat na 10 miliona integer-a:** - Memorija: array 256 MB vs SFA 152 MB → **SFA štedi 1.68×** - Iterate: array 12.2 ms vs SFA 93.8 ms → **SFA je 7.7× SPORIJI** - Populate: array 56.5 ms vs SFA 108.8 ms → **1.9× sporiji** - Random reads (1M): array 23.9 ms vs SFA 98.5 ms → **4× sporiji** Očekivao sam OOM crossover, pa sam gurao sweep do milijardu integer-a nadajući se da će obični array dostići RAM plafon. **Nije.** Na 1B elemenata: array 24 GB peak vs SFA 14.9 GB. SFA je gubio po brzini na svakom tier-u. ![B02 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B02.png) **Verdikt:** `SplFixedArray` na modernom PHP-u je samo memorija, nikada brzina. Folklor „koristi SplFixedArray za velike numeričke podatke jer je brži" — to je savet iz 2014. JIT u PHP 8.4 optimizuje upakovane integer-keyed array-e toliko agresivno da specijalizovana struktura gubi od opšte. Posegni za SFA kada si memorijski ograničen unutar long-running worker-a. Ne očekuj ubrzanje. Ovo je najkontraintuitivniji nalaz u članku. Nisam ni sam verovao, pa sam ponovo pustio ceo sweep dva puta. Brojevi se drže. --- ## B03: Object pool — jedina klasična optimizacija koja se još isplati **Hipoteza:** u hot loop-u, ponovno korišćenje malog pula prealociranih objekata je brže od `new` na svakoj iteraciji. Llama.cpp paralela — tensor allocator nikad ne zove `malloc` unutar inner loop-a. Radi protiv prealocirane arene kroz `ggml_new_tensor_impl`. **Prevod:** pul od 5 instanci `Point3D`-a, ponovo korišćen kroz direktno postavljanje properties: ```php final class Point3D { public function __construct( public float $x = 0.0, public float $y = 0.0, public float $z = 0.0, ) {} } $pool = array_map(fn() => new Point3D(), range(0, 4)); $idx = 0; for ($i = 0; $i < 5_000_000; $i++) { $p = $pool[$idx++ % 5]; $p->x = $x; $p->y = $y; $p->z = $z; // ... rad sa $p } ``` **Rezultat na 5 miliona alokacija:** naivni 813 ms vs pool 179 ms → **4.43× brže**. GC ciklusa: nula u oba slučaja. `Point3D` nije ciklični, PHP GC se ne aktivira. Sva ušteda dolazi iz allocator path-a: `new` u Zend Engine-u je lagan ali ne-nulti code path (`zend_object_new` → `emalloc` → property init × N). Pet miliona puta — to se zbraja. **Verdikt:** radi kao što se očekuje. U CLI skriptama win je realan ali ne kritičan. U long-running worker-ima (queue, websocket, daemon), tail latency od pritiska allocator-a se nakuplja vremenom i postaje glavobolja — tu se pooling isplati. --- ## B04: Lookup table pobeđuje match i switch (a ova dva su jednaka) **Hipoteza:** za dispatch logiku sa 16+ slučajeva u hot loop-u, array lookup pobeđuje `match` i `switch`. Llama.cpp paralela — token dispatch u `llama_token_to_piece` koristi tabele, ne switch-eve. **Prevod:** klasifikator sa 32 slučaja implementiran na tri načina — `switch`, `match` i preunbild `$lookup = [0 => 'A', 1 => 'B', ...]`. **Rezultat na 10 miliona dispatch-eva:** - `switch`: 358 ms (27.9M ops/sec) - `match`: 365 ms (27.4M ops/sec) - `lookup`: **61.7 ms (162M ops/sec) — 5.8× brže** `match` i `switch` su **izjednačeni.** Oba se kompajliraju u istu jump tabelu za integer slučajeve. PHP 8.4 JIT poliše obe forme do istog rezultata. Ako si prepravio `switch` u `match` zbog „modernizacije" — dobio si čitljivost, ne brzinu. **Gde lookup win nestaje:** ako dispatch proizvede string za downstream `===` poređenja, dobitak pojedu string poređenja niz pipeline. **Verdikt:** match-shaped problemi (zatvoren compile-time skup, traži se exhaustiveness) ostaju u `match`. Data-driven dispatch (tabela učitana iz konfiga, generisana u runtime-u) ide u lookup. Debata „match vs switch za perf" je zatvorena — ekvivalentni su. --- ## B05: Generator — glavni alat za preživljavanje na velikim stream-ovima **Hipoteza:** generator smanjuje peak memoriju sa O(N) na O(1) uz sitnu kaznu po throughput-u. Llama.cpp paralela — tokeni se stream-uju kroz callback umesto da se akumuliraju u baferu (`llama_decode` → `llama_get_logits_ith`). **PHP prevod:** zameni `function process(): array` sa `function process(): Generator`: ```php function records(): Generator { foreach (read_csv('data.csv') as $row) { yield ['id' => $row[0], 'value' => $row[1]]; } } ``` **Rezultat na 5 miliona zapisa:** - Wall time: naivni 525 ms vs gen 449 ms → **gen 1.24× brži** - Peak memorija: naivni 1.88 GB vs gen **0 bajtova PHP heap-a** Generator nije samo manje-memorijski — on je i brži po wall time-u, jer array nikada ne mora biti potpuno materijalizovan pre nego što obrada počne. **Sad skala.** Na 100 miliona zapisa, naivni — OOM, kernel ubija proces sa SIGKILL nakon 28.6 sekundi. Generator završava istih 100M za **10.4 sekunde** uz nula PHP heap. Na 500M, generator još uvek radi (45.7 sekundi). Naivni i ne pokušava. ![B05 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B05.png) Da moram da izvučem jednu rečenicu iz cele ove statije i stavim je na baner, bila bi ova: > **Na 100.000 zapisa, generator je 1.24× nice-to-have. Na 100 miliona, on je jedini put kojim kod može završiti.** **Verdikt:** podrazumevani izbor za bilo koji single-pass stream koji ne moraš ponovo da posetiš. Materijalizuj array samo kada ti treba random access, više prolaza ili `count()` pre obrade. --- ## B06: Kolonski layout — nije cache locality, nego beg od boxing-a **Hipoteza:** na analitičkim single-column skenovima, kolonski layout je brži od row-orientisanog zbog cache locality. Llama.cpp paralela — tenzori se čuvaju per-channel (SoA), ne per-element (AoS). **PHP prevod:** umesto `SplFixedArray` od `stdClass` sa 5 polja — 5 paralelnih `SplFixedArray` instanci, po jedna za svako polje: ```php // Row-orientisani (naivni) $rows = new SplFixedArray($n); for ($i = 0; $i < $n; $i++) { $obj = new stdClass(); $obj->f1 = ...; $obj->f2 = ...; /* ... */ $obj->f5 = ...; $rows[$i] = $obj; } $sum = 0; foreach ($rows as $r) $sum += $r->f3; // Kolonski (optimizovan) $f3 = new SplFixedArray($n); // i tako za svako polje for ($i = 0; $i < $n; $i++) $f3[$i] = ...; $sum = 0; for ($i = 0; $i < $n; $i++) $sum += $f3[$i]; ``` **Rezultat na 5 miliona zapisa:** kolonski je **8.66× brži** na single-column skenu. Na full-row skenu (sum f1..f5) — 1.92× brži. **I tu postaje zanimljivo.** Očekivao sam stepenice na ns/record grafu — gde working set prestaje da staje u L1, pa u L2, pa u L3 keš. **Nisam ih video.** Krive su ravne kroz ceo opseg 100K → 100M: kolona se drži na ~9.5–11.5 ns/record. Row na ~80–93 ns/record. Bez stepenica. ![B06 scaling chart](/images/articles/i-scaled-php-until-it-broke/scaling-B06.png) Ovo je *jači* uvid od „evo stepenica". Cache efekti unutar oba layout-a ih ne razlikuju. Razlikuje ih sam layout. Row-orientisani troši ~30+ bajtova po `stdClass`-u (zval header + property table + GC info) za 8 bajtova stvarnog payload-a. Na 100M zapisa to je 28 GB samo na boxing. Kolonski na istih 100M = 7.45 GB, jer je svaka kolona upakovan `SplFixedArray` bez boxing-a. **Na 100M zapisa,** row — OOM, 28+ GB `stdClass` objekata ne staje. Kolonski završava sken za **959 milisekundi** uz 7.45 GB. **Verdikt:** kolonski layout nije cache optimizacija (kako sam pretpostavio). To je beg od overhead-a PHP objekata na skali. Na bilo kom analitičkom radnom opterećenju nad velikim setovima podataka — kolonski. Row ostaje primeren kada se DTO prosleđuju između slojeva ili kada je working set mali. --- ## Šta se dešava na skali Mikro-benchmarci na 1–10 miliona elemenata daju jednu sliku. Skaliranje na milijarde — drugačiju. Tri od šest paterna na velikim podacima prelaze iz „optimizacije" u „nužnost": - **B05 generator** — na 100M, naivni — OOM. Generator završava. - **B06 kolonski layout** — na 100M, row — OOM. Kolonski završava sken za 959 ms. - **B01 mmap** — na 1B, JSON fixture *fizički ne postoji* (100+ GB). mmap učita 16 GB binara za 228 ms. Dva paterna ostaju „samo optimizacije" bez obzira na skalu: - B03 object pool: ~4× na bilo kojoj veličini. - B04 lookup table: ~5× na bilo kojoj veličini. Jedan patern je ispao uzak — štedi memoriju, ali nikad brzinu: - B02 SplFixedArray: 38% manje memorije, uvek sporiji po brzini. Oba puta rade sve do 1B. Ovo je verovatno najvažnije reframovanje u članku. Kada neko kaže „X je brži od Y", to je tvrdnja o specifičnoj veličini podataka. Na malim podacima, polovina tvrdnji se lomi. Na velikim podacima, polovina se pretvara u „X radi, Y ne postoji". I još jedna stvar vredna posebne linije: **JIT u PHP 8.4 nastavlja da jede optimizacije svakim release-om.** Između run-ova na PHP 8.3.31 i 8.4.21, B03 je ubrzao sa 2.78× na 4.43×, B04 sa 3.75× na 5.81×. Nije bug — JIT se prosto nastavlja unapređivati. Za godinu dana, ovi brojevi će se opet pomeriti. --- ## Tri pravila PHP performansi u 2026. Iz ovih šest eksperimenata izronio je radni framework. **1. Veruj JIT-u.** Ne pokušavaj da ga nadmudriš na nivou sintakse. `match` vs `switch` — JIT kompajlira obe forme u istu jump tabelu. `SplFixedArray` vs upakovan array — JIT optimizuje obični array toliko agresivno da specijalizovana struktura gubi po brzini. FFI dereference vs `$arr[$id]` — JIT-kompajliran array access pobeđuje FFI cast-ove unutar hot loop-a. Ako je tvoja optimizacija o tome „koji jezički konstrukt izabrati" — JIT je već napravio taj izbor za tebe. **2. Optimizuj ono što JIT ne vidi.** - **Cache locality** (B06: kolonski layout) — JIT ne upravlja memory layout-om. To je tvoja arhitektura. - **Allocation pressure** (B03: object pool) — JIT ne eliminiše alokacije, samo ih ubrzava. - **I/O batching** (batched INSERT od 1000 redova vs single-row) — JIT ne optimizuje round trip-ove ka Postgres-u. - **Cross-process resource sharing** (B01: mmap + page cache) — JIT radi po procesu. - **Streaming vs materializacija** (B05: generator) — JIT ti neće ukloniti 30 GB peak memorije. **3. Na dovoljno velikoj skali, optimizacije prestaju da budu optimizacije.** Postaju prag preživljavanja. Generator na 100K zapisa je 1.24× brži. Na 100M, on je jedini kod koji završava. Kolonski layout na 5M je 8.66× brži. Na 100M, on je jedini kod koji ne pojede 28 GB na overhead-u `stdClass`-a. mmap na 10M je sporiji po pozivu. Na 1B, on je jedini način da učitaš tabelu unutar sekunde. To je strukturalno razmišljanje, ne sintaksičko. I to je ono što llama.cpp pretvara iz „teško optimizovane C++ biblioteke" u *učeći artefakt* za PHP developer-a. Ne „evo trikovi, ukradi ih". Nego „evo granice jezika koje vidiš samo kad u njih udariš". --- ## Zaključak Sav benchmark kod i reproducibilan Docker setup žive na GitHub-u: **[vbcherepanov/php-llamacpp-benchmarks](https://github.com/vbcherepanov/php-llamacpp-benchmarks)**. Pun sweep traje ~15 minuta (`make all`), uključujući case study koji uveze 100K redova u realan PostgreSQL. Napomena o repozitorijumu: direktorijum `data/` je gitignore-ovan — fixture (do 16 GB binarnih lookup fajlova na 1B tier-u) se generišu lokalno kroz `make fixtures`. Nemoj pokušavati da kloniraš sa njima. Ako pronađeš bug u metodologiji ili hoćeš da dodaš tier — pošalji PR. Bavim se ovakvim stvarima kroz **[Braincore](https://getbraincore.com)** — Go-based meta-agent sa cost-aware routing-om i memorijskim slojem za AI coding agente. Ako su ovi benchmarci bili korisni i hoćeš da podržiš još ovakvih stvari, tu je [Ko-fi](https://ko-fi.com/vbcherepanov). --- ## Šta su 16 paralelnih Claude agenata izgradili oko sebe: razlaganje Anthropic eksperimenta sa C kompajlerom - URL: https://vbcherepanov.com/sr/articles/what-16-parallel-claude-agents-built - Canonical (medium): https://medium.com/@vbcherepanov/what-16-parallel-claude-agents-built-around-themselves-deconstructing-anthropics-c-compiler-f2fa6335b1ca - Published: 2026-05-09 - Reading time: 10 min - Tags: AI Agents, Open Source - Language: sr > Razlaganje Anthropic eksperimenta gde 16 paralelnih Claude instanci pravi C kompajler u Rust-u — koja je infrastruktura morala da se izmisli oko agenata jer primitivi nisu postojali. ![16 paralelnih Claude agenata — naslovna](/images/articles/what-16-parallel-claude-agents-built.png) 5. februara 2026, Nicholas Carlini iz Anthropic-a [objavio je tekst](https://www.anthropic.com/engineering/building-c-compiler) o eksperimentu koji značajno prednjači onome što većina nas trenutno radi sa LLM agentima. Šesnaest paralelnih instanci Claude Opus 4.6, dve nedelje rada, ~2.000 Claude Code sesija, budžet oko $20.000. Rezultat: 100.000 linija C kompajlera u Rust-u koji gradi Linux 6.9 na x86, ARM i RISC-V; prolazi 99% GCC torture test suite-a; kompajlira PostgreSQL, SQLite, FFmpeg, Redis i QEMU; pokreće Doom. [Repozitorijum je otvoren](https://github.com/anthropics/claudes-c-compiler) i svako može da ga pročita i isproba. To je ozbiljan inženjerski rad, a sam tekst je odlično štivo za svakoga ko razmišlja o autonomnim agentima u produkciji. Carlini je iskren oko onoga što je radilo i što nije, prolazi kroz pet konkretnih lekcija iz dizajna harness-a, i deli brojeve i metrike. Upravo takvi izveštaji nedostaju industriji — izveštaj iz prve ruke o tome kako stvarno izgledaju dugotrajni autonomni run-ovi. Naslovi su se podelili u dva tabora. „AI je zamenio programere" sa jedne strane. „To je samo demo" sa druge. Oba promašuju ono što je stvarno zanimljivo. Ako pažljivo pročitaš tekst, ono što Carlini dokumentuje nije „AI piše kompajler". On dokumentuje **koliko je infrastrukture moralo da se izgradi oko agenata jer u 2026. još uvek ne postoji nikakva infrastruktura između samih agenata**. Lockfile-ovi u deljenom direktorijumu kao mehanizam sinhronizacije. README-ovi koje agent piše sam sebi. GCC mobilisan kao known-good referentni orakl. Ralph-loop omotan oko Docker-a za neograničenu autonomiju. Svaki od ovih pristupa je odgovor na konkretan problem koji danas jednostavno **nema gde da se gurne u standardni sloj**. I to je prava vrednost teksta. Ne kao „AI demo", već kao **detaljna mapa nedostajućih primitiva**, koju je nacrtao neko ko je rukama izgradio rešenja za njih. Poslednjih nekoliko meseci radim upravo na tim primitivima, i Carlini-jev tekst je odličan povod da se priča o tome šta je potrebno sledećoj generaciji timova agenata. ## Svaka sesija počinje sa amnezijom Carlini je izgradio harness koji pokreće Claude-a u beskonačnoj petlji — kad agent završi jedan zadatak, preuzima sledeći. Arhitektonski je to poznati „Ralph-loop" pattern: ciklus `while true` u bash skripti, omotan u Docker zbog bezbednosti. U jednom od run-ova, Claude je slučajno ubio sam sebe sa `pkill -9 bash`, što Carlini navodi kao zabavan sporedni efekat. Ključna detalj je da je svaki od ~2.000 lansiranja počeo **u svežem Docker kontejneru sa praznim kontekstom**. Bez memorije između sesija. Svaki agent je iznova ustanovljavao: šta je ovaj repozitorijum, šta je urađeno, koji je status zadataka, šta je probano i palo. Carlini-jev workaround je bio da uputi Claude-a da sam vodi opširne README-ove i progress fajlove, ažurirajući ih često. Kad agent zaglavi u bag-u, takođe vodi running doc o neuspelim pristupima i preostalim zadacima. To radi u okvirima trenutnog tooling-a — i u tome je njegova vrednost. Ali kad pogledaš skaliranje, dve arhitektonske tačke počinju da škripe. Prvo, tekstualni fajl nije strukturiran. Ako želiš da pitaš „koja su poslednja tri buga koje sam fiksirao u oblasti parsera i kako su završili?", imaš samo `grep` i regularne izraze. Na malom projektu je podnošljivo. Na 100.000 linija koda i 2.000 sesija — postaje usko grlo. Drugo, suptilnije: svaki agent vodi te fajlove za sebe. Žive u deljenom git repozitorijumu, ali nema mehanizma koji kaže „pre nego što preuzmeš zadatak X, pogledaj šta je ostalih 16 agenata pisalo o ovoj oblasti u poslednjih 6 sati". Svaki agent piše svoj README, mergeuje tuđe izmene, i nada se da će se stvari konvergirati. Ovo je **prva generacija deljene memorije** — implementirana kao plain text jer ni jedan zgodniji primitiv još nije postao standard. ## Lockfile-ovi kao koordinacija Paralelizam je implementiran minimalno. Svaki agent radi u svom Docker kontejneru; deljeni bare git repozitorijum drži stanje. Koordinacija zadataka ide kroz lockfile-ove: agent piše fajl tipa `current_tasks/parse_if_statement.txt`, radi `git push` i time „preuzima" zadatak. Ako dva agenta pokušaju da preuzmu isti, git sinhronizacija primorava drugog da uzme nešto drugo. Gotovo — obriši lockfile. Carlini direktno opisuje trenutno stanje sistema: *„no other method for communication between agents... I don't use an orchestration agent."* Bez mehanizma da agenti „pitaju jedni druge". Bez centralne koordinacije. Svaki Claude sam odlučuje šta da radi sledeće — obično „sledeći očigledan problem". Lockfile ovde radi tačno jednu stvar — funkcioniše kao **mutex**, štiteći od paralelnih zauzimanja jednog zadatka. To je vredno. Ali ne rešava drugi problem: dva agenta koji rade na različitim zadacima **u istoj oblasti koda** mogu pisati konfliktni kod pod različitim imenima zadataka. Upravo to se desilo sa Linux kernel-om u eksperimentu — agenti su konvergirali na isti bug, popravljali ga drugačije, prepisivali tuđe izmene, i paralelizam je privremeno prestao da se isplati. Carlini-jevo rešenje bio je odvojen test harness koji koristi GCC kao **known-good kompajler-orakl**: većina kernel-a se kompajlira sa GCC, a slučajan podskup fajlova prolazi kroz Claude-ov kompajler. Ako se kernel ne pokreće, bug je negde u Claude-ovom podskupu, i možeš dalje sužavati. Pametna i elegantna ideja, i radila je tačno kako je zamišljeno. Vredi napomenuti granice u kojima ovo radi. GCC orakl je precizno rešenje za **ovaj konkretan zadatak**, jer zadatak ima tri zgodne osobine: postoji gotov referentni kompajler za istu specifikaciju, zadatak se dekomponuje na nivou pojedinačnih fajlova, i ishod je binarni (boot-uje ili ne). U većini realnih projekata — produktni razvoj, legacy refaktorisanje, ML pipeline-ovi, mobilne aplikacije — ovi pogodnosti ne postoje. Nema gotovog known-good za poređenje. Nema prirodne dekompozicije po fajlovima. Ishodi nisu binarni. Što znači da se **GCC-orakl tehnika ne može generalizovati kao primitiv** — radi gde radi, i ne postoji gde ne radi. Posmatran u celini, Carlini-jev toolkit lepo se postavlja po dve ose: | Šta timovima agenata treba | Šta je u eksperimentu | Priroda rešenja | |---|---|---| | Otkrivanje agenata | hard-kodovan broj kontejnera | hard-kodovano | | Inter-agent komunikacija | lockfile preko git push | mutex bez messaging-a | | Delegiranje zadataka | next-most-obvious iz reda | bez routing-a | | Deljeno stanje / memorija | README + progress fajlovi | plain text | | Kauzalna istorija | running doc o neuspelim pristupima | lični log | | Verifikacija | GCC orakl | specifično za zadatak | Ovo su **dve nezavisne ose problema**: komunikacija (kako agenti razgovaraju jedni sa drugima) i memorija (šta pamte između sesija i da li to dele). Ove ose zahtevaju različite primitive i različita rešenja. I na svakoj industrija trenutno konvergira ka standardima i open-source implementacijama. Sledeće — šta je danas dostupno na svakoj osi. ## Osa komunikacije: A2A protokol i a2abridge Komunikacija se brzo razvijala i već je stigla do zrelog standarda. U aprilu 2025, Google je [otvorio A2A protokol](https://a2a-protocol.org/latest/) — Agent-to-Agent. U avgustu 2025, IBM-ov ACP se [spojio sa A2A pod Linux Foundation-om](https://lfaidata.foundation/communityblog/2025/08/29/acp-joins-forces-with-a2a-under-the-linux-foundations-lf-ai-data/), i do aprila 2026 spec je na verziji 1.2, podržana od 150+ organizacija (Microsoft, AWS, Salesforce, SAP, ServiceNow, IBM među njima), i nativno ugrađena u Google ADK, LangGraph, CrewAI, LlamaIndex Agents, Semantic Kernel i AutoGen. A2A je u praksi **dobio rat protokola**. Spec je namerno minimalan: **Agent Card** je JSON opis mogućnosti agenta (šta radi, koji endpoint da se pogađa). **Task** je jedinica rada sa statusima i artefaktima. Transport je JSON-RPC 2.0 preko HTTPS, sa Server-Sent Events za stream-ove. Analogija koja se svuda koristi je HTTP. HTTP ti ne govori šta je u tvom backend-u (Rails, Django, Go) — samo definiše oblik zahteva i odgovora. A2A ti ne govori koji LLM, framework ili bazu koristiš — definiše ugovor između agenta A i agenta B. Minimum povrh kojeg možeš graditi ostalo. Ako bi Carlini-jev scenarijo prepisao na A2A, umesto lockfile-a u `current_tasks/`, agent bi pitao directory servis „ko sad radi na parseru?", dobio Agent Card suseda i poslao `Task` sa streaming odgovorom preko SSE. To je primitiv komunikacije koji njegov harness još nema. Poslednjih nekoliko meseci pišem **a2abridge** — otvorenu Go implementaciju A2A 1.0 ciljanu na praktičan scenarijo „nekoliko različitih AI agenata na mašini jednog developera". U trenutku objave podržano je šest IDE-ova: Claude Code, Codex CLI, Cursor, Cline, Continue i Gemini CLI. Bilo koji A2A-kompatibilan agent (uključujući buduće Google ADK, LangGraph, CrewAI implementacije) je first-class peer bez glue koda. Arhitektonski je to **jedan Go binary** (~10 MB) sa nekoliko subkomandi. `a2abridge directory` je discovery servis na 127.0.0.1:7777 koji radi kao user-level system service (launchd na macOS, systemd-user na Linux-u, Windows Service na Windows-u, ispravno radi unutar WSL2). `a2abridge bridge` je per-agent proces koji hostuje i MCP stdio server (kroz koji IDE vidi a2abridge kao običan MCP server sa alatima) i A2A HTTP server na nasumičnom port-u, sa Agent Card-om na `/.well-known/a2a` i punim setom JSON-RPC 2.0 metoda iz §7 spec-a: `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetExtendedAgentCard`. Lifecycle bridge-a se poklapa sa trajanjem IDE sesije — kad se MCP stdio zatvori, bridge umire, bez orphan procesa. Ono što Claude Code (ili bilo koji drugi IDE) vidi kao MCP alate: `a2a_whoami`, `a2a_list_agents`, `a2a_send_message`, `a2a_send_streaming`, `a2a_get_task`, `a2a_cancel_task`, `a2a_inbox`, `a2a_complete_task`. Unutar sesije, agent može **samostalno** otkrivati druge agente na mašini, slati im zadatke, čekati odgovore i čitati svoj inbox — bez intervencije korisnika. Iznad protokola postoji proaktivni sloj koji nije u spec-u ali je potreban za realno korišćenje. Bridge piše inbox fajl `./.a2a/inbox-.json` svaki put kad se promeni red poruka. UserPromptSubmit hook ubacuje pristigle poruke u sistemski prompt **pre prvog tool call-a** — što znači da Claude vidi „imaš poruku od peer-a sa FYI o breaking API change-u" **pre** nego što počne slepo da deluje. SSE fast-path isporučuje odgovore u milisekundama, fallback je polling svakih 5 sekundi. Za Claude Code postoji i **skill** `a2a-bridge` koji se auto-učitava samo na trigger promptove — bez globalno učitanih pravila koja troše tokene na svakoj sesiji. U Carlini-jevom scenariju to bi izgledalo ovako: agent 5 preuzima zadatak „fix kernel build error in `mm/page_alloc.c`". Pre delovanja, zove `a2a_list_agents`, vidi da agent 2 ima otvoren Task sa capability `kernel-debug` u istoj oblasti. Šalje `a2a_send_message`: „na čemu radiš, imaš li hipotezu?". Dobija streaming odgovor: „probao alignment fix, pao na test_kernel_boot, trenutno gledam reorder header includes". Bira drugi ugao. Zašto otvoren protokol a ne još jedan custom wire format. U ovoj niši već postoji nekoliko rešenja: **Anthropic Agent Teams** radi samo Claude↔Claude i vezan je za pretplatu. **CCB** i **claude-multi-agent-bridge** su zatvoreni formati zaključani na specifične kombinacije agenata. **Ruflo** je odličan za enterprise federacije 100+ agenata sa centralnim queens, ali to je drugi razred problema. Niša koju a2abridge cilja je **cross-vendor open-protocol mesh**, gde danas ulaze Claude i Codex, a sutra bilo koji A2A-kompatibilan agent, bez prepisivanja glue. Ako industrija ide ka standardu, bridge bolje da govori taj standard. Production zrelost: cross-machine federacija sa mTLS + ed25519 (opt-in, za scenarijo „kućni Mac ↔ kancelarijski Linux"), mDNS auto-discovery na lokalnoj mreži, PII/secret screen sa 11 regex detektora pre slanja (AWS ključevi, GitHub tokeni, Anthropic/OpenAI/Google/Stripe/Slack tokeni, JWT-ovi, PEM blokovi — zamenjeni sa `[REDACTED:]`, secret nikad ne napušta bridge), Push Notifications po A2A 1.0 §9.5, HTTP+REST binding po §7.3, 35 test slučajeva pod `-race`, GitHub Actions release matrix i cross-platform `a2abridge doctor` sa 9-provera health audit-om. Instalacija je jedna komanda kroz `install.sh` ili `install.ps1`, sa auto-detekcijom svakog IDE-a na mašini i `.bak` backup-ovima njihovih konfiguracija pre izmena. Repozitorijum: **[github.com/vbcherepanov/a2abridge](https://github.com/vbcherepanov/a2abridge)** — MIT, Go 1.25, trenutni release v2.0. ## Osa memorije: total-agent-memory i BrainCore Memorija je u drugačijem stanju. Standarda na nivou A2A još nema — svako gradi svoj sloj, i različiti pristupi se biraju za različite zadatke. Ono što agent piše sam sebi u README-u u suštini je kauzalni log u tekstualnom obliku: „probao A, pao na B, prešao na C". Struktura je ispravna; implementacija je još plain text. Radim na dva proizvoda na ovoj osi. **[total-agent-memory](https://github.com/vbcherepanov/total-agent-memory)** — open-source implementacija. Osnovni retrieval patterni, MCP integracija i osnovni causal-chain model žive ovde. Svako može da klonira, vidi kako radi, i uključi u svoj Claude Code ili Cursor. **[BrainCore](https://github.com/vbcherepanov/braincore)** — production-grade. Go binary, lokalni SQLite + WAL, tree-sitter za code-graph kroz 14 jezika (PHP, TypeScript, Python, Ruby, Rust, Java, Kotlin, C/C++, C#, Swift, Bash, Lua, YAML, plus Go kroz svoj nativni AST), internal git za time-travel memoriju, MCP protokol za povezivanje sa Claude Code, Cursor, Codex CLI, Windsurf i još nekoliko agenata. Trenutno u beti. Arhitektonski postoje tri tačke gde oba projekta odstupaju od ravnog bag-of-facts sa cosine pretragom. Prvo, **kauzalni decision chains** umesto ravnih činjenica. Ne „funkcija X je u fajlu Y", već „agent 3 u zadatku `fix kernel build` formulisao hipotezu `alignment issue`, verifikovao kroz test_kernel_boot, pao, prešao na hipotezu `header reorder`". Svaki korak je tipiziran, povezan kauzalnom strelicom, i pretraživ od strane svakog agenta. Drugo, **AST-stabilna identitet koda**. Kad nekoliko agenata refaktorišu paralelno, tekstualni diff-ovi brzo postanu kaša, a merge konflikti beskrajni. AST čvor ostaje čvor čak i ako se funkcija premestila iz `parser.rs` u `frontend/lexer.rs` i preimenovala iz `parse_decl` u `parse_declaration`. U grafu, to je **isti čvor** sa istorijom premeštanja. Svaki agent gleda na istu apstrakciju, ne na „linije 127-145 fajla X". Treće, **persistencija kroz restart kontejnera**. Memorija živi **van** Docker kontejnera: na host-u kroz volume, ili udaljeno preko MCP. Upit `brain.causal_lookup(area="parser", lookback="6h")` vraća isti rezultat bez obzira u kom svežem kontejneru se nalaziš. Prepisivanje Carlini-jevog scenarija sa memorijom: agent 5 ide u BrainCore, dobija kauzalni log „agent_2 probao alignment fix → pao, agent_7 probao header reorder → pao na L98, trenutna hipoteza od agent_3 je alignment issue, in progress", bira četvrtu hipotezu, piše je u kauzalni lanac. Agenti 2, 3 i 7 vide tu odluku na sledećem pull-u. Bez README-ova, bez grep-ova. ## Kako se uklapaju zajedno a2abridge i BrainCore su **različiti slojevi, ne konkurenti**. Jedan odgovara na „kako agenti razgovaraju jedni sa drugima", drugi na „šta pamte". Puna slika za tim agenata izgleda ovako. **BrainCore** drži deljeno stanje sveta: code-graph, kauzalne lance, hipoteze, zaključke. **a2abridge** obezbeđuje stvarnu komunikaciju između agenata: discovery, delegiranje, streaming odgovore, inbox sa context injection-om. Kad rade zajedno, agent 5 vidi poruku u inbox-u od agenta 2 („radim na X"), upita BrainCore za detalje („šta je konkretno probano u ovoj oblasti"), donosi informisanu odluku, odgovara agentu 2 o nameri da preuzme susedni zadatak, i piše rezultat u deljenu memoriju. To je arhitektura koju Carlini gradi rukama u eksperimentu kroz kombinaciju lockfile-ova + README-ova + GCC orakla. Sa nezavisnim primitivama umesto samogradnog lepka, infrastruktura radi i na zadacima gde nema gotovog known-good kompajlera. ## Šta ovi primitivi ne rešavaju Carlini je apsolutno u pravu oko glavne lekcije teksta: **kvalitetan test harness je temelj svega**. Nikakva deljena memorija i nikakav A2A te neće spasti ako je verifier zadatka neprecizan — agenti će autonomno rešavati pogrešan zadatak. CI pipeline-ovi, dobro dizajnirani logovi, odbrane od zagađenja kontekst prozora, borba sa „time blindness"-om — to radi na bilo kom nivou infrastrukture i **ostaje prvi prioritet**. GCC orakl u zadatku kompajlera je zaista optimalan izbor. Binarna verifikacija je skoro uvek bolja od poređenja kauzalnih hipoteza. Ako u tvom projektu postoji gotov known-good — koristi ga. Nikakva memorija ne zamenjuje dobrog verifier-a. Ali u većini realnih zadataka — produktni razvoj, refaktorisanje, ML pipeline-ovi, biznis logika — GCC ekvivalent ne postoji. I tu primitivi komunikacije i memorije postaju ne „poboljšanje" već **neophodan uslov** da tim od 16 agenata bude produktivniji od jednog. Da je Carlini morao da gradi ceo ovaj tekst-i-fajl sloj u 2026. nije mana njegovog pristupa već simptom trenutka: infrastruktura za timove agenata se još uvek formira. Anthropic eksperiment je najbolja moguća ilustracija toga kako se formira i kuda ide. I to je, po mom mišljenju, prava vrednost Carlini-jevog teksta: iskren izveštaj sa najranije tačke krive po kojoj će ova infrastruktura rasti. --- ## Open source i linkovi - Originalni Anthropic tekst: [Building a C compiler with a team of parallel Claudes](https://www.anthropic.com/engineering/building-c-compiler) - Repozitorijum kompajlera: [anthropics/claudes-c-compiler](https://github.com/anthropics/claudes-c-compiler) - A2A protocol specifikacija: [a2a-protocol.org](https://a2a-protocol.org/latest/) - **a2abridge** — otvoren A2A 1.0 mesh za 6 IDE-ova (Claude Code, Codex, Cursor, Cline, Continue, Gemini): [github.com/vbcherepanov/a2abridge](https://github.com/vbcherepanov/a2abridge) (MIT, v2.0 izdat) - **total-agent-memory** — open-source memory layer: [github.com/vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) - **BrainCore** — production memory infrastructure: [github.com/vbcherepanov/braincore](https://github.com/vbcherepanov/braincore) (beta) Ako gradiš svoj tim agenata i nailaziš na ove probleme — javi se. Razmena iskustava je vredna u svakom slučaju, a feedback na rane verzije proizvoda je najbolje što može da se desi autorima. --- ## Pravo AI agenta da ćuti - URL: https://vbcherepanov.com/sr/articles/the-right-of-an-ai-agent-to-stay-silent - Canonical (medium): https://medium.com/@vbcherepanov/the-right-of-an-ai-agent-to-stay-silent-db29c478e577 - Published: 2026-05-09 - Reading time: 6 min - Tags: AI Agents - Language: sr > Produkcijski AI bi trebalo da optimizuje za nula sigurno-pogrešnih akcija, ne za procenat tačnosti. Odbijanje kao first-class ishod, self-tasking i kognitivni runtime — put ka pouzdanoj autonomiji. ![Pravo AI agenta da ćuti — naslovna](/images/articles/the-right-of-an-ai-agent-to-stay-silent.png) > **Deo 3 od 3 — „Memorija za AI agente"** > Zašto prava metrika nije accuracy — već nula sigurno-pogrešnih akcija Zamisli dva scenarija. U prvom — stariji kardiohirurg gleda u snimak i kaže: *„Ne znam. Postoje dve konkurentske hipoteze ovde, simptomi se preklapaju. Trebaju nam dodatni testovi — konkretno ova tri, plus CT sa kontrastom. Dok ih ne vidim, neću dati odgovor koji bih branio."* U drugom — mladi stažista samouvereno isporučuje dijagnozu za trideset sekundi, oslonjen na sličan slučaj iz udžbenika prošle nedelje. Sigurno. Jasno. Bez sumnje. Kome bi poverio operaciju svoje majke? Trenutno, svaki AI agent koji deplojujemo je drugi doktor. Samouveren. Brz. Nikad ne kaže *„ne znam."* I upravo zato im ne možeš poveriti ništa bolnije od prepravljanja README-a. Danas — kako to promeniti. Ne algoritamski. **Arhitektonski.** --- ### Trula metrika koja nas je sve otrovala Postoji nepisani konsenzus u industriji koji smatram katastrofom: merimo modele i sisteme po **accuracy** — procentu tačnih odgovora na benchmark-u. GPT-4 ima 86% na MMLU. Claude — 88%. Gemini — 90%. Bolje, bolje, još bolje. Broj raste. Ono što taj broj **ne** pokazuje: preostali 10–14%. To nisu *„odgovori koje model nije dao."* To su **samouvereno generisani pogrešni odgovori**, vizuelno neodvojivi od tačnih. Model nema svetlo upozorenja za *„nisam siguran ovde."* Sve generiše sa istom tekstualnom samouverenošću. Kad koristiš takav model za pisanje beleški — okej. Kad ga koristiš za produkcijski kod, medicinske odluke, pravna mišljenja, finansijske transakcije — **10% sigurnih halucinacija znači 10% slučajeva u kojima ti sistem laže pravim licem**. Prava metrika za produkcijski AI zvuči drugačije: > **0% sigurno-pogrešnih akcija pri prihvatljivom abstain rate-u.** Ne *„procenat tačnih odgovora."* Već *„procenat pogrešnih akcija"* — nula. I odvojeno — `abstain rate`: koliko često sistem pošteno kaže *„ne znam, treba mi podatak / verifikacija / razjašnjenje."* Nula pogrešnih akcija plus 30% abstain je **deset puta** više production-ready od 90% accuracy sa 10% sigurnih halucinacija. Primeti: nisam rekao *„0% pogrešnih odgovora."* Rekao sam *„0% pogrešnih **akcija**."* Razlika je važna. Odgovor su reči. Akcija je commit, transakcija, dijagnoza, API poziv, izmena u produkciji. Reči se mogu ponovo pročitati i odbaciti. Akcija se već desila. I to razdvajanje između *„odgovora"* i *„akcije"* — to je ono što arhitektonski nedostaje u modernim AI agentima. --- ### Abstain kao first-class ishod U delu 2 ove serije postavio sam sedam principa prave memorije, i drugi je bio `strict mode`. Brzo podsećanje: pre nego što činjenica dospe u prompt kontekst, prolazi kroz **vrata** — izvor, confidence, temporal validity, bez nerešenih kontradikcija. Ako nijedna činjenica nije prošla — sistem vraća `abstain = true`, sa eksplicitnim razlogom. Postoji detalj koji želim da podvučem odvojeno. **Abstain nije greška.** To je **rezultat**. Toliko first-class koliko i *„odgovor"* ili *„akcija."* Ako tvoj AI ima tačno dva moguća ishoda — *„odgovorio"* i *„pogrešio"* — nema arhitektonskog mesta za pošteno *„ne znam."* Što znači da će izmišljati. U razumnom sistemu, postoji **najmanje četiri ishoda**: - **answer** — dovoljno dokaza, odgovor dat, akcija izvršena - **clarification request** — delimični dokaz, treba korisnikov input - **abstain → brain task** — nedovoljno dokaza, zabeleženo kao backlog zadatak sa eksplicitnim zahtevom za podatak - **escalation** — postoji kontradikcija koja zahteva ljudsku reviziju I poslednja tri nisu fallback-ovi. Ne *„kad sve pođe naopako."* Oni su puni, očekivani, dizajnirani putevi. Kad pitam `braincore` da nađe odluku o auth flow-u na projektu na kome radimo tri meseca — nalazi je. Kad pitam o projektu koji sam upravo počeo, gde ništa nije zabeleženo — ne izmišlja. Kaže: *„Nemam dokaze o ovom pitanju. Kreiran brain task: skupiti odluke o auth, izvor — naš trenutni design dokument, vlasnik — ti. Kad popuniš, pitaj ponovo."* Ovo **nije bug**. To je ispravno ponašanje. Primeti šta se desilo: sistem me nije blokirao. Nije rekao *„error, no data."* On je **pretvorio neznanje u zadatak**, koji sad živi u njegovom backlog-u i periodično će se sam podsećati. --- ### Self-Tasking. Mozak sa backlog-om, a ne pasivni search engine Stvar koja me najviše plaši kod modernih *„AI agenata"* je da su **pasivni**. Čekaju prompt. Svaki. Put. Ne pamte ništa između sesija. Nemaju **interni backlog**. Ne shvataju da imaju nerešena pitanja. To nije *„agent."* To je **funkcija u kostimu agenta**. Funkcija uzima input, vraća output. Agent ima ciljeve, stanje i sopstvene zadatke između zahteva. U pravom kognitivnom runtime-u postoji odvojen entitet — **brain tasks**. Automatski se pojavljuju: - `truth.contradiction` — kontradikcija nađena u knowledge graph-u → zadatak za rešavanje - `truth.staleness` — činjenica nije potvrđena dugo vremena → zadatak za verifikaciju - `strict.abstain` — sistem je odbio da odgovori → zadatak za nalaženje dokaza - `selflearn.skill_scorecard` — veština počela često da pada → zadatak za popravku - `specs.evidence_gap` — zahtev bez dokaza pokrivenosti → zadatak za prikupljanje - `tests.failing_coverage` — testovi ne prolaze → zadatak za popravku - `learning.failure_pattern` — detektovan ponavljajući pattern greške → zadatak za generalizaciju u pravilo Svaki zadatak **sam sebi prioritizuje** po jednostavnoj formuli: ```plaintext priority = f(urgency, impact, confidence, risk, effort, dependency_readiness) ``` I u bilo kom trenutku korisnik može da pita: *„pokaži sledećih pet zadataka, zašto su važni, koje mogu bezbedno da uradim sad, koji zahtevaju moj input."* To nije isti chat gde svaki put počinješ sa praznom tablom. To je radno okruženje sa sopstvenom memorijom o tome šta nije urađeno. Ovo je obrt u perspektivi. Ne *„korisnik se pojavi i pita, agent odgovori."* Već *„agent radi u pozadini, akumulira otvorene niti, i sam ti kaže — evo šta je važno sad."* Pokaži mi RAG stack koji to radi. Spojler: ne postoji. Jer **RAG je search engine, ne agent**. I kad neko kaže *„naš RAG-bazirani AI ima agency"* — to je marketinška fikcija. Agency zahteva **interno stanje**, **ciljeve**, **backlog** i **self-assessment**. RAG nema ništa od toga. --- ### Cognitive Runtime > Veličina modela Poslednji mit za rušenje. *„Kad izađe GPT-5 / Claude 5 / Gemini 3 — memorija će se sama rešiti."* Ne. Neće. Nikad. Memorija **nije osobina modela**. To je **osobina sistema** u kome model radi. Analogija: > Čovek ima dobru memoriju ne zato što neuroni brzo računaju. > Čovek ima dobru memoriju zato što postoji hipokampus, neokorteks, konsolidacija tokom sna, emocionalno filtriranje kroz amigdalu, i arhitektonsko razdvajanje između working / episodic / semantic / procedural memory. > **To je infrastruktura, ne računarska snaga.** Učini LLM deset puta većim — memorija se i dalje ne pojavljuje. Izgradi runtime oko postojećeg LLM-a koji implementira sedam principa iz dela 2 plus abstain plus self-tasking — i **slab lokalni model** u tom runtime-u počinje da radi stvari koje GPT-5 sa RAG-memorijom **arhitektonski ne može**. Ne zato što je pametniji. Već zato što **runtime radi za njega ono što ne bi morao da radi sam**: pamti, verifikuje, odbija, sam sebi postavlja zadatke. To je, uzgred, jedini smislen put napred u svetu gde su foundation modeli **commodity**. Kad svi imaju otprilike ekvivalentne Claude/GPT/Gemini — konkurentska prednost može doći samo od **onoga oko modela**. Domain-specific kognitivni runtime. Project-specific memorija. Team-specific pravila. I ova opklada je takođe o privatnosti. O suverenitetu podataka. O činjenici da je **memorija tvog projekta tvoj kapital**, i predaja je third-party vector DB-u za mesečnu rentu je strateška greška koju ćeš primetiti tek za tri godine, kad više ne možeš da odeš. Zato je, uzgred, `braincore` **lokalni** Go binary koji po default-u radi **bez** OpenAI-ja i bez Anthropic-a. Ne zato što sam protiv njih (paying customer sam oba). Već zato što je **arhitektonski ispravan put** runtime u kome je model zamenljiva komponenta, a ne centar gravitacije. --- ### Checklist za sve koji grade AI proizvode upravo sad Ako si pročitao celu seriju i misliš *„okej, slažem se, šta da radim u ponedeljak ujutru?"* — evo deset stavki po kojima možeš početi da se krećeš **bez obzira** da li koristiš `braincore` ili ne. 1. **Izbaci reč „memorija" iz svog stack-a ako imaš RAG.** Zovi to retrieval ili search — odmah skida 80% naduvanih očekivanja. 2. **Uvedi `truth_status` za svaku činjenicu.** Minimum: `hypothesis | confirmed | deprecated`. Ne dozvoli `confirmed` bez `source_ref`. 3. **Uvedi `valid_from` / `valid_until`.** Bilo koja činjenica bez temporal validity je hipoteza, ne činjenica. 4. **Učini abstain first-class ishodom.** Ne *„kad stvari pođu naopako"* — već kao jedan od četiri validna rezultata. 5. **Razlikuj `staging | working | consolidated | archived`.** Ne gomilaj sve u jednu kolekciju. 6. **Negative memory.** Šta se polomilo — beleži eksplicitno, sa linkom na failing test ili commit. 7. **Entity disambiguation.** Nikad ne auto-merge-uj entitete pri niskoj confidence. Kreiraj `ambiguity record`. 8. **Kauzalni lanci za odluke.** Ne „tekst" — `problem → alternatives → decision → reasoning → outcome`. 9. **Lokalno gde je moguće.** Memorija projekta je **tvoj** kapital. 10. **Metrika nije „*procenat tačnih odgovora*."** Već `0% pogrešnih akcija pri prihvatljivom abstain rate-u`. Ne sve odjednom. Izaberi dva-tri i počni. Za mesec dana imaćeš AI sistem kome možeš verovati više nego većini koji postoje. --- ### Epilog. Kognitivna higijena za AI industriju Umoran sam od toga što reč *„memorija"* lepe na svaku vector bazu sa embedding-ima. To je devalvacija termina — kao da nazoveš tabelu sa jednom kolonom `text VARCHAR` knowledge base-om. Tehnički — da. Suštinski — ne. Memorija je: - **struktura**, ne ravna lista - **znanje granice**, ne sigurni bullshit - **kauzalni lanci**, ne chunk-ovi - **entity-aware**, ne string-aware - **temporal-aware**, ne *„kreirano juče, validno zauvek"* - **samokorigujuća**, ne samozavaravajuća - **upravljana**, ne *„gomilaj šta god, sortiraj kasnije"* - **abstain-sposobna**, ne *„uvek odgovara"* Ako tvoj *„AI sa memorijom"* ne radi bar polovinu od ovog — tvoj AI nema memoriju. Ima rezultate pretrage. To nisu iste stvari. Poslednja stvar. Ne kažem ti da bacaš RAG. RAG je odličan alat za svoju klasu zadataka (nađi mi paragraf o X u 100 dokumenata). Kažem ti da **prestaneš da zoveš RAG memorijom** i počneš da gradiš prave kognitivne runtime — sporije, disciplinovanije, sa eksplicitnim vratima i eksplicitnim abstain. To je jedini put ka AI sistemima kojima možeš verovati za bilo šta važnije od prepravljanja README-a. Ako si startup sa *„naš AI ima long-term memory na vector bazi"* u pitch deck-u — zatvori taj slajd, prepravi ga, i za dve godine ćeš sebi zahvaliti. Ako si developer koji se bori sa agentom koji zaboravlja šta si rekao juče — to nije agentova krivica. To je krivica onoga ko ti je prodao search engine umotanog u mozak. Dobar AI agent **nije onaj koji uvek odgovara**. Dobar AI agent **je onaj koji nikad ne preduzme sigurno-pogrešnu akciju**. Između te dve rečenice leži cela provalija koja razdvaja AI tooling 2024. od AI tooling-a kome će se moći verovati u 2027. Izabrao sam svoju stranu provalije. Gradim `braincore` — otvoren, Apache-2.0, u repozitorijumu. Ako prepoznaješ sebe u ovoj seriji — u istom smo brodu. Ako nešto u tvom stack-u radi drugačije — reci mi, zaista želim da znam. Jedna stvar koju ne možeš da uradiš je da ćutiš. --- > **TL;DR cele serije:** > > - **Deo 1:** RAG = Ctrl+F sa embeddings-ima. To je pretraga, ne memorija. Mem0/Letta/Zep — RAG u oblogama. 1M kontekst je RAM, ne disk. > - **Deo 2:** Prava memorija = sedam principa u kombinaciji. Atomske jedinice + lifecycle + truth_status + temporal + kauzalni lanci + AST identitet + internal git + memory scoring + negative memory. Svaki postoji izolovano. Spojeno — drugačiji proizvod. > - **Deo 3:** Metrika za produkcijski AI nije accuracy — već *0% sigurno-pogrešnih akcija*. Abstain je first-class ishod, ne greška. Kognitivni runtime > veličina modela. > > Ako tvoj AI „pamti" preko `vector_db.query(top_k=5)` — ima demenciju prerušenu u sigurnost. Popravi arhitekturu, ne model. --- *Deo 3 od 3. Serija završena. Ako je odjeknulo — podeli. Ako se ne slažeš — reci mi u komentarima, volim suštinske argumente.* --- ## Sedam principa prave memorije za AI agente - URL: https://vbcherepanov.com/sr/articles/seven-principles-of-real-memory-for-ai-agents - Canonical (medium): https://medium.com/@vbcherepanov/seven-principles-of-real-memory-for-ai-agents-3029d7d877ac - Published: 2026-05-06 - Reading time: 8 min - Tags: AI Agents, Memory - Language: sr > Atomske jedinice znanja sa lifecycle-om, strict mode sa abstention pravom, kauzalni lanci odluka, AST identitet koda, internal git, scoring i negative memory — ono što razlikuje pravu memoriju od Ctrl+F. ![Sedam principa prave memorije — naslovna](/images/articles/seven-principles-of-real-memory-for-ai-agents.png) > **Deo 2 od 3 — „Memorija za AI agente"** > Arhitektura. Konkretno. Sa formulama i lifecycle-om. U prethodnom postu razložio sam pitch *„RAG = memorija"* na tri neugodna problema: chunk ne zna da je chunk; retrieval nema strukturu, samo cosine; vreme ne postoji kao first-class koncept. Ukratko — RAG je pretraga obučena u marketinšku reč *„memorija."* Danas — šta bi trebalo da bude **umesto toga**. Odmah disclaimer. Ne tvrdim da sam izmislio nijednu stavku sa ove liste. Atomske činjenice idu unazad do Wittgensteina. Temporal validity je osnovna logika. Knowledge graph-ovi su celo polje sa udžbenicima. Lifecycle za podatke je standard u svakom normalnom informacionom sistemu. Tvrdim nešto drugačije. Tvrdim da **svih sedam osobina mora da radi u jednom sistemu istovremeno**, i da bilo koji sistem u kome zapravo radi samo pet od sedam nastavlja da laže korisniku samouverenim licem. Postoji samo jedan način da se to vidi — pokušaj da sklopiš svih sedam u jedan kodbeis i posmatraj šta se dešava. Probao sam. Radilo je. Nazvao sam ga `braincore`. Open source, Apache-2.0, jedan Go binary, MCP-stdio. Neću pretvoriti tekst u pitch — ali u svakoj sekciji ispod dodaću jednu liniju o tome kako se to radi u `braincore`-u, da bude jasno da ne pričamo teoriju. Idemo. --- ### Princip 1. Atomic Knowledge Units sa lifecycle-om, ne „chunk-ovi u Qdrant-u" **Bol.** U RAG-u, bilo koji dolazni tekst — dijalog, design dokument, git commit, transkript sastanka — biva isečen na chunk-ove i poslat u vektor bazu bez pitanja. Odatle, šta god da se dešava — svi chunk-ovi su ekvivalentni, svi podjednako „sveži," svi podjednako „istiniti." Šest meseci kasnije, jedna kolekcija drži supu od zastarelih, trenutnih, hipotetičkih i opovrgnutih činjenica. I svaka od njih ima tačno jednu šansu da uđe u retrieval — preko cosine. **Šta bi trebalo da bude u shemi.** Bilo koja dolazna informacija **ne ulazi u memoriju direktno**. Prolazi kroz pipeline: ```plaintext input → initial trust (po izvoru: user=0.9, llm=0.3, web=0.4..0.7) → parse (entity / fact / relation / event / rule / hypothesis) → atomic knowledge units → validate (source / graph / dedup / contradiction / temporal / rule) → link (najmanje 1 ivica u graf ILI review item) → working memory (TTL + activation) → iterative verification loop → consolidation → long-term memory (samo potvrđeno + povezano) → edge strengthening (usage + success + co-occurrence − decay) ``` Osnovno pravilo: **ništa ne ulazi u long-term memory odmah**. Svaka atomic knowledge unit ima minimalno: - `truth_status`: `hypothesis | candidate | confirmed | contradicted | deprecated` - `lifecycle`: `staging | working | consolidated | archived` - `source_ref` — odakle je došlo - `confidence` — numerička procena sigurnosti - `valid_from` / `valid_until` — kad je istinito Uporedi to sa RAG chunk-om koji ima samo `text` i `embedding`. To je razlika između fioke za smeće i magacina sa inventarom. **Šta to omogućava.** Kad si juče rekao *„koristimo Postgres"* a danas *„prešli smo na ClickHouse, Postgres je sada samo OLTP"* — stara činjenica automatski dobija `valid_until = today` i `superseded_by = new_fact_id`. Pri retrieve-u, ili se uopšte ne pojavljuje, ili dolazi obeležena *„istorijsko, ne trenutno."* Ne zbog pametnog modela. Zbog **sheme**. **Kako braincore to radi.** Pipeline `staging → working → consolidated` je implementiran bukvalno — tri odvojene SQLite tabele plus posrednu verification loop. Zapis dospeva u `consolidated` samo ako je `truth_status = confirmed`, ima bar jednu graf ivicu, nema nerešenih kontradikcija, i `confidence ≥ threshold`. Inače ostaje u `working` sa TTL-om, ili se premešta u `review queue`. --- ### Princip 2. Strict Mode i pravo na abstain Ovo je možda najvažnija tačka u celoj seriji. I najodsutnija iz komercijalnih memory framework-a. **Bol.** Standardna metrika po kojoj se AI sistemi mere — *„koliko često daju ispravan odgovor."* Ovo je **trula** metrika. 95% tačnih odgovora i 5% sigurnih halucinacija je sistem **kome ne možeš verovati u produkciji**. Jer ne znaš unapred u kojih 5% si trenutno. Prava metrika glasi: > **0% sigurno-pogrešnih akcija pri prihvatljivom abstain rate-u.** Ne *„uvek odgovori."* Već *„nikad ne preduzimaj pogrešnu akciju bez verifikacije."* A ako verifikacija nedostaje — **reci „ne znam"** i sam sebi dodeli zadatak da to popraviš. **Šta bi trebalo da bude u shemi.** Pre nego što činjenica dospe u prompt kontekst, prolazi kroz **vrata**: - da li postoji `source_ref`? - `confidence ≥ threshold`? - `trust_score ≥ threshold` (za izvor)? - `temporal_valid == true` (validno u vreme upita)? - nema nerešenog `contradiction` u grafu? - nema nerešenog `ambiguity`? Ako čak jedan zahtev ne uspe — činjenica **ne stiže** do konteksta. Ako nijedna činjenica nije prošla za upit — sistem vraća `abstain = true` sa `reason = no_accepted_facts` (ili `contradiction_unresolved`, ili `temporal_invalid` — uvek eksplicitno). I — pažnja, ovde se dešava magija — **abstain se ne dostavlja korisniku kao ćorsokak**. Postaje **brain task** u backlog-u: *„Treba mi dokaz za X da odgovorim sa sigurnošću. Izvor je tu, konkretan konflikt je tu."* Sistem zna šta ne zna, i sam sebi dodeljuje zadatak da to popravi. **Šta to omogućava.** AI agent kome možeš verovati. Ne zato što je uvek u pravu — već zato što kad nije siguran, **ćuti** ili **traži razjašnjenje**. A kad preduzima akciju — akcija je utemeljena na činjenicama koje su **prošle vrata**, ne „pa, ChatGPT je mislio da je ovo bolje." Pokaži mi jedan RAG stack koji to radi. Sačekaću. **Kako braincore to radi.** Paket `internal/strictmode` je odvojen modul sa eksplicitnim pravilima vrata. Po default-u, svaki upit prolazi kroz strict mode; za UX scenarije gde abstain nije prihvatljiv (brainstorming, na primer), možeš ga isključiti eksplicitnim flag-om `--allow-uncertainty`. Svi abstain događaji se loguju kao brain tasks sa svojim izvorom i razlogom. --- ### Princip 3. Causal Decision Chains, ne ravne činjenice **Bol.** U RAG-u, bilo koja odluka se čuva kao *„tekst o odluci."* Pri retrieve-u, dobiješ komad teksta koji **opisuje** odluku — ali ne odgovara na *„zašto?"*, *„koje smo alternative razmatrali?"*, *„šta je iz toga ispalo?"* Šest meseci kasnije, pitaš *„zašto smo izabrali JWT umesto sesija?"* — RAG vraća tri fragmenta deklaracije, i model sam popunjava reasoning. Ponekad ispravno. Ponekad izmišlja iz popularnih pattern-a u svojim trening podacima. Ne znaš koje od to dvoje je ovog puta. **Šta bi trebalo da bude u shemi.** Entitet nije *„dokument"* niti *„memory entry."* Entitet se zove **decision** i ima shemu: ```plaintext problem → šta smo rešavali alternatives → šta smo razmatrali i odbacili (sa razlozima) decision → šta smo izabrali reasoning → zašto baš to outcome → šta je iz toga ispalo (popunjava se kasnije, post-hoc) superseded_by → link na novu odluku ako je ova revidirana ``` Ovo nije *„daj da nabacimo tekst u embedding."* Ovo je **kauzalni lanac** koji odgovara na **ZAŠTO**, ne samo na **ŠTA**. **Šta to omogućava.** Šest meseci kasnije, pitaš *„zašto JWT?"* — sistem vraća strukturirani odgovor: - **Problem:** skaliranje sesija + audit zahtevi. - **Alternatives (rejected):** stateful sesije sa Redis-om (krši audit), opaque tokeni sa centralizovanim lookup-om (latency). - **Decision:** JWT sa kratkim TTL-om. - **Reasoning:** stateless, audit-neutralan, latency prihvatljiv. - **Outcome (zabeleženo 4 meseca kasnije):** kompleksnost invalidacije veća od očekivane; dodali refresh tokene. - **Superseded by:** none. RAG vraća tri fragmenta. Decision chain vraća **reasoning**. To su različiti proizvodi. **Kako braincore to radi.** Decisions su odvojen tip entiteta u grafu sa obaveznim poljima `problem`, `alternatives[]`, `decision`, `reasoning`, i opcionim `outcome`/`superseded_by`. Čuvaju se ne kao chunk-ovi već kao strukturirani zapisi sa eksplicitnim ivicama u code graph i u druge decisions. --- ### Princip 4. Stabilan identitet koda kroz AST, ne stringove **Bol.** Ovaj princip je specifičan za AI agente koji rade sa kodom — ali pogađa sve njih. Preimenovao si `GetUser → FetchUser`, premestio iz `pkg/auth` u `pkg/user`, promenio signaturu sa pointer receiver-a na value receiver. Sve reference u RAG memoriji koje pokazuju na *„GetUser u pkg/auth"* sad su **mrtve**. Jer je RAG vezan za **stringove**. I niko ti to ne kaže. Chunk nastavlja da živi u Qdrant-u, njegov cosine na auth-related upite ostaje visok. Agent vuče mrtvu informaciju i radi protiv nje. Čestitke, imaš memory rot maskiran kao memorija. **Šta bi trebalo da bude u shemi.** Parsiranje koda kroz `go/ast` (za Go) i tree-sitter (za PHP, JS, TS, Python, Rust, Java, i dalje). **Identitet čvora** se gradi ne od stringa i ne od putanje fajla, već od strukturnog hash-a: ```python node_id = sha256(qualified_name + kind + signature_hash) ``` Što znači: - Preimenovanje funkcije **ne lomi** reference ka njoj (`qualified_name` se promenio, ali se link automatski ažurira pri sledećem parse-u, sa back-referencom na stari `node_id` kao `renamed_from`). - Premeštanje između paketa — ista stvar. - Promena signature (pointer → value receiver) — `signature_hash` se menja, i stare reference **automatski se obeležavaju kao `stale`** — mozak **zna** da sada zahtevaju review. **Šta to omogućava.** Kad AI agent treba da edituje `FetchUser`, sistem podiže tri prethodne odluke o toj funkciji, dve regresije u ovom modulu, i aktivna pravila projekta — **pre** nego što agent počne da piše kod. Ne zato što se cosine slučajno poklopio. Već zato što je to **code graph**, i `FetchUser` ima ivice ka odlukama, regresijama i pravilima **po identitetu**, ne po tekstualnoj sličnosti. Ovo nazivam pre-edit warning. I to je kvalitativno drugačija vrsta prevencije grešaka od *„hajde da pokrenemo linter posle generacije."* **Kako braincore to radi.** Code graph je odvojen sloj iznad AST/tree-sitter, sa background reindex-om na filesystem watch događaje. Identity hash-evi žive u SQLite, ivice takođe tu. Pri pre-edit hook-u, agent dobija kontekst povezanih decisions/rules/regressions automatski. --- ### Princip 5. Internal Git kao memory versioning **Bol.** RAG nema koncept vremena izvan `created_at`. To su metapodaci o **zapisu**, ne o **stanju znanja**. Ne možeš da pitaš *„pokaži mi šta sam znao o ovom kodu pre mesec dana."* Ne možeš da rollback-uješ stanje memorije pre nego što je agent dovukao smeće. Ne možeš da pređeš na feature granu i imaš paralelno mentalno stanje za nju. **Šta bi trebalo da bude u shemi.** Svaka promena u memoriji je **commit**. Ne metaforički. Bukvalno, kroz `go-git`, u skriveni `.internal-git/` repozitorijum koji živi paralelno sa glavnim repo projekta. To ti daje: - `git log` preko **memorije** projekta — šta je dodato, šta se promenilo, kad. - `git checkout` da bi rollback-ovao stanje mozga unazad N dana — za audit, za istraživanje regresija, za testove. - Kad pređeš na feature granu u glavnom repo-u, mozak to **ogleda**, i svaka grana ima svoje mentalno stanje. Eksperiment u feature grani ne zagađuje master memoriju. **Šta to omogućava.** Time-travel upiti: *„koju sam odluku smatrao trenutnom pre 30 dana?"* Audit: *„kad je tačno agent počeo da veruje da koristimo ClickHouse?"* Branch izolacija: *„u feature/oauth imamo drugačiji pristup auth-u, ali to znanje ne bi trebalo da curi u main."* RAG to ne može. RAG nema koncept *„stanje znanja"* — samo set vektora koji raste. **Kako braincore to radi.** `.internal-git/` se kreira pri `braincore init`. Commit-ovi se prave automatski pri svakoj promeni knowledge units i graf ivica. Branch tracking je sinhronizovan sa glavnim git-om kroz post-checkout hook. --- ### Princip 6. Memory Scoring — jer nije svako znanje jednako **Bol.** U RAG-u, svi chunk-ovi su jednaki. Top-k po cosine ne razlikuje *„ovo je potvrđeno deset puta u prošlom korišćenju"* od *„ovo je napisano juče i nikad više nije korišćeno."* Ne razlikuje *„ovo je kritično za arhitekturu"* od *„ovo je slučajna beleška u uglu."* Ne razlikuje *„ovo je u aktivnoj upotrebi"* od *„ovo skuplja prašinu od prošle godine."* **Šta bi trebalo da bude u shemi.** Svaka knowledge unit ima kompozitni `MemoryScore`, izračunat kao otežana suma: ```plaintext MemoryScore = + 0.22 * ImportanceScore (eksplicitna važnost ili izvedena iz povezanosti) + 0.22 * TrustScore (pouzdanost izvora + istorija potvrda) + 0.20 * TaskRelevanceScore (relevantnost za trenutni kontekst rada) + 0.12 * UsageScore (koliko često se koristi) + 0.10 * RecencyScore (svežina) + 0.10 * StabilityScore (koliko često se menja — stabilno je pouzdanije) + 0.08 * NoveltyScore (novost kao mali boost) − 0.18 * RiskScore (potencijalna šteta od korišćenja) − 0.18 * NoiseScore (šum, duplikati, niska koherentnost) ``` I pri retrieve-u, ono što radi **više nije cosine similarity**, već: ```plaintext RetrievalScore = + 0.35 * semantic_similarity + 0.20 * memory_score + 0.15 * graph_relevance + 0.15 * temporal_validity + 0.10 * trust_score − 0.15 * ambiguity_penalty ``` Ovi težinski koeficijenti nisu apsolutna istina — empirijski su podešeni i pomeraju se sa profilom korišćenja. Poenta nije u brojevima, već u **arhitektonskom pomeranju**: retrieval prestaje da bude *„tekstualna sličnost"* i postaje *„sličnost × važnost × poverenje × svežina."* Lifecycle prelazi automatski: - `memory_score ≥ 0.80` i `trust ≥ 0.75` → `consolidated` (znanje postaje „firmware") - `memory_score ≥ 0.55` → ostaje u `working` - `memory_score ≥ 0.30` → `staging` - `memory_score < 0.30` → `archive candidate` **Šta to omogućava.** Aktivnu memoriju. Ne skladište. **Aktivno okruženje** u kome se važno jača kroz korišćenje, a šum **sam opada** — kao u biološkom mozgu, gde retko korišćene sinapse slabe a često korišćene jačaju. > RAG = hard disk koji se nikad ne defragmentira. > Brain = mozak u kome đubre **samo se sleže** i automatski arhivira. **Kako braincore to radi.** Scoring se ponovo računa background job-om svakih N sati. Lifecycle prelazi su atomski i logovani (vidi Princip 5). Svi težinski koeficijenti su izloženi u config-u — podesi ih po projektu. --- ### Princip 7. Negative Memory i Rule Engine **Bol.** Evo šta svaki LLM agent radi danas: **ponavlja greške**. Juče je polomio migraciju — danas će polomiti sličnu. RAG neće pomoći, jer **polomljena migracija ne ulazi u RAG**. Ono što ulazi u RAG je *„kako pisati migracije"* iz zvanične dokumentacije. Činjenica da si ti lično već stao na ove grabulje — nigde nije zabeležena. **Šta bi trebalo da bude u shemi.** Odvojena klasa — **negative memory**: šta se polomilo, zašto se polomilo, kako je popravljeno, koji commit/test to potvrđuje. First-class entitet, ne marginalno polje. I pri planiranju, svaki patch prolazi kroz **Rule Engine** pre generacije koda: ```plaintext patch → architectural rules → code rules → security rules → performance rules → anti-patterns (uključujući „ovaj baš sam pre lomio") → repair plan ILI abstain ``` Ako je prekršeno pravilo sa severity `critical` ili `high` — **kod se ne piše**. Kreira se repair plan. Ako je popravka nemoguća — `abstain` (vidi Princip 2). Bez „nadajmo se da ovo prođe" generacije. I, kritično, **safe execution pipeline** zatvara petlju: ```plaintext checkpoint → apply patch → rules validate → build → tests → success → commit → fail → rollback → zapis u negative memory ``` Svaka **izvršena** akcija je ili potvrđena testovima, ili rollback-ovana, ili zapisana kao **negative evidence** za buduće odluke. **Šta to omogućava.** Agenta koji **ne može** da ponovi tvoju prošlogodišnju grešku. Ne zato što ima odličan model — već zato što **rule engine fizički odbija da pusti** bilo kakav patch koji krši pravilo izvedeno iz te greške. > RAG pomaže agentu da nešto nađe. Dobra memorija **sprečava** agenta da nešto polomi. To su različiti proizvodi. I žao mi je onih koji ih nastavljaju da mešaju. **Kako braincore to radi.** Negative memory je odvojen tip entiteta sa obaveznim linkom na failing test ili git commit. Rule engine je pre-execution gate, severity-aware, sa override mogućim samo kroz eksplicitnu korisnikovu potvrdu. --- ### Bonus princip. Entity Disambiguation Formalno specijalan slučaj Principa 1 (atomske jedinice), ali se lomi odvojeno dovoljno često da zaslužuje svoj poziv-aut. U RAG-u, ne postoji koncept **entiteta**. Postoji samo tekst. Ako tvoj projekat ima dve `User` klase — jednu u `pkg/auth`, drugu u `pkg/billing` — za RAG to su dva komada teksta sa sličnim embedding-ima. Pri retrieve-u, **mešaju se**, i model samouvereno objašnjava auth logiku u kontekstu billing-a. Ovo nije teorija. Ovo se dešava **upravo sad** u svakom code RAG agentu. Popravka — **EntityFingerprint**: ```python fingerprint(symbol) = hash( project_id + file_path + symbol_name + symbol_type + signature + language ) ``` Dva `User` entiteta u različitim fajlovima = dva fingerprint-a = dva različita entiteta koji se **nikad automatski ne spajaju**. Kad stigne novi kandidat, izračunava se `SameEntityScore`: ```plaintext SameEntityScore = + 0.30 * name_similarity + 0.20 * alias_match + 0.20 * context_similarity + 0.15 * graph_neighborhood_similarity + 0.10 * temporal_consistency + 0.05 * source_consistency ``` I: - `≥ 0.92` → `auto_merge` - `≥ 0.82` → `same_as` link (mekan link, ne merge) - `≥ 0.65` → `ambiguous` — kreira se **ambiguity record**, koji zahteva ljudski review - inače — novi entitet Osnovno pravilo: **nikad ne spajaj entitete pri niskom confidence-u**. Bolje kreirati ambiguity zapis i pitati čoveka, nego ih tiho zalepiti i lagati zauvek posle. --- ### Zašto sve ovo zajedno Namerno ne uobličavam ovo kao *„nigde ovo nije urađeno, prvi sam."* Svaki od sedam principa već postoji. Atomske činjenice sa lifecycle-om — u sistemima knowledge management. Strict mode + abstain — u expert sistemima prošlog veka. Kauzalni lanci — u alatima decision support. AST identitet — u IDE-ima. Internal git — u alatima poput Pijul-a i u Datalog database eksperimentima. Memory scoring — u istraživačkim radovima o episodic memory. Negative memory — u RL i reliability engineering-u. Jedinstvenost nije u idejama. To je u **sklapanju**. Ako imaš atomske jedinice ali nemaš strict mode — imaš strukturiranu bazu halucinacija. Ako imaš strict mode ali nemaš kauzalne lance — odbijaš bez razumevanja zašto. Ako imaš kauzalne lance ali nemaš AST identitet — tvoje odluke pokazuju u prazno posle dva refaktorisanja. Ako imaš sve gore navedeno ali nemaš memory scoring — imaš savršeno strukturiran dump u kome važno tone u šumu. Svaka osobina izolovano je poboljšanje. Svih sedam zajedno je drugačija kategorija proizvoda. Ovo je, uzgred, odgovor na pitanje koje najčešće dobijam: *„zašto pisati nešto novo ako već imam Mem0/Letta/Zep?"* Odgovor — pogledaj njihove sheme i proveri koliko od sedam principa je implementirano **ne kao marketinški claim, već kao enforced gate u kodu**. Za većinu, iskren broj je dva ili tri. Za neke — četiri. Nisu loši proizvodi. Oni su **delimična rešenja**, koja je iskrenije zvati *„structured retrieval"* nego *„memorija."* --- ### U Delu 3 Sedam principa je inženjering. Šta **bi trebalo da bude** u arhitekturi. Ali iza inženjeringa stoji dublje pitanje: **zašto bi AI agent trebalo da zna šta ne zna?** Zašto uopšte abstain, ako može jednostavno da odgovori? Deo 3 je o pravu AI agenta da ćuti. O self-tasking-u. O tome zašto je kognitivni runtime važniji od veličine modela. I o tome zašto prava metrika za produkcijski AI nije accuracy, već *zero confidently-wrong actions at an acceptable abstain rate*. To je najkraći i najfilozofskiji deo serije. Izlazi sledeće nedelje. --- *Deo 2 od 3. Ako si propustio [Deo 1 — ovde](https://medium.com/@vbcherepanov/rag-isnt-memory-it-s-ctrl-f-with-embeddings-c461b90ac7b1) (o tome zašto je RAG pretraga a ne memorija). Ako je odjeknulo — repost bi pomogao.* --- ## RAG nije memorija. To je Ctrl+F sa embeddings-ima. - URL: https://vbcherepanov.com/sr/articles/rag-isnt-memory-its-ctrl-f-with-embeddings - Canonical (medium): https://medium.com/@vbcherepanov/rag-isnt-memory-it-s-ctrl-f-with-embeddings-c461b90ac7b1 - Published: 2026-05-01 - Reading time: 7 min - Tags: AI Agents, Memory - Language: sr > Vector search je pretraga, ne memorija. Tri propusta: chunks gube smisao, nema strukturne razlike među činjenicama, nema temporalne validnosti. Zato RAG halucinira sa sigurnošću. ![RAG nije memorija — naslovna](/images/articles/rag-isnt-memory-its-ctrl-f-with-embeddings.png) > **Deo 1 od 3 — „Memorija za AI agente"** > Razlažemo mit o long-term memory u LLM sistemima 3 ujutru. Treća noć debag-ovanja AI agenta. Stojim u kuhinji sa šoljom čaja, gledam u diff i tiho psujem. Agent je samouvereno prepisao auth funkciju — na osnovu chunk-a koji pripada grani obrisanoj iz repozitorijuma pre dva meseca. Chunk živi u Qdrant-u. Cosine sličnost sa mojim upitom je visoka. Top-1 u retrieval-u. Agent ga je pošteno uzeo, pošteno ušio u prompt, pošteno generisao „ispravan" patch. Protiv koda iz druge realnosti. Zatvaram laptop i mislim: okej, imam RAG. Imam vektore. Imam long-term memory. Imam sve što svaki AI konferencijski deck obećava poslednje dve godine. Zašto je moj agent upravo predložio fix za kod koji više ne postoji? Zato što moj agent nema memoriju. Moj agent ima rezultate pretrage sa cosine umesto BM25. I između te dve rečenice leži cela razlika između *„AI kojem možeš verovati u produkciji"* i *„AI iza kojeg moraš da paziš na svakoj liniji."* Ovaj tekst je o toj razlici. I o tome zašto smo mi, inženjeri, krivi što to više ne vidimo. --- ### Devalvacija reči „memorija" Budimo iskreni. Šta je tipična „memorija" AI agenta u 2026? ```plaintext tekst → podela na chunkove 512-1024 tokena → embedding (bge / text-embedding-3 / openai) → vector DB (Qdrant / pgvector / Chroma / Pinecone) → cosine similarity top-k → konkatenacija u prompt ``` Ovo **nije** memorija. Ovo je pretraga. To je stari Lucene iz 2003., prefarban u nervne boje. Cosine umesto TF-IDF. Embeddings umesto inverted indexa. Ista stvar. Da to tako i zovemo — *„vector search,"* *„semantic retrieval"* — ne bih imao primedbu. Zovi Lucene Lucene, nema problema. Ali kad se to prodaje pod barjakom *„moj AI ima long-term memory"* — izvini. Moj AI ima istovremeno déjà vu i amneziju. Ovo nije terminološka pritužba. Ovo je pitanje očekivanja. Kad inženjer čuje *„memorija,"* zamišlja sistem koji **pamti**: ko je šta rekao, kad, u kom kontekstu, šta je tad bilo tačno naspram šta je sad tačno. Kad inženjer dobije RAG, dobije Ctrl+F. I umesto da gradi iskrenu arhitekturu oko tog Ctrl+F — sa iskrenim ograničenjima — gradi zamak od peska i čudi se zašto agent meša prošlost sa sadašnjošću. --- ### Tri rupe kroz koje može da prođe kamion Tri konkretna propusta. Svaki sam ulovio u produkciji. Nije teorija. **Rupa #1: Chunk ne zna da je chunk.** Uzmi sasvim normalnu deklaraciju iz design dokumenta: > *„Prešli smo na JWT jer opaque sesije nisu mogle da skaliraju za naš profil saobraćaja. Alternativa su bile stateful sesije sa Redis klasterom, ali smo ih odbacili zbog audit zahteva klijenta — ne dozvoljava state sesija van perimetra. JWT rešava oba, ali dodaje kompleksnost invalidacije, koju ublažavamo kratkim TTL-om i refresh tokenima."* Chunker ovo deli na četiri komada od 512 tokena. Pri retrieval-u dolazi upit: *„zašto smo izabrali JWT?"* Top-3 vraća tri fragmenta iste odluke. Bez kauzalnosti. Bez alternative koju smo odbacili. Bez trade-off-a koji smo prihvatili. Odluka koja je bila **celovita** pretvara se u tri paralelne „činjenice". Model ih pošteno spaja u uverljiv tekst — i **izmišlja** veze koje nedostaju. Jer mu je posao da generiše uverljiv tekst. I to će uraditi, bez treptanja. Ovo nije bug u chunker-u. Ovo je arhitektonska osobina celog pristupa. Svaka deklaracija odluke biva mlevena u prah i ponovo sklapana sa strukturnim gubicima. Svaki put. **Rupa #2: U memoriji nema strukture. Samo cosine.** Kad ti čovek objašnjava projekat, on kaže: - *evo cilja* - *evo opcija koje smo razmatrali* - *evo šta smo izabrali i zašto* - *evo šta se polomilo dva meseca kasnije* - *evo šta smo promenili, i ta odluka sada zamenjuje staru* U RAG-u ništa od ovoga ne postoji. Nula. RAG ne razlikuje *„hipotezu,"* *„potvrđenu činjenicu,"* *„odbačenu alternativu,"* *„deprecated odluku premeštenu u arhivu."* Za RAG, sve su to ekvivalentne tačke u 384-dimenzionalnom prostoru. Zamisli da pokušavaš da snimiš trideset godina života u jednu ravnu tabelu `entries(text, vector)` i onda je pretražuješ po cosine. Iznenađen što ti se sećanja zamagljuju? To nije tvoja memorija što popušta. To je struktura u koju si je nabio — struktura koja ne dozvoljava razlikovanje između *„razmišljao sam o tome"* i *„uradio sam to,"* između *„probao sam i radilo je"* i *„probao sam i bolelo je."* U RAG-u ne postoje polja za ove razlike. Ne zato što developeri nisu mislili na to. Zato što **sama paradigma vektor-plus-distanca** ne smešta kauzalnost i vreme. To je matematičko ograničenje. Ne popravlja se product features-ima. **Rupa #3: Vreme ne postoji kao first-class koncept.** Pre tri nedelje sam u memoriju agenta zapisao: *„koristimo Postgres."* Danas sam zapisao: *„prešli smo na ClickHouse za analitiku, Postgres je sada samo OLTP."* U RAG-u **obe** činjenice tu sede. Obe imaju visok cosine prema upitu o bazi. Top-k vraća obe. Model bira onu koja „zvuči" bolje u svom pretrain-u — obično Postgres, jer se češće pojavljuje u trening podacima. Ovo **nije** memorija. Ovo je rulet prerušen u sigurnost. Kad si poslednji put video polja `valid_from`, `valid_until`, `deprecated_by`, `replaced_by`, `superseded_by` u produkcijskom RAG sistemu? Ja nikad. Jer u standardnom RAG-u, **nisu u shemi**. I opet — ne zato što su devovi lenji. Zato što shema *„text plus embedding"* nema mesto za lifecycle znanja. Bez pojma *„ovo je tačno sada"* naspram *„ovo je bilo tačno tad."* Sve se kolapsira u jedan vremenski presek — sadašnjost koja nekako sadrži juče, prošlu godinu i deprecated-pre-tri-kvartala u isto vreme. > Ctrl+F sa embeddings-ima ne **pamti**. On **nalazi**. Različiti glagoli. --- ### „Ali memory framework-ovi to popravljaju, zar ne?" Okej, kaže verujući. Postoji mem0, Letta, Zep, Cognee, MemGPT, ceo zoološki vrt long-term memory. Dodali su sloj smisla iznad RAG-a. Memory-aware su. Budimo iskreni. Koristio sam ih. Jednog za drugim. Dugo. Gledao ispod haube, ne samo landing stranice. Svaki od njih uzima **jedan** komad prave memorije — kod nekih je to LLM-ekstrakcija pre upisa, kod nekih buffer hijerarhija kao kod OS-a, kod nekih post-hoc graph ekstrakcija iz dijaloga, kod nekih per-fact temporal validity — i implementira **tu jednu stvar**, bez upletanja u ostalo. To je toplije od vanilla Qdrant-a. To **nije** rešenje. Jer prava memorija zahteva **sedam** osobina koje rade zajedno. Svaka od njih, izolovano, već postoji u literaturi ili u open source-u. Koliko mogu da kažem, niko nije sklopio svih sedam u jedan sistem. Kojih sedam tačno — to je deo 2 ove serije. Ovde, samo ograničenje koje ujedinjuje **sva** flat-fact rešenja, kako god se obavijala: **Nijedno od njih nema pravo da kaže „ne znam."** Pokaži mi bilo koji od ovih sistema sa formalnim abstain mehanizmom: vratima kroz koja činjenica **neće** proći u prompt kontekst ako nema izvor, nema confidence, nema temporal validity, ili nerešenu kontradikciju. Sačekaću. U standardnom flow-u svih ovih framework-a, odgovor sistema na *„u memoriji je kontradikcija ili nema dovoljno podataka"* je *„pa, model će već da se snađe."* Što se prevodi sa marketinškog na inženjerski kao *„model će halucinirati, i to postaje tvoj problem u produkciji."* Dobra memorija nije *„pamtiti puno."* To je **znati granicu onoga što ne pamtiš**. Deo 2 ove serije izgrađen je oko te teze. --- ### „Zašto jednostavno ne push-ovati kontekst do 1M tokena?" Ovo je druga moda poslednje dve godine, i zaslužuje svoje razlaganje, jer vodi industriju u istu slepu ulicu pod drugačijim barjakom. *„Zašto nam treba memorija ako Gemini ima 2M kontekst, Claude ima 1M?"* Četiri problema, bez preambule. **Jedan — ekonomija.** Jedan projektni razgovor od 800K tokena sa isključenim prompt cache-om košta desetine dolara **po zahtevu**. Bez agresivnog cache-ovanja, bankrotirao si za nedelju. Sa agresivnim cache-ovanjem, gradiš tačno istu hijerarhiju kao Letta — samo skuplje i zaključano za jednog vendora. **Dva — recall.** Svaki long-context benchmark (NIH, Ruler, LongMemEval) pokazuje istu stvar: modeli **utapaju** se u sopstvenom kontekstu posle 200-300K tokena. Pažnja je nejednako raspoređena. Ovo je **lost-in-the-middle**, i ne popravlja se veličinom prozora — delom se ublažava arhitektonskim trikovima unutar modela, ali ne nestaje. Što više ubaciš, manje od toga se zaista uzima u obzir. **Tri — persistencija.** Kontekst se ne čuva. Zatvori sesiju, nema ga. Sutra se isti agent pojavi sa praznim kontekstom. Znači moraš ponovo da mu nahraniš 800K tokena „istorije". Problem nije rešen — sakriven je u tvom novčaniku i u tvojoj latency. **Četiri — učenje.** Ako je agent juče pogrešio i ti si ga ispravio, to iskustvo nije strukturirano za budućnost. Sutra će ponoviti grešku. Kontekst je RAM, ne disk. I kad neko kaže *„samo povećaj kontekst umesto da gradiš memoriju"* — to je isto kao da kaže *„zašto mi treba baza, imam terabajt RAM-a."* Tehnički se reči rimuju. U praksi su to neuporedivi koncepti. Veliki kontekst ne zamenjuje memoriju. Dozvoljava ti da nabiješ više u jednu sesiju — i to je sve. --- ### Šta da radiš sutra ujutru Ako si pročitao do ovde i misliš *„okej, slažem se, RAG je pretraga, ne memorija. Šta sad?"* — imam dve vesti. Loša: sistemski ispravno rešenje zahteva prepisivanje memory sloja od sheme do lifecycle-a, i to su meseci rada. Ne vikend. Dobra: postoji nekoliko stvari koje možeš da uradiš **sutra ujutru** i već skineš pola bola. Nije magija — samo inženjerska higijena. - **Izbaci reč „memorija" iz svog stack-a ako imaš RAG.** Zovi to retrieval ili search — odmah iskrenije. Samo to skida 80% naduvanih očekivanja korisnika i tima. - **Uvedi `valid_from` i `valid_until` za svaku činjenicu.** Bilo koja činjenica bez temporal validity je hipoteza, ne činjenica. Stare činjenice treba automatski da ispadnu iz retrieval-a, ne da se takmiče sa novima na cosine. - **Razlikuj `staging`, `working`, `consolidated`, `archived`.** Ne gomilaj sve u jednu kolekciju. Tek pristigla činjenica i znanje potvrđeno testovima — različiti su entiteti sa različitom težinom u retrieval-u. - **Učini abstain first-class ishodom.** Ako u retrieve-u nijedna činjenica nije prošla confidence prag, sistem **mora** da ima pravo da kaže *„ne znam, treba mi podatak."* I to *„ne znam"* treba da postane zadatak u backlog-u, ne ćorsokak za korisnika. Ovo nije kompletna lista — to je minimum da pokreneš tranziciju iz *„imam RAG, zovem ga memorija"* u *„imam memoriju, i ona zna svoje granice."* Puna lista od sedam principa je u delu 2. --- ### Odakle ovo dolazi Sedim duboko u ovoj kuhinji — Claude Code, Cursor, Codex, Windsurf, MCP serveri, mem0, Zep, lokalni RAG stack-ovi na Postgres + pgvector, Qdrant, Chroma. Poslednjih nekoliko meseci sam isprobao, mislim, sve što postoji na tržištu. Imam svoj MCP memory server sa oko hiljadu petsto unosa, koji sam tri puta prepisao iz nule, jer sam svaki put udario u jednu od tri rupe iznad. U jednom trenutku, umorio sam se. Ne od AI-ja — od onoga što zovemo memorija kod AI-ja. Seo sam i počeo da pišem svoj kognitivni runtime koji **ne pretvara se da zna**, koji **zna šta ne zna**, i koji **sam sebi postavlja zadatke** da zatvori praznine. Nazvao sam ga `braincore`. Jedan Go binary, lokalni, MCP-stdio, Apache-2.0. Ne pitch jer je open source — samo primer da kažem *„ovo se može"* ne teorijski. Sedam arhitektonskih principa na kojima je izgrađen — to je deo 2 ove serije. Izlazi za nedelju dana. Pokriću atomic knowledge units, lifecycle, strict mode, kauzalne decision chains, AST identitet za kod, internal git kao memory versioning, memory scoring i negative memory. I zašto sve to zajedno daje kvalitativno drugačiji rezultat od bilo kog od tih komada izolovano. Deo 3 je filozofski — o **pravu AI agenta da ćuti**, i zašto prava metrika za produkcijski AI nije accuracy već *zero confidently-wrong actions at an acceptable abstain rate*. O self-tasking-u. O tome zašto kognitivni runtime bitniji od veličine modela. --- Ako si pročitao do ovde i prepoznao se u uvodnom paragrafu — u istom smo brodu. Ako imaš RAG koji zoveš memorijom i radi — reci mi kako, ozbiljno, želim da znam, možda sam pogrešio. Jedna stvar koju ne možeš da uradiš je da ćutiš. --- *Deo 1 od 3. Sledeće — „Sedam principa prave memorije za AI agente" — izlazi sledećeg utorka.* --- ## Zašto je AI-generisan kod tehnički dug od dana nula - URL: https://vbcherepanov.com/sr/articles/why-ai-generated-code-is-technical-debt-from-day-zero - Canonical (medium): https://medium.com/@vbcherepanov/why-ai-generated-code-is-technical-debt-from-day-zero-da3421a73989 - Published: 2026-04-15 - Reading time: 7 min - Tags: AI Agents, Backend - Language: sr > Patterni koji tiho gomilaju dug: fantomske apstrakcije, copy-paste kroz promptove, tiha degradacija grešaka, amnezija između zahteva i dekorativni testovi. Lek — ljudska kontrola nad arhitekturom i hitno refaktorisanje. ![AI-generisan kod kao tehnički dug — naslovna](/images/articles/why-ai-generated-code-is-technical-debt-from-day-zero.png) Poslednjih šest meseci generišem kod kroz Claude Code 6–8 sati dnevno. Ne kao eksperiment — kao primarni radni alat. Pokrećem 7 prilagođenih sub-agenata, MCP servere, hookove, persistent memory. Nisam neki teoretičar koji je pročitao par blog postova i odlučio da ima mišljenje. I upravo zato kažem ovo: većina AI-generisanog koda je tehnički dug koji počinje da truli u trenutku kad uđe u commit. Ne zato što su modeli loši. Zato što ih ljudi koriste pogrešno. ## „Radi" nije kvalitet Glavna zamka: opišeš zadatak, dobiješ 200 linija koda, pokreneš — radi. Testovi su zeleni (ako si ih uopšte tražio). PR je merge-ovan. Svi su srećni. Tri nedelje kasnije otvoriš taj fajl, i imaš više pitanja nego odgovora: - Zašto su tu tri sloja apstrakcije za pisanje u jednu tabelu? - Zašto servis zna za HTTP zaglavlja? - Odakle je došao ovaj `catch (Exception $e)` koji tiho guta greške? - Zašto DTO ogleda strukturu Entity-ja 1:1, i čemu uopšte služi? Model ne piše loš kod namerno. On piše *uverljiv* kod. Kod koji statistički liči na ono što je video u trening podacima. A trening podaci su Stack Overflow, GitHub repozitorijumi sa 2 zvezdice, junior-level tutorijali i legacy projekti na PHP 5.6. Uverljivo ≠ tačno. Uverljivo ≠ održivo. ## Konkretni patterni propadanja Neću da teoretišem. Evo šta vidim u realnim projektima svake nedelje: ### 1. Fantomske apstrakcije Model voli da pravi interfejse sa jednom implementacijom, fabrike za objekte koji se instanciraju na jednom mestu, i servisne slojeve koji samo proksiraju pozive ka repozitorijumu. Radi to zato što je „tako uobičajeno" u kodu na kome je treniran. Ali apstrakcija bez razloga nije arhitektura — to je šum. U jednom PHP/Symfony projektu sa 15 entiteta, izbrojao sam 47 AI-generisanih interfejsa. Stvarna potreba za polimorfizmom postojala je u 3 slučaja. ### 2. Copy-paste kroz prompt Ljudi copy-paste-uju ručno. AI radi isto, ali u razmeri. Tražiš „sličan endpoint za narudžbine" — dobiješ punu kopiju users endpoint-a sa zamenjenim imenima. Bez ponovnog korišćenja. Bez generalizacije. Samo klon sa drugačijim imenima promenljivih. Šest meseci kasnije imaš 30 kontrolera sa identičnim error handling-om, validacijom i strukturom paginacije — sve blago drugačije. Jer je svaki generisan kao nezavisan zahtev. ### 3. Tiha degradacija Modelu se zaista ne sviđa da vraća greške. Radije će sve obaviti u try-catch, logovati i vratiti prazan niz. Ili null. Ili default vrednost. U Go-u izgleda još gore: `if err != nil { return nil }` — i o problemu saznaješ tri sloja poziva dublje, kad su podaci već zapisani na pogrešno mesto. Ovo nije bug. To je pattern: model optimizuje za „kod se kompajlira i ne ruši se", ne za „kod ispravno prijavljuje probleme". ### 4. Amnezija konteksta AI ne pamti šta je napisao 40 promptova ranije. Svaki novi zahtev je čista tabla. Možeš završiti sa dva servisa koji rade istu stvar, konfliktnim pristupima validaciji u različitim delovima aplikacije, tri različita načina rada sa datumima u jednom projektu. U monolitu čovek bar vidi susedni fajl. AI vidi samo ono što si mu pokazao. I gradi u vakuumu. ### 5. Dekorativni testovi Traži od AI-ja da napiše testove — dobićeš testove. Lepe, dobro strukturisane, sa mock-ovima i assertion-ima. Problem: testiraju implementaciju, ne ponašanje. Krhki su. Lome se na svakom refaktorisanju. I stvaraju iluziju coverage-a. Video sam test-suite sa 94% coverage-a koji nije uhvatio nijednu pravu grešku biznis logike. Svaki test je proveravao da metoda poziva drugu metodu sa pravim argumentima. Drugim rečima, testirali su da je kod napisan onako kako je napisan. Hvala, vrlo korisno. ## Zašto je ovo gore od običnog tech debt-a Običan tehnički dug se uzima svesno. „Hajde da uradimo brzo sad, refaktorisaćemo kasnije". Tačno znaš gde si presekao ćošak. Znaš šta će se polomiti. AI tech debt je skriven. Kod izgleda čisto. Naming je u redu. Struktura foldera — kao iz udžbenika. Nijedan code reviewer neće naći zamerku. Ali ispod: - Nema jedinstvene arhitektonske odluke — samo skup lokalno-optimalnih fragmenata - Nema razumevanja biznis ograničenja — samo formalna ispravnost - Nema analize trade-off-ova — samo „prvi uverljiv pristup" To je kao kuća gde je svaku sobu projektovao drugačiji arhitekta koji nikad nije razgovarao sa ostalima. Svaka soba je u redu. Kuća kao celina je nenastanjiva. ## Znači, ne koristiti AI? Ne. Koristim Claude Code svakodnevno, i moja brzina je drastično porasla. Ali se prema AI kodu odnosim kao prema draftu, ne kao prema finalnom rezultatu. Moj workflow: 1. **Arhitektonske odluke su moje.** Definišem strukturu, slojeve, ugovore između modula. AI dobija konkretne, ograničene zadatke unutar već donetih odluka. 2. **Pregled svake generacije.** Ne „letimično pogledao" — pravi review. Zašto ovaj interfejs? Zašto tri zavisnosti ovde? Šta će se desiti ako ovaj servis padne? 3. **Kontekst je moj posao.** Vodim `CLAUDE.md` fajlove sa arhitektonskim pravilima za svaki projekat. Konvencije imenovanja, pristupi obradi grešaka, zabranjeni patterni. Bez ovoga, svaka generacija je lutrija. 4. **Refaktoriši odmah.** Ne „kasnije". Odmah posle generacije — skini suvišne apstrakcije, ujedini sa ostatkom kodbase-a, proveri edge case-ove. 5. **AI ne piše biznis logiku iz nule.** On implementira ono što sam već promislio. Razlika je između „nacrtaj mi kuću" i „izgradi po ovom projektu". ## Suština AI generacija koda nije srebrni metak, niti je kraj profesije. To je moćan alat koji u rukama inženjera ubrzava rad, a u rukama prompt-operatera generiše tehnički dug brzinom koja je ranije bila fizički nemoguća. Razlika između „koristim AI za razvoj" i „AI razvija umesto mene" je razlika između alata i štake. Ako ne možeš da objasniš zašto svaka linija u generisanom kodu postoji — ne programiraš. Gomilaš dug koji će neko morati da otplati. Možda ti. Za tri meseca. Sa kamatom. --- *15+ godina u produkciji. PHP/Symfony, Go, Vue/Nuxt, PostgreSQL. Pišem o realnom iskustvu sa AI alatima u svakodnevnom razvoju.* --- ## Vaš AI coding asistent ima amneziju. Evo kako sam je izlečio. - URL: https://vbcherepanov.com/sr/articles/your-ai-coding-assistant-has-amnesia - Canonical (medium): https://medium.com/@vbcherepanov/your-ai-coding-assistant-has-amnesia-heres-how-i-fixed-it-a8429f7f7e38 - Published: 2026-04-13 - Reading time: 9 min - Tags: AI Agents, Memory, Open Source - Language: sr > Total Agent Memory — open-source MCP server sa trajnim znanjem kroz sesije. 32 alata: skladištenje, self-improvement kroz error tracking, knowledge graph, episodic recall, procena veština. ![AI asistent sa amnezijom — naslovna](/images/articles/your-ai-coding-assistant-has-amnesia.png) *Kako sam izgradio sistem trajne memorije koji čini da Claude Code i Codex CLI pamte sve između sesija.* 11 uveče, utorak. Proveo si poslednja tri sata duboko u Claude Code sesiji, refaktorišući payment servis. Claude savršeno razume tvoju arhitekturu — repository pattern, lanac middleware-a, konvencije imenovanja na koje ste se složili pre dve nedelje. Kucaš `/compact` poslednji put, udaraš u limit konteksta, zatvaraš terminal. Sreda ujutru. Nova sesija. Novi Claude. Ne zna ništa. „Kakva je struktura tvog projekta?" pita. Opet. Objašnjavaš arhitekturu. Opet. Ispravljaš istu grešku koju je napravio prošlog četvrtka — koristio `map[string]interface{}` umesto tipiziranih DTO-ova. Opet. Lepiš isti dokument sa konvencijama. Opet. Ako ovo zvuči poznato, nisi sam. Proveo sam dva meseca živeći u ovoj petlji kroz 72 projekta pre nego što sam odlučio da to popravim. --- ## Pravi problem: stateless by design Claude Code i OpenAI Codex CLI su izvanredni alati. Ali dele fundamentalno ograničenje: **nula trajne memorije između sesija**. Svaki razgovor počinje iz nule. Ovo nije bug — to je arhitektura. Ovi alati su stateless by design. Ali za bilo koga ko radi ozbiljan, trajan razvoj, statelessness je ubica produktivnosti. Evo šta gubiš svaki put kad se sesija završi: - Arhitektonske odluke i razloge iza njih - Rešenja bug-ova koje si već rešio - Konvencije projekta na kojima su se izgrađivale sesije - Greške koje je Claude napravio (i ispravke koje si dao) - Mentalni model celog tvog codebase-a Umoran sam od toga što sam ljudski memory bank za svog AI asistenta. Pa sam izgradio **total-agent-memory** — open-source MCP server koji daje Claude Code-u (i Codex CLI-ju, Cursor-u, Cline-u, Continue, Aider-u, Windsurf-u, Gemini CLI, OpenCode-u — svemu što govori MCP) trajni mozak. **Sajt:** [totalmemory.dev](https://totalmemory.dev) · **GitHub:** [vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) > 💡 **Update (maj 2026):** Projekat je originalno objavljen kao `claude-total-memory`, a u v12.0.0 preimenovan je u **total-agent-memory** — radi sa bilo kojim MCP klijentom, ne samo sa Claude Code. Stari `claude-total-memory` paket na PyPI sada je deprecation shim (povlači `total-agent-memory>=12.0.0`), pa postojeće instalacije nastavljaju da rade. --- ## Šta on zaista radi total-agent-memory je Python MCP server koji stoji uz Claude Code. Pruža **32 alata u 6 kategorija** koji omogućavaju Claude-u da čuva, pretražuje, povezuje i uči iz znanja koje opstaje zauvek. Razmišljaj o tome kao o nadogradnji Claude-a sa briljantnog kolege sa amnezijom na onog koji vodi detaljan inženjerski notes. ### Pre i posle **Pre (svako ponedeljak ujutru):** ``` Ti: Nastavljamo rad na payment servisu Claude: Rado ću pomoći! Možeš li da mi kažeš o strukturi tvog projekta, konvencijama i šta smo do sad uradili? Ti: *uzdišeš, lepiš 2000 tokena konteksta* ``` **Posle:** ``` Ti: Nastavljamo rad na payment servisu Claude: [memory_recall("payment service architecture")] Razumem. Prošla sesija smo refaktorisali PaymentService da koristi gateway pattern. Tinkoff integracija je gotova, sledeći je Stripe. Preferiraš constructor injection i metrics middleware koji smo postavili u internal/middleware/metrics.go. Da nastavim odakle smo stali. ``` To je razlika. Bez ponovnog objašnjavanja. Bez lepljenja konteksta. Claude jednostavno *zna*. --- ## 32 alata u 6 kategorija ### 1. Core Memory (12 alata) Temelj. Čuvaj i pretražuj znanje sa pet tipova: `decision`, `solution`, `lesson`, `fact` i `convention`. ```python # Claude čuva odluku tokom tvoje sesije memory_save( content="Koristi UUID v7 za sve primarne ključeve umesto SERIAL. Razlozi: sortabilan po vremenu, nema sequence contention, bolji za distribuirane sisteme.", type="decision", tags=["database", "postgresql", "architecture"], project="payment-service" ) ``` ```python # Sledeće nedelje, druga sesija, Claude pretražuje memory_recall( query="primary key strategy for postgresql", detail="full" ) # Vraća: tačno odluku iznad, rangiranu po relevantnosti ``` Pretraga nije samo keyword matching. To je **4-tier hibridni pipeline**: ``` Upit: "docker networking between services" │ ├── Tier 1: FTS5 + BM25 keyword search │ └── Nalazi tačna podudaranja: "docker", "networking" │ ├── Tier 2: Semantic search (ChromaDB vectors) │ └── Nalazi povezano: "container communication", "bridge network" │ ├── Tier 3: Fuzzy matching (SequenceMatcher) │ └── Hvata greške u kucanju: "dokcer netowrking" i dalje radi │ └── Tier 4: Graph expansion └── Sledi relacije: docker networking → compose config → env variables Svi tier-ovi se spajaju kroz Reciprocal Rank Fusion (RRF) ``` Ovo je važno. BM25 sam postiže 89% na retrieval benchmark-ima. Semantic search sam pogađa 91%. Ceo 4-tier pipeline sa RRF fusion-om? **97.45% na LongMemEval R@5** — prevazilazi MemPalace-ovih 96.6%. ### 2. Self-Improvement (6 alata) — Killer Feature Ovde postaje zanimljivo. Claude ne *samo* skladišti znanje — on **uči iz svojih sopstvenih grešaka**. Evo pipeline-a: ``` Sesija 1: Claude koristi `npm install` unutar Docker-a → Hook detektuje grešku → self_error_log(category="docker", error="running npm outside container") Sesija 3: Ista greška ponovo → Brojač grešaka za "docker" kategoriju: 2 Sesija 5: Treći put → 3+ grešaka u istoj kategoriji triggeruje auto-insight → self_insight("Always run package managers inside Docker containers") Insight dobija sigurnost kroz uspešnu primenu... → Promovisan u SOUL pravilo (importance >= 5, confidence >= 0.8) → Pravilo se učitava na SVAKOM početku sesije → Claude više nikad ne pravi tu grešku ``` Alati: `self_error_log`, `self_insight`, `self_rules`, `self_patterns`, `self_reflect`, `self_rules_context`. Koncept SOUL pravila — trajnih pravila ponašanja koja oblikuju kako Claude radi — je ono što ovo čini više od baze podataka. To je feedback loop. Claude bukvalno postaje bolji u radu sa *tvojim* codebase-om tokom vremena. ### 3. Knowledge Graph (4 alata) Znanje nije ravno. Odluke se odnose na druge odluke. Rešenja referenciraju probleme koje su rešila. Graph hvata ove veze. ```python memory_relate( from_id=42, # "Use gateway pattern for payments" to_id=67, # "Tinkoff API requires idempotency keys" relation="context" ) ``` Kad Claude poziva odluku o gateway pattern-u, automatski povlači povezan kontekst o zahtevima Tinkoff API-ja. Bez ručnog povezivanja nakon postavljanja početne relacije. ### 4. Episodic Memory (2 alata) Činjenice ti govore *šta*. Epizode ti govore *šta se desilo*. ```python memory_episode_save( content="Proveo 3 sata debug-ujući race condition u order servisu. Korenski uzrok: deljeni database connection pool između gorutina bez pravilnog context cancellation. Popravljeno dodavanjem per-request connection checkout.", context="payment-service sprint 4" ) ``` Kad Claude naiđe na sličan concurrency problem mesecima kasnije, ne samo da zna fix — pamti i debug putovanje i lažne početke. ### 5. Skills & Competencies (3 alata) Claude prati u čemu je dobar i gde se muči. ```python memory_skill_get(skill="kubernetes-debugging") # Vraća: nivo veštine, poslednje vežbanje, putanja poboljšanja memory_self_assess() # Vraća: jake strane, slabosti, slepe tačke na osnovu istorije grešaka ``` ### 6. Advanced Cognitive Tools (5 alata) Spreading activation (`memory_associate`), automatska izgradnja konteksta (`memory_context_build`), logovanje opservacija (`memory_observe`) i on-demand refleksija (`memory_reflect_now`). --- ## Tehnička arhitektura Ispod haube, namerno je jednostavno: ``` ┌─────────────────────────────────────────────┐ │ MCP Server (Python). │ │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ │ │ │ SQLite │ │ ChromaDB │ │ Graph │ │ │ │ FTS5 │ │ (vectors) │ │ Engine │ │ │ │ + BM25 │ │ │ │ │ │ │ └──────────┘ └───────────┘ └──────────┘ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ Privacy Layer (auto-redacts secrets) │ │ │ └──────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ Web Dashboard (localhost:37737) │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ ``` Ključni dizajnerski izbori: - **SQLite FTS5** za keyword pretragu sa pravilnim BM25 rangiranjem — bez potrebe za eksternim search engine-om - **ChromaDB** za vektorsku sličnost sa binary quantization za brzinu - **Decay scoring** sa 90-dnevnim half-life — sveže znanje rangira više, ali ništa se ne baca prerano - **Retention zone**: active (default) → archived (180 dana, više se ne poziva) → purged (365 dana arhivirano) - **Auto-deduplication** kroz Jaccard similarity (>0.85 threshold) sprečava nadimanje znanja - **Privacy stripping** automatski uklanja API ključeve, JWT-ove i email adrese pre skladištenja - **Nula eksternih servisa** — sve radi lokalno. Tvoje znanje o kodu nikad ne napušta tvoju mašinu. --- ## Radi sa Claude Code I Codex CLI Pošto je MCP server, bilo koji alat koji govori MCP protokol može ga koristiti. Konfiguriši jednom, i Claude Code i OpenAI Codex CLI dele istu memory bazu. Prebacuj se između alata bez gubitka konteksta. --- ## Realni brojevi iz 2+ meseca svakodnevne upotrebe Ovo nije weekend projekat o kome teoretišem. Radi na mojoj mašini u svakodnevnoj produkciji već više od dva meseca: | Metrika | Vrednost | | --- | --- | | Aktivnih zapisa znanja | 1.683 | | Praćenih projekata | 72 | | Graph čvorova | 1.847 | | Graph ivica | 20.925 | | Naučenih veština | 177 | | Zabeleženih epizoda | 164 | Subjektivna razlika je dramatična. Ponedeljak jutro je sa 15–20 minuta obnavljanja konteksta palo na praktično nulu. Claude nastavlja tačno odakle je stao — ne zato što je sesija opstala, već zato što je svaki važan deo konteksta sačuvan i instant pretraživ. --- ## Self-Improvement Loop u praksi Hajde da prođem kroz pravi scenarijo. Nedelja 1: radim na Go projektu. Claude generiše handler sa 30 linija biznis logike unutra. Ispravljam ga — „handleri treba da budu tanki, ispod 15 linija, delegiraju u service sloj". Claude se prilagodi. Ispravka se čuva kao `convention`. Nedelja 2: drugi projekat, isti stack. Claude generiše još jedan debeo handler. Hook hvata moju ispravku, loguje je kroz `self_error_log`. To je greška #2 u kategoriji „go-architecture". Nedelja 3: dešava se ponovo. Greška #3. Sistem detektuje pattern i generiše insight: „Go handleri moraju biti tanki (`<=15` linija) — delegiraj svu biznis logiku u service sloj". Posle nekoliko uspešnih primena, insight se promoviše u SOUL pravilo. Sad svaka nova sesija počinje sa tim da Claude zna ovo pravilo. Greška debelog handler-a prestaje da se dešava. Ne zato što sam ga podsetio — već zato što je *naučio*. To je razlika između alata koji čuva tekst i onog koji se zaista poboljšava. --- ## Kako početi Izaberi šta odgovara tvom stack-u — svih šest putanja instalacije vode do istog MCP servera. Pune instrukcije na **[totalmemory.dev](https://totalmemory.dev)**. ```bash # Node (zero-install preko npx) npx -y total-agent-memory connect claude-code # Python preko uv (brzo) uvx total-agent-memory # Python preko pipx (izolovan venv) pipx install total-agent-memory # Homebrew (macOS / Linuxbrew) brew install vbcherepanov/tap/total-memory # Docker (multi-arch) docker run --rm -p 37737:37737 -v ~/.tam:/data ghcr.io/vbcherepanov/total-agent-memory:latest # Ručni clone git clone https://github.com/vbcherepanov/total-agent-memory.git ~/total-agent-memory && cd ~/total-agent-memory && ./install.sh ``` Putanja kroz `npx` takođe upisuje MCP entry u izabrani IDE — `connect claude-code`, `connect codex`, `connect cursor`, `connect cline`, `connect continue`, `connect aider`, `connect windsurf`, `connect gemini-cli`, `connect opencode`. Ako više voliš ručno — dodaj u `~/.claude/settings.json`: ```json { "mcpServers": { "memory": { "command": "/absolute/path/to/.venv/bin/total-agent-memory", "env": { "TAM_MEMORY_DIR": "~/.tam" } } } } ``` To je to. Sledeći put kad Claude Code pokrene se, ima 32 nova dostupna alata. Počni sa `memory_save` i `memory_recall` — ostalo se gradi odatle. --- ## Ograničenja (iskreno gledište) Nijedan projekat nije savršen. Evo šta da znaš: - **First-session cold start**: Memorija je u početku prazna. Treba 2–3 sesije aktivne upotrebe pre nego što se benefiti naslažu. - **Skladište raste**: 1.600+ zapisa zauzima oko 50MB sa vektorima. Nije briga za moderne mašine, ali nije ni nula. - **Claude treba podsticaj na početku**: Dok se SOUL pravila ne nakupe, možda ćeš morati da podsećaš Claude-a da koristi `memory_recall` na početku sesija. Hooks pomažu da se ovo automatizuje. - **Python zavisnost**: Server zahteva Python 3.10+ i nekoliko paketa. Install skripta to rešava, ali nije jedan binary. --- ## Zašto open source Izgradio sam ovo za sebe. Onda sam shvatio da svaki Claude Code korisnik ima isti problem. Projekat je **MIT licenciran** — koristi, fork-uj, modifikuj, doprinesi mu. Memory problem je verovatno najveća tačka trenja u AI-asistiranom razvoju danas. Context windows nastavljaju da rastu, ali nikad neće biti beskonačni. A i da jesu, ponovno učitavanje konteksta svake sesije je rasipanje. Trajna memorija je prava apstrakcija. --- ## Probaj Ako koristiš Claude Code ili Codex CLI za pravi posao — ne demo, ne toy projekte, već stvaran trajan razvoj — ovo će promeniti tvoj workflow. Daj zvezdicu repo-u, probaj nedelju dana, i vidi razliku kad tvoj AI asistent zaista pamti ko si i šta gradiš. **Sajt:** [totalmemory.dev](https://totalmemory.dev) · **GitHub:** [vbcherepanov/total-agent-memory](https://github.com/vbcherepanov/total-agent-memory) **Licenca:** MIT — besplatno zauvek. --- *Vitalii Cherepanov je software inženjer koji gradi alate na preseku AI-ja i developer produktivnosti. Piše Go i PHP danju i uči Claude-a da pamti stvari noću.* --- ## Proučio sam etcd codebase — i to je promenilo način na koji pišem PHP - URL: https://vbcherepanov.com/sr/articles/i-studied-the-etcd-codebase - Canonical (medium): https://medium.com/@vbcherepanov/i-studied-the-etcd-codebase-and-it-changed-how-i-write-php - Published: 2026-04-20 - Reading time: 11 min - Tags: Backend, Architecture - Language: sr > Sedam arhitektonskih principa izvedenih iz etcd Go koda i primenjenih na svakodnevni PHP/Symfony rad: tipizirani ugovori, single-responsibility servisi, kompozicija middleware-a, observability kao arhitektura, fasadni API-ji, transparentan put zahteva i eksplicitne zavisnosti. ![Proučavanje etcd codebase-a — naslovna](/images/articles/i-studied-the-etcd-codebase.jpg) Postoji uobičajen savet: „Hoćeš da pišeš bolji kod? Čitaj dobar kod." Zvuči očigledno. Retko se primenjuje. Problem je što su većina open-source projekata lavirinti. Otvoriš repo, vidiš 200 direktorijuma i zatvoriš tab. Kubernetes je dva miliona linija. Linux kernel — ne razmišljaj o tome. Odakle početi? Moj odgovor: **etcd**. Za one koji nisu upoznati: etcd je distribuirani key-value store napisan u Go-u. Kičma Kubernetes-a — svaki deo stanja klastera tu živi. Ali mene etcd ne zanima kao proizvod. Zanima me kao **primer arhitekture koju zaista možeš pročitati od početka do kraja**. Evo šta me je iznenadilo: principi ugrađeni u etcd nisu o Go-u. Oni su o dizajnu softvera uopšte. Radim sa PHP-om i Symfony-jem svakodnevno, i skoro sve što sam našao u etcd-u prevelo se direktno u moje projekte. Sedam principa, konkretni primeri, bez praznog hoda. --- ## 1. Jedan izvor istine za tvoj API U etcd-u, svaki API je definisan u `.proto` fajlovima. Otvoriš `rpc.proto` i vidiš sve operacije: `Range`, `Put`, `DeleteRange`, `Txn`. Svako polje je tipizirano. Nema mesta za „čekaj, prihvatamo li ovde string ili integer?". U PHP-u, umesto protobuf-a, imamo **strogo tipizirane DTO-e**: ```php final readonly class CreateOrderRequest { public function __construct( public string $customerId, /** @var OrderItemDto[] */ public array $items, public ?string $promoCode = null, ) {} } ``` Jedna klasa — i svi znaju šta endpoint prihvata. Frontend developer gleda DTO, backend developer piše logiku protiv njega, OpenAPI shema se generiše automatski kroz NelmioApiDocBundle. Uporedi to sa onim što sam video (i pisao) u realnim projektima: ```php $data = json_decode($request->getContent(), true); $customerId = $data['customer_id'] ?? null; $items = $data['items'] ?? []; // Koji format ima items? Da li je promoCode stvar? Ko zna. ``` Kad ti je ugovor „pa, neki niz dolazi", bilo koja izmena nešto polomi neočekivano. Kad ti je ugovor DTO sa tipovima, PHPStan hvata problem pre nego što produkcija to uradi. --- ## 2. Svaki servis radi jednu stvar etcd ima jasno razdvojene gRPC servise: `KV` (čitanje-pisanje), `Watch` (pretplata na promene), `Lease` (TTL-ovi za ključeve), `Auth` (autorizacija). Svaki je odvojen interfejs. `Watch` ne dira pisanja. `KV` ne proverava tokene. U Symfony-ju — ista ideja, drugačiji alati: ```php class OrderController { #[Route('/orders', methods: ['POST'])] public function create( CreateOrderRequest $request, OrderService $orderService, ): JsonResponse { return new JsonResponse( $orderService->create($request) ); } } ``` `OrderService` kreira narudžbine. Ne šalje email-ove — to je `NotificationService` koji sluša `OrderCreatedEvent`. Ne procesuira plaćanja — to je `PaymentService`. A onda postoji alternativa koju redovno viđam: ```php class OrderController { public function create(Request $request) { // 40 linija validacije // 20 linija autorizacije // 60 linija biznis logike // 15 linija slanja email-a // 10 linija logovanja // Ukupno: 150 linija, ne može da se testira } } ``` 500-linijski god controller. Svi smo bili tu. etcd mi je pomogao da konačno artikulišem *zašto* je to loše: ne zato što je „pattern pogrešan", već zato što **ne možeš da pratiš šta sistem radi**. --- ## 3. Middleware se sastavlja kao Lego Svaki gRPC zahtev u etcd-u prolazi kroz lanac interceptor-a: logging → auth → metrics → handler → metrics → response. Svaki interceptor je mali, jednonamenski. Snaga dolazi iz kompozicije. U Symfony-ju, ovo se mapira na Event Listenere i Messenger Middleware: ```php class MetricsMiddleware implements MiddlewareInterface { public function __construct( private PrometheusCollector $metrics, ) {} public function handle(Envelope $envelope, StackInterface $stack): Envelope { $start = microtime(true); try { $result = $stack->next()->handle($envelope, $stack); $this->metrics->increment('messages_processed_total', [ 'type' => $envelope->getMessage()::class, 'status' => 'success', ]); return $result; } catch (\Throwable $e) { $this->metrics->increment('messages_processed_total', [ 'type' => $envelope->getMessage()::class, 'status' => 'error', ]); throw $e; } finally { $this->metrics->histogram( 'message_duration_seconds', microtime(true) - $start, [$envelope->getMessage()::class] ); } } } ``` Jedan middleware, jedan posao. Metrike ovde, logovi tamo, retry negde drugde. Sastavi lanac u `messenger.yaml`. Antipattern — kad svaki handler ima ovo ručno: ```php public function handle(CreateOrderCommand $command): void { $this->logger->info('Starting order creation...'); $start = microtime(true); // ... actual logic ... $this->metrics->record(microtime(true) - $start); $this->logger->info('Order created'); } ``` 50 handler-a, 50 kopija istog boilerplate-a. Zaboraviš jedan — nema metrika. Promeniš format loga — promeni ga na 50 mesta. --- ## 4. Observability je arhitektura, ne dodatak U etcd-u, Prometheus je ugrađen u gRPC sloj od prvog dana. Ne „dodato šest meseci posle launch-a". Kod se ne smatra završenim bez metrika. U PHP-u: ```php class PaymentService { public function charge(Order $order): PaymentResult { $timer = $this->metrics->startTimer('payment_charge_duration'); try { $result = $this->gateway->process($order); $this->metrics->increment('payments_total', [ 'provider' => $result->provider, 'status' => $result->isSuccess() ? 'success' : 'declined', ]); return $result; } catch (GatewayTimeoutException $e) { $this->metrics->increment('payments_total', [ 'provider' => $order->paymentMethod, 'status' => 'timeout', ]); throw $e; } finally { $timer->observe(); } } } ``` Svako plaćanje — u metrikama. Koliko je uspelo, koliko je timeout-ovalo, koji provider je spor. Ne zato što je neko tražio, već zato što bez toga letiš slepo. Pamtim projekat gde je produkcija bila pala 40 minuta i jedini način da razumem šta se dešava bio je `tail -f /var/log/symfony.log | grep ERROR`. Nikad više. Paket: `promphp/prometheus_client_php`. Pet minuta za instalaciju, petnaest za povezivanje Grafane. --- ## 5. Spolja jednostavno, iznutra raketna nauka `clientv3` u etcd-u je masterclass facade pattern-a: ```go client.Put(ctx, "name", "value") ``` Jedna linija. Pod haubom: izbor čvora, reconnect pri padu, retry sa exponential backoff-om, protobuf serijalizacija, Raft consensus, upis na disk, potvrda kvoruma. Isti princip u PHP-u: ```php // Calling code. Jednostavan i jasan. $paymentService->charge($order); ``` Unutar `charge()`: ```php public function charge(Order $order): PaymentResult { if ($existing = $this->findExistingPayment($order)) { return $existing; // idempotency } $provider = $this->providerResolver->resolve($order); $result = $this->withRetry( fn () => $provider->process($order), maxAttempts: 3, backoff: 'exponential', ); if ($result->isSuccess()) { $this->fiscalService->createReceipt($order, $result); } $this->events->dispatch(new PaymentProcessed($order, $result)); return $result; } ``` Kontroler koji zove `charge()` ne zna ništa o fiskalnim računima, retry-ima ili izboru provider-a. I ne treba da zna. Znak dobrog servisa: možeš objasniti šta radi u jednoj rečenici — „naplaćuje kupcu narudžbinu" — dok je implementacija 200 linija pažljive logike. --- ## 6. Možeš pratiti zahtev prstom U etcd-u, putanja zahteva se čita linearno: ```plaintext gRPC handler → EtcdServer.Put() → Raft → apply → bbolt (disk) ``` Bez magije. Bez skrivenih poziva. Bez „odakle se ovo uopšte triggeruje?". U Symfony-ju — ista stvar, ako ne zloupotrebljavaš sistem događaja: ```plaintext Request → Controller (raspakivanje DTO-a) → Service (biznis logika) → Repository (baza) → EventDispatcher (sporedni efekti) → Response ``` Otvoriš kontroler — vidiš koji se servis poziva. Otvoriš servis — vidiš šta radi. Otvoriš repository — vidiš upit. Šta ubija praćenje: - `@PostPersist` na entitetu koji tiho šalje SMS - `prePersist` listeneri koji menjaju podatke pre upisa — i provedeš 30 minuta tražeći ko dira polje `updatedAt` - Deset `EventSubscriber`-a na istom događaju sa nejasnim redosledom izvršavanja Event-driven je odličan. Ali ako novi developer ne može da objasni „zahtev ulazi ovde, odgovor izlazi tamo" za 2 minuta — imaš problem. --- ## 7. Bez skrivenih zavisnosti U etcd-u, sve zavisnosti se prosleđuju eksplicitno: ```go func NewKVServer(s *EtcdServer) KVServer { ... } ``` Vidiš konstruktor — vidiš sve što klasa treba. U Symfony-ju — constructor injection, ista stvar: ```php class OrderService { public function __construct( private OrderRepository $orders, private PaymentGateway $payment, private EventDispatcherInterface $events, private LoggerInterface $logger, ) {} } ``` Četiri zavisnosti. Sve vidljive. Hoćeš da testiraš? Ubaci mock-ove. Hoćeš da razumeš klasu? Pogledaj konstruktor. Antipatterni koji još uvek preživljavaju: ```php // Service locator: odakle ovo dolazi? $payment = $this->container->get('payment.gateway'); // Statički pozivi: ne može se testirati Cache::put('key', $value); // new SomeService() unutar drugog servisa: nevidljivo spajanje $validator = new OrderValidator(); ``` Symfony autowiring nije magija u lošem smislu. Container povezuje zavisnosti po tipu, ali ih i dalje vidiš u konstruktoru. To je pogodnost, ne skriveno ponašanje. --- ## Moja checklist Posle proučavanja etcd-a, izvukao sam checklist koji sad primenjujem na svaki novi servis: 1. **Ugovor definisan?** DTO-ovi postoje, tipovi su zadati, OpenAPI se generiše iz njih 2. **Kontroler tanak?** Najviše 10 linija, sva logika u service sloju 3. **Cross-cutting concerns izvučeni?** Logovanje, metrike, retry — kroz middleware, ne copy-paste 4. **Metrike prisutne?** Ako ne, servis nije production-ready 5. **API jednostavan spolja?** Pozivajući kod ne zna o internoj kompleksnosti 6. **Putanja zahteva sledljiva?** Novi developer nalazi handler za 2 minuta 7. **Zavisnosti eksplicitne?** Sve u konstruktoru, ništa iz vazduha Ništa od ovoga nije revolucionarno. Osnovna higijena koju je lako zaboraviti pod pritiskom rokova. etcd me je samo podsetio kako izgleda codebase kad ta higijena nije preskočena. I da je moguće čak i u velikom produkcijskom sistemu. --- *Koji open-source codebase je promenio kako pišeš kod? Voleo bih da napravim reading listu — bacaj svoje u komentare.* --- ## ClearVibeArchitecture (CVA) — kompletan vodič - URL: https://vbcherepanov.com/sr/articles/clear-vibe-architecture - Published: 2025-10-16 - Reading time: 45 min - Tags: Architecture, Backend, Distributed Systems - Language: sr > Arhitektonski stil za backend sisteme: Hexagonal, Outbox/Inbox, Observability, obavezne feature flags i vizuelizacija tokova. ## Садржај 1. [Увод и TL;DR](#део-1-увод-и-tldr) 2. [Речник и терминологија](#део-2-речник-и-терминологија) 3. [Архитектурна мапа и слојеви](#део-3-архитектурна-мапа-и-слојеви) 4. [Принципи и правила](#део-4-принципи-и-правила) 5. [Обрасци](#део-5-обрасци) 6. [Шема података и уговори](#део-6-шема-података-и-уговори) 7. [Посматрање](#део-7-посматрање) 8. [Безбедност](#део-8-безбедност) 9. [DevEx и продуктивност](#део-9-devex-и-продуктивност) 10. [Референтни скелет (Go)](#део-10-референтни-скелет-go) 11. [Референтни скелет (Symfony/PHP)](#део-11-референтни-скелет-symphonyphp) 12. [Политике еволуције](#део-12-политике-еволуције) 13. [Feature Toggles](#део-13-feature-toggles) 14. [Визуализација](#део-14-визуализација) 15. [Чек-листе имплементације](#део-15-чек-листе-имплементације) 16. [Лиценцирање и допринос](#део-16-лиценцирање-и-допринос) --- ## Део 1: Увод и TL;DR ### 1.1 Шта је ЧистаВајбАрхитектура (CVA) **ЧистаВајбАрхитектура** (ClearVibeArchitecture, CVA) је архитектурни стил за backend системе који комбинује: - **Hexagonal (Ports & Adapters)** као основу за изолацију домена - **Догађаје по подразумеваном** (Domain Events + Outbox/Inbox, идемпотентност) - **Посматрање по подразумеваном** (трасирање, метрике, структурни логови, временска линија) - **Обавезне Feature Toggles** као пресечни образац за сву нову функционалност - **Визуализацију тока (web/VR)** као продуктни артефакт, а не "пријатну опцију" **Укратко:** хексагонална архитектура "са батеријама" — транспарентна, посматрива, еволуционо управљива. ### 1.2 Које проблеме решава 1. **Тешко је безбедно мењати систем** → feature flags + canary/blue-green 2. **Не види се "шта се дешава унутра"** → otel-трасирање + бизнис метрике + временска линија 3. **Поуздана интеграција између сервиса** → Outbox/Inbox + идемпотентност 4. **Програмери тону у инфраструктури** → чист домен и уговори на границама 5. **Тешко објаснити бизнису понашање система** → визуализатор тока захтева/догађаја ### 1.3 Циљеви дизајна - **Транспарентност:** сваки захтев и догађај се прати кроз све слојеве - **Изолација домена:** модел домена је независан од инфраструктуре - **Поуздана испорука:** догађаји се публикују трансакционо (Outbox) - **Управљана еволуција:** све нове гране иза feature flags са верзионисањем уговора - **Developer Experience:** шаблони модула, јединствене CI/CD праксе, тестови као стандард ### 1.4 Нефункционални захтеви (NFR) - **Поузданост:** идемпотентни handler-и, поновна покушаја, дедупликација - **Посматрање:** p95-латенција, error rate, saturation као обавезне метрике - **Безбедност:** Zero-Trust границе; потписивање порука; ревизија промена flags/уговора - **Перформансе:** in-proc кеш одлука flags, back-pressure, I/O батчинг - **Компатибилност:** SemVer за API/догађај уговоре, подршка n-1 ### 1.5 TL;DR принципа - **Domain First:** домен не зна за ORM/HTTP/broker - **Ports & Adapters:** сав I/O кроз интерфејсе (in/out-портови) - **Events First:** доменски догађаји су првокласни; публикација преко Outbox - **Observability First:** trace_id свуда; бизнис метрике на доменским догађајима - **Feature-Toggles Everywhere:** нова функционалност не живи без флага и kill-switch - **Contracts First:** API/догађај шеме су верзионисане, проверљиве, компатибилне - **Visualize the Flow:** обавезна визуализација путање захтева/gebeurtenaja ### 1.6 Област примене - Микросервиси и велики монолити са перспективом декомпозиције - Високо интеграционо оптерећење, чести релизи, A/B експерименти - Тимови којима је важна транспарентност и репродукција инцидената **Није најбољи избор** ако је систем изузетно једноставан, ретко се мења и не захтева телеметрију/догађаје. ### 1.7 Критеријуми "спремности за CVA" - Све нове функције иза feature flags са canary и kill-switch - Постоје Outbox/Inbox и идемпотентни handler-и - Укључен OpenTelemetry (трасирање), структурни логови и кључне бизнис метрике - API/догађај уговори описани шемама, CI-валидирани, води се SemVer - Постоји web/VR визуализатор тока (минимум — web граф + временска линија) - Шаблони/генератори модула, линтери, PR чек-листе, основни e2e тестови ### 1.8 Излазни артефакти - Архитектурна мапа слојева и портова (in/out), модел домена и границе контекста - Каталог уговора (OpenAPI/Proto/JSON-Schema/Avro) са SemVer и провера - Outbox/Inbox библиотека/модул, FeatureGate SDK, otel-интеграција - SLO dashboard-и и визуализатор тока (минимум — web) - Шаблони кода (Go, Symfony/PHP), Makefile/Compose, CI-pipeline --- ## Део 2: Речник и терминологија ### 2.1 Основни ентитети - **Домен** — пословна област која описује пословна правила и инваријанте - **Агрегат** — корени ентитет који инкапсулира стање и понашање повезаних објеката - **Догађај (Domain Event)** — непроменљива чињеница забележена у домену - **Команда (Command)** — намера да се промени стање (креирај, ажурирај, обриши) - **Упит (Query)** — намера да се добију подаци без споредних ефеката - **Use Case** — апликативна логика која повезује команду/упит и домен ### 2.2 Портови и адаптери - **InPort** — интерфејс уласка у систем (API, UI, CLI, тест) - **OutPort** — интерфејс изласка из система (база података, message broker, спољни API-ји) - **Adapter** — имплементација порта за конкретну инфраструктуру ### 2.3 Догађајне компоненте - **Outbox** — табела/складиште за поуздано публиковање догађаја са трансакцијом - **Inbox** — табела/складиште за прием и дедупликацију долазних догађаја - **Saga/Process Manager** — координира дуге процесе кроз секвенцу догађаја ### 2.4 Observability - **Trace ID** — јединствени идентификатор захтева, прослеђује се кроз све слојеве - **Span** — део трасе који одражава корак (нпр: SQL упит, API позив) - **Metric** — мерење (латенција, error rate, бизнис метрика) - **Structured Log** — лог у JSON формату са обавезним пољима (timestamp, trace_id, level, message) ### 2.5 Feature Toggles - **Feature Flag** — бинарни прекидач или варијанта параметар који контролише доступност функционалности - **Variant** — вредност варијанте флага (A/B/n-тест) - **Kill-Switch** — флаг за тренутно искључивање проблематичне функције - **Exposure Event** — догађај приказивања функционалности кориснику ### 2.6 Уговори - **API Contract** — формални опис API-ја (OpenAPI, gRPC proto) - **Event Contract** — опис структуре догађаја (JSON Schema, Avro) - **Backward Compatibility** — нови опис не квари клијенте претходних верзија - **SemVer** — верзионисање уговора по правилима Semantic Versioning --- ## Део 3: Архитектурна мапа и слојеви ### 3.1 Општа шема ``` ┌─────────────────────────────────────────────────────┐ │ Interface Layer │ │ (REST, gRPC, GraphQL, CLI, VR/AR) │ └────────────────┬────────────────────────────────────┘ │ InPorts ┌────────────────▼────────────────────────────────────┐ │ Application Layer │ │ (Commands, Queries, Handlers, Policies) │ └────────────────┬────────────────────────────────────┘ │ Domain Services ┌────────────────▼────────────────────────────────────┐ │ Domain Layer │ │ (Entities, Aggregates, Events, Value Objects) │ └─────────────────────────────────────────────────────┘ ▲ │ OutPorts ┌────────────────┴────────────────────────────────────┐ │ Infrastructure Layer │ │ (DB, Brokers, Outbox/Inbox, Observability) │ └─────────────────────────────────────────────────────┘ ``` ### 3.2 Interface Layer - **Улаз:** REST, gRPC, GraphQL, CLI, VR/AR-визуализатор - **Задаци:** примање команди/упита, конверзија у DTO, прослеђивање у Application - **Карактеристике:** логовање, трасирање, authN/authZ ### 3.3 Application Layer - Срце апликативне логике: команде, упити, handler-и, политике - Координација: orchestrators, saga, process managers - Валидација и DTO мапирање - Гаранција трансакционих граница: један use case = једна трансакција - Сви споредни ефекти кроз Outbox ### 3.4 Domain Layer - Entities, Aggregates, Value Objects - Инваријанте, пословна правила, ubiquitous language - Domain Events као првокласни објекти - Domain Services — чисте функције без инфраструктуре - Domain не зна за ORM, HTTP, broker-е ### 3.5 Infrastructure Layer - Имплементација портова: репозиторијуми, broker адаптери, спољни API клијенти - Outbox/Inbox — обавезне компоненте за публиковање/пријем догађаја - Observability stack: otel-интеграција, логовање, метрике - Кеширање, file складишта, интеграције са спољним сервисима --- ## Део 4: Принципи и правила ### 4.1 Чисто језгро (Domain First) - Домен је изолован од инфраструктуре - Забрањено коришћење ORM анотација, SQL или SDK-ова спољних сервиса унутар домена - Инваријанте се проверавају само у домену ### 4.2 Портови и адаптери - Све спољне интеракције кроз интерфејсе (Ports) - Имплементације интерфејса само у Infrastructure (Adapters) - Application позива OutPorts, Domain не зна ништа о њима ### 4.3 Догађаји по подразумеваном - Свака значајна промена се бележи као Domain Event - Догађаји се публикују кроз Outbox са трансакцијом - За долазне догађаје користи се Inbox са идемпотентношћу ### 4.4 Трансакционе границе - Један use case = једна трансакција - Споредни ефекти (слање у broker, интеграције) се бележе у Outbox - Обрада Outbox је асинхрона и може се поновити ### 4.5 Посматрање по подразумеваном - Сваки захтев и догађај прати trace_id - Сви handler-и и адаптери морају да логују и мере свој рад - Пословни догађаји постају део метрика (нпр: order.created.count) ### 4.6 Уговори на границама - API и догађаји описани уговорима (OpenAPI/Proto/Avro/JSON Schema) - Уговори су верзионисани (SemVer) - Backward Compatibility је обавезан захтев (подршка n-1 верзија) ### 4.7 Feature Toggles свуда - Свака нова функционалност имплементира се само иза флага - Feature flag је обавезан чак и ако ће функционалност бити увек укључена касније - Сваки флаг има власника, sunset датум и kill-switch ### 4.8 Визуализација тока - Временска линија захтева и догађаја мора бити репродуктивна у визуализатору (web/VR) - Уска грла се аутоматски истичу (heatmap) - Визуализација је део производа, а не "додатни алат" --- ## Део 5: Обрасци ### 5.1 Hexagonal / Ports & Adapters - Основни архитектурни образац - Домен је изолован, улазне/излазне зависности као InPorts/OutPorts - Интеракција са спољним светом кроз адаптере ### 5.2 CQRS (опционално) - Раздвајање команди (промене стања) и упита (читање података) - Користи се тамо где се читање значајно разликује од писања - Омогућава скалирање читања и писање оптимизованих пројекција ### 5.3 Outbox / Inbox - **Outbox:** трансакционо бележење догађаја са променом у БД - **Inbox:** регистрација и дедупликација долазних порука - Гаранција "exactly-once delivery" на апликативном нивоу ### 5.4 Saga / Process Manager - Управљање дуготрајним процесима - Saga: ланац корака са компензацијама - Process Manager: реагује на догађаје, координира акције више агрегата/сервиса ### 5.5 Feature Toggles (обавезно) - Пресечни образац: свака нова функционалност имплементира се иза флага - Флагови имају власника, sunset датуме, kill-switch - Подржани процентуални rollout, сегменти корисника, A/B/n-тестови - Сваки exposure event (чињеница коришћења флага) се логује ### 5.6 Observability обрасци - **Distributed Tracing:** trace_id кроз све слојеве - **Structured Logging:** JSON логови са кључним пољима - **Metrics & SLOs:** пословне и техничке метрике; ауто-алерти - **Timeline Visualization:** ток догађаја и захтева приказан у web/VR ### 5.7 Deployment обрасци - **Feature Flags + Canary Release:** нова функционалност укључује се постепено - **Blue/Green Deployment:** два паралелна окружења, брзо пребацивање - **Rollback по метрикама:** аутоматско искључивање функције/верзије при деградацији --- ## Део 6: Шема података и уговори ### 6.1 API уговори - Сви јавни API-ји описани у OpenAPI или gRPC Proto - Уговори пролазе аутоматску CI валидацију - Строго верзионисање: SemVer (1.2.3) - Backward compatibility: подршка n-1 верзије клијента - Уговори објављени у артефакт репозиторијуму ### 6.2 Уговори догађаја - Догађаји описани у JSON Schema, Avro или Protobuf - Сваки догађај има: event_name, version, timestamp, trace_id, payload - Обавезна поља: id, trace_id, source - Догађаји пролазе валидацију шеме пре публиковања ### 6.3 Верзионисање - Уговори верзионисани независно од сервиса - Подршка више верзија догађаја: потрошачи могу читати v1 и v2 паралелно - При прелазу: producer почиње да публикује оба формата (dual write) ### 6.4 Шеме база података - Миграције управљају алатима: Liquibase/Flyway/golang-migrate/DoctrineMigrations - Свака промена у БД прати migration script - Подржан shadow-write/read за сложене промене шеме - Све миграције пролазе кроз CI и тест базу --- ## Део 7: Посматрање ### 7.1 Принцип "Observability by Default" - Сваки нови сервис или модул у CVA мора имати уграђена средства за посматрање - Метрике, логови и трасе се не додају "касније" — они су део архитектуре - Посматрање покрива и техничке и пословне процесе ### 7.2 Трасе (Distributed Tracing) - Користи се OpenTelemetry или компатибилан стандард - Сваки захтев има јединствени trace_id - Свака операција (SQL, RPC, спољни API) се бележи као span - Сви догађаји (Domain Events) такође носе trace_id - Визуализатор (web/VR) гради временску линију на основу trace_id ### 7.3 Логови (Structured Logging) - Формат: JSON - Обавезна поља: timestamp, level, trace_id, service, message - Логови се пишу у stdout → централизовано складиште (ELK, Loki) - Грешке и пословни догађаји бележе се подједнако структурирано ### 7.4 Метрике - **Техничке:** латенција (p95/p99), error_rate, saturation, throughput - **Пословне:** број наруџбина, конверзија, број одбијања по правилима - **Feature Flags:** метрике експозиције (колико корисника види функцију), enable_rate - Метрике доступне у Prometheus/Grafana или еквивалентима ### 7.5 Алерти и SLO - Свака критична метрика има SLO (Service Level Objective) - Кршења → алерти (PagerDuty, Slack, Email) - Алерти морају бити акционабилни (јасно шта радити при окидању) - За feature flags постоји ауто-rollback по SLO --- ## Део 8: Безбедност ### 8.1 Zero Trust принцип - Сваки сервис и компонента комуницира са другима само кроз проверене канале - Нема поверења по подразумеваном чак ни унутар исте мреже - Ауторизација и аутентификација примењују се на сваком нивоу ### 8.2 Аутентификација (AuthN) - Спољни позиви: OAuth2 / OpenID Connect / mTLS - Интерни позиви: service accounts + mTLS - Сви захтеви морају садржати корелациони trace_id и токен ауторизације ### 8.3 Ауторизација (AuthZ) - RBAC (Role-Based Access Control) или ABAC (Attribute-Based Access Control) - Провере права извршавају се у Application Layer (policies) - Feature Flags могу бити ограничени по улогама/сегментима ### 8.4 Шифровање - У транзиту: TLS 1.3 (API, broker-и, БД) - У мировању: шифровање дискова/табела/објеката (AES-256) - Поверљиви подаци (PII) увек шифровани ### 8.5 Управљање тајнама - Тајне се не чувају у коду или променљивима окружења - Користи се Vault/KMS (HashiCorp Vault, AWS KMS, GCP Secret Manager) - Приступ тајнама строго по принципу минималних права --- ## Део 9: DevEx и продуктивност ### 9.1 DevEx-first принцип - Архитектура треба да буде погодна за програмере - Шаблони, алати и процеси уграђени у CVA - Програмери троше минимум времена на рутину и инфраструктуру ### 9.2 Scaffold и генератори - Генератори кода за ентитете, команде, догађаје, портове и адаптере - `make scaffold module=Order` → креира структуру: - Domain: entity, events, value objects - Application: commands, handlers, policies - Infrastructure: repository, adapters - Гаранција уједначености по пројектима ### 9.3 Стил кода и линтери - Јединствени стил кода (Go, PHP, JS/TS) - Pre-commit hooks: линтери, тестови, security scans - Обавезна провера trace_id, логова, feature flags ### 9.4 CI/CD pipeline CI проверава: - компилацију/build - тестове (unit, integration, e2e) - шеме уговора (API, догађаји) - миграције БД - security scan CD подржава blue/green и canary deployment --- ## Део 10: Референтни скелет (Go) ### 10.1 Структура пројекта ``` /clearvibe /cmd/app/main.go /internal /domain/order entity.go, events.go, service.go /app/order commands.go, handler.go, policies.go, ports.go /infra /db: order_repo_pg.go /broker: outbox.go, inbox.go /http: server.go /obs: tracing.go, metrics.go, logging.go /flags: feature_gate.go ``` ### 10.2 Интерфејси портова ```go type OrderRepo interface { Save(*Order) error FindByID(id string) (*Order, error) } type FeatureGate interface { Enabled(ctx FeatureContext, key string) bool } ``` ### 10.3 Команда и Handler ```go type CreateOrderCmd struct { CustomerID string Items []Item } func (h *createOrderHandler) Handle(cmd CreateOrderCmd) (string, error) { if h.flags.Enabled(FeatureContext{UserID: cmd.CustomerID}, "orders.dynamic_pricing") { // нова грана логике } ord, _ := domain.NewOrder(cmd.CustomerID, cmd.Items) h.repo.Save(ord) h.outbox.Add("order.created", ord.ID, OrderCreated{ID: ord.ID}) return ord.ID, nil } ``` --- ## Део 11: Референтни скелет (Symfony/PHP) ### 11.1 Структура пројекта ``` src/ Domain/Order/ Entity/Order.php Event/OrderCreated.php Application/Order/ Command/CreateOrder.php Handler/CreateOrderHandler.php Infrastructure/ Persistence/Doctrine/OrderRepository.php Messaging/OutboxPublisher.php FeatureFlags/RedisFeatureGate.php ``` ### 11.2 Ентитет и догађај ```php final class Order { private string $id; private string $customerId; private float $total; public static function create(string $customerId, array $items): self { return new self($customerId, $items); } } final class OrderCreated { public function __construct( public readonly string $id, public readonly float $total, ) {} } ``` --- ## Део 12: Политике еволуције ### 12.1 Општи принцип Еволуција је управљана и посматрива. Свака промена пролази кроз feature flags, уговоре и миграције, има власника, plan ризика и путеве за rollback. ### 12.2 Верзионисање уговора (API/Events) - SemVer: MAJOR.MINOR.PATCH - n-1 компатибилност: подржана је барем једна претходна верзија - Dual write/read: током миграције догађаја, producer публикује vN и vN+1 - CI валидатори компатибилности: забрањују breaking-changes без MAJOR ### 12.3 Животни циклус функције 1. **Draft:** флаг креиран (disabled), власник, циљ, метрике успеха, sunset датум 2. **Internal:** укључен за dev/stage/own team 3. **Canary:** 1% → 5% → 25% → 50% → 100% (детерминистичко bucketing) 4. **General Availability:** флаг подразумевано on 5. **Sunset:** флаг и мртав код уклоњени, уговори стабилизовани ### 12.4 Стратегије релиза - **Blue/Green:** два окружења, пребацивање саобраћаја - **Canary:** инкрементални rollout са ауто-чуварима по SLO - **Dark Launch:** код deployan, функција искључена флагом до активације --- ## Део 13: Feature Toggles ### 13.1 Принцип Feature Toggles су обавезан и пресечни образац у CVA. Свака нова функционалност имплементира се само иза флага. ### 13.2 Типови флагова - **Boolean:** on/off - **Percentage:** проценат саобраћаја (canary rollout) - **Segment:** укључивање по атрибутима (држава, ниво, улога) - **Multivariant:** више варијанти за A/B/n-тестове ### 13.3 Складиштење - Складиште: Postgres (извор истине) + Redis (кеш, pub/sub) - Шема: flags(id, key, type, rules, default, owner, version, ttl, updated_at) - Верзионисање: SemVer за уговоре флагова, подршка n-1 ### 13.4 Метрике и посматрање - Exposure догађаји: flag.exposed, flag.variant - Метрике: exposure_count, enable_rate, error_rate - Dashboard-и: поређење варијанти (A/B), корелација са пословним метрикама - Trace_id: увек прослеђен у exposure догађајима --- ## Део 14: Визуализација ### 14.1 Принцип У CVA, визуализација није "пријатна опција" већ обавезан слој. Систем мора показати ток података и догађаја. ### 14.2 Временска линија - Приказује кораке захтева хронолошки - Примери: HTTP захтев примљен, Handler извршен, Догађај забележен у Outbox - Сваки корак одговара span-у (трасе) - Временска линија доступна у web интерфејсу и експортована у VR ### 14.3 Flow граф - Чворови: сервиси, агрегати, адаптери - Ивице: позиви и догађаји - Подршка филтера: по trace_id, типу догађаја, грешкама/латенцији ### 14.4 Heatmap - Истицање уских грла - Метрике латенције/error_rate визуализоване у боји - Црвено = проблем, зелено = нормално ### 14.5 VR визуализација - Ток података може се "видети" у 3D (VR/AR) - Чворови-сервиси у простору, линије-догађаји се крећу у реалном времену - Корисно за обуку нових чланова тима и анализу инцидената --- ## Део 15: Чек-листе имплементације ### 15.1 Општа чек-листа пројекта - Дефинисани Bounded Contexts и Ubiquitous Language - Формирана архитектурна мапа слојева - Именован власник архитектуре ### 15.2 Domain Layer - Сви ентитети описани као Entity/ValueObject - Инваријанте фиксиране и покривене тестовима - Domain Events дефинисани и документовани - Нема зависности на ORM/HTTP/broker-е ### 15.3 Application Layer - Свака акција формирана као Command/Query - Handler за сваку команду - Policies валидирају приступ и правила - Трансакционе границе дефинисане - Feature Flags интегрисани у use cases ### 15.4 Infrastructure Layer - Репозиторијуми имплементирани преко OutPorts - Повезан Outbox (обавезно) - Повезан Inbox за долазне догађаје - Конфигурисани адаптери за API/broker-е/БД - Повезане observability компоненте ### 15.5 Feature Toggles - Сваки нови код има feature flag - Флагови имају власника, sunset датуме, kill-switch - Exposure догађаји логују се - Rollout сценарији документовани - Тестови укључују on/off сценарије ### 15.6 Observability - Сви захтеви/догађаји имају trace_id - Логови структурирани (JSON) - Метрике: техничке + пословне - Dashboard-и и алерти конфигурисани - Визуализатор повезан (временска линија/flow граф/VR) --- ## Део 16: Лиценцирање и допринос ### 16.1 Лиценца - Open Source (MIT/Apache 2.0): архитектурни принципи, чек-листе и шаблони слободно доступни - Може се користити у комерцијалним и некомерцијалним пројектима без ограничења - Једини захтев — одржавање помена ауторства и лиценце ### 16.2 Водич за допринос Pull Request прихваћен само ако: - Написан ADR за промене - Прошао CI (уговори, тестови, миграције) - Ажуриране чек-листе и документација - Додати примери (Go/PHP SDK, миграције, метрике) ### 16.3 Управљање - **Maintainers:** одговорни за review и издавање нових верзија CVA манифеста - **Contributors:** доносе идеје, исправке грешака, додатке чек-листама и обрасцима - Одлуке се доносе кроз RFC/ADR --- ## Кључне разлике CVA ### Од класичне Hexagonal/Clean архитектуре **Иста основа** (DDD + Hexagonal), али CVA чини обавезним ствари које се обично "дорађују касније": - Догађаји (Domain Events + Outbox/Inbox) - Посматрање (трасирање/метрике/структурни логови) - Feature flags са пресечном интеграцијом - Управљана еволуција (canary/blue-green, верзионисање уговора) - Визуализација тока (web/VR) **Идеја:** систем је иницијално "жив, транспарентан и безбедно еволуира". ### Где је CVA бољи 1. **Транспарентност и отклањање грешака** — уграђен OpenTelemetry, пословне метрике, временска линија 2. **Поуздана интеграција** — Outbox/Inbox, идемпотентност, поновни покушаји по подразумеваном 3. **Безбедни релизи** — feature flags, canary rollout, kill-switch, ауто-rollback 4. **Управљана еволуција** — SemVer, dual write/read, shadow write/read 5. **Developer Experience** — шаблони, чек-листе, стандарди 6. **Разумевање за бизнис** — визуализација тока, пословне метрике из кутије 7. **Безбедност по подразумеваном** — RBAC, ревизија, zero-trust ### Када изабрати CVA - Микросервиси и интеграционо тешки системи - Чести релизи и експерименти (growth/product тимови) - Високи захтеви за поузданост испоруке догађаја - Потреба за "транспарентно" објашњавање технологије бизнису - Планирано скалирање тима и пројекта ### Када остати на класичној Hexagonal - Једноставан сервис/монолит без активне еволуције - Мали тим, нема ресурса за телеметрију и feature flags - Скоро нема спољних интеграција, меки SLA --- ## Закључак **ЧистаВајбАрхитектура** није само скуп образаца, већ холистички приступ изградњи модерних backend система. CVA чини систем: - **Транспарентним** — сваки захтев видљив од почетка до краја - **Поузданим** — догађаји испоручени са гаранцијама - **Безбедним** — промене контролисане и могу се вратити - **Разумљивим** — визуализација помаже свима: од програмера до бизниса Ово је "Hexagonal на стероидима" — скупље на почетку, али вишеструко се исплаћује на средњим и великим системима. --- ## VR vizuelizator mikroservisa - URL: https://vbcherepanov.com/sr/articles/vr-microservices-visualizer - Published: 2025-10-15 - Reading time: 4 min - Tags: Backend, Distributed Systems - Language: sr > Imersivan VR alat za debagovanje i analizu tokova mikroservisa sa Go, gRPC i Unity VR. Upoznajte **VR Microservices Visualizer** — alat koji sam razvio kako bih pojednostavio debugovanje, analizu i upravljanje složenim mikroservisnim arhitekturama koristeći Go, gRPC i Unity VR.\ _2 minuta čitanja · Vitalii Cherepanov_ ## Problem: kompleksno upravljanje mikroservisima Rad sa distribuiranim arhitekturama često dovodi do uskih grla, neefikasnog korišćenja resursa i otežanog praćenja tokova zahteva. Klasični alati za monitoring i vizuelizaciju nemaju intuitivan interfejs i imerzivni uvid u realnom vremenu. _Projekt je u aktivnom razvoju._ ## Moja uloga: autor i glavni developer Kao kreator i full-stack developer VR Microservices Visualizer-a, odgovoran sam za kompletno tehničko rešenje: - Dizajn arhitekture i modela podataka - Backend u Go-u sa event-driven pristupom i real-time osvežavanjem - gRPC komunikacija između servisa - Imerzivan front-end u Unity VR za Oculus Quest 3 ## Rešenje: VR vizuelizacija mikroservisa VR Microservices Visualizer je aplikacija koja pruža jasnu i interaktivnu sliku interakcija mikroservisa u VR okruženju. Kombinacija Go-a, gRPC-a i Unity VR pomaže timovima da: - Brže pronađu uska grla - Optimizuju potrošnju resursa - Jasno razumeju tokove zahteva i događaja u realnom vremenu ### Tehnologije - **Go (Golang)** — backend mikroservisa i obrada podataka u realnom vremenu - **gRPC** — brza i pouzdana komunikacija između servisa - **Unity VR** — interaktivna i imerzivna vizuelizacija - **Oculus Quest 3** — dostupno VR uređaj za svakodnevnu upotrebu ## Rezultat: brže debugovanje i analiza Alat značajno pojednostavljuje debugovanje, monitoring i upravljanje mikroservisnim arhitekturama. Korisnici ističu: - Brže otkrivanje problema i anomalija - Bolju kontrolu nad resursima - Dublje razumevanje kompleksnih sistema i scenarija Na taj način razvoj postaje efikasniji, vreme zastoja se smanjuje, a poslovna vrednost visoko-opterećenih rešenja raste.