第 十 章 Agent 架构实操 深入学习 · 交互式 含 QA 测试

从“一锅炖”到“模块化”:重塑 AI Agent 的逻辑骨架

💡 DynamicPromptRenderer 固定顺序渲染:identity → tools → workspace → skills → memory。
一大段硬编码字符串本质是把配置藏在代码里。本章把 System Prompt 拆成固定顺序、运行态取值的五个 section,每个能力有唯一来源,Prompt 只负责渲染。缓存必须属于会话。
本章进度
0%
1 本章要掌握的目标
  • 固定顺序:identity → tools → workspace → skills → memory,可选 section 只追加尾部不插队。
  • 空值有形状:无工具写 (none)(行为要变),skills/memory 为空整段省略(行为不变)。
  • Context 必须是严格 JSON,非法值更新缓存前失败;undefined、Date/Map、NaN、循环引用都拒。
  • 缓存属于会话实例,键覆盖六项输入,少一项就会发过期指令;失败不污染缓存。
  • 分清运行态生效边界:工具/选中记忆下一轮生效,磁盘新增 Skill 是构建期快照,需重建 Agent。
2 核心知识点
固定顺序,运行态取值

renderer 直接读取已参与运行的对象,按固定顺序输出五个 section。没有工具时 tools 明确写 (none)——工具是核心能力,「没有」是模型需要知道的事实;skills/memory 是补充信息,「没有」就等于不存在,整段省略。判断标准:这个 section 为空时模型行为该不该变。可选 section 只 push 尾部,用测试断言相对位置而非包含。

const sections = [
  `## identity\n${identity}\ncontext: ${contextJson}`,
  `## tools\n${toolCatalog}`,
  `## workspace\n${workspace}`,
];
if (skillCatalog.length > 0) sections.push(`## skills\n${skillCatalog}`);
if (memoryBody.length > 0) sections.push(`## memory\n${memoryBody}`);
Context 必须是严格 JSON

只接受 JSON object,值递归限制为 scalar / array / object;拒绝 NaN、无穷、循环引用、symbol key、Date/Map/类实例;undefined 也拒(与 stringify 静默丢键不同)。JSON.parse 产生的 __proto__ 是普通自有键。循环检测用 active Set 且 finally 删除——同一对象在兄弟位置出现(合法 DAG)不误判。合法值按键排序、紧凑序列化,语义相同必然文本相同。

type JsonValue = boolean | number | string | null
  | readonly JsonValue[] | JsonObject;
缓存必须属于会话

缓存放在 renderer 实例字段,每次 buildAgent 新建,天然会话隔离。键 = identity + context + tools + workspace + skills + memory 六项,少一项的后果不是缓存效率低,而是给模型发过期指令(如新注册的工具模型不知道)。校验全部在写缓存之前完成,非法输入不会污染缓存;cacheHits 是可测试性指标。它与供应商 API prompt cache 不是一回事:一个省进程内拼装 CPU,一个省服务端重复前缀费用。

buildAgent(P10, ...) → 新 DynamicPromptRenderer
cacheKey = identity + context + tools
  + workspace + skills + selected memory
零参数 Provider 接入唯一 Loop

AgentRunner 只依赖一个零参数 provider,每次模型请求前调用 render()。两道校验:构造时验证 render 是函数;每轮验证返回非空字符串。失败不回退到构建期固定 systemPrompt——那段字符串不含 workspace/工具/Skill/记忆,用它等于换了个 Agent。Loop 也不捕获 renderer 内部异常:数据源坏了让它炸比静默降级安全。

render(): string {
  return this.#renderer.render({
    identity, tools, workspace, context,
    ...(skills ? { skills } : {}),
    ...(memory ? { memory } : {}),
  });
}
记忆只注入一次

P09 由 MemorySession.beforeModel() 追加系统 context 消息;P10 改为 selected memory 进入动态 Prompt 的 memory section,并关闭原消息注入(emitContextMessages: !dynamicPrompt)。注意只是关注入不关功能:选择、提取、整理照常。注入位置从「用户问题之后 index=2」挪到「系统提示内部 index=0」,请求少一条消息;测试断言出现次数 =1 而非包含。

new MemorySession({
  ...,
  emitContextMessages: false,  // P10 起关闭独立消息注入
});
createMemorySession(deps, !dynamicPrompt)  // P09 用 true
运行态变化的生效边界

「下一轮生效」要按真实实现理解,不是什么都会自动更新。工具与选中记忆每轮重读;workspace 是构建期绑定;SkillRegistry 是构建期扫描的快照,磁盘新增/删除不生效,要重建 Agent。不监视目录是刻意的:每轮扫磁盘会给每次请求加 I/O,而 Skill 目录在一次会话里几乎不变。

ToolRegistry 注册/移除      → 下一轮生效
本轮选中记忆变化           → 下一轮生效
workspace 路径             → 构建期固定,不变
磁盘新增/删除 Skill        → 不生效,需重建 Agent
3 机制流程
1
读取运行态对象

renderer 读取工具/选中记忆的当前值;Skill 目录是构建期快照,变更需重建。

2
按固定顺序渲染

identity → tools → workspace → (可选)skills → (可选)memory。

3
生成缓存键

覆盖全部可见输入的规范快照;相等则复用。

4
Provider 每轮调用 render

Loop 在每次模型请求前读取零参数 provider。

4 术语表
DynamicPromptRenderer纯渲染器:校验输入、规范 context、按固定顺序拼字符串、维护缓存;不发现依赖、不读磁盘、不选记忆。
DynamicPromptProvider构建期绑定 renderer 与运行态对象的适配层,对外只暴露零参数 render()。
sectionidentity / tools / workspace / skills / memory;前三个无条件存在,后两个按需。
规范 JSON键排序、紧凑输出的唯一序列化形式;语义相同必然文本相同,缓存键的基础。
运行态取值渲染时才读数据源当前值;prompt 层不重新实现工具发现或记忆选择。
API prompt cache供应商侧按请求前缀命中的缓存;与本章进程内渲染缓存不是一回事,当前适配器未分块接入。
5 QA 测试环节(自测题)
已完成 0 / 6 · 答对 0
Q1. Prompt 五个 section 的固定顺序是?
Q2. 没有选择记忆时,memory section 会?
Q3. 缓存属于哪里?
Q4. 非法 context(如 NaN)会发生?
Q5. P10 中选中的记忆如何进入模型?
Q6. render() 返回空字符串会?
6 验证与实验
  • npm run test:ch10:25 个测试文件 / 226 个用例全部通过,覆盖固定顺序、实时来源、严格 context、缓存隔离、失败不污染、记忆单次注入。
  • npm run ch10 -- --prompt "列出当前 Agent 的可用工具":模型能准确报出 8 个工具名,说明 tools section 真的到了它面前。
  • 验证运行态变化下一轮生效;磁盘新增 Skill 需重建 Agent 才可见(实验四)。
  • 六个离线实验可复现三阶段渲染、context 接受/拒绝清单、缓存失效与生效边界。