第 6 章 · 记忆与上下文
02 · 运行时状态跟踪
实现 ContextManager,跟踪 agent 在会话中读过的文件、失败的命令和最近的搜索。
ContextManager 的职责
ContextManager 是一个纯 JavaScript 对象,负责记录 agent 在一次会话中的操作历史。它不做任何决策——只负责收集和格式化信息。
三类跟踪数据:
| 数据类型 | 什么时候记录 | 记录什么 |
|---|---|---|
| 已读文件 | read_file 工具执行后 | 文件路径 + 内容前 3 行摘要 |
| 失败命令 | run_command 工具返回错误后 | 命令 + 错误信息 |
| 最近搜索 | search 工具执行后 | 搜索关键词 + 结果第一行 |
实现
在 src/memory/context.ts 中:
export class ContextManager {
private readFiles = new Map<string, string>();
private failedCommands = new Map<string, string>();
private recentSearches: Array<{ query: string; summary: string }> = [];
private static readonly MAX_RECENT_SEARCHES = 5;
/** 记录一次文件读取,只保留前 3 行作为摘要 */
recordFileRead(path: string, content: string): void {
const lines = content.split("\n").slice(0, 3);
this.readFiles.set(path, lines.join("\n"));
}
/** 记录一次命令失败,截断过长命令 */
recordCommandFailure(command: string, error: string): void {
const key = command.length > 100 ? command.slice(0, 100) : command;
this.failedCommands.set(key, error);
}
/** 记录一次搜索,保留最多 5 条 */
recordSearch(query: string, result: string): void {
const summary = result.split("\n")[0] ?? "";
this.recentSearches.push({ query, summary: summary.slice(0, 200) });
if (this.recentSearches.length > ContextManager.MAX_RECENT_SEARCHES) {
this.recentSearches.shift();
}
}
/** 格式化为可注入系统提示词的文本 */
formatForPrompt(): string {
const parts: string[] = [];
if (this.readFiles.size > 0) {
const fileList = Array.from(this.readFiles.entries())
.map(([path, summary]) => {
const preview = summary.split("\n")[0] ?? "";
return `- ${path}: ${preview.slice(0, 80)}`;
})
.join("\n");
parts.push(`## 已读文件\n\n${fileList}`);
}
if (this.failedCommands.size > 0) {
const cmdList = Array.from(this.failedCommands.entries())
.map(([cmd, error]) => `- \`${cmd}\`: ${error}`)
.join("\n");
parts.push(`## 失败过的命令\n\n${cmdList}`);
}
if (this.recentSearches.length > 0) {
const searchList = this.recentSearches
.map((s) => `- "${s.query}": ${s.summary.slice(0, 100)}`)
.join("\n");
parts.push(`## 最近搜索\n\n${searchList}`);
}
return parts.length > 0 ? parts.join("\n\n") : "";
}
clear(): void { /* ... */ }
}设计要点
用 Map 而不是数组。 readFiles 和 failedCommands 用 Map 存储,key 是文件路径或命令。同一个文件被重复读取时,Map 自动覆盖旧记录,不会产生重复条目。
搜索记录用数组,但限制数量。 recentSearches 保留最近 5 条搜索。超过时用 shift() 移除最旧的。5 条够用——模型不需要看到很久以前的搜索记录。
formatForPrompt 只在有内容时生成文本。 如果没有任何记录,返回空字符串,不会在系统提示词中产生空段落。注入逻辑会跳过空字符串。
摘要截断。 文件内容只保留前 3 行,搜索结果只保留第一行,命令超过 100 字符就截断。这些截断是在记录时做的,不是在格式化时做的——这样 Map 里存的始终是摘要,不会意外地存储大量内容。
在 agent 中记录
ContextManager 通过 RunAgentOptions 传入 agent 循环。在工具执行成功后,根据工具类型记录信息:
const result = await tool.execute(toolCall.arguments, state);
stats.toolCallCount++;
if (ctx) {
if (toolCall.name === "read_file" && !result.startsWith("错误")) {
ctx.recordFileRead(String(toolCall.arguments.path ?? ""), result);
} else if (toolCall.name === "search") {
ctx.recordSearch(String(toolCall.arguments.query ?? ""), result);
} else if (toolCall.name === "run_command" && result.includes("错误")) {
ctx.recordCommandFailure(
String(toolCall.arguments.command ?? ""),
result.split("\n")[0] ?? "",
);
}
}三个条件判断:
read_file: 只在读取成功时记录(!result.startsWith("错误"))。文件不存在或权限不足时不需要记录。search: 无条件记录所有搜索,包括空结果——"搜了但没找到"也是有价值的信息。run_command: 只在失败时记录(result.includes("错误"))。成功的命令不需要特别记住。
在 main.ts 中持久化
关键点:ContextManager 在 main.ts 的交互循环外创建,在整个会话期间持续存在:
const contextManager = new ContextManager();
while (true) {
const input = await rl.question("> ");
// ...
const result = await runAgent(state, model, tools, {
permissionGuard,
emitter,
contextManager, // 同一个实例跨轮次传递
projectRules,
});
// ...
}每一轮对话都传入同一个 contextManager 实例。agent 在这一轮记录的信息,下一轮还在——这就是跨轮次记忆的实现。
下一节讲解项目规则加载器。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。