第 17 章

解释器

在 Deep Agents 中运行轻量级代码,组合工具、编排子代理并转换结构化数据

从"逐个工具调用"到"编写小程序编排一切"的关键一步

为什么需要它

你有没有试过让 Agent 连续调用十几个工具,每一步都要等模型决定下一步?

逐个工具调用的困境

  • 每个工具调用都要模型推理一次,Token 消耗巨大
  • 中间结果全部回到上下文,窗口快速膨胀
  • 循环、分支、重试?全靠 prompt 硬编码,不可控
  • 想把结果排序、过滤、聚合?没有通用方式
  • 并发调用多个工具?做不到,只能串行等待

解释器的解法

  • Agent 编写一段代码,运行时一次性执行完
  • 中间变量留在运行时,只返回精简结果给模型
  • 代码里写循环、分支、try-catch,完全可控
  • 排序、过滤、聚合?直接用 Array.reduce 等 JS 方法
  • 用 Promise.all 实现并行批量调用
问题核心:当 Agent 需要组合多个步骤、对结构化数据做推理或管理中间状态时,逐个工具调用就不够用了。解释器给 Agent 一个可编程的工作空间,让模型"写程序"而非"一步步选工具"。

什么是解释器

一个内存中的代码运行时——Agent 编写代码,解释器执行并返回结果

解释器为 Agent 提供了一个可编程的工作空间。Agent 通过编写代码来表达意图,一个基于 QuickJS 的轻量级 JavaScript 运行时执行代码并返回结果。默认不暴露文件系统、网络、Shell 或时钟 API。

卸甲:"可编程的工作空间"只是一个比喻,帮助你直觉理解。实际上,解释器是一个在 Agent 内存中隔离运行的 QuickJS 实例。Agent 编写 JavaScript 代码,该实例执行代码,然后将精简结果返回给 Agent 循环。它不涉及任何"空间"概念,就是一个受限制的代码执行器。
图:解释器工作流程——模型编写程序,运行时执行并返回精简结果

沙箱(Sandboxes)

代码优先的方式操作环境——运行 Shell 命令、安装依赖、编辑文件。适合需要完整操作系统级别执行的场景。

解释器(Interpreters)

代码优先的方式在 Agent 循环内操作——组合工具、保持状态、决定哪些信息返回给模型。适合编排和数据处理场景。

实验性功能:解释器目前是实验性功能,API 和生命周期行为可能在不同版本之间发生变化。建议在非关键路径上先试用。

何时使用

选择正确的执行路径——简单用工具,复杂用解释器,系统级用沙箱

你的需求应该使用
一次或两次简单的外部调用 普通工具调用
需要循环、分支、重试或聚合的小型程序 解释器
许多需要从代码中运行的选择性工具调用 解释器 + PTC
跨线程使用的可复用辅助函数 解释器 + 解释器技能
Shell 命令、包安装、测试、完整文件系统 沙箱(Sandboxes)

适合解释器的场景

  • 用代码组合多个工具调用,包括循环和并发
  • 从代码中协调子代理,将结果拼接为综合报告
  • 将中间值保留在运行时,不污染模型上下文
  • 确定性转换:排序、分组、解析、验证、评分
  • 探索大的变量空间,只返回精选证据给模型

不适合解释器的场景

  • 只需要调用一两个工具——直接用普通工具调用
  • 需要安装 npm 包——用沙箱
  • 需要运行 Shell 命令——用沙箱
  • 需要完整的文件系统访问——用沙箱
  • 需要持久化存储——用文件系统后端

添加解释器

安装 QuickJS 中间件包,一行代码就能启用解释器

# 安装 deepagents 和 QuickJS 中间件
$ npm install deepagents @langchain/quickjs
import { createDeepAgent } from "deepagents";
import { createCodeInterpreterMiddleware } from "@langchain/quickjs";
// 注意:@langchain/quickjs 的部分版本实际导出的是 createQuickJSMiddleware
// (此时工具名为 js_eval 而非 eval,且部分默认值不同)。
// 若上述 import 报错,请改用:
//   import { createQuickJSMiddleware } from "@langchain/quickjs";
//   middleware: [createQuickJSMiddleware()]
// 两者是同一能力的不同封装层。本教程遵循 docs.langchain.com 官方文档的写法。

// 创建带解释器的 Agent
// 中间件会自动添加一个名为 "eval" 的工具
const agent = createDeepAgent({
  model: "openai:gpt-5.5",
  middleware: [createCodeInterpreterMiddleware()],  // 一行启用解释器
});

中间件做了什么

  • 为 Agent 添加一个名为 eval 的工具
  • Agent 调用 eval 时,在 QuickJS 运行时中执行代码
  • 捕获 console.log 输出
  • 返回最后一个表达式的结果

QuickJS 是什么

QuickJS 是一个专为嵌入式执行设计的轻量级 JavaScript 运行时。

  • 默认不暴露文件系统、网络、Shell
  • 内存和执行时间有严格限制
  • 代码在隔离环境中运行,安全可控
  • 是解释器代码的执行边界
如果看到报错:Cannot find module '@langchain/quickjs' → 确认已安装:$ npm install @langchain/quickjs。该包是独立的中间件,不会随 deepagents 自动安装。

运行代码

中间件添加了 eval 工具——Agent 编写代码,运行时执行并返回结果

启用中间件后,Agent 就能通过 eval 工具在持久化上下文中运行 JavaScript。运行时捕获 console.log 输出,返回最后一个表达式的值。

// Agent 自动编写的代码示例:用 reduce 聚合团队得分
// 这段代码由模型生成并提交给 eval 工具执行
const rows = [
  { team: "alpha", score: 8 },
  { team: "beta", score: 13 },
  { team: "alpha", score: 21 },
];

// 使用 reduce 按团队累加得分,中间结果留在运行时中
const totals = rows.reduce((acc, row) => {
  acc[row.team] = (acc[row.team] ?? 0) + row.score;
  console.log(`${row.team} score: ${acc[row.team]}`);  // 输出会被捕获并返回
  return acc;
}, {});

totals;  // 最后一个表达式的值作为 eval 的返回值
// 控制台输出:
// alpha score: 8
// beta score: 13
// alpha score: 29
// 返回值:{ alpha: 29, beta: 13 }
关键理解:这段代码由 Agent 自动编写并提交给 eval 工具。你不需要手写解释器代码——模型根据任务需要,自己决定写什么代码。开发者只需启用中间件,Agent 负责使用。

编程式工具调用

PTC——把选定的 Agent 工具暴露到解释器的全局 tools 命名空间

编程式工具调用(PTC)让解释器代码能直接调用 Agent 的工具。Agent 不再需要"调用一个工具 → 等模型 → 调用下一个",而是写一段代码批量处理。

工作原理

  1. 你通过 ptc 白名单选择可调用的工具
  2. 中间件将工具暴露为 tools.xxx() 异步函数
  3. Agent 在代码中用 await tools.xxx() 调用
  4. 解释器运行工具桥接,接收结果后继续执行
  5. 模型只收到最终的精简输出

命名转换规则

工具名称自动转为驼峰命名法:

  • web_search → tools.webSearch()
  • read_file → tools.readFile()
  • task → tools.task()

输入对象仍遵循工具的原始 schema。

// PTC 调用示例:通过 tools 命名空间直接调用 Agent 工具
// 工具名自动转为驼峰命名:web_search → tools.webSearch()
const result = await tools.webSearch({
  query: "deepagents interpreters",
});
重要限制:PTC 调用通过解释器桥接执行,不会经过正常的工具调用路径。因此 interrupt_on 审批工作流不会对每个 PTC 调用生效。需要审批的操作应在解释器外进行。

启用 PTC

通过显式白名单启用——只暴露 Agent 需要的工具

import { createDeepAgent } from "deepagents";
import { createCodeInterpreterMiddleware } from "@langchain/quickjs";

// 通过 ptc 白名单启用 PTC,只暴露指定的工具给解释器代码
const agent = createDeepAgent({
  model: "openai:gpt-5.4",
  middleware: [
    createCodeInterpreterMiddleware({
      ptc: ["task"],  // 白名单即权限边界,只允许调用 task 工具
    }),
  ],
});
安全原则:PTC 白名单就是权限边界。只暴露 Agent 需要的工具,避免桥接那些可以访问敏感系统、花费资金或修改数据的广泛工具。
PTC 实用模式解释器可以做什么
批量处理遍历多个输入,为每个输入调用工具
并行工作对独立调用使用 Promise.all
条件逻辑根据先前结果选择下一次工具调用
提前终止一旦满足成功条件就停止调用
数据过滤仅返回相关的行、片段或摘要给模型
递归编排反复调用 task,在代码中合并子代理结果

PTC 实战

并行启动多个子代理,合并结果后再返回给模型

// 并行启动多个子代理,各自研究一个主题
// Promise.all 让所有子代理同时运行,而非串行等待
const topics = ["retrieval", "memory", "evaluation"];

const reports = await Promise.all(
  topics.map((topic) =>
    tools.task({  // 通过 PTC 调用 task 工具启动子代理
      description: `Research ${topic} in Deep Agents and return three concise findings.`,
      subagent_type: "general-purpose",
    }),
  ),
);

// 在解释器代码中合并所有子代理的报告
// 只有这个最终结果会返回给模型,中间结果留在运行时
reports.join("\n\n");
// 本地处理失败——不需要让模型介入
try {
  const report = await tools.task({
    description: "Check the migration notes and return breaking changes.",
    subagent_type: "general-purpose",
  });
  console.log(report);
} catch (error) {
  console.log(`Subagent failed: ${error.message}`);
}

没有 PTC 时

  • 模型发出工具调用 → 等结果 → 决定下一步
  • 每个子代理结果都塞进上下文
  • 想并行?做不到
  • 想处理错误?只能靠模型重试

有了 PTC 后

  • 一段代码搞定所有工具调用
  • 中间结果留在运行时变量中
  • Promise.all 原生并行
  • try/catch 原生错误处理

递归语言模型

用解释器作为分解工作的工作空间——变量空间与模型上下文分离

递归语言模型是一种高级模式:模型将大型输入保存在运行时变量中,编写代码拆分和检查数据,对较小部分调用子代理,最后在代码中拼接结果。

图:递归语言模型——变量空间与模型上下文分离
核心思想:变量空间(解释器中的信息)与 Agent 上下文(模型实际处理的内容)是分离的。模型决定哪些片段成为子任务、哪些结果需要再处理、什么最终结果应返回到主对话。

递归实战

从大量笔记中筛选迁移相关条目,并行分析风险

// 递归语言模型实战:迁移风险分析
const candidates = notes
  .filter((note) => note.includes("migration"))  // 筛选迁移相关笔记
  .slice(0, 5);  // 只取前 5 条,控制并行调用数量

// 并行调用子代理分析每一条迁移笔记
const riskReports = await Promise.all(
  candidates.map((note) =>
    tools.task({  // 通过 PTC 调用 task 工具启动子代理
      description: `Analyze this migration note for release risk.
Return risks, affected users, and recommended follow-up:\n\n${note}`,
      subagent_type: "general-purpose",
    }),
  ),
);

// 在解释器代码中拼接综合报告,只返回精简结果给模型
const releaseSummary = riskReports
  .map((report, index) => `## Candidate ${index + 1}\n${report}`)
  .join("\n\n");

releaseSummary;  // 最后一个表达式作为 eval 的返回值,这是唯一返回给模型的内容

数据流

  1. 所有笔记存在变量 notes 中(不进入模型上下文)
  2. 代码筛选出前 5 条迁移相关笔记
  3. 并行启动 5 个子代理各自分析
  4. 代码拼接综合报告

Token 节省

  • 模型只看到最终的精简报告
  • 5 条原始笔记 + 5 个子代理的完整输出全留在变量空间
  • 如果没有解释器,这些中间结果会全部塞进上下文
论文参考:递归语言模型的理论基础来自 Recursive LM 论文。在 Deep Agents 中,递归调用通常通过 PTC 暴露的 task 工具实现。

解释器技能

向解释器暴露可复用的代码模块——用于结构化数据工作流

解释器技能是向解释器暴露代码模块的 Skills。当配置了解释器中间件时,Agent 可以从代码中导入这些模块,用于确定性的辅助逻辑。

适合用解释器技能的场景

  • 排序、分组、评分、解析、验证、聚合数据
  • 跨线程复用的结构化数据处理函数
  • 不需要模型推理的确定性逻辑
  • 需要在不同 Agent 之间共享的辅助函数

配置方式

  • 在技能定义中添加 module 条目
  • 配置 skillsBackend 或 skills_backend
  • Agent 通过 import 在解释器代码中导入
  • 详细的设置方式参见 Skills 章节
与普通技能的区别:普通技能为 Agent 提供工作流和领域知识;解释器技能为解释器代码提供可导入的模块。前者面向模型推理,后者面向确定性计算。

安全与限制

QuickJS 严格隔离——默认什么都不能做,显式授权才能访问

功能默认可用如何暴露
JavaScript 执行是添加解释器中间件
顶层 await是在解释器代码中使用 Promise
console.log 捕获是用 captureConsole: false 禁用
Agent 工具否添加 PTC 白名单
解释器技能模块否配置 module 和 skillsBackend
文件系统访问否通过 PTC 白名单添加内置文件系统工具
网络访问否通过 PTC 暴露特定的网络工具
墙钟或日期时间否暴露显式的时间工具
Shell / 包安装 / 测试否使用沙箱后端
安全提示:将解释器视为有限范围的运行时,而非完整的生产级沙箱。通过 PTC 暴露的每个工具都是解释器代码可以使用的外部功能。仅暴露必要的工具,特别是涉及敏感系统、资金消耗或数据修改的工具。

中间件选项

createCodeInterpreterMiddleware 的完整配置项

选项默认值用途
ptc 省略 PTC 白名单:工具名称数组或工具实例
memoryLimitBytes 64 MB QuickJS 内存限制(字节)
maxStackSizeBytes 320 KB QuickJS 栈大小限制(字节)
executionTimeoutMs 5000 每次评估的超时时间(毫秒),负值禁用
systemPrompt null 覆盖内置的解释器系统提示词
skillsBackend 省略 用于解析解释器技能模块的后端
maxPtcCalls 256 每次评估的最大 tools.* 调用次数
maxResultChars 4000 返回结果的最大字符数
toolName "eval" 暴露给模型的工具名称
captureConsole true 是否捕获 console.log 输出
常用调优:如果你的解释器代码处理大量数据,可能需要调高 memoryLimitBytes;如果代码涉及多次工具调用,注意 maxPtcCalls 的上限;如果返回结果被截断,调高 maxResultChars。

动手练习

用解释器 + PTC 构建一个并行研究 Agent

现在轮到你了。设计一个并行研究 Agent,需求如下:

智能体应该:
1. 接收一个研究主题列表(如 ["React", "Vue", "Svelte"])
2. 为每个主题并行启动子代理做深度研究
3. 在代码中收集和汇总所有子代理的结果
4. 对结果进行排序、过滤或评分
5. 只返回最终的精选摘要给模型

任务 1:配置中间件

  • PTC 白名单应该包含哪些工具?
  • 需要调整哪些中间件选项?
  • executionTimeoutMs 应该设多少?

任务 2:设计代码逻辑

  • 如何用 Promise.all 并行调用?
  • 如何在代码中处理子代理失败?
  • 如何对结果排序或过滤?

任务 3:思考安全边界

  • PTC 白名单是否暴露了过多的工具?
  • 子代理的权限如何控制?
  • 结果大小如何限制?
提示:参考前面"递归实战"幻灯片中的代码模式。核心思路是:筛选 → 并行调用 → 聚合 → 返回精简结果。

参考答案

一种可行的实现方案——不是唯一答案,但逻辑合理

import { createDeepAgent } from "deepagents";
import { createCodeInterpreterMiddleware } from "@langchain/quickjs";

const agent = createDeepAgent({
  model: "openai:gpt-5.4",
  middleware: [
    createCodeInterpreterMiddleware({
      ptc: ["task"],                // 只暴露 task 工具
      executionTimeoutMs: 30000,    // 30 秒超时,并行调用需要更长时间
      maxPtcCalls: 50,              // 最多 50 次工具调用
    }),
  ],
});

// Agent 生成的解释器代码(伪代码,展示概念):
// const topics = ["React", "Vue", "Svelte"];
//
// const results = await Promise.all(
//   topics.map(async (topic) => {
//     try {
//       return await tools.task({
//         description: `研究 ${topic} 的核心优势、生态和适用场景,返回三条关键发现。`,
//         subagent_type: "general-purpose",
//       });
//     } catch (e) {
//       return `${topic} 研究失败:${e.message}`;
//     }
//   }),
// );
//
// // 过滤失败结果,按质量排序
// const validResults = results
//   .filter((r) => !r.includes("失败"))
//   .map((report, i) => ({ topic: topics[i], report }))
//   .sort((a, b) => b.report.length - a.report.length);
//
// validResults
//   .map((r) => `## ${r.topic}\n${r.report}`)
//   .join("\n\n");
你的方案可能不一样——这很正常。关键是你能解释清楚:为什么选这些工具进白名单、超时设多少合适、如何在代码中处理失败、如何过滤和排序结果。

本章回顾

到这里,你已经掌握了 Deep Agents 解释器的完整用法

你学到了什么

  • 解释器是 Agent 的可编程工作空间,基于 QuickJS 运行
  • 通过 createCodeInterpreterMiddleware 一行启用
  • PTC 让解释器代码能调用白名单中的 Agent 工具
  • 递归语言模型:变量空间与模型上下文分离
  • 解释器技能:可复用的确定性数据处理模块
  • 默认严格隔离,显式授权才能访问外部功能

核心设计原则

  • 白名单即权限边界——只暴露必要工具
  • 中间状态留在运行时——不污染模型上下文
  • 代码编排优于 prompt 编排——循环、分支、错误处理
  • 并行优于串行——Promise.all 原生支持
  • 精简结果返回模型——在代码中过滤和聚合
核心结论:解释器让 Agent 从"一步步选工具"进化为"写程序编排一切"。当你需要组合工具、处理结构化数据、编排子代理时,解释器是比逐个工具调用更高效、更可控、更省 Token 的选择。

延伸阅读

继续本课程

官方文档

相关论文