沙箱 REPL 与子代理编排langchain-quickjs call 模式系统提示词快照深度解析【免费下载链接】deepagentsThe batteries-included agent harness.项目地址: https://gitcode.com/GitHub_Trending/de/deepagents本篇指南以 libs/partners/quickjs 中CodeInterpreterMiddleware在call模式下注入 Agent 的系统提示词快照 quickjs_system_prompt_no_tools_call.md 为骨架完整解读该提示词如何在每次调用创建全新沙箱 REPL、如何通过顶层task()在 JavaScript 内编排 Deep Agents 子代理并结合_prompt.py、_repl.py、_subagent.py等源码揭示每一段提示词背后的实现机制。读完你将掌握快照测试的运作原理、call/turn/thread 三种状态模式的差异、task()原语的参数语义与审批边界以及如何用「数组进、数组出」的心智模型在单个eval调用中完成大规模并行子代理编排。一、快照从何而来系统提示词快照测试机制该 Markdown 文件位于tests/unit_tests/smoke_tests/snapshots/目录是快照测试的预期输出。测试代码在 test_system_prompt.py 中使用create_deep_agent配合CodeInterpreterMiddleware(modemode)构建 Agent其中mode参数化遍历thread、turn、call三种取值通过自定义的_SmokeChatModel继承GenericFakeChatModel捕获 Agent 首次模型调用收到的SystemMessage将提示词文本与snapshots/目录下已提交的快照逐字节比对任何不一致即测试失败。这正是本文件的定位防止 deepagents SDK 升级时静默改变 quickjs 中间件所拼装提示词的措辞。测试模块的 docstring 明确说明CI 会在 SDK 变更而 quickjs 未变更时运行专门的test-quickjs-sdk-smoke任务来捕获这种漂移。当提示词被有意修改后可用pytest ... --update-snapshots重新生成快照。文件名中的no_tools表示该快照对应「未启用 PTCProgrammatic Tool Calling编程式工具调用且 Agent 无宿主工具」的场景因此提示词中没有tools.*命名空间的 API Reference 段落_call后缀表示modecall。与之并列的 quickjs_system_prompt_no_tools.mdthread 模式与 quickjs_system_prompt_no_tools_turn.mdturn 模式则用于对比不同持久化模式下提示词的差异。二、Interpreter 段call 模式下的沙箱 REPL 语义快照开头是### Interpreter小节告诉模型存在一个eval工具。在call模式下其核心语义如下每次调用全新环境An \eval tool is available. It runs JavaScript in a fresh sandboxed REPL for each invocation.且「State (variables, functions) does not persist across tool calls. Each invocation starts from a blank environment.」——每次eval 都在全新的 REPL 中运行变量与函数不跨调用保留顶层 await 可用Promises 会在调用返回前解析完毕运行时沙箱没有内置文件系统、网络、标准库与墙钟 APIfetch、require、fs、process、真实的Date.now()均不可用或被桩替代纯计算隔离REPL 无法访问宿主工具、文件或网络「it is pure computation. Return values to communicate results.」——结果只能通过返回值传递资源配额单次调用超时 5.0 秒总内存 64 MBconsole 捕获console.log输出会被捕获并与结果一同返回。这段提示词并非硬编码在快照中而是由 _prompt.py 中的render_repl_system_prompt()函数按mode分支渲染_REPL_SYSTEM_PROMPT_TEMPLATE模板。对比三个快照可以清晰看到差异只集中在两行modeREPL 简介行状态持久化行callruns JavaScript in afresh sandboxed REPL for each invocationState does not persist across tool callsturnruns JavaScript in apersistent REPLState persists across callswithin a single turn跨 turn 不保留thread默认runs JavaScript in apersistent REPLState persists across callsand across multiple turns源码中_resolve_mode()见 middleware.py负责归一化mode取值None或thread归为thread非法值直接抛出ValueError。此外render_repl_system_prompt的ptc_attached参数还会决定「外部副作用」那一行未挂 PTC 时声明「REPL 是纯计算」挂载 PTC 时则改为指引模型通过tools.*命名空间访问外部副作用。三、task()原语从 REPL 内派发子代理快照的第二大部分### Dispatching Subagents with \task是本文件的重头戏。它说明task 是「从 JavaScript REPL 内部运行已配置子代理」的原语模型的任务是分发工作而非亲力亲为——用普通 JavaScript 实现扇出fan-out、过滤、去重、多阶段流水线与结果合成。3.1 原语签名与四个参数await task({ description, // full autonomous task prompt subagentType, // configured subagent name label, // optional short UI label for this dispatch responseSchema, // optional JSON Schema for structured output }); // - PromiseunknownsubagentType必填已配置的子代理名称。task会为该子代理运行完整的 agentic loop——它可以使用自己配置的工具、迭代、检查上下文最终返回一个结果description必填子代理收到的唯一提示词。快照强调必须写完整目标、约束、要检查什么、期望的返回形状或详细程度。上下文应使用定位符文件路径与符号名而非粘贴的文件内容——即使模型已读过某文件也应把路径传给子代理让其自行读取因为每次派发对调用方而言都是无状态的无法向同一子代理运行发送后续消息label可选仅在实时进度 UI 中显示不发送给子代理、不影响执行缺省时回退到由 description 推导的短标签。对应实现中_event_label()对显式 label 截断到 120 字符、对 description 回退截断到 60 字符见 _subagent.pyresponseSchema可选结构化输出的 JSON Schema。快照特别强调任何结果要供后续代码使用的派发都应设置它——确定性的、带类型的形状才能可靠地支持下一步组合索引、排序、比较字段、分支、合并而不是解析自由文本。提供后解析得到的值已经是匹配 schema 的 JS 类型值除非子代理有意返回 JSON 字符串否则不要调用JSON.parse。动态 schema 适用于声明式declarative子代理runnable 支撑的子代理会拒绝动态 schema因为其 runnable 已编译。实现层面_validate_response_schema()见 _subagent.py对responseSchema施加了硬性上限序列化后不超过 4096 字节、嵌套深度不超过 5 层、属性总数不超过 32 个。这解释了快照中「动态 schema」与「编译期固定」的边界——桥接层必须先校验再注入 structured-output 配置。3.2 审批模型绕开父级 HITL快照明确说明task从已在运行中的eval调用内部发起派发不会经由父 Agent 由ToolNode管理的task工具也不会对每次派发触发父级的interrupt_on/ HITL 审批。声明式子代理仍会遵守其 spec 内部配置的审批中间件。若需要在从父级启动子代理前审批应在 JavaScript 之外使用常规task工具或确保eval调用本身被审批门控。这是与 PTC 相同的安全边界逻辑middleware.py 的文档字符串警告task(...)运行在已获批准的eval调用内如需按派发进行父级审批应门控eval工具本身、在子代理 spec 内添加审批中间件或设置subagentsFalse。_repl.py中还把顶层task用Object.freeze冻结为不可写、不可配置的全局属性_repl.py防止模型脚本意外覆盖它。3.3 心智模型数组进、数组出快照给出编排的核心心智模型在 JS 中持有工作——一个待处理项数组进一个结果数组出把每次派发结果合并回对应条目。多阶段分析意味着先跑一轮在 JS 中过滤或重组数组再对幸存者跑下一轮。整条工作流可以放进一次eval调用也可以拆成多次——两者皆可。单次端到端脚本生成、比较、选出赢家或审查每项、再综合在能一气呵成时最干净想在阶段之间检查中间结果时拆分也完全可以。无论哪种方式不要跨调用重复劳动——复用已在作用域内的变量见下文「复用此前 eval 留在作用域中的值」。四、编排模式五个实战准则快照用五个小节给出了模型在 REPL 内编排子代理的具体范式这些是 prompt 设计层面的核心资产。4.1 有界并发扇出用Promise.all并行派发独立任务但按10 个一批显式分批避免一次启动数百个子代理桥接层对单个 REPL 施加32 个并发子代理调用的硬上限。const files [/src/a.ts, /src/b.ts, /src/c.ts]; // found while exploring const batchSize 10; const reviewed []; for (let i 0; i files.length; i batchSize) { const batch files.slice(i, i batchSize); reviewed.push(...(await Promise.all(batch.map(async (file) { const result await task({ description: Read file and review it for SQL injection. Cite line numbers., subagentType: reviewer, responseSchema: { type: object, properties: { vulnerabilities: { type: array, items: { type: object, properties: { type: { type: string }, line: { type: number }, evidence: { type: string }, }, required: [type, line, evidence], }, }, }, required: [vulnerabilities], }, }); return { file, ...result }; })))); }「32 并发上限」在源码中有精确对应_repl.py 定义_MAX_TASK_CALLS_PER_THREAD 32并在_ainit()中用它初始化asyncio.Semaphore(_MAX_TASK_CALLS_PER_THREAD)同文件 L400_bridge每次派发都async with task_calls申请信号量。这是 REPL 层面保护宿主不被子代理风暴压垮的关键闸门。4.2 先用自有工具探索再分发模型在编写编排脚本前应先用常规工具读取、列出、glob、grep理解任务——这些是独立于eval工具的普通工具调用读数据文件、列出/glob 目录、grep 关键内容然后决定如何切分工作。永远不要在eval代码里派生子代理去读文件或列目录——那是确定性的步骤用直接工具调用即可完成为一个 agentic loop 花掉一整个循环是浪费。理解工作形态后有充分的拆分自由度条目已相互独立时按文件或按记录逐一派发自行切分大输入——读取、拆分必要时把每块写入小文件——然后按块各派一个子代理先做一轮廉价分类只对值得深挖的条目派发更深的子代理。然后用eval工具编写 JavaScript把繁重的 agentic 工作通过task()分发给子代理分析文件内容、探索代码库、做判断、重写代码、合成报告。给子代理传定位符而不是载荷。子代理有自己的文件工具任何以文件形式存在的内容要审查、重写、审计的文件都应传路径让其自行读取。不要在读取整个文件后把内容粘贴进 description——那会膨胀每次派发并在派发之间复制文件。内联内容只留给没有独立路径的小型或派生数据一条解析过的记录或从大输入中切出的一块如果很大把块写成文件再传路径。结果在 JS 中汇总。4.3 多阶段组合在阶段之间过滤数组快照给出的典型两阶段流水线先做廉价分类过滤出风险项再只对这些项做深度审查。const tagged await Promise.all(files.map((file) task({ description: Read file and classify it as handler, util, test, or config., subagentType: reviewer, responseSchema: { type: object, properties: { kind: { type: string }, risky: { type: boolean } }, required: [kind, risky], }, }).then((tag) ({ file, ...tag })) )); const riskyHandlers tagged.filter((it) it.kind handler it.risky); const deepReviews await Promise.all(riskyHandlers.map((it) task({ description: Deep security review of it.file . Cite line numbers., subagentType: reviewer, }).then((review) ({ ...it, review })) ));注意第一阶段为每项返回{ kind, risky }的带类型结果——正是responseSchema让filter与后续深审的拼接变得可靠。这个模式也是 README 中「一次eval调用完成并行分类、再深审风险文件」示例的 prompt 版本。4.4 用最后一个表达式返回结果而非console.log一次eval调用中最后一个表达式的值或解析后的顶层await会作为结果返回给模型。应让最终表达式成为持有结果的变量并从中读取。console.log只用于临时调试其输出有上限且会被截断而返回值不会——绝不要用console.log输出实际结果。大型中间集合应保留在 JS 变量中只返回紧凑摘要或一小片数据而不是整个数据集。要持久化完整输出让子代理写入或在eval调用之外用自有文件工具写入。实现佐证_repl.py 中_aeval_async通过ctx.eval_handle_async求值并驱动最终表达式 Promise 解析后再 marshal_ConsoleBuffer同文件 L152-L185则对console.*输出按max_result_chars上限截断与返回值走完全不同的通道。4.5 复用此前 eval 留在作用域中的值REPL 在单个 turn 内是持久的每个顶层变量、函数、类都会保留可在下一次eval调用中使用每个都被提升到全局作用域。因此后续步骤需要先前 eval 产生或绑定的内容时按名字引用该变量——不要重新写一个字面量把之前 eval 已返回或计算的数据再敲一遍。// An earlier eval bound this: // const auditResults await Promise.all(files.map(/* ...audit... */)); // A later eval — reference it; do NOT paste the findings back in as a literal: const findings auditResults.flatMap((r) r.findings.map((f) ({ ...f, file: r.file })) ); const verified await Promise.all(findings.map((f) task({ description: Verify this finding: f.evidence, subagentType: verifier, }).then((v) ({ ...f, ...v })) ));快照给出一条自我诊断提示如果发现自己把先前调用产生的大数组或对象重新粘贴为字面量那就是信号——变量仍在作用域内直接用即可。重新输入先前结果作为新字面量会浪费 token并偏离实际运行的数据。4.6 用户提到「workflow」时当用户请求提到「workflow」或以其他方式使用「工作流」一词时模型应把工作扇出给子代理而不是自己全部完成。必要时先用自有工具探索然后在eval工具中编写 JavaScript用task()派发子代理并汇总结果。要点是并行分发繁重工作而不是一次工具调用一次地硬磨。五、从提示词到实现关键代码路径快照描述的行为均有明确实现与之对应理解这些路径有助于在实际项目里定位与调试提示词渲染render_repl_system_prompt() 负责 Interpreter 段含 mode 分支与ptc_attached副作用行render_subagent_system_prompt() 负责task段模板中{tool_name}会被替换为实际工具名默认eval。中间件在wrap_model_call/awrap_model_call中把拼装好的片段追加到系统消息middleware.py。子代理派发call_subagent_task_tool() 调用 Deep Agents 的task工具并在 LangGraph 的custom流上发出start/complete/error三种subagent生命周期事件含duration_ms与按 eval 分组的eval_id供 UI 渲染实时扇出面板。_repl.py中的_ainvoke_task_on_outer_loop保证子代理 runnable 在父级 LangGraph 事件循环上执行使回调、上下文与异步循环亲和性与常规工具执行一致。隔离与配额middleware.py 的类文档说明每个 LangGraph 线程拥有独立的 QuickJS slotworker runtime context会话间全局变量互不可见_Registry_repl.py按thread_id管理这些 slotafter_agent钩子负责快照与回收。六、配置该提示词行为的参数速查快照文本中的硬编码数值5.0s 超时、64 MB 内存均可在构造中间件时调整middleware.py 提供了完整配置项CodeInterpreterMiddleware( memory_limit64 * 1024 * 1024, # bytes跨 context 共享的 QuickJS 堆上限 timeout5.0, # 每次 eval 的 VM 执行超时秒 max_ptc_calls256, # 单次 eval 内 tools.* 桥接调用上限None 禁用有 DoS 风险 tool_nameeval, # 暴露给模型看的工具名 max_result_chars4000, # 结果与 stdout 各自的截断字符数 capture_consoleTrue, # 是否安装 console.log/warn/error 桥接 subagentsTrue, # 宿主有 Deep Agents task 工具时暴露顶层 task(...) modethread, # thread | turn | call max_snapshot_bytesNone, # 默认等于 memory_limit更大的快照会被丢弃 ptcNone, # None | list[str] | list[BaseTool] )几个与快照直接相关的要点subagentsFalse会关闭顶层task()对应快照中整个Dispatching Subagents段不再出现modecall即本快照场景且每次 eval 结束后_registry.reset_repl(slot_id)会重建 REPL 以兑现「每次调用全新环境」timeout度量的是 QuickJS VM 执行时间而非 Python 墙钟时间——等待tools.*宿主调用Python 协程的时间不计入慢宿主调用可能让 eval 超出 timeout不要依赖它约束总墙钟时长。七、小结quickjs_system_prompt_no_tools_call.md看似只是一份测试快照实则是 langchain-quickjs 中间件与模型之间行为契约的权威文档它定义了 call 模式下每次全新沙箱 REPL 的边界无状态、纯计算、超时与内存配额、console 捕获并给出了子代理编排的完整方法论——task()原语、审批边界、有界并发10 一批、32 上限、先探索后分发、多阶段过滤、末表达式返回、跨 eval 复用作用域以及「workflow 即扇出」。配合 _prompt.py、_repl.py、_subagent.py 阅读可以在不修改仓库的前提下把这份提示词快照当作理解整个 REPL 中间件设计意图的入口并在自己的 Agent 项目中复现同样的编排模式。【免费下载链接】deepagentsThe batteries-included agent harness.项目地址: https://gitcode.com/GitHub_Trending/de/deepagents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考