第 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 而不是数组。 readFilesfailedCommands 用 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 在这一轮记录的信息,下一轮还在——这就是跨轮次记忆的实现。

下一节讲解项目规则加载器。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page