Agent 默认属于 TS/Python,但企业落地未必:我们为什么仍然选择 Java

2026-07-30 26 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:14 分钟

今天讨论 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 等同于某个框架。一个最小的生产结构通常只需要几个清晰边界:

  1. ModelClient:把内部请求转换成具体模型协议;
  2. Planner:根据目标和历史观察决定下一步;
  3. ToolRegistry:维护可调用工具及其参数契约;
  4. Policy:检查身份、权限、目录范围和副作用等级;
  5. AgentLoop:控制最大轮数、超时、重试与终止条件;
  6. 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 正好是承载这条边界的现实选择。


相关推荐