Agent memoryprompt designLLMKorean

Examples beat instructions

Memories extracted from Korean conversations were being stored in English. No placement of the instruction helped, because the instruction was never the problem.

Aug 15, 20269 min

I was building long-term memory for an agent. Facts a user drops during a conversation get extracted, stored, and recalled in later sessions.

One day I opened the store and read this:

User asked why pgvector 0.8 is necessary for service deployment.

The conversation had been in Korean from beginning to end.

Where this happens

Structure first. The memory service does not own the transcript. The transcript lives in exactly one place, and this service holds only summaries, cursors, and memories.

writesignalre-readextractrecallUserAgentTranscript storesource of truthSignal queuestreamIngest workerMemory storevector + ledger

Hover a node to reveal its connections.

Not replicating the transcript was the best early decision we made. Retention, deletion requests, and the boundary of personal data all stay the problem of one store. The cost is that the worker has to re-read the original every time.

Why per-turn extraction does not work

The first design was simple: a turn arrives, extract that turn.

Counting tokens killed it.

What one extraction call sends · 9,564 tokens
Fixed extraction prompt7,606 tokens79.5%
Previous 5 turns965 tokens10.1%
Existing memories (20)800 tokens8.4%
The actual new turn193 tokens2.0%

Hover a segment or a row for detail.

We were resending 98% to process 2%. When fixed cost dominates, there is one answer: batch, and share the fixed cost across turns.

Cost of processing 1,000 turns by batch size
Turns per batch
5
200
Extraction calls
2.07M
Total input tokens
4.6× cheaper
vs per-turn

Batches of five cut it by 4.6×. Push the slider further and it keeps improving, but the returns fall off a cliff. Sharing the fixed cost pays out early; after that you are only adding latency before a memory becomes available. Sessions were the natural unit anyway, and five turns sat closest to how the service was actually used.

AgentQueueWorkerTranscriptsMemory storeturn happenedwakewait for N turns / idleread after cursorreturn turnsmask, then extractstore
1 / 7

01A signal every turn, but never the content. The queue only says that something occurred.

No placement of the instruction worked

If fixed cost is 80%, batching is not the only lever. You can shrink the fixed cost itself. Reading that prompt is how I ran into the language bug.

The first attempt was to just say it: write in the same language as the input. Four conditions:

ConditionInstruction placementResult
A8 lines at the end of the user promptEnglish
BEnd of the system prompt, forcefullyEnglish
CStart of the system prompt, forcefullyEnglish
EMinimal prompt with no examples + one lineKorean

A, B and C all failed and only E passed. That was the answer. Placement was never the variable. What E removed was not a position but the examples.

The cause was fourteen examples

The prompt was 33,661 characters and contained fourteen English few-shot examples. In that entire prompt, the word language appeared zero times. Not one Korean character either.

The model had not ignored the instruction. It read it, and then fourteen consecutive demonstrations of "take Korean, write English" outvoted a single line of prose.

Same input, different prompt

I cut the examples to three and made one of them a Korean input/output pair. The language rule is now carried by demonstration, not by a sentence.

Only the prompt changes; the input is identical
Input — Korean conversation
user우리 서비스 배포할 때 pgvector 0.8 이 꼭 필요한 이유가 뭐였지?
assistantHNSW 가 post-filter 라서 스코프 필터가 강하면 결과가 모자랍니다. 0.8 의 iterative index scan 이 그걸 해결합니다.
Extracted memories
User asked why pgvector 0.8 is necessary for service deployment.
실패Stored in English from a Korean conversation — and the technical fact the assistant supplied was dropped entirely.

Measurements

Two rounds on synthetic data, then a third on six real conversation threads. A/B order was reversed and run twice to cancel position bias.

Prompt length
Before
33,661 chars
After
3,549 chars

A 9.5× reduction. The output schema was left untouched; only guidelines and examples were compressed.

Input tokens per extraction (mean over 6 real threads)
Before
12,810
After
6,112

6,698 tokens saved per call — 52.3%. The ratio looks modest because these threads had unusually long bodies, which dilutes the prompt's share.

50% → 100%
Korean preserved (3/6 → 6/6)
9 : 3
Quality judgement, compact wins
97 → 69
Extractions. Read as less over-splitting

The number that hurt was 50%. The English-extraction rate I believed until then was 13.4%, measured on synthetic data. Reality was nearly four times worse.

What came along with it

Rewriting the examples was also a chance to encode two contaminations that had been sitting there.

The first is recall re-ingestion. A user asks "what did I say I liked?", the agent lists stored memories back, and that listing gets extracted as new facts. Copies of the same fact multiply every time the topic comes up.

The second is absence statements. "There is no record of that" was being stored as a memory that there is no record. A lookup result is not a memory.

ContaminationBeforeAfter
Recall re-ingestion2/70/7
Absence statements5/70/7
Attribution accuracyno regressionno regression

How fast copies actually accumulate only shows up across sessions. Fifteen consecutive sessions, counting cumulative duplicates:

Duplicate copies accumulating across sessions
017.535sessioncumulative115
No suppression33First pass17Small-model reference7Final3

Without suppression, copies grow linearly. What matters is less that the final line is low and more that it flattens early: restating the same fact stops producing a new copy. Legitimate facts held at 22 of 24 throughout.

What I could not fix

It also became clear what a prompt cannot do.

The agent suggests "navy is a safe choice", the user says nothing at all, and "the user prefers navy" gets stored. Instructions did not stop it. Extra examples did not. A dedicated non-response example did not. Three attempts, three failures.

Fabricating decisions that were never made is fully blocked by rules now. But whose opinion a stated thing belongs to is not something a prompt adjudicates. That belongs in a filtering layer after ingestion.

What is left

Three things.

For a model, an example is a stronger signal than an instruction. When a prompt contains both a rule and a demonstration that contradict each other, the demonstration wins. So trying to fix it by strengthening the instruction will keep failing. Find the contradicting demonstration instead.

Synthetic benchmarks only fix the order of magnitude. The distance between 13.4% and 50% is that lesson. Using synthetic data to place a threshold is fine; treating that number as evidence about production is not.

A long prompt is both a cost and a bug surface. Nobody knew that language appeared zero times in 33,661 characters. Shrinking it was the act of reading it, and the bug was found while reading.