面试知识库

23 · 代码执行工具(Code Interpreter)接入 DocMind#

目的:给 agentic 检索循环补上”对检索到的数据做精确计算/统计”的能力。需求是开放式数据分析——用户提问无法预先枚举(如”剔除异常值后中间 5 年的 CAGR 并与行业均值对比”),只能让 LLM 现写分析代码执行。

落地时间:2026-06。配套执行计划见 ~/.claude/plans/10-stage-0-stage-unified-puddle.md


0 · 执行摘要#

维度结论
要解决的问题agentic 循环原本只有 4 个检索类工具(searchDocs / keywordSearch / webSearch / recall_memory),缺”算”的能力。检索回来一堆数字,均值/同比/环比/CAGR 只能让模型心算——不可靠。
为什么不是计算器计算需求开放、无法预先枚举参数,必须让 LLM 现写代码。这就引出”执行不可信代码”问题。
为什么必须沙箱决定要不要沙箱的是代码由谁写,不是复杂度。LLM 生成的是不可信代码,进程内 eval 是大忌(n8n 因进程内 Pyodide 吃过 9.9 分 RCE)。→ 进程外沙箱
选型不自造轮子。选 阿里开源 OpenSandbox:Apache-2.0、可自托管 Docker、原生 Java/Kotlin SDK(零 HTTP 胶水)、内建 Code Interpreter(Jupyter 有状态)、可选 gVisor/Kata/Firecracker 隔离、CNCF Landscape。
接入点executeCode 作为 agentic 循环的第 5 个工具(不是独立一步)——因为 LLM 必须先”看到”检索回来的数据,才能照着写代码。
结果回流新增「计算结果通道」复刻 memoryContext 那条链,把计算结果带回最终 prompt,不被 rerank 丢弃、不占检索 token 预算
安全边界仅内部(不进对外 MCP);动态一键开关;禁出网(egress deny-all,fail-closed);超时/输出/代码长度三道护栏;userId/kbIds 不可被 LLM 篡改沿用既有收口。

1 · 为什么是”代码执行”而不是”计算器”#

一个固定签名的 calculate(numbers, op) 计算器只能覆盖预先枚举的操作。真实的数据分析问题是开放的:

“把这几年的营收去掉最高最低后求几何平均,再和上一年同比,顺便算个标准差看波动。”

这种问题无法用有限的工具参数覆盖。唯一通用的做法是让 LLM 现写一段分析代码。工具签名因此取最通用形态

executeCode(code: String)   // LLM 把所需数据内联进 Python 代码,用 print(...) 输出结果
java

不预设数据 schema、不假设”一定是 10 年数据”。Python 为主(沙箱镜像也支持多语言)。


2 · 为什么必须进程外沙箱(这道题面试常被追问”需要沙箱吗”)#

判据:决定要不要沙箱的,是”代码由谁写”,不是”计算复杂不复杂”。

  • 我自己写死的计算逻辑 → 可信,进程内跑没问题。
  • LLM 现写的代码 → 不可信输入。在 JVM 进程内 eval 等于把任意代码执行权交给模型输出,可读环境变量、连内网、删文件。

行业反例:n8n 的 Code 节点早期用进程内 Pyodide,吃过 CVE-2025-68668(CVSS 9.9) 沙箱逃逸 RCE,后来才转向独立 task-runner 进程隔离。FastGPT 早期 vm2/seccomp 缺 cgroups 也踩过坑。

结论:不可信代码 → 进程外强隔离,不在 JVM 里 eval

顺带砍掉了一个一度想用的方案:GraalVM/Rhino 进程内多语言执行。主流产品基本没人这么干(面试不好讲),且仍在进程内,隔离性不足。直接弃用。


3 · 选型:主流产品的沙箱实现横向对比#

产品隔离技术备注
OpenAI Code InterpretergVisor用户态内核,syscall 拦截
E2BFirecracker microVM强隔离,但自托管偏重
Dify(dify-sandbox)seccomp + chroot轻,但无 Java SDK
FastGPTvm2 / seccomp早期缺 cgroups → CVE
n8nPyodide → task-runner进程内吃过 9.9 RCE,后转独立进程
Coze Studio / RizaWASM沙箱化但生态/包支持受限
microsandboxlibkrun microVM轻量 VM
阿里 OpenSandbox多 runtime(docker / gVisor / Kata / Firecracker 可选)原生 Java/Kotlin SDK、内建 Code Interpreter、Apache-2.0、CNCF Landscape

选 OpenSandbox 的决定性理由:DocMind 是 Java 后端,OpenSandbox 提供原生 Java SDKcom.alibaba.opensandbox:code-interpreter),不用自己拼 HTTP/SSE 胶水;其余开源轻量沙箱要么只有 Python/JS SDK,要么得自己造客户端。隔离档位可按环境从 docker 一路升到 gVisor/Kata/Firecracker,dev 轻、prod 强。

方法论复盘:第一轮我用关键词搜索得到的候选偏 SEO/榜单(dify-sandbox 之类),漏了 OpenSandbox/AIO Sandbox 这类厂商新项目。教训:选型调研要枚举 awesome-list + 厂商最新开源,不能只信搜索头部结果。


4 · 架构:计算结果如何回流到最终答案#

4.1 关键约束:生成阶段是终态,不调工具#

DocMind 的生成阶段(SSE 流式作答)不调用任何工具,只有写进 AgentToolContext 的东西能进最终 prompt。所以 executeCode 的结果必须走一条”通道”回流——复刻已有的 memoryContext 那条链

AgentToolContext.computations          (ThreadLocal,工具执行时写入)
   → SupervisorResult.computations()   (收尾时打包)
   → AgentState.computations           (DocMindAgent 合并,与 memoryContext 并列)
   → PromptAssembler.formatComputations → {{computations}} 模板占位符
plaintext

收益:计算结果不参与 rerank、不被压缩丢弃、不占检索 token 预算,按原样作为”已执行的精确计算”区注入 prompt。

4.2 为什么 executeCode 在 agentic 循环里,而不是独立一步#

因为 LLM 必须先看到检索回来的数据(chunk 内容在工具结果消息里可见),才能照着把数字内联进代码。所以它是循环里的第 5 个工具,模型自驱”先检索 → 再写代码算 → 看结果决定下一步”。

4.3 引用处理的取舍(容易被问)#

计算结果不分配 [n] 编号。因为 [n] 引用体系绑定在 参考来源(chunks)上,CitationParser 会按 sources 校验越界编号(越界 → invalidRefs、压低覆盖率)。做法:把计算块作为权威数值区单列,提示模型”数值以计算为准、但对数字所依据的原始数据来源[n]”。既保证数值可信,又不破坏引用覆盖率。


5 · 三阶段落地#

阶段内容验证
Stage 0跑通 OpenSandbox + 薄 Java 客户端(SandboxConfig + SandboxCodeExecutor)+ 冒烟测试。先证明”Spring 能调沙箱跑 Python 拿回结果”再往下做。mvn compile 对真 JAR 校验 API;SandboxSmokeTest 环境门控,2+2=4
Stage 1executeCode @Tool + 接入 agentic 白名单 + 计算结果通道 + prompt 注入。修早停护栏(executeCode 只产计算不产 chunk,须计入”本轮产出”否则被误判零增益提前收尾)。单测:白名单含 executeCode;computation 流入 SupervisorResult.computations()
Stage 2护栏(sys_ai_config 动态:enabled/超时/输出上限/代码长度)+ 禁出网(egress deny-all)+ 可观测(code_exec SSE 事件 + 持久化 code+result 到 trace)+ 前端时间线”执行”节点 + 隔离档位文档。139 单测全绿;前端 type-check 通过。

Stage 3(未做,记录待办):pandas/numpy/matplotlib + 图表产物(PNG → MinIO → 返回 URL)。届时 executeCode 接口不变,仅扩展结果通道支持二进制产物。


6 · 安全边界与护栏(fail-closed)#

护栏实现默认
一键开关sandbox.enabled(sys_ai_config,热切换)。关闭时 executeCode 不进 agentic 白名单(模型根本看不到),工具层再做 no-op 纵深防御。false
禁出网docmind.sandbox.deny-egress → 建箱时 NetworkPolicy.defaultAction(DENY),deny-all egress,防数据外泄/拉恶意载荷。true(fail-closed)
执行超时sandbox.timeout_seconds → 沙箱生命周期 TTL,超时 server 回收(OpenSandbox server 硬下限 60s,低于会被钳到 60;沙箱用后即 kill,TTL 仅兜底)。60s
输出上限sandbox.max_output_chars → stdout/返回值截断,防超长输出灌爆生成上下文。10000
代码长度上限sandbox.max_code_chars → 超长直接拒绝、不进沙箱。10000
临时沙箱每次执行临时创建 + 用后 kill()(finally 保证),换强隔离;池化复用列为后续优化。
仅内部@Tool 供 agentic 反射 + 进白名单,但加入 McpToolsConfig 的对外 MethodToolCallbackProvider → 不对外暴露(对外跑任意代码安全面太大)。
越权防护userId/kbIds 沿用既有 AgentToolContext 收口,LLM 不可篡改。
隔离档位dev 容器隔离 + drop_capabilities + no_new_privileges;prod 可切 gVisor/Kata/Firecracker。

所有执行异常(连接失败/运行时错误/超时)都被收敛成 ExecResult.failure,工具优雅返回 error——模型可在”无计算结果”下继续作答,不中断主流程。


7 · 面试讲法 / 高频质疑应答#

Q:为什么不直接让大模型算? A:模型对多步数值计算(同比、CAGR、剔异常值后的统计量)容易算错、不可复现。把计算下沉到确定性的代码执行,数值有据可查、可回放(trace 持久化了 code+result)。

Q:跑 LLM 写的代码不危险吗? A:所以必须进程外沙箱。判据是”代码由谁写”——LLM 写的是不可信输入,绝不在 JVM 里 eval。我们用 OpenSandbox 进程外隔离 + 禁出网 + 超时/输出/长度三道护栏 + 仅内部暴露,且默认整条能力关闭(fail-closed)。n8n/FastGPT 的进程内方案都吃过 RCE,是反面教材。

Q:为什么不用 E2B / 自己写 Docker 调度? A:E2B 自托管偏重;自己调度 Docker 是造轮子且易踩 cgroups/seccomp 配置坑。OpenSandbox 开源可自托管、有原生 Java SDK、隔离档位可调,工程量最小、最稳。

Q:计算结果怎么进最终答案、会不会被检索管线丢掉? A:生成阶段是终态不调工具,所以我复刻了 memoryContext 的回流链做了一条「计算结果通道」,计算结果不参与 rerank/压缩、不占检索预算,按原样注入 prompt 的”已执行的精确计算”区;引用仍标在底层数据源上,不破坏引用覆盖率。

Q:关掉沙箱会怎样? A:sandbox.enabled=false(默认)时 executeCode 根本不进 agentic 白名单,模型看不到这工具;常规构建/运行/测试零影响(冒烟测试环境门控自动跳过)。


8 · 关键文件#

  • 连接/执行器:config/DocmindSandboxPropertiesconfig/SandboxConfigservice/sandbox/SandboxCodeExecutorservice/sandbox/ExecResult
  • 工具:mcp/CodeExecTool(@Tool executeCode,仅内部)
  • 通道:agent/AgentToolContext(computations)、agent/supervisor/SupervisorResultagent/state/AgentStateagent/DocMindAgent(合并)
  • 接入 + 可观测:agent/supervisor/AgenticSearchOrchestrator(白名单门控 + 早停修正 + code_exec 事件 + 日志持久化)、agent/emit/StageEmitter#codeExec
  • prompt:service/rag/PromptAssembler#formatComputationsprompts/knowledge_qa.txt / knowledge_qa_low_confidence.txt
  • 护栏配置:config/AiConfigInitializer(sandbox.* 动态项)
  • 部署:docker-compose.yml(全栈,含 opensandbox + 后端经 DOCMIND_SANDBOX_DOMAIN=opensandbox:8090 连接)与 docker-compose.dev.yml(host-dev 轻量 infra,也含 opensandbox)、docker/opensandbox/config.toml
  • 前端:views/chat/ChatView.vue(code_exec 时间线节点)
  • 依赖:pom.xmlcom.alibaba.opensandbox:code-interpreter:1.0.12 + Maven Central 兜底源)

8.5 · 本机联调踩坑(真实部署到跑通的 4 个坑)#

把 SDK 接进来能编译 ≠ 能跑通。本机起 OpenSandbox server 实测时,依次踩了 4 个坑,都已在代码里修掉(默认值即正确):

  1. 沙箱 timeout 有硬下限 60sPOST /v1/sandboxes 校验 timeout ge 60,传 15s 直接 422 Unprocessable Entity。→ 执行器把 builder timeout 钳到 max(60, 配置值)sandbox.timeout_seconds 默认/最小值改为 60(沙箱用后即 kill,TTL 只是兜底)。
  2. resourceLimits 必填:无 poolRef 时 server 要求 resourceLimits,SDK Sandbox.builder() 默认不带 → 422。→ 加静态配置 cpu-limit(默认 “1”) / memory-limit(默认 “1Gi”),经 .resource(Map.of("cpu",..,"memory",..)) 传入。
  3. 桥接网络下沙箱 endpoint 宿主直连不通:JVM 在宿主、server/sandbox 在 Docker 桥接网,SDK 默认直连沙箱 endpoint → health check timed out after 30s。SDK 报错里就给了答案:useServerProxy=true。→ ConnectionConfig.useServerProxy(true)(默认开),所有沙箱流量经 server 代理转发,免暴露沙箱端口 / 配 host_ip。
  4. kotlinx-serialization 版本冲突(最隐蔽):沙箱能建、能跑,但 SDK 反序列化执行结果时 AbstractMethodError: GeneratedSerializer.typeParametersSerializers()。根因:SDK 要 kotlin-stdlib 2.2.21 + kotlinx-serialization 1.9.0,而 Spring Boot 3.4 BOM 把它们下钉到 kotlin 1.9.25 + serialization 1.6.3——1.9.0 编译出的 $$serializer 调了 1.6.3 没有的接口方法。→ 在 spring-boot-dependencies 之前导入 kotlin-bom:2.2.21 + kotlinx-serialization-bom:1.9.0(dependencyManagement 同 GA 首条生效 → 覆盖 BOM 下钉)。

面试点:这 4 个坑里第 4 个最值得讲——第三方 Kotlin SDK 接进 Spring Boot(Java)项目,最容易踩 BOM 版本下钉导致的运行时 AbstractMethodError。排查靠 mvn dependency:tree 对比”SDK POM 要的版本”vs”classpath 实际解析的版本”,解法是用官方 BOM 在 Spring Boot BOM 之前导入抢占版本。

本机验证记录(已通过)#

docker compose -f docker-compose.dev.yml up -d opensandbox   # server healthy: {"status":"healthy"}
# 预拉镜像(9.37GB,国内走阿里云 registry):opensandbox/code-interpreter:v1.0.2 + execd:v1.0.18
SANDBOX_SMOKE=1 mvn test -Dtest=SandboxSmokeTest
#   → [SandboxSmokeTest] ok=true stdout=4 result=4 error=null latencyMs=8937  BUILD SUCCESS
mvn test -Dtest='!MediRagApplicationTests'   # 139 passed / 0 failed(沙箱冒烟无 SANDBOX_SMOKE 时自动跳过)
plaintext

镜像约 9.37GB(含 Python/Java/Go/Node 多运行时);首次建箱拉镜像慢,建议预拉。沙箱用后即 kill,实测无残留容器。

9 · 来源#

  • OpenSandbox:github.com/opensandbox-group/OpenSandbox(原 alibaba/OpenSandbox,已迁移);server/docker-compose.example.yaml、server/configuration.md
  • Maven Central:com.alibaba.opensandbox:code-interpreter 1.0.12(2026-06)
  • n8n CVE-2025-68668(进程内 Pyodide 沙箱逃逸 RCE,CVSS 9.9)