СИСТЕМА КОМПЬЮТЕРНОГО ДОСТУПА

Системы памяти агентов: постоянный контекст для вашего ИИ


У каждого ИИ-агента есть проблема с памятью. Каждый вызов модели является безсостоятельным. Модель не помнит предыдущий запрос. Она не помнит имя пользователя, его предпочтения или предыдущие решения. Без внешней памяти ваш агент начинает с нуля на каждом ходу.

В этом руководстве рассматриваются четыре стратегии памяти — от простейшей до наиболее мощной — и когда использовать каждую. Все примеры используют TypeScript и Anthropic SDK.

Почему агентам нужна память

Рассмотрим агента проверки кода. При первом запуске пользователь объясняет конвенции своей команды: никаких типов any, всегда обрабатывать ошибки явно, предпочитать функциональный стиль. Агент производит хорошую проверку.

При втором запуске пользователь отправляет другой файл. Агент забыл всё. Он пропускает те же нарушения конвенций, которые отметил вчера.

Это основная проблема: агенты безсостоятельны, но полезным агентам нужна непрерывность.

Память даёт агентам три возможности:

  • Воспоминание — извлечение фактов из предыдущей части разговора или из прошлого сеанса
  • Персонализация — адаптация к предпочтениям пользователя со временем
  • Координация — в мультиагентных системах, совместное использование состояния между агентами

Четыре типа памяти

Прежде чем писать код, полезно чётко назвать стратегии. Память агента делится на четыре паттерна:

ТипГде хранитсяОбластьЛучше всего для
БуферВ контексте (массив сообщений)Текущий сеансКороткие разговоры
РезюмеВ контексте (сжатый)Текущий сеансДлинные разговоры
СемантическийВнешний (векторная БД)Между сеансамиВоспоминание знаний
ЭпизодическийВнешний (хранилище ключ-значение)Между сеансамиФакты о пользователе, предпочтения

У каждой стратегии разный компромисс между простотой, стоимостью и возможностями. Выбирайте минимум, решающий вашу задачу.

Стратегия 1: Буфер разговора

Простейшая стратегия — передавать полную историю разговора на каждый вызов модели. Модель видит каждое предыдущее сообщение и отвечает с полным контекстом.

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Message {
role: "user" | "assistant";
content: string;
}
// Буфер содержит полную историю разговора
const buffer: Message[] = [];
async function chat(userMessage: string): Promise<string> {
// Добавить новое сообщение пользователя в буфер
buffer.push({ role: "user", content: userMessage });
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
// Передавать полный буфер при каждом вызове
messages: buffer,
});
const assistantMessage =
response.content[0].type === "text" ? response.content[0].text : "";
// Добавить ответ ассистента в буфер
buffer.push({ role: "assistant", content: assistantMessage });
return assistantMessage;
}
// Пример использования
await chat("My name is Alex and I prefer Python over TypeScript.");
await chat("What language should I use for my next project?");
// Модель помнит предпочтение, указанное в первом сообщении

Когда использовать: Разговоры менее 20 ходов. Простые чат-боты. Прототипы.

Ограничение: Контекстные окна конечны. Длинный сеанс в конечном счёте превысит лимит токенов, и старейшие сообщения должны быть отброшены. Модель молча теряет ранний контекст, что вызывает непоследовательное поведение.

Стратегия 2: Скользящее резюме

Когда разговоры становятся длинными, суммируйте старые ходы вместо их отбрасывания. Поддерживайте сжатое резюме того, что произошло раньше, со скользящим окном последних сообщений.

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface ConversationState {
summary: string; // Сжатая история старых ходов
recentMessages: Array<{ role: "user" | "assistant"; content: string }>;
maxRecentTurns: number; // Сохранять дословно последние N ходов
}
const state: ConversationState = {
summary: "",
recentMessages: [],
maxRecentTurns: 6, // 3 пользователь + 3 ассистент хода
};
// Сжать старые ходы в текущее резюме
async function compressSummary(
currentSummary: string,
messagesToCompress: Array<{ role: string; content: string }>
): Promise<string> {
const formatted = messagesToCompress
.map((m) => `${m.role.toUpperCase()}: ${m.content}`)
.join("\n");
const response = await client.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 512,
messages: [
{
role: "user",
content: `Update the conversation summary by adding the new exchanges below.
Return only the updated summary. Keep it under 200 words.
Current summary:
${currentSummary || "(none)"}
New exchanges:
${formatted}`,
},
],
});
return response.content[0].type === "text" ? response.content[0].text : currentSummary;
}
async function chat(userMessage: string): Promise<string> {
// Когда буфер превышает лимит, сжать старейшую половину
if (state.recentMessages.length >= state.maxRecentTurns * 2) {
const toCompress = state.recentMessages.splice(0, state.maxRecentTurns);
state.summary = await compressSummary(state.summary, toCompress);
}
state.recentMessages.push({ role: "user", content: userMessage });
// Построить массив сообщений: системное резюме + последние дословные ходы
const messages = [
...(state.summary
? [
{
role: "user" as const,
content: `Context from earlier in this conversation:\n${state.summary}`,
},
{
role: "assistant" as const,
content: "Understood. I will use that context.",
},
]
: []),
...state.recentMessages,
];
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages,
});
const assistantMessage =
response.content[0].type === "text" ? response.content[0].text : "";
state.recentMessages.push({ role: "assistant", content: assistantMessage });
return assistantMessage;
}

Вызов резюме использует быструю, дешёвую модель (Haiku). Основной разговор использует способную модель (Sonnet). Это разделение сохраняет низкие затраты при сохранении качества.

Когда использовать: Сеансы более 20 ходов. Агенты поддержки. Агенты долгосрочных задач.

Ограничение: Резюме теряют детали. Конкретные факты, указанные в начале разговора, могут быть сжаты в расплывчатые утверждения. Для точного воспоминания конкретных фактов используйте эпизодическую память.

Стратегия 3: Эпизодическая память

Эпизодическая память хранит конкретные факты о пользователе или сеансе в хранилище ключ-значение. Агент извлекает факты во время разговора и извлекает их в будущих сеансах.

import Anthropic from "@anthropic-ai/sdk";
import fs from "fs/promises";
import path from "path";
const client = new Anthropic();
// В производстве замените это файловое хранилище на Redis или базу данных
const MEMORY_PATH = "./memory.json";
interface EpisodicStore {
[userId: string]: Record<string, string>;
}
async function loadMemory(): Promise<EpisodicStore> {
try {
const data = await fs.readFile(MEMORY_PATH, "utf-8");
return JSON.parse(data);
} catch {
return {};
}
}
async function saveMemory(store: EpisodicStore): Promise<void> {
await fs.writeFile(MEMORY_PATH, JSON.stringify(store, null, 2));
}
// Извлечь структурированные факты из последнего обмена
async function extractFacts(
userMessage: string,
assistantReply: string
): Promise<Record<string, string>> {
const response = await client.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 256,
messages: [
{
role: "user",
content: `Extract any personal facts, preferences, or important decisions from this exchange.
Return a JSON object of key-value pairs. Return {} if there is nothing worth remembering.
User: ${userMessage}
Assistant: ${assistantReply}`,
},
],
});
try {
const text = response.content[0].type === "text" ? response.content[0].text : "{}";
const match = text.match(/\{[\s\S]*\}/);
return match ? JSON.parse(match[0]) : {};
} catch {
return {};
}
}
// Форматировать сохранённые факты как инъекцию системного промпта
function formatMemory(facts: Record<string, string>): string {
const entries = Object.entries(facts);
if (entries.length === 0) return "";
return (
"What you know about this user:\n" +
entries.map(([k, v]) => `- ${k}: ${v}`).join("\n")
);
}
async function chat(userId: string, userMessage: string): Promise<string> {
const store = await loadMemory();
const userFacts = store[userId] ?? {};
const systemPrompt = formatMemory(userFacts);
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: systemPrompt || undefined,
messages: [{ role: "user", content: userMessage }],
});
const assistantMessage =
response.content[0].type === "text" ? response.content[0].text : "";
// Извлечь факты и сохранить их для будущих сеансов
const newFacts = await extractFacts(userMessage, assistantMessage);
if (Object.keys(newFacts).length > 0) {
store[userId] = { ...userFacts, ...newFacts };
await saveMemory(store);
}
return assistantMessage;
}
// Сеанс 1
await chat("user-123", "I prefer concise answers. No bullet points unless necessary.");
// Сеанс 2 (другой процесс, дни спустя)
await chat("user-123", "Explain how TCP handshakes work.");
// Агент помнит предпочтение форматирования из сеанса 1

Когда использовать: Агенты, ориентированные на пользователя, работающие в нескольких сеансах. Персонализация. Отслеживание предпочтений.

Ограничение: Факты накапливаются со временем. Устаревшие факты могут производить неверное поведение (пользователь сменил предпочтительный язык; старое предпочтение всё ещё в хранилище). Добавьте механизм обновления или истечения срока действия фактов.

Стратегия 4: Семантическая память (векторный поиск)

Эпизодическая память хранит дискретные факты. Семантическая память хранит документы, код или фрагменты разговоров, индексированные по смыслу. Когда агенту нужна информация, он выполняет поиск по индексу с помощью запроса.

Это основа Retrieval-Augmented Generation (RAG).

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Document {
id: string;
content: string;
embedding?: number[];
}
// Сгенерировать вектор embedding для текстовой строки
async function embed(text: string): Promise<number[]> {
// Claude не предоставляет endpoint embeddings напрямую.
// Используйте специализированную модель embeddings. Этот пример использует заглушку для иллюстрации.
// В производстве: используйте text-embedding-3-small (OpenAI), embed-english-v3 (Cohere),
// или саморазмещённую модель, например nomic-embed-text.
throw new Error("Replace this stub with a real embedding call");
}
// Вычислить косинусное сходство между двумя векторами
function cosineSimilarity(a: number[], b: number[]): number {
const dot = a.reduce((sum, val, i) => sum + val * b[i], 0);
const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
return dot / (magA * magB);
}
class SemanticMemory {
private documents: Document[] = [];
// Индексировать документ для последующего извлечения
async add(id: string, content: string): Promise<void> {
const embedding = await embed(content);
this.documents.push({ id, content, embedding });
}
// Извлечь top-k наиболее релевантных документов для запроса
async search(query: string, topK = 3): Promise<Document[]> {
const queryEmbedding = await embed(query);
return this.documents
.filter((doc) => doc.embedding)
.map((doc) => ({
doc,
score: cosineSimilarity(queryEmbedding, doc.embedding!),
}))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map(({ doc }) => doc);
}
}
const memory = new SemanticMemory();
async function answerWithContext(question: string): Promise<string> {
// Извлечь релевантные документы
const relevant = await memory.search(question);
const context = relevant.map((d) => d.content).join("\n\n---\n\n");
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: context
? `Answer the question using the following context:\n\n${context}`
: undefined,
messages: [{ role: "user", content: question }],
});
return response.content[0].type === "text" ? response.content[0].text : "";
}
// Пример: индексировать базу знаний и запрашивать её
await memory.add("doc-1", "The team uses React 18 with the app router pattern.");
await memory.add("doc-2", "All API routes must validate input with Zod schemas.");
await memory.add("doc-3", "Database queries use Drizzle ORM. Avoid raw SQL.");
const answer = await answerWithContext("How should I write a new API endpoint?");
// Агент извлекает doc-2 и doc-3 и генерирует обоснованный ответ

Для производственного использования замените хранилище в памяти специализированной векторной базой данных. Pgvector (расширение PostgreSQL) — простейший вариант, если вы уже используете Postgres. Chroma и Qdrant — хорошие автономные варианты.

Когда использовать: Ассистенты документации. Агенты поиска кода. Q&A по базе знаний. Всё, что требует воспоминания из большого корпуса.

Подключение памяти к MCP

Если вы следовали руководству по MCP-серверу, вы можете предоставить память как MCP-ресурс. Это делает ваш слой памяти доступным для любого MCP-совместимого клиента, включая Claude Code.

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "memory-server", version: "1.0.0" },
{ capabilities: { tools: {}, resources: {} } }
);
// Инструмент: сохранить факт
server.setRequestHandler("tools/call", async (request) => {
if (request.params.name === "remember") {
const { key, value, userId } = request.params.arguments as {
key: string;
value: string;
userId: string;
};
await storeEpisodicFact(userId, key, value);
return { content: [{ type: "text", text: `Stored: ${key} = ${value}` }] };
}
if (request.params.name === "recall") {
const { userId } = request.params.arguments as { userId: string };
const facts = await loadEpisodicFacts(userId);
return {
content: [{ type: "text", text: JSON.stringify(facts, null, 2) }],
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);

Любой агент, поддерживающий MCP, теперь может вызывать remember и recall как стандартные инструменты. Память становится общим сервисом, а не реализацией для каждого агента.

Выбор правильной стратегии

Используйте эту таблицу решений как отправную точку:

СитуацияСтратегия
Один сеанс, менее 20 ходовБуфер
Один сеанс, длительныйРезюме
Несколько сеансов, предпочтения пользователяЭпизодическая
Большая база знаний для запросаСемантическая
Всё вышеперечисленноеКомбинировать: резюме для сеанса + эпизодическая/семантическая между сеансами

Начните с буфера. Добавьте слой резюме, когда разговоры становятся длинными. Добавьте эпизодическую память, когда пользователи возвращаются между сеансами. Добавьте семантическую память, когда у вас есть корпус документов для извлечения.

Не добавляйте сложность раньше, чем она нужна. Буфер разговора — правильный ответ для большинства прототипов. Слишком раннее усложнение памяти добавляет стоимость, задержку и поверхность обслуживания без пропорциональной пользы.

Заключение

Память — это не одна вещь, а стек стратегий, решающих разные проблемы в разных областях. Буфер обрабатывает текущий сеанс. Резюме расширяет этот сеанс. Эпизодическая память соединяет сеансы. Семантическая память основывает ответы на базе знаний.

Выберите слой, решающий задачу, которая у вас есть сегодня. Оберните его за чистым интерфейсом, чтобы вы могли позже заменить реализации. И если ваши агенты общаются через MCP, предоставьте память как общий инструмент — это сохраняет каждый агент простым, давая системе в целом постоянный мозг.


Связанные статьи