第 6 章 · 记忆与上下文

06 · 实战:记忆与多轮对话

用三轮连续对话展示 ContextManager 的效果:第一轮搜索的信息在后续轮次中被记录,上下文随着会话逐步积累。

无记忆 vs 有记忆

前三节实现了 ContextManager、ProjectRulesLoader 和会话持久化。这一节用真实的多轮对话来展示记忆的效果。

先看没有 ContextManager 时会发生什么:

> 这个项目用什么测试框架?

(搜索 vitest、jest、mocha...)
(读取 package.json...)
→ 使用 vitest

> 帮我写一个 search 工具的测试

(再次搜索测试框架...)     ← 重复搜索
(再次读取 vitest.config...)← 重复读取
→ (按 vitest 风格写测试)

第二轮任务明明跟第一轮相关,但 agent 毫无记忆,重新搜索了同样的信息。加上 ContextManager 后,每一轮积累的上下文会自动注入到后续轮次的系统提示词中。

三轮连续对话实录

用真实的任务序列跑一遍,观察上下文如何跨轮次积累:

第一轮:探索项目结构

> 这个项目有哪些源文件?

>> 开始处理: 这个项目有哪些源文件?
  ... 思考中
  > glob: src/**/*.ts
  + glob: src/ui/events.ts
  >  执行操作
  > glob: src/*.ts
  + glob: src/agent.ts
  >  执行操作
  > glob: src/**/*.json
  + glob: 未找到匹配 "src/**/*.json" 的文件。
  >  执行操作
  = 完成 [4次模型调用, 4次工具调用]

ContextManager 在这轮只记录了 glob 调用。由于 glob 不触发 recordFileRead(只有 read_file 才记录),readFiles 仍然为空。

第二轮:深入查看特定文件

> src/tools/search.ts 的执行逻辑是什么?

>> 开始处理: src/tools/search.ts 的执行逻辑是什么?
  ... 思考中
  > read_file: src/tools/search.ts
  + read_file: 文件: src/tools/search.ts (94 行)
  >  执行操作
  = 完成 [2次模型调用, 1次工具调用]

这一轮 read_file 被执行了,ContextManager 记录:

--- 当前上下文注入 ---
## 已读文件

- src/tools/search.ts: 文件: src/tools/search.ts (94 行)
--- 上下文结束 ---

从现在开始,后续每一轮的系统提示词中都包含了"agent 已经读过 search.ts"这个信息。

第三轮:利用记忆回答问题

> search.ts 中有没有处理空查询的逻辑?

>> 开始处理: search.ts 中有没有处理空查询的逻辑?
  ... 思考中
  > read_file: src/tools/search.ts
  + read_file: 文件: src/tools/search.ts (94 行)
  >  执行操作
  = 完成 [2次模型调用, 1次工具调用]

观察: 第三轮模型仍然调用了 read_file 重新读取 search.ts。为什么?因为 ContextManager 的文件摘要只保留了前 3 行:

/** 只保留前 3 行作为摘要 */
const lines = content.split("\n").slice(0, 3);
const summary = lines.join("\n");
this.readFiles.set(path, summary);

read_file 工具返回的第一行是 文件: src/tools/search.ts (94 行)——这是工具的格式化输出头部,不是文件内容。3 行摘要不够让模型判断空查询逻辑,所以模型选择重新读取。

这恰好展示了记忆系统的真实局限——摘要策略决定了记忆的有效性。如果摘要包含了足够的关键信息,模型就能跳过重复读取;如果不够,模型会自行补全。

formatForPrompt 输出追踪

三轮对话后,contextManager.formatForPrompt() 的完整输出保持不变:

## 已读文件

- src/tools/search.ts: 文件: src/tools/search.ts (94 行)

这个文本被追加到系统提示词的末尾,模型在每一轮都能看到。这就是"记忆"的实现方式——不是数据库,不是文件系统,就是把之前的操作摘要塞进系统提示词。

记忆的边界:命令失败也记住

ContextManager 还记录失败的命令。如果 agent 尝试运行一个不存在的命令:

> 运行测试

  > run_command: npm test
  + run_command: 错误: Command not found: npm

ContextManager 记录:

failedCommands: {
  "npm test": "错误: Command not found: npm"
}

下一轮如果 agent 需要运行测试,系统提示词中会出现:

## 失败过的命令

- `npm test`: 错误: Command not found: npm

模型看到这个信息,会改用 pnpm testnpx vitest run——避免重蹈覆辙。

记忆的容量限制

ContextManager 有两个容量限制:

/** 文件摘要只保留前 3 行 */
const lines = content.split("\n").slice(0, 3);

/** 最近搜索最多保留 5 条 */
private static readonly MAX_RECENT_SEARCHES = 5;

这些限制的目的是控制上下文长度。如果系统提示词太长,模型的有效上下文窗口会变小,响应质量下降。所以记忆是"精选摘要"而不是"完整记录"。

这一章做了什么

回顾第 6 章的成果:

  • ContextManager:三类信息(已读文件、失败命令、最近搜索),每类有独立的容量策略
  • formatForPrompt():把记忆格式化为可注入系统提示词的 markdown
  • ProjectRulesLoader:从 AGENTS.md 加载项目规则
  • 会话持久化:ContextManager 在 main.ts 的 while 循环外创建,跨轮次传递
  • agent 集成:记忆自动注入系统提示词,不需要额外参数

agent 从第 5 章的"无状态工具"进化为"有状态助手"。下一章会给 agent 加上技能系统,让它能根据任务类型切换工作方式。

登录以继续阅读

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

立即登录

On this page