CodeInterpreterMiddleware 执行流程

CodeInterpreterMiddleware 用于为 Deep Agents 提供了一个可编程的工作空间,让他们可以探索数据、协调工具调用、执行 skill 脚本,并将中间工作排除在模型上下文之外。Deep Agents 编写代码表达意图,然后内存运行时执行该代码并返回相关结果。
适用场景如下表:

需求用什么
需要循环、分支、重试或聚合结果的小程序CodeInterpreterMiddleware
从代码中发起多个 tool 调用CodeInterpreterMiddleware + 程序化 tool calling
跨线程可复用的辅助函数CodeInterpreterMiddleware + Interpreter Skills

整体架构

Agent Loop
    │
    ├─ before_agent()          ← 恢复 REPL 快照
    │
    ├─ wrap_model_call()       ← 注入 system prompt + 安装 PTC 工具
    │       ↓
    │   [Model 决定调用 eval tool]
    │       ↓
    │   eval(code)             ← 执行 JS
    │
    └─ after_agent()           ← 保存快照 / 清理

一、初始化阶段 __init__

CodeInterpreterMiddleware(
    mode="thread",      # 持久化模式
    ptc=["web_search"], # 暴露给 JS 的 Python 工具
    skills_backend=..., # skill 文件后端
)

初始化做三件事:

1. 解析持久化模式(_resolve_persistence_flags)

mode含义snapshotreset_between_calls
thread跨 turn 持久✅❌
turn仅当前 turn 内持久❌❌
call每次 eval 都重置❌✅

2. 构建 _Registry:管理所有 thread 的 QuickJS REPL 槽位。

3. 构建 eval Tool:注册为 self.tools,LangGraph 会把它暴露给模型。


二、Turn 开始:before_agent

def before_agent(self, state, runtime):
    payload = state.get("_quickjs_snapshot_payload")  # 从 state 取快照
    repl = self._registry.get(thread_id)
    repl.restore_snapshot(payload)                    # 还原 JS heap

只有 mode="thread" 时才执行,把上一轮序列化的 QuickJS 堆内存还原回来,使变量、函数定义等全局状态跨 turn 存活。


三、每次模型调用:wrap_model_call

def wrap_model_call(self, request, handler):
    prompt = self._prepare_for_call(request)  # ← 核心
    return handler(request.override(system_message=...))

_prepare_for_call 做两件事:

① 安装 PTC 工具桥

Python tools (web_search, etc.)
        ↓ install_tools()
QuickJS globalThis.tools.webSearch(input) => Promise<string>

名称转换:web_search → tools.webSearch(snake_case → camelCase)

② 构建并缓存 system prompt addendum

base_prompt(REPL 基础说明)
    +
ptc_prompt(tools.xxx 的 TS 签名,按 exposed_names 缓存)

注入到 system message 里,让模型知道可以调用哪些工具。


四、模型调用 eval tool

模型生成 eval(code="...") 调用后,走到 async_eval / sync_eval:

async def async_eval(runtime, code):
    thread_id = _resolve_thread_id(fallback_id)  # 1. 定位线程
    repl = middleware._repl_for_eval(thread_id)   # 2. 取 REPL 槽
    skills = middleware._skills_for_eval(runtime) # 3. 解析可用 skills
    
    outcome = await repl.eval_async(              # 4. 执行 JS
        code,
        skills=skills,
        skills_backend=...,
        outer_runtime=runtime,
    )
    
    if reset_between_calls:
        registry.reset_repl(thread_id)            # 5. call 模式:立即重置
    
    return ToolMessage(format_outcome(outcome))   # 6. 返回结果

thread_id 解析优先级:

LangGraph config["configurable"]["thread_id"]
    ↓ 没有时
middleware 实例级 fallback id(session_xxxxxxxx)

这保证同一个中间件实例内 wrap_model_call 和 eval 看到的是同一个 REPL。

Skills 过滤逻辑(_skills_for_eval):

for skill in state["skills_metadata"]:
    required_ptc = skill["metadata"]["required-ptc-tools"].split()
    if any(tool not in ptc_config for tool in required_ptc):
        logger.warning(...)   # 跳过,不报错(eval 时宽松)
        continue
    result[skill.name] = skill

注意:before_agent 阶段用 _validate_required_ptc_tools 严格报错,eval 阶段只 warning 跳过。


五、Turn 结束:after_agent

def after_agent(self, state, runtime):
    if mode == "call" or mode == "turn":
        registry.evict(thread_id)    # 直接销毁,不保存
        return None
    
    # mode == "thread"
    payload = repl.create_snapshot()         # 序列化 JS heap
    registry.evict(thread_id)               # 释放内存中的 REPL
    return {"_quickjs_snapshot_payload": payload}  # 写入 LangGraph state

快照超过 max_snapshot_bytes 时静默丢弃(_snapshot_update 里处理),下一轮从空 REPL 开始。


六、完整 thread 模式时序图

Turn 1                          Turn 2
──────────────────────          ──────────────────────
before_agent                    before_agent
  └─ state 无快照,跳过            └─ 还原 JS heap(x=42 还在)

wrap_model_call                 wrap_model_call
  └─ 注入 system prompt           └─ 注入 system prompt
  └─ 安装 PTC 桥                  └─ 安装 PTC 桥

eval("var x = 42")              eval("x + 1")  → 43
  └─ QuickJS 执行                  └─ 读到上轮的 x

after_agent                     after_agent
  └─ 序列化 heap                  └─ 序列化 heap
  └─ 写入 state                   └─ 写入 state
  └─ 释放内存 REPL slot            └─ 释放内存 REPL slot

关键设计决策小结

为什么要 evict 再 snapshot,而不是保持 REPL 常驻?

QuickJS Runtime 有 memory_limit,所有 context 共享。Turn 结束后 evict 释放堆内存,下轮从 snapshot 字节还原,避免多个并发 thread 撑爆内存限制。

为什么 PTC 桥在 wrap_model_call 而不是 before_agent 安装?

wrap_model_call 能拿到当前 request 的 live tool list(上游 middleware 可能过滤了部分工具),确保暴露给 JS 的 tools.* 和模型实际能调用的完全一致。

Skills 的代码它放到哪儿去了?

Skills 代码不写磁盘,全程在内存里流转,最终以虚拟文件系统的形式注册进 QuickJS 模块解析器。

数据流
磁盘 / 远端(BackendProtocol)
    ↓ download_files()
内存 bytes
    ↓ decode UTF-8
files dict { "index.ts": "export async function run()..." }
    ↓ ModuleScope(files)
QuickJS 模块解析器虚拟 FS
    ↓ import("@/skills/my-skill") 触发时
QuickJS 执行上下文(oxidase 实时 strip 类型后执行)
eval 时的安装流程

eval(code) 调用进入 _ThreadREPL.eval_async 后,在真正执行代码前会先做三步:

1. scan_skill_references(code)
   扫描代码里所有 "@/skills/xxx" 字符串字面量

2. aload_skill(metadata, backend)
   对每个引用的 skill:读取目录下所有 .ts/.js → 构建 files dict → 返回 LoadedSkill

3. context.install_scope("@/skills/xxx", scope)
   把 ModuleScope 注册进 QuickJS,之后 import() 就能找到
缓存机制

_ThreadREPL 内部维护一个 install cache。同一 thread 内已加载过的 skill 不会重复 download_files,后续 eval 直接复用已注册的 ModuleScope。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐