
The usual way to give expert knowledge to a coding agent is to write a Markdown file and trust that the agent reads it. That works until the document grows: every token of guidance is a token that isn't available for the code, for the conversation history or for the output. Context is a scarce resource, and large skills consume it even when the task doesn't need them.

[Modern Web Guidance](https://github.com/GoogleChrome/modern-web-guidance-src), the Chrome team's project to stop agents from writing outdated patterns, inverts that pattern. Its main skill holds no knowledge: it holds instructions for going out to find it. And the search engine is an embeddings model that runs locally, with no network and no API keys.

It's an interesting catalogue of guides, but what grabs me is the architecture underneath, because it's replicable in any project that needs to bring domain knowledge to an agent.

## The inverted pattern

The numbers in the published package explain the decision well. The `modern-web-guidance` distribution includes 139 Markdown guides that add up to about 975 KB of text, in the order of a quarter of a million tokens. Loading all of them isn't an option: they don't fit, and even if they did, filling the window with 138 irrelevant guides degrades the model's attention on the one that does matter.

The `SKILL.md` that gets installed is 5.6 KB. Of that, the `description` block in the frontmatter is about 1,000 characters, in the order of 250 tokens, and it's the only part permanently in context. The body of the skill loads only when the agent decides to activate it, and what it holds isn't a guide, it's two commands:

```sh
# Step 1: search for relevant use cases
npx -y modern-web-guidance@latest search "<query>"

# Step 2: retrieve the full guide by ID
npx -y modern-web-guidance@latest retrieve "<id>"
```

> Right here we can already see a dependency on Node and npm.

The agent uses about 250 tokens at all times, about 1,400 once it activates the skill, and only then decides which specific guide is worth putting into context. Search returns the `tokenCount` of each candidate, so the agent itself can reason about the cost before asking for it.

The project also keeps a _megaskill_, a single document with all the content concatenated, for agents that can't run commands. It's the plan B, not the main solution.

## Local semantic search, no network and no API keys

The `search` command doesn't call any API: it runs an embeddings model inside the Node process.

The model is `all-MiniLM-L6-v2`, a small BERT from the sentence-transformers family that produces 384-dimension vectors. It's converted to a TensorFlow.js Graph Model and sits inside the npm package: 22.5 MB of weights in `group1-shard1of1.bin` plus 576 KB of topology. Unquantized, in float32, a decision the repository justifies as maximum accuracy parity with the original model.

Two details of the conversion matter. The graph has the _mean pooling_ and L2 normalization layers baked in, so the output tensor is already the final normalized vector and there's no post-processing in JavaScript. And the BERT tokenizer resolves with `local_files_only` against a cache that also ships inside the package, so there isn't a single request to Hugging Face at runtime.

```js
// serving/lib/tfjs-embedder.ts (simplified)
await setBackend("cpu");
this.model = await loadGraphModel(ioHandler);
this.tokenizer = await BertTokenizer.from_pretrained(
  "Xenova/all-MiniLM-L6-v2",
  { local_files_only: true }
);
```

The index is precomputed at build time, not at query time. Each guide is split by headings and each fragment is embedded with a prefix that gives it context about where it lives:

```js
const embeddingText = `${id} (${category})\nFeatures: ${featuresUsed.join(", ")}\n\n${chunk}`;
const vector = await embedder.embed(embeddingText);
```

The result is serialized into a 5.6 MB `use-cases.vectors.gen.json.gz` that ships with the package. On each search, the CLI computes the embedding of the query, walks the vectors in memory calculating cosine similarity, drops anything below 0.3 and returns five results. Since there are several fragments per guide, it groups by ID and keeps the highest similarity of each one: the fragments compete with each other, but the guide shows up only once.

There's no vector database and no approximate index. At this volume, a loop over an array and a dot product are enough.

Why embeddings and not `grep`? Because the agent asks by intent and the guides are written with specification vocabulary. This query shares no meaningful word with the guide that answers it:

```console
$ npx -y modern-web-guidance@latest search "close a popup when the user clicks outside of it"
[{"id":"light-dismiss-a-dialog","description":"Create a modal dialog that can be closed via light dismiss (i.e. clicking or tapping outside of the dialog)","category":"ui-behaviors","featuresUsed":["<dialog closedby>"],"tokenCount":1085,"similarity":0.6284},
...
```

Whoever writes the guide says _light dismiss_ and `<dialog closedby>`; whoever asks says _popup_ and _clicks outside_. A lexical search fails; the vector space gets it right at 0.63 similarity.

Switch queries to compare both methods over the same 139 guides. The results are real: the meaning column comes from running `search`, and the keyword column from counting occurrences in the package files.

<figure>
  <iframe
    id="mwg-search-demo"
    src="/demos/semantic-vs-keyword-search-en.html"
    width="100%"
    height="620"
    style="border: none; border-radius: 8px; display: block;"
    title="Interactive comparison between keyword search and semantic search over the Modern Web Guidance guides"
    loading="lazy"
  ></iframe>
</figure>
<script>
window.addEventListener('message', function (ev) {
  if (ev.data && typeof ev.data.mwgSearchH === 'number') {
    var f = document.getElementById('mwg-search-demo');
    if (f) f.style.height = (ev.data.mwgSearchH + 8) + 'px';
  }
});
</script>

The third case is the one I like most, because it doesn't work out cleanly: embeddings improve precision, they don't guarantee it. Latency sits around 1.2 to 1.5 seconds per query on CPU (MacBook Air M4), process startup included. For an operation that happens once at the start of a task, that's a reasonable price for spending no network and no context.

The "local" part deserves a qualifier: inference never leaves the machine, but the CLI does send usage telemetry, and the query text goes in it along with the returned IDs and the latency. It's on by default and turns off with `DISABLE_TELEMETRY=1`.

## The guide as an evaluable unit

The second good decision in the design is that a guide isn't a document, it's a directory with a contract. Each use case brings together five pieces:

- `guide.md`: the directives the agent reads.
- `demo.html`: the reference implementation, written by hand.
- `expectations.md`: the assertions that must hold if the guide is followed properly.
- `negative-demo.html`: a deliberately incorrect implementation.
- `grader.ts`: a Playwright test that scores any HTML against those expectations.

The expectations are verifiable and deterministic, not judgments:

```markdown
- Cards with class name `.off-screen-card` must set `content-visibility: auto`.
- Cards with class name `.off-screen-card` must set `contain-intrinsic-size`.
- The `.debug-panel` must be hidden using `content-visibility: hidden`.
```

The piece that closes the system is calibration: the grader has to score 100% on the good demo and 0% on the negative one. If it doesn't, the pipeline deletes it, regenerates it with the failing tests passed in as context, and retries. A grader that passes both implementations measures nothing, and without this check nobody would notice: it would keep scoring 100% in every evaluation and the guide would look perfect.

On the content side, the guides don't repeat compatibility data by hand. They interpolate it with macros that expand at build time against `web-features` and MDN's compatibility data:

```markdown
{{ BASELINE_STATUS("content-visibility") }}
```

Which produces, with today's data:

```text
Baseline status for content-visibility: Newly available. It's been Baseline since 2025-09-15.
Supported by: Chrome 108 (Nov 2022), Edge 108 (Dec 2022), Firefox 130 (Sep 2024), and Safari 26 (Sep 2025).
```

The care about density reaches the format of that line. The code that generates it consolidates desktop and mobile versions into a single label when they match, and splits them out only when they diverge, justifying it with an analysis over the compatibility data. It looks like a formatting detail, but it's a context engineering decision.

## Calibrating the content with evaluations

With reliable graders, the question "is this guide worth anything?" stops being an opinion. The harness runs each task twice, unguided and guided, and compares the rate of assertions that pass.

The project uses two metrics to decide. _Opportunity_ is 100% minus the unguided rate: if the model already solves the task well on its own, there's nothing to gain and the guide is redundant. _Uplift_ is the difference between both rates, and it's what gets optimized. Guides shed everything models already know, because that content takes up context without changing the result.

The repository publishes aggregate results over 130 tasks and more than a thousand assertions. Across different models, unguided agents land around 54-57% of assertions passed, and guided ones between 82% and 89%. The jump is around 30 percentage points and stays fairly stable across agents. Read it with the usual caution: it's a benchmark designed by the people who write the guides, over tasks they pick themselves. What matters isn't the figure, it's that the closed loop exists.

## What we can reuse in our own project

The architecture replicates with small pieces and none of them needs infrastructure.

We can separate the index from the content, leaving in the skill only the instructions for searching. We can precompute embeddings at build time and ship them as a compressed artifact, because a few dozen or a few hundred documents don't justify a vector database. We can split by headings and prefix each fragment with its identity, which is what keeps similarity from depending on the fragment repeating its own context. And we can demand of every document a test that proves the guide changes the result.

That last part is the most demanding and the one that pays off most. Writing an agent's documentation without measuring its effect is writing blind, and the natural bias is to always add more text. With a grader in front of us, removing content becomes a defensible decision.

## Conclusion

I love how Modern Web Guidance approaches context: it treats it as a budget. Keeping the skill present costs about 250 fixed tokens, retrieval is delegated to a semantic search engine that runs on the machine of whoever is programming, and only then does it spend context on the specific guide that's needed.

If we're writing skills for our teams, the pattern of searching first and retrieving after with local embeddings is cheaper to set up than it looks and scales much better than Markdown that grows out of control. The code is available in [modern-web-guidance-src](https://github.com/GoogleChrome/modern-web-guidance-src) and a real guide, the [navigation-drawer](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/navigation-drawer.md) one, shows the final format that reaches the agent.

## Glossary

Terms that show up in the article and are worth having clear:

- **Skill**: a set of instructions an agent loads to solve a type of task. In the usual format, a `SKILL.md` with YAML frontmatter where the `description` field decides when it activates.
- **Embedding**: a representation of a text as a vector of numbers. Texts with similar meaning end up close together in that space, even when they share no words.
- **Cosine similarity**: a measure of the angle between two vectors, between 0 and 1. It's what decides which guide best answers the query.
- **Chunk**: the fragment a document is split into before computing its embedding. Here, each section delimited by a heading.
- **Grader**: a program that scores an implementation against a list of expectations. In this project, a Playwright test that opens the HTML and checks each assertion.
- **Calibration**: checking that the grader scores 100% on the correct implementation and 0% on the incorrect one. Without this, we don't know whether the grader measures anything.
- **Harness**: the infrastructure that runs the evaluations, launches the agent with and without guidance, and collects the results.
- **Opportunity**: 100% minus the unguided pass rate. It measures how much room for improvement there is. If it's low, the model already does fine on its own and the guide adds nothing.
- **Uplift**: the difference between the guided and unguided pass rates, in percentage points. It's what gets optimized when writing and removing content.
- **Baseline**: the availability status of a web feature across the main browsers. _Newly available_ when it has just landed in all of them; _Widely available_ after 30 months.
