AI编码
dev.to ·
@Nicolas Micaud
我不再让 AI Agent 自己读项目记忆,改由服务端预注入
作者运行约 9 个并行 Claude Code 会话,长期问题是会话不读取项目记忆导致重复实现已定标准。v1 方案是在首条提示中要求模型先调用 memory_search,但约九分之一的会话会忽略该指令,造成静默的规范违背。v2 改为在会话启动、模型获得第一轮对话之前,由服务端确定性地执行语义检索,将 top_k 条记忆结果直接注入会话首条消息,并给出 _memory_preseed 函数与注入后任务文本示例。作者认为记忆召回不应放在提示层,而应放在管道层。
AI推荐理由 作者以运行约 9 个并行 Claude Code 会话的实践为背景,指出让模型自行调用 memory_search 的提示词方案在并行场景下不可靠,改为在会话启动前由服务端确定性执行语义检索并把结果注入首条消息,并给出 _memory_preseed 代码片段与注入后的任务示例。这是具体可复用的工程模式(把记忆召回从提示层移到管道层),对构建多 Agent 开发流程有参考价值;局限是单一个体经验、无对照数据或量化效果,且为系列文章续篇,需结合前文理解。
72
正文
Follow-up to I run 9 parallel Claude Code sessions — the bottleneck wasn't
the model, it was memory.
That post described the problem. This one is about the fix I shipped, and the
uncomfortable thing it taught me about "instructing" a model.
The setup
I build and run our products — a Swiss job platform, a handful of sites, the
infrastructure under them — with a cockpit that keeps ~9 Claude Code sessions
alive in parallel. Each session is a task: a kanban card you drag, and it
becomes a working agent.
The recurring failure was never intelligence. It was amnesia. Session #7 would
happily reimplement an error format we'd standardized weeks ago, because nothing
told it that decision existed. The decision was written down — in a markdown
note, in the project memory — but the session never looked.
v1: I asked nicely
My first fix was the obvious one. When a session spawns, append an instruction
to its first prompt:
{the task}
Start by calling the memory_search tool to load any relevant project context,
then propose a plan — don't execute anything without my go-ahead.
Enter fullscreen mode
Exit fullscreen mode
This is what most "give your agent memory" setups do: a system prompt, a tool,
and trust that the model uses them. And it mostly works. Mostly.
Here's the thing about "mostly" when you have nine of them running. One session
in nine ignoring the instruction isn't a 1-in-9 annoyance — it's a silent,
guaranteed defect generator. The model doesn't announce "I skipped the memory
this time." It just dives into the code, does something locally reasonable, and
violates a convention you'd forgotten you even needed to defend. You find out at
review, or worse, after merge.
I spent a while trying to make the instruction stickier — stronger wording,
putting it last, tool descriptions that begged. That's when it clicked: I was
trying to make a non-deterministic thing reliable by asking harder. Wrong
layer.
v2: I stopped asking
The recall doesn't belong in the prompt. It belongs in the plumbing.
Now, when a session spawns, the server runs the semantic search — before the
model gets a single turn — and injects the results directly into the session's
first message:
def _memory_preseed(query: str, top_k: int = 5) -> str:
hits = memory_search(query, top_k=top_k) # server-side, deterministic
if not hits:
return "" # empty memory → fall back to the ritual
lines = ["=== Project memory (auto-recalled) ==="]
for h in hits:
star = "★ " if h.get("priority") else ""
lines.append(f"- {star}[{h['note_name']}] {h['description']} — {h['snippet']}")
return "\n".join(lines)
Enter fullscreen mode
Exit fullscreen mode
The task the model receives now looks like:
Fix the healthcheck flapping on staging.
=== Project memory (auto-recalled) ===
- ★ [staging-stack] Staging API is on port 6443, deploys land on host "callisto"
- [deploy-ritual] Deploys go exclusively through `make ship-v2`, never raw rsync
The notes above were auto-recalled from project memory for this task. Call
memory_get on any note you need in full… then propose a plan and wait for my go.
Enter fullscreen mode
Exit fullscreen mode
The model can't not have the context now. It's not a tool it might call. It's
in the first tokens it reads.
Crucially, the old ritual is still there — as a fallback. If the memory is
empty (fresh project) or the embedding backend is down, the injected block is
empty and the prompt reverts to "please go search." Determinism where it
matters, graceful degradation where it doesn't.
Does it actually work? I planted a trap.
Claims about memory systems are cheap. So the release has an end-to-end test
that seeds a throwaway instance with two notes containing facts that exist
nowhere in the code: the staging port is 6443, deploys go to a host called
callisto, via make ship-v2. The workspace README is deliberately wrong
about all three.
Then it spawns a session asking: "what port does staging run on, which host do
we deploy to, and with what command?"
A session that reads the code (or the lying README) fails. A session with
working recall answers 6443 / callisto / make ship-v2 — and, in the transcript,
literally says it answered "without needing to search the codebase." That's
the whole product in one assertion: the fact reached the model because the
server put it there, not because the model went looking.
The boring parts (on purpose)
Notes are just markdown with YAML frontmatter — the same files Claude Code
already writes. One fact per file, a description: that doubles as the
embedding text, optional priority: high.
The index is incremental sqlite. Vectors live in the same store. No
pgvector, no ANN library — at a few hundred notes, dot-product over a warm
in-process model is instant, and I'd rather ship boring than clever.
Embeddings run locally by default (fastembed / ONNX, a ~120MB multilingual
MiniLM). No external embedding service unless you point one.
[[wikilinks]] are indexed, so a session can walk the graph with a
memory_links tool, and the cockpit draws it with type filters and connected
clusters.
priority: high is a bounded, multiplicative nudge — I started with a flat
additive boost and it let a weak match jump above genuinely relevant notes.
Multiply, don't add.
What I deliberately didn't build
This isn't an autopilot orchestrator. There's no "let the swarm run overnight."
Every irreversible action waits for a human click — the helm, not the
autopilot. The point of the memory work isn't to remove me from the loop; it's
to make sure that when I'm steering nine sessions, none of them is flying blind.
The lesson I keep relearning
When you need a model to do something every single time, don't put it in the
prompt and hope. Move it to a layer that doesn't have opinions. Prompts are for
judgment. Plumbing is for guarantees. I keep having to relearn which is which.