第 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 不再需要"调用一个工具 → 等模型 → 调用下一个",而是写一段代码批量处理。
工作原理
- 你通过
ptc白名单选择可调用的工具 - 中间件将工具暴露为
tools.xxx()异步函数 - Agent 在代码中用
await tools.xxx()调用 - 解释器运行工具桥接,接收结果后继续执行
- 模型只收到最终的精简输出
命名转换规则
工具名称自动转为驼峰命名法:
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 的返回值,这是唯一返回给模型的内容
数据流
- 所有笔记存在变量
notes中(不进入模型上下文) - 代码筛选出前 5 条迁移相关笔记
- 并行启动 5 个子代理各自分析
- 代码拼接综合报告
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 的选择。
延伸阅读
继续本课程
- 下一章:Profiles — Agent 配置文件详解
- 沙箱 — 系统级代码执行环境
- 技能 — 包含解释器技能的详细配置
官方文档
- @langchain/quickjs — QuickJS 中间件包
- QuickJS 仓库 — 轻量级 JavaScript 运行时
- Deep Agents 仓库 — 源码和示例
相关论文
- Recursive LM 论文 — 递归语言模型的理论基础
- LangChain 文档索引 — 完整的文档导航