给 AI Agent 装上“大脑“:Java语言中Code Interpreter 的设计与实现

给 AI Agent 装上"大脑":Java语言中Code Interpreter 的设计与实现

当我们为 Agent 逐个硬编码工具时,其实是在用"人类穷举"来弥补"AI 自主性"的不足。本文探讨一种更优雅的思路------让 Java Agent 像 ChatGPT Code Interpreter 一样,在运行时自己写代码来解决问题。


一、问题背景:硬编码工具的困境

在构建 AI Agent 时,最常见的架构是 Tool Calling------为 Agent 预先定义一组工具(函数),Agent 通过 LLM 推理决定调用哪个工具、传什么参数。这个模式在 OpenAI Function Calling、LangChain Tools、Spring AI 等主流框架中被广泛采用。

然而,当我们真正开始为 Agent 编写工具时,会迅速掉入一个陷阱:工具永远写不完

想让 Agent 知道今天星期几?写一个 DateTimeTool。想让它做数学计算?再写一个 MathCalculatorTool。想让它生成 UUID?再来一个 UUIDGeneratorTool。Base64 编解码?正则匹配?日期差值计算?单位换算?JSON 格式化?......

每种场景都要写一个工具类------定义参数 Schema、实现 execute 方法、编写描述文档。工具列表无限膨胀,维护成本直线上升。

更荒谬的是:LLM 本身就会编程 。它明明可以写代码算出 2026年4月29日到国庆节还有多少天,却因为没有对应的工具而无法回答。我们用硬编码工具,实际上是在人为限制 AI 的能力边界

ChatGPT Code Interpreter 给出的启示

OpenAI 在 2023 年推出的 Code Interpreter 优雅地解决了这个问题------Agent 遇到需要计算、数据处理的场景时,自己写一段 Python 代码,然后在沙箱中执行,拿到结果后整理回复。

一个工具,顶一万个工具。

这个思路的本质是一个认知转换 :与其给 Agent 100 种专用工具,不如给它一个"万能工具"------代码执行能力

那么问题来了:Python 有 exec(),Java 生态怎么做到?


二、技术选型:为什么是 Groovy?

Java 是编译型语言,不像 Python 那样可以直接 exec() 执行动态代码。但 JVM 生态提供了多种动态执行方案:

方案 原理 优势 劣势
javax.script (Nashorn) 内嵌 JS 引擎 JDK 自带 JDK 11 已废弃,功能受限
GraalJS 新一代 JS 引擎 性能好 需要额外依赖 GraalVM
Jython JVM 上的 Python Python 语法 仅支持 Python 2,生态老旧
Java Compiler API 运行时编译 Java 完整 Java 能力 需要 JDK 环境,编译慢
Groovy JVM 动态语言 语法兼容 Java,可直接调用 Java API 需要额外依赖(~7MB)

我们最终选择了 Groovy,原因很直接:

2.1 语法与 Java 几乎完全兼容

LLM 最擅长生成的语言之一就是 Java/Groovy。Agent 不需要"学"一门新语言,直接用 Java 风格写代码即可:

groovy 复制代码
// 这段代码既是合法的 Java,也是合法的 Groovy
LocalDateTime now = LocalDateTime.now();
println(now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

同时 Groovy 还提供了更简洁的语法糖,LLM 可以灵活使用:

groovy 复制代码
// Groovy 独有的便利写法
println((1..100).sum())                    // Range + 集合方法
println([1,2,3].collect { it * 2 })        // 闭包
println(UUID.randomUUID())                 // 直接调用 Java API

2.2 可以直接调用所有 Java 标准库

这是最关键的优势。Groovy 运行在 JVM 上,可以直接使用 java.timejava.utiljava.mathjava.text 等所有 Java 标准库,无需额外适配。这意味着 Agent 获得了与宿主 Java 应用同等的计算能力。

2.3 即时执行,无需编译

Groovy 脚本通过 GroovyShell 直接解析执行,无需 javac 编译步骤,启动速度快,适合 Agent 实时交互场景。


三、架构设计:两层结构,各司其职

Code Interpreter 的设计遵循单一职责原则,拆分为两层:

复制代码
┌──────────────────────────────────────────────────────┐
│                   Agent Engine                        │
│     用户提问 → LLM 生成代码 → 调用 code_interpreter    │
├──────────────────────────────────────────────────────┤
│                                                       │
│   ┌───────────────────────┐                          │
│   │  CodeInterpreterTool  │  ← 工具协议层             │
│   │  - 定义工具 Schema     │     面向 LLM 的接口       │
│   │  - 参数解析            │     遵循 Function Calling │
│   │  - 结果格式化          │     协议规范              │
│   └──────────┬────────────┘                          │
│              │                                        │
│              ▼                                        │
│   ┌───────────────────────┐                          │
│   │    GroovySandbox      │  ← 安全执行层             │
│   │  - AST 安全检查       │     编译期拦截危险代码     │
│   │  - 超时控制           │     运行期限制执行时间     │
│   │  - 输出截断           │     防止无限输出           │
│   │  - 自动导入           │     开箱即用              │
│   └───────────────────────┘                          │
│                                                       │
└──────────────────────────────────────────────────────┘
  • 工具协议层CodeInterpreterTool):面向 LLM,定义工具的 JSON Schema、参数描述和示例,让 LLM 知道"怎么用这个工具"。这一层是可替换的------无论你的 Agent 框架是 LangChain、Spring AI 还是自研的,只需要按框架的工具接口适配即可
  • 安全执行层GroovySandbox):面向安全,纯粹负责"在受控环境中安全地执行一段 Groovy 代码"。这一层是框架无关的,可以独立使用

四、安全沙箱:GroovySandbox 的三道防线

让 Agent 动态执行代码,安全是第一优先级 。如果 LLM 生成了一段 Runtime.getRuntime().exec("rm -rf /") 的代码,后果不堪设想。

GroovySandbox 构建了三道防线:

4.1 第一道防线:AST 编译期拦截

Groovy 提供了 SecureASTCustomizer,可以在代码编译阶段(而非运行时)就拦截危险代码。这是最前置、最彻底的防护------代码还没运行就被拒绝了。

黑名单包(禁止 import *

java 复制代码
private static final List<String> BLOCKED_STAR_IMPORTS = Arrays.asList(
        "java.io",              // 文件读写
        "java.nio",             // NIO 文件操作
        "java.net",             // 网络请求
        "java.lang.reflect",    // 反射
        "java.lang.invoke",     // 方法句柄
        "java.sql",             // 数据库操作
        "javax.net",            // SSL/网络
        "javax.script",         // 脚本引擎(防止嵌套执行)
        "groovy.lang",          // Groovy 内部 API
        "org.codehaus.groovy"   // Groovy 编译器 API
);

黑名单类(精确拦截)

java 复制代码
private static final List<String> BLOCKED_CLASSES = Arrays.asList(
        "java.lang.Runtime",        // 系统命令执行
        "java.lang.ProcessBuilder", // 进程创建
        "java.lang.System",         // System.exit() 等
        "java.lang.Thread",         // 线程操作
        "java.lang.ClassLoader",    // 类加载器
        "java.lang.Class"           // 反射入口
);

这意味着,以下代码在编译阶段就会被拒绝,根本不会执行:

groovy 复制代码
// ❌ 全部会被 AST 检查拦截
Runtime.getRuntime().exec("whoami")           // 系统命令
new File("/etc/passwd").text                  // 文件读取
new URL("http://evil.com").text               // 网络请求
Thread.sleep(999999)                          // 线程阻塞
System.exit(0)                                // 强制退出

4.2 第二道防线:执行超时控制

即使代码通过了 AST 检查,仍然可能出现死循环或超长计算。GroovySandbox 将代码执行放入独立线程,并设置严格的超时限制:

java 复制代码
public static SandboxResult execute(String code, Integer timeoutSeconds) {
    int timeout = (timeoutSeconds != null && timeoutSeconds > 0)
            ? Math.min(timeoutSeconds, 30) : DEFAULT_TIMEOUT_SECONDS;  // 最长 30 秒

    ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> {
        Thread thread = new Thread(runnable, "groovy-sandbox");
        thread.setDaemon(true);  // 守护线程,不阻止 JVM 退出
        return thread;
    });

    try {
        Future<SandboxResult> future = executor.submit(createTask(code));
        return future.get(timeout, TimeUnit.SECONDS);
    } catch (TimeoutException e) {
        return SandboxResult.error("代码执行超时(限制 " + timeout + " 秒)");
    } finally {
        executor.shutdownNow();  // 超时后立即终止
    }
}

设计要点:

  • 默认 10 秒 超时,最大 30 秒,由 Agent 传入参数控制
  • 使用 守护线程(daemon),即使代码卡死也不会阻止应用退出
  • 超时后通过 shutdownNow() 强制中断执行

4.3 第三道防线:输出长度限制

防止代码生成海量输出(如打印一个超大数组),导致内存溢出或响应体过大:

java 复制代码
private static final int MAX_OUTPUT_LENGTH = 10000;

String printedOutput = outputWriter.toString();
if (printedOutput.length() > MAX_OUTPUT_LENGTH) {
    printedOutput = printedOutput.substring(0, MAX_OUTPUT_LENGTH)
            + "\n... [输出被截断,超过 " + MAX_OUTPUT_LENGTH + " 字符]";
}

println() 的标准输出和表达式的返回值都做了截断处理,确保输出可控。


五、开箱即用:自动导入常用类

为了让 LLM 生成的代码尽量简洁,GroovySandbox 通过 ImportCustomizer 预先导入了最常用的 Java 标准库:

java 复制代码
// 通配符导入
imports.addStarImports(
        "java.util",              // List, Map, Set, Collections...
        "java.util.stream",       // Stream API
        "java.time",              // LocalDate, LocalDateTime, Instant...
        "java.time.format",       // DateTimeFormatter
        "java.math",              // BigDecimal, BigInteger
        "java.text",              // SimpleDateFormat, NumberFormat
        "java.util.concurrent"    // CountDownLatch, ConcurrentHashMap...
);

// 精确导入高频使用类
imports.addImports(
        "java.util.UUID",
        "java.time.LocalDate",
        "java.time.LocalDateTime",
        "java.time.ZoneId",
        "java.time.format.DateTimeFormatter",
        "java.math.BigDecimal",
        "java.util.Base64"
        // ... 更多
);

这样,Agent 生成的代码可以直接使用这些类,无需写 import 语句:

groovy 复制代码
// 不需要 import,直接使用
println(LocalDate.now())
println(UUID.randomUUID())
println(Base64.encoder.encodeToString("Hello".bytes))

六、工程设计:可选依赖与条件加载

在实际工程中,并非所有应用都需要 Code Interpreter 能力。一个好的框架设计应该做到按需加载------需要的人自动获得,不需要的人零感知。

在 Spring Boot 生态中,这可以通过 @ConditionalOnClass 优雅地实现:

java 复制代码
@Component
@ConditionalOnClass(name = "groovy.lang.GroovyShell")
public class CodeInterpreterTool implements AgentTool {
    // 仅当 classpath 中存在 Groovy 时才加载
}

配合 Maven 的 <optional>true</optional> 声明:

xml 复制代码
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>3.0.19</version>
    <optional>true</optional>  <!-- 不传递给下游 -->
</dependency>

行为如下:

集成方操作 结果
主动引入了 Groovy 依赖 GroovyShell 类存在 → Code Interpreter 自动注册 ✅
未引入 Groovy 依赖 GroovyShell 类不存在 → 不加载,不报错 ✅

这是 Spring Boot 生态中"能力随依赖而来"的标准设计模式,也适用于非 Spring 的框架------核心思想是:代码执行引擎作为可选模块,与 Agent 核心解耦


七、端到端工作流

当用户向 Agent 提问时,Code Interpreter 的完整工作流如下:

复制代码
用户:"2026年4月29日到国庆节还有多少天?"
                    │
                    ▼
        ┌─── Agent Engine ───┐
        │  组装 system prompt │
        │  包含 code_interpreter │
        │  工具的描述和参数说明  │
        └────────┬───────────┘
                 │
                 ▼
        ┌─── LLM 推理 ──────┐
        │  分析:这是日期计算问题 │
        │  决定:调用 code_interpreter │
        │  生成代码:                  │
        │  ┌──────────────────────┐   │
        │  │ def today = LocalDate │   │
        │  │   .of(2026, 4, 29)   │   │
        │  │ def national = LocalDate │ │
        │  │   .of(2026, 10, 1)   │   │
        │  │ def days = ChronoUnit │   │
        │  │   .DAYS.between(     │   │
        │  │     today, national) │   │
        │  │ println("还有${days}天") │ │
        │  └──────────────────────┘   │
        └────────┬───────────────────┘
                 │
                 ▼
        ┌─── CodeInterpreterTool ──┐
        │  解析参数(code, timeout) │
        │  调用 GroovySandbox.execute │
        └────────┬─────────────────┘
                 │
                 ▼
        ┌─── GroovySandbox ────────┐
        │  1. AST 安全检查 → 通过 ✅ │
        │  2. 自动导入 java.time.*  │
        │  3. 编译 + 执行脚本       │
        │  4. 捕获 println 输出     │
        │  5. 检查超时 → 未超时 ✅   │
        │  6. 截断检查 → 长度正常 ✅ │
        └────────┬─────────────────┘
                 │
                 ▼
        ┌─── 返回结果 ────────────┐
        │  {"success": true,      │
        │   "stdout": "还有155天"} │
        └────────┬────────────────┘
                 │
                 ▼
        ┌─── LLM 整理回复 ───────┐
        │  "从2026年4月29日到      │
        │   国庆节(10月1日),     │
        │   还有 155 天。"         │
        └─────────────────────────┘

八、实际效果:一个工具的万能性

以下是 Agent 实际使用 code_interpreter 解决各类问题的示例:

8.1 日期时间

用户:"今天星期几?距离下周五还有几天?"

Agent 生成的代码

groovy 复制代码
def today = LocalDate.now()
def dayOfWeek = today.getDayOfWeek()
println("今天是: " + dayOfWeek)

def nextFriday = today.with(java.time.temporal.TemporalAdjusters.next(DayOfWeek.FRIDAY))
def daysUntil = java.time.temporal.ChronoUnit.DAYS.between(today, nextFriday)
println("距离下周五还有 " + daysUntil + " 天")

8.2 数学计算

用户:"一个圆的半径是 7.5 厘米,它的面积和周长分别是多少?"

Agent 生成的代码

groovy 复制代码
def radius = 7.5
def area = Math.PI * Math.pow(radius, 2)
def circumference = 2 * Math.PI * radius
println(String.format("面积: %.2f 平方厘米", area))
println(String.format("周长: %.2f 厘米", circumference))

8.3 数据转换

用户 :"帮我把 Hello World 2026 做 Base64 编码"

Agent 生成的代码

groovy 复制代码
def text = "Hello World 2026"
def encoded = Base64.encoder.encodeToString(text.bytes)
println("原文: " + text)
println("Base64: " + encoded)

8.4 生成 UUID

用户:"帮我生成 5 个 UUID"

Agent 生成的代码

groovy 复制代码
(1..5).each { i ->
    println("${i}. ${UUID.randomUUID()}")
}

8.5 复杂数据处理

用户:"我有一组成绩 [85, 92, 78, 95, 88, 76, 91, 83],帮我算平均分、最高分、最低分,以及及格率(60分以上)"

Agent 生成的代码

groovy 复制代码
def scores = [85, 92, 78, 95, 88, 76, 91, 83]
def avg = scores.sum() / scores.size()
def max = scores.max()
def min = scores.min()
def passRate = scores.count { it >= 60 } / scores.size() * 100

println("成绩列表: " + scores)
println(String.format("平均分: %.1f", avg))
println("最高分: " + max)
println("最低分: " + min)
println(String.format("及格率: %.0f%%", passRate))

关键洞察:以上五类场景,如果用传统的硬编码方式,至少需要 5 个工具类、几百行代码。而 Code Interpreter 只需要一个工具,LLM 自己"写"出解决方案。


九、更大的图景:Code Interpreter + 自主记忆

Code Interpreter 不是终点,而是一个更强大闭环的起点。

如果 Agent 还具备自主记忆能力(如 Hermes Agent 提出的 Skill 自学习机制),Code Interpreter 就会与记忆系统形成有趣的协同效应:

复制代码
第一次遇到问题                          第 N 次遇到同类问题
┌────────────────────┐               ┌────────────────────┐
│ 用户:"算一下复利"    │               │ 用户:"算一下复利"    │
│        ↓            │               │        ↓            │
│ LLM 从零推理        │               │ 从记忆中检索到 Skill │
│ 生成 Groovy 代码    │               │ 直接套用代码模板     │
│ 沙箱执行 → 成功     │               │ 沙箱执行 → 成功     │
│        ↓            │               │        ↓            │
│ 记忆系统记录并提炼   │  ──学习──→   │ 更快、更准、更省Token │
│ 存储为可复用 Skill   │               │                    │
└────────────────────┘               └────────────────────┘

这个闭环的关键在于:

  1. Agent 使用 code_interpreter 成功解决了一个问题
  2. 记忆系统追踪到这次执行:记录了"用户问了什么 → Agent 生成了什么代码 → 执行结果是什么"
  3. 提炼为 Skill 并持久化:将具体的参数值抽象为通用的代码模板
  4. 下次遇到类似问题时,Skill 被注入到 system prompt 中:Agent 直接复用已验证的代码模式

这形成了一个"代码能力自进化 "的闭环------Agent 不仅能动态写代码,还能从自己写的代码中学习和积累经验。每解决一个新问题,Agent 就变得更聪明一点。


十、设计权衡与反思

任何架构选择都有取舍。以下是我们在实践中的思考:

10.1 当前的权衡

维度 选择 理由 另一种选择及其代价
安全模型 黑名单 + AST 平衡安全性和可用性 白名单更安全,但会严重限制 LLM 的代码生成自由度
超时策略 固定上限 30 秒 Agent 交互场景足够 无限制会有死循环风险;过短则复杂计算无法完成
语言选择 仅 Groovy 与 Java 语法兼容,LLM 生成质量高 多语言增加复杂度,且 LLM 在不同语言间切换容易出错
沙箱粒度 JVM 进程内 零部署成本,延迟低 独立进程/容器更安全,但引入了分布式调用的复杂度

10.2 未来可能的演进方向

  • 可配置安全策略 :允许不同场景自定义允许/禁止的包和类,如内网场景可放开 java.net
  • 编译缓存:对相同代码做编译结果缓存,避免重复编译开销(Groovy 编译约 50-200ms)
  • 多语言支持:通过 GraalVM Polyglot 支持 Python、JavaScript 等语言,让 LLM 选择最适合的语言
  • 资源配额:限制内存使用量,防止代码消耗过多堆内存
  • 容器级隔离:高安全要求场景下,将代码执行放入独立容器,实现操作系统级的资源隔离
  • 代码审计日志:记录所有执行过的代码和结果,供安全审计和合规检查

总结

回到开头的问题:Agent 的工具永远写不完,怎么办?

答案是:不要穷举工具,而是给 Agent 编程能力

Code Interpreter 的本质是一个认知转换------从"我来预定义 Agent 能做什么",到"让 Agent 自己决定怎么做"。这不仅解决了工具膨胀的问题,更释放了 LLM 最擅长的能力之一------代码生成

在 Java 生态中,Groovy 是实现这一理念的理想载体:

  • 语法兼容:LLM 用 Java 风格写代码,零学习成本

  • API 共享 :直接调用 java.timejava.util 等标准库,与宿主应用同等能力

  • 安全可控:AST 编译期拦截 → 运行时超时控制 → 输出长度限制,三道防线保障安全

    一个 Code Interpreter
    = DateTimeTool + MathCalculatorTool + UUIDGeneratorTool
    + StringTool + Base64Tool + RegexTool + JsonTool + ...∞

与其给一个人 100 种专用工具,不如教会他编程。

一个工具,无限可能。

相关推荐
QuZero1 小时前
StampedLock Mechanism
java·算法
Javatutouhouduan1 小时前
Java小白如何快速玩转Redis?
java·数据库·redis·分布式锁·java面试·后端开发·java程序员
xuhaoyu_cpp_java1 小时前
Spring学习(一)
java·经验分享·笔记·学习·spring
kyriewen112 小时前
奥特曼借GPT-5.5干杯,而你的Copilot正按Token收钱
前端·gpt·ai·copilot
kybs19912 小时前
springboot视频推荐系统--附源码72953
java·spring boot·python·eclipse·asp.net·php·idea
无限进步_2 小时前
C++ 多态机制完全解析:从虚函数重写到动态绑定原理
java·c语言·jvm·数据结构·c++·windows·后端
薛定谔的猫3692 小时前
AI Agent 与 MCP 协议:构建标准化大模型交互的新范式
ai·llm·agent·mcp·software engineering
风雅GW2 小时前
多 Agent 系统设计参考框架(OpenClaw 实现版)
人工智能·ai·agent·openclaw
知识汲取者3 小时前
巨量引擎 Marketing API Java SDK 介绍
java·开发语言