今天讨论 Coding Agent 或 AI 编程助手时,技术栈几乎已经形成默认答案:Node.js、TypeScript 和 Python。模型 SDK 先支持它们,工具生态围绕 npm 和 pip 生长,演示通常运行在开发者本机,通过现代 IDE 直接访问外部模型 API。
这种选择很合理。Agent 需要快速试错、拼接大量 API,并频繁调整提示词、工具定义和控制流程,动态语言天然适合探索阶段。但当 Agent 从个人电脑上的原型进入企业研发系统,真正限制项目的往往不再是“能否在两天内接通模型”,而是权限、审计、部署、稳定性和既有系统集成。此时,坚持 Java 并不等于否定 TS/Python,而是在优化另一组约束。
默认栈解决的是探索速度
TS/Python 成为主流,核心原因不是它们拥有某种 Agent 专属能力,而是它们缩短了实验路径:
- 模型厂商通常优先提供 Python 和 JavaScript SDK;
- JSON、HTTP、流式响应和函数调用可以用较少代码完成;
- npm、pip 中有大量解析、检索、浏览器自动化和向量数据库组件;
- 脚本运行方式适合快速修改提示词与工具协议;
- 社区示例多,遇到问题更容易找到可运行的参考实现。
如果目标是验证模型能否理解代码库、调用工具或完成某类任务,TS/Python 通常仍是更高效的起点。为了语言统一而强行用 Java 做所有早期实验,可能只会增加编译、类型建模和依赖配置工作。
问题在于,原型阶段的优势不能自动推导为生产阶段的最优解。一个 Coding Agent 真正进入组织后,通常要连接代码仓库、制品库、工单、内部知识库和部署平台。它执行的不只是一次模型请求,而是一条带身份、权限和副作用的业务流程。
Java 的价值出现在系统边界上
企业里的 Java 应用往往已经具备成熟的工程基础:统一认证、权限模型、配置中心、日志规范、链路追踪、服务治理和发布流程。Agent 如果最终要成为内部平台的一部分,复用这些能力通常比重新搭建一套 Node 或 Python 运行环境更重要。
Java 在这类场景中的优势主要体现在四个方面。
1. 工具调用可以进入现有权限体系
Agent 的工具本质上是受模型驱动的函数入口。读取仓库和查询文档风险较低,但创建分支、修改工单、执行命令或触发部署都可能产生真实副作用。
在 Java 服务中,可以把每个工具继续放在现有的认证与授权边界内:调用者是谁、允许访问哪个项目、参数是否合规、操作是否需要审批,都由确定性代码控制。模型负责提出意图,系统负责决定是否执行。
2. 类型系统适合固定工具契约
Agent 的提示词可以快速变化,但生产工具的输入输出不应该模糊。用 Java record、枚举和校验器描述工具参数,可以尽早暴露字段缺失、非法状态和协议漂移。
类型并不能阻止模型生成错误参数,却能把错误挡在业务方法之外。相比把模型输出直接传给内部接口,这层显式转换非常重要。
3. 可观测性能够沿用现有标准
一次 Agent 任务通常包含多轮模型调用和工具执行。只记录最终回答,很难解释成本、延迟与失败原因。生产系统至少要记录:
- 任务 ID、用户和权限上下文;
- 每轮模型、提示词版本和 token 用量;
- 工具名称、参数摘要、耗时和结果状态;
- 重试、超时、人工审批和终止原因;
- 对仓库、工单或环境产生的变更。
如果团队已经使用 Java 的日志、指标和追踪体系,Agent 可以作为普通服务接入,而不必维护孤立的观测链路。
4. 部署一致性可能比 SDK 丰富度更重要
许多企业能够稳定交付 JVM 服务,却未必允许生产环境临时安装 npm 包、pip 包,或者让进程直接访问公网模型接口。Java Agent 可以遵循既有镜像基线、依赖扫描、灰度发布和回滚流程,也可以通过内部模型网关访问不同供应商。
这里的关键不是“Java SDK 是否功能最多”,而是模型能力能否被封装在组织已经能够治理的运行时中。
把 Agent 拆成协议、循环与工具
不要把 Agent 等同于某个框架。一个最小的生产结构通常只需要几个清晰边界:
ModelClient:把内部请求转换成具体模型协议;Planner:根据目标和历史观察决定下一步;ToolRegistry:维护可调用工具及其参数契约;Policy:检查身份、权限、目录范围和副作用等级;AgentLoop:控制最大轮数、超时、重试与终止条件;AuditSink:记录模型决策和工具执行结果。
模型厂商、SDK 或 Agent 框架可以变化,但业务工具与权限边界不应该跟着重写。对 Java 团队而言,更稳妥的做法是让模型适配层保持薄,把核心治理能力留在自己的代码中。
一个可运行的 Java 最小 Agent 循环
下面的示例使用 Java 17,不依赖第三方库。为了能够直接运行,Planner 使用确定性实现模拟模型决策:第一轮读取指定文件,第二轮根据观察结果结束任务。接入真实模型时,只需替换 DemoPlanner,保留工具注册、路径限制和循环上限。
将代码保存为 MiniAgent.java:
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class MiniAgent {
record ToolCall(String name, Map<String, String> arguments) {}
record Decision(ToolCall toolCall, String finalAnswer) {
static Decision call(String name, Map<String, String> arguments) {
return new Decision(new ToolCall(name, arguments), null);
}
static Decision finish(String answer) {
return new Decision(null, answer);
}
}
interface Tool {
String name();
String execute(Map<String, String> arguments) throws Exception;
}
interface Planner {
Decision next(String goal, List<String> observations);
}
static final class ReadTextFileTool implements Tool {
private final Path workspace;
ReadTextFileTool(Path workspace) {
this.workspace = workspace.toAbsolutePath().normalize();
}
@Override
public String name() {
return "read_text_file";
}
@Override
public String execute(Map<String, String> arguments) throws Exception {
String rawPath = arguments.get("path");
if (rawPath == null || rawPath.isBlank()) {
throw new IllegalArgumentException("path is required");
}
Path target = workspace.resolve(rawPath).normalize();
if (!target.startsWith(workspace)) {
throw new SecurityException("path escapes workspace");
}
String content = Files.readString(target, StandardCharsets.UTF_8);
int limit = Math.min(content.length(), 2_000);
return content.substring(0, limit);
}
}
static final class DemoPlanner implements Planner {
private final String requestedPath;
DemoPlanner(String requestedPath) {
this.requestedPath = requestedPath;
}
@Override
public Decision next(String goal, List<String> observations) {
if (observations.isEmpty()) {
return Decision.call(
"read_text_file",
Map.of("path", requestedPath)
);
}
return Decision.finish(
"Task completed. File observation:\n" + observations.get(0)
);
}
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: java MiniAgent <relative-file-path>");
System.exit(2);
}
Path workspace = Path.of(".");
Tool readFile = new ReadTextFileTool(workspace);
Map<String, Tool> tools = Map.of(readFile.name(), readFile);
Planner planner = new DemoPlanner(args[0]);
List<String> observations = new ArrayList<>();
String goal = "Inspect a file in the current workspace";
for (int step = 0; step < 3; step++) {
Decision decision = planner.next(goal, List.copyOf(observations));
if (decision.finalAnswer() != null) {
System.out.println(decision.finalAnswer());
return;
}
ToolCall call = decision.toolCall();
Tool tool = tools.get(call.name());
if (tool == null) {
throw new IllegalStateException("Unknown tool: " + call.name());
}
try {
String result = tool.execute(call.arguments());
observations.add("SUCCESS: " + result);
} catch (Exception e) {
observations.add("ERROR: " + e.getMessage());
}
}
throw new IllegalStateException("Agent exceeded maximum steps");
}
}
编译并让它读取当前目录中的 README.md:
javac MiniAgent.java
java MiniAgent README.md
这个例子刻意保留了几个生产 Agent 必须具备的约束:工具白名单、工作目录隔离、输入校验、输出截断和最大执行轮数。接入真实模型后,模型返回的工具名称与参数仍然必须经过这些检查,不能因为它们来自结构化输出就默认可信。
真实项目还应继续补充:每轮超时、总 token 预算、幂等键、取消机制、敏感信息脱敏,以及对写操作的人工审批。若允许执行 Shell 命令,则需要容器或沙箱隔离,不能只依赖字符串黑名单。
不必把所有部分都改写成 Java
选择 Java 不意味着模型实验、离线评测和数据分析也必须全部使用 Java。更实用的组合是:
- Python 用于提示词实验、数据处理和离线评测;
- TypeScript 用于 IDE 插件或需要快速迭代的交互层;
- Java 承载身份、权限、工具执行、审计和长时间运行的服务;
- 各组件通过 HTTP、消息队列或版本化 JSON Schema 协作。
这种拆分承认 TS/Python 的生态优势,同时把高风险执行边界放回团队最熟悉的生产体系。代价是需要维护跨语言协议,因此必须给工具定义、错误码和事件格式做版本管理。
采用前要回答的几个问题
决定是否用 Java 构建 Agent,不应该从语言偏好出发,而应检查项目的实际瓶颈:
- 当前阶段是在验证模型能力,还是建设长期运行的平台?
- Agent 是否要访问内部仓库、工单、数据库或部署系统?
- 团队是否已经有成熟的 JVM 交付与治理体系?
- 工具调用是否包含写入、执行命令或发布等高风险操作?
- 模型 SDK 的功能缺口能否通过标准 HTTP 和内部网关解决?
- 团队是否愿意承担 Java 生态示例较少、框架迭代较慢的成本?
如果任务仍处于探索期,TS/Python 往往更省时间。如果 Agent 已经进入企业系统,需要稳定运行并接受审计,Java 的类型、治理和部署一致性会逐渐体现价值。
真正需要坚持的不是某种语言,而是一条工程原则:模型可以是不确定的,但权限、工具契约、执行边界和审计记录必须由确定性的系统掌控。对于已经以 JVM 为核心的团队,Java 正好是承载这条边界的现实选择。