尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java做AI的真实路径:从Tool Calling到智能体系统重塑

Java做AI的真实路径:从Tool Calling到智能体系统重塑 作为一个长期写 Java 的人前几年每次听到“人工智能”这三个字第一反应就是完了又得去学 Python 了当年我带着这种成见浪费了不少时间直到真正把大模型接进公司项目才意识到这个判断错得有多离谱。Java 在 AI 领域不是没有位置而是位置和 Python 完全不同。如今再看这个领域Java 做人工智能的核心战场根本不在训练算法而在工具调用、智能体编排、以及把存量系统重塑成 AI 原生系统。这篇文章我就围绕“工具调用”和“系统重塑”这两个关键词把 Java 做 AI 的真实路径、嵌套 arguments 的踩坑根因、以及从单个工具到完整智能体的演进方向一次性讲透。1. “Java 不适合人工智能”这个判断错在了哪里1.1 AI 开发三层栈Java 不需要全占很多人一提 AI眼前就是 PyTorch、TensorFlow、模型训练、调参、GPU。这种印象不能说错但它只覆盖了 AI 技术栈的第一层算法与模型研发层。这一层确实 Python 占据绝对统治地位。研究论文、开源实现、训练框架的生态大多以 Python 为第一语言。如果你的目标是训练一个新模型、做学术研究、参加算法竞赛老老实实学 Python 没问题。但企业里真正缺的往往不是“再训练一个模型”的人而是“把模型接进业务流程”的人。这一层我习惯叫AI 应用集成层做的是接口调用、数据清洗、流程编排、权限校验、事务管理、结果落库、系统监控。这一层恰恰是 Java 最熟悉的地盘。再往上是智能体编排层也就是现在大火的大模型应用开发。让模型调用工具、读取数据库、操作第三方 API、按步骤完成任务。这里的代码量里大概七成是工程代码三成是模型交互代码。Java 的强类型、成熟框架、可观测性体系在工程代码这部分占尽便宜。所以结论很直接如果您的业务是“训练模型”Java 确实不占优但如果您的业务是“把 AI 用起来”Java 不但能做而且某些场景比 Python 更稳。1.2 Java 在应用层的现成底牌很多 Java 开发者的另一个误区是以为 Java 没有 AI 生态。这印象至少过时了五年。现在 Java 生态里能用的东西非常明确框架/库定位适合场景Spring AI大模型应用开发框架企业级 LLM 接入、Tool Calling、RAGLangChain4jJava 版 LangChainAgent、记忆、文档问答、工具调用Deep Java Library (DJL)深度学习推理/训练图像分类、目标检测、EmbeddingONNX Runtime Java API跨平台模型推理已有 ONNX 模型的部署Hugging Face Java 客户端模型与数据集访问快速拉取模型元数据、推理接口各大云厂商模型网关 SDK模型 API 接入国内国外主流模型的统一调用这套组合下来Java 基本能覆盖“模型推理 大模型调用 智能体编排”整条应用链路。真正缺的不是工具而是开发者对这条链路该怎么走的理解。接下来我从最关键的 Tool Calling 说起。2. Tool Calling 到底在解决什么把模型从“会说话”变成“会干活”2.1 一次工具调用的完整生命周期大模型再怎么强大本质还是一个“文本生成器”。它的知识来自训练数据训练截止日期之后的事情它不知道你公司的订单状态它也不知道用户所在地的实时天气它还是不知道。过去我们怎么解决这个问题要么把知识塞进提示词里要么把知识灌进向量数据库做检索增强也就是 RAG。但这两个思路都解决不了“让模型主动做事”的问题。Tool Calling也叫 Function Calling不同平台叫法略有差异解决的是让模型在生成回复之前先决定“要不要调用某个工具”并输出一个结构化调用请求由应用代码真正执行再把结果还给模型让模型基于真实结果组织最终回答。拿一个客服场景举例。用户说“帮我查一下订单 12345 到哪了”。模型本身不连接任何数据库但您可以给它注册一个工具getOrderStatus(orderId)。对话过程如下用户提问进入模型请求里带上了工具清单清单里包含工具名、描述、参数格式定义。模型判断“这个问题需要查订单”在响应里返回一个特殊的tool_calls字段内容是{name: getOrderStatus, arguments: {\orderId\:\12345\}}。Java 应用收到这个响应自己调用getOrderStatus(12345)得到“已发货预计明天到达”。应用把“工具执行结果”作为一条新的消息交给模型。模型看到结果组织出对用户友好的回答“您好您的订单已发货预计明天送达。”这里有个新手最容易绕晕的点模型从头到尾只负责“决定”和“表达”不负责“执行”。真正动手调用接口、查数据库、发指令的永远是您自己的 Java 代码。这种“模型决策、应用执行、结果回填”的循环其实就是智能体的最小原型。2.2 Java 生态里的 Tool Calling 框架选择在 Java 里做 Tool Calling主流路线有三条Spring AI如果您已经在用 Spring Boot这个是最顺的。它把工具调用抽象成Tool注解的方法框架自动完成 JSON Schema 生成、arguments 解析、结果回传侵入性低适合想快速上手的团队。LangChain4j对 Agent、记忆、多工具编排的支持更细适合做复杂任务的智能体。它的Tool注解设计也很干净底层还支持 Spring Boot 集成。纯 HTTP 手写直接调用大模型的/chat/completions接口自己维护协议解析、循环调用。好处是彻底理解原理不被框架黑盒限制坏处是要自己处理的东西很多比如嵌套 arguments、超时重试、上下文拼接。我的建议是第一次做项目用 Spring AI 或者 LangChain4j 快速跑通跑通之后一定抽时间手写一遍 HTTP 协议。因为框架把太多细节隐藏了而 Tool Calling 的坑几乎都藏在细节里。下面我两部分都讲。3. 手写一个 Java Function Calling 示例从 Spring AI 到原生 HTTP3.1 用 Tool 注解把天气方法暴露给模型先看框架层面的写法。以 Spring AI 为例实现一个“查询天气”工具代码量少得让人怀疑是不是搞错了Component public class WeatherTools { Tool(description 查询指定城市的实时天气) public String getWeather(String city) { return weatherService.fetchCurrent(city); } }然后在业务代码里装配模型客户端RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder, WeatherTools weatherTools) { this.chatClient builder .defaultTools(weatherTools) .build(); } PostMapping(/chat) public String chat(RequestBody String userMessage) { return chatClient.prompt(userMessage) .call() .content(); } }启动项目后您问“北京现在适合出门跑步吗”最终回答会自动触发getWeather(北京)再把天气数据带进回答。整个过程里Tool注解会被 Spring AI 解析成模型能理解的 JSON Schema模型返回的arguments也会被自动绑定到方法参数上。这就是框架的价值把最枯燥的“协议翻译”藏起来让业务代码保持干净。但藏起来不等于不存在您迟早会在某个晚上被arguments的格式问题拷打。所以接下来必须看协议层。3.2 模型输出 toolCalls 之后Java 端发生了什么框架帮我们做的事情摊开其实是四步构造一段工具描述的 JSON放进请求的tools字段{ type: function, function: { name: getWeather, description: 查询指定城市的实时天气, parameters: { type: object, properties: { city: { type: string, description: 城市名 } }, required: [city] } } }模型在需要时返回tool_calls。注意arguments是一个字符串里面包着一层 JSON{ choices: [ { message: { role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: getWeather, arguments: {\city\:\北京\} } } ] } } ] }Java 端解析tool_calls执行真实方法然后把结果拼成一条tool角色消息{ role: tool, tool_call_id: call_abc123, content: {\temperature\:25,\condition\:\晴\} }带上之前的全部对话记录重新调一次模型。模型看到工具结果生成最终回答。如果您用 Spring AI这四步全在框架内部完成。如果是手动写这四步的每一步都值得写单元测试。3.3 不依赖框架手写一次 HTTP 调用看得更清楚假设您已经把一个模型的 Chat Completions 接口封装成了callWithTools(chatModel, messages, tools)。一个裸写的循环大概是public String runWithTools(String userInput, ListToolSpec tools) { ListMessage messages new ArrayList(); messages.add(new UserMessage(userInput)); while (true) { ChatResponse resp chatModel.chat(messages, tools); AssistantMessage assistant resp.assistantMessage(); if (assistant.toolCalls() null || assistant.toolCalls().isEmpty()) { return assistant.content(); } messages.add(assistant.toMessage()); for (ToolCall call : assistant.toolCalls()) { Object result toolExecutor.execute(call.name(), call.arguments()); messages.add(new ToolMessage(call.id(), toJson(result))); } } }这个while(true)就是 Agent 循环的雏形。模型调一个工具结果回来如果它觉得还缺信息可以再调另一个工具直到它认为可以给出最终答案为止。理论上这个循环会一直进行下去所以真实项目里必须加最大迭代次数限制比如 5 次、8 次防止模型陷入死循环。把这段代码跑通您对 Tool Calling 的理解就超过绝大多数只会调框架的开发者了。但别高兴太早接下来这个坑几乎每个做 Java Tool Calling 的人都会撞上那就是嵌套 arguments 问题。4. 嵌套 arguments 会反复踩坑问题根因与防御性解析4.1 复现一次嵌套参数翻车现场有段时间我在团队里做一个数据分析 Agent注册了一个查询销售报表的工具方法签名大概是这样Tool(description 按条件查询销售报表) public String querySalesReport(ReportQueryParam param) { // ... } public class ReportQueryParam { private Filters filters; private Integer page; private Integer pageSize; } public class Filters { private ListString category; private String region; }模型按 Schema 输出了的arguments是这个样子的{ filters: { category: [手机, 电脑], region: 华东 }, page: 1, pageSize: 20 }按理说挺标准。但在早期实现里组里的同事为了图省事直接对arguments字符串做了正则提取和String.indexOf式截取一旦遇到这种嵌套结构代码立刻炸。更邪门的是同样的 prompt 换个模型arguments里可能会把filters整个丢掉直接把category提到最外层。今天修好了明天换个模型版本又坏了。这就是“嵌套 arguments 的问题反复”的真实场景。4.2 为什么“模型按 schema 输出”这句话不解决一切有人会说您给模型的 JSON Schema 都定义好了它按规矩输出不就完了问题是模型的计算结果本质是概率性的不是 KV 存储。您定义了一个嵌套对象它大概率会按嵌套输出但不是每次都能稳定做到。加上不同厂商的模型对 JSON Schema 的遵循能力差异巨大有的会漏字段有的会用null占位有的会在arguments外层包一层 markdown 代码块标记。我见过最离谱的一次模型把arguments输出成了arguments: json\n{\page\:1,\filters\:{\category\:[\手机\]}}\n直接拿JSON.parse解析这段字符串大概率会抛异常因为前后多了反引号。所以成熟的 Java 端解析策略绝不能假设模型输出是“完美的 JSON”。必须把arguments当作一段可疑输入来处理。4.3 一套稳的 arguments 解析流程经过几个项目打磨我沉淀了一套比较稳的处理流程核心原则只有一条永远用结构化解构器Jackson/Gson把 arguments 整段反序列化成 Java 类型严禁手工拼接、截取、正则提取。具体步骤如下第一步拿到arguments字符串后先去除首尾空白再判断是否有代码块包裹有则剥离反引号String raw call.arguments().trim(); if (raw.startsWith()) { raw raw.replaceAll(^[a-zA-Z]*\\n*, ).replaceAll($, ); }第二步用 Jackson 反序列化到工具的参数对象开启忽略未知字段ObjectMapper mapper new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); ToolRequestParam param mapper.readValue(raw, ToolRequestParam.class);第三步在工具入口做一层防御式校验。不要假定param.filters一定不为 null不要假定category一定是一个数组。模型可能只给了page其他全没给。if (param.getFilters() null || param.getFilters().getCategory() null) { // 按默认条件查询或者返回错误提示让模型重新询问用户 }第四步把解析成功的样本沉淀成单元测试用例越怪越好。每次切换模型、升级模型版本的时候把这些用例全跑一遍。这是防止“问题反复”最有效的手段比临时改代码强得多。我在项目里就是这么做的现在维护的解析 JSON 目录下已经积累了几十个用例包括漏字段、嵌套缺失、null 值、代码块包裹等场景。谁想偷懒跳过这步后面就会反复被同样的坑咬。5. 从工具调用走向系统重塑Java 智能体系统的三层演进5.1 工具编排层Harness 才是集成重点当您只有一个工具时随便写个 if 分支就行。但当工具增加到十几个、几十个问题就来了每个工具的权限怎么控制鉴权信息从哪来哪些工具结果要缓存多个工具之间如果有依赖执行顺序怎么保证这个负责把模型“翻译”成系统操作、并统一管理路由和执行的中枢业界管它叫 Harness。用 Java 实现时我通常抽象成一个ToolExecutionServiceService public class ToolExecutionService { Autowired private ListToolHandler toolHandlers; public ToolResult execute(ToolCall call, UserContext ctx) { // 1. 鉴权判断当前用户是否允许调用该工具 // 2. 路由根据 call.name() 找到对应 Handler // 3. 限流对高频工具做并发控制 // 4. 执行绑定参数并调用真实业务方法 // 5. 审计记录入参、出参、耗时、执行人 // 6. 超时超过阈值直接中断不让模型无限等待 // 7. 返回统一包装成模型便于理解的文本或 JSON } }这一层做到位您才真正拥有一个“可以上生产”的工具系统。很多人以为做 Agent 难在模型提示词实际上一大半工作量在这一层把零零散散的工具方法收敛成一套安全、可观测、可治理的执行基础设施。5.2 状态记忆让调用链不再失忆单次 Tool Calling 是无状态的但真实的业务系统有状态。用户上一轮可能说“按华东区筛选”这一轮说“再按手机类目过滤一下”模型必须知道上一轮发生了什么。Java 在这一层有天然优势。您完全可以借助已有的会话存储、Redis、数据库把每轮对话、每次工具调用、每次工具结果都持久化下来。我的做法是给每次会话分配一个conversationId把所有消息连同工具调用记录一起存起来。这样有两个好处第一模型下次请求时可以把完整上下文带上对话记忆自然就有了第二出了问题可以回溯审计查清楚某次乱操作是模型误判还是用户误导。public class ConversationMemory { private String conversationId; private ListMessage messages; private ListToolCallRecord toolCallRecords; }当您在 Java 里把记忆模块做成一个标准的 CRUD 组件Agent 的上下文管理就不再神秘它就是一套您写了很多年的业务代码。5.3 业务架构重塑把决策权移交但保留控制权“系统重塑”这个词听起来很大但落地到我参与过的项目里无非是一件事把原来散落在 if-else、状态机、规则引擎里的人工决策替换成“模型理解意图 工具执行动作”的新流程。举个例子。以前做一个订单售后系统用户申请退款要走一遍复杂的状态机校验订单状态、计算可退金额、调用支付网关、更新库存、发送通知。每一步都是硬编码规则。重塑之后思路变成了这样传统实现AI 重塑后的实现用规则引擎判断用户输入关键词来分流用 LLM 理解用户自然语言识别退款意图和订单号状态机里写死每一步的前置条件状态机依然保留但由 Agent 动态编排调用顺序用户问“为什么不能退”时只能命中预设话术Agent 查询真实订单状态生成针对性解释新增售后类型要改代码发版新增对应工具方法即可决策逻辑不写死流程这里有一条铁律一定要记住重塑不是把核心业务代码删掉换成模型调用。恰恰相反核心业务能力——支付、库存、订单——依然以 Java 方法的形式存在。LLM 只负责“决定做哪件事、按什么顺序做”真正“做”的始终是您的 Java 业务逻辑。这就好比大脑负责决策肌肉负责执行。Java 方法成了肌肉LLM 成了大脑。这个架构下即使模型抽风底层业务操作也永远不会失控该校验的校验该幂等的幂等。6. 工程化路上的必修课类型、超时、幂等与测试6.1 声明 Java 强类型但没做好反序列化照样白搭Java 的强类型在工具调用上是一把双刃剑。好处是框架能把arguments字符串反射绑定到方法的强类型参数上编译期就能发现问题。坏处是模型返回的arguments是字符串绑定过程发生在运行时一旦模型当前输出和你定义的类型不搭异常就在线上炸开。我特别提醒几个高频问题数字类型模型可能输出01、1.0或者干脆输出一。这时候绑定到Integer、Double参数会直接失败。稳妥做法是参数都用String接收在方法内部再转换。数组字段缺失模型可能漏掉category字段导致绑定后List为 null后续遍历直接 NPE。解析完成后必须做空值兜底。未知字段模型偶尔会脑补一些 schema 里没有的字段。反序列化时一定要配置FAIL_ON_UNKNOWN_PROPERTIESfalse否则整个解析直接崩掉。嵌套对象内部再嵌套比如参数对象里套了一个MapString, List...的复杂结构极容易解析成LinkedHashMap如果代码里强转成自定义类型又是运行时异常。这一条的经验就是不要把“框架能解析”当“框架一定能正确解析”所有工具入参解析都值得写一层防御代码和单元测试。6.2 把工具调用当外部接口看待超时和幂等一个不能少工具调用虽然发生在您自己的 JVM 里但它背后可能调用了第三方接口、数据库、消息队列。在实际生产里工具执行时间不可控。模型一次 call 过来您得在工具执行上设置独立的超时时间不能让它无限占线程。用 Java 做超时控制方式有很多。简单点用Future或者CompletableFuture配合get(timeout)复杂点用Resilience4j的TimeLimiter。我的习惯是给所有外部 IO 型工具设置统一的超时比如 5 秒超时后返回一段 对模型友好的提示“工具超时请告知用户稍后重试”。还有一个常被忽略的坑幂等。用户提问后如果网络抖动导致客户端重试模型可能连续触发两次同样的工具调用。对于“查询”类工具这没问题但对于“退款”“下单”这类写操作重复执行就是生产事故。所以写操作的工具必须做幂等要么在工具层校验一个业务请求号要么在业务系统里支持重复请求去重。这是 Java 企业级开发的老本行千万别在 AI 项目里丢了。6.3 Mock 策略让 Agent 开发不烧 Token 也能持续集成连真实模型调试是大忌。一方面费用不可控另一方面模型输出有随机性同一个 prompt 两次结果可能不同导致排查问题时根本没法复现。最佳实践是开发期把模型接口 Mock 掉固定返回一段 toolCalls 的 JSON测试期按场景准备多套 Mock 数据只有集成验证阶段才连真实模型。如果用的是 Spring AI可以自己实现一个ChatModel接口的测试替身返回预设响应。如果走的是 HTTP 调用也可以用 WireMock 模拟/chat/completions接口。这样一来您的工具解析、嵌套参数处理、超时降级、幂等去重全都可以在 CI 环境里稳定跑回归。真实模型的随机性只留给少数“冒烟测试”场景。这也是很多 Java 团队做 AI 项目能保持高质量的原因他们并没有发明新东西只是把传统软件工程的好习惯带了过来。7. Java 开发者的 AI 进化清单怎么从会用工具走到重塑系统最后很多读者问过我作为 Java 开发者到底按什么路径学 AI 最务实我自己总结了一个五步清单可能比刷一堆“人工智能入门”视频更有用。第一步选一个 LLM 的 Java SDK 玩熟。推荐 Spring AI 或 LangChain4j先用它跑通一个“问什么答什么”的项目把模型调用的基础概念建立起来。第二步吃透 Tool Calling 协议。手动构造一次请求和响应完整跑通“模型返回 toolCalls → 应用执行 → 结果回填 → 模型最终回答”的循环。这个步骤一定要自己手写一遍不要只依赖框架。第三步做一个任务型 Agent 项目。比如一个客服助手注册订单查询、库存查询、退换货申请三个工具实现多轮对话中的动态工具选择和状态记忆。第四步引入向量检索和长期记忆。用 DJL 或云服务做 Embedding把知识库文档向量化配合 Redis 存会话记忆做一个能回答私有知识问题的系统。第五步工程化打磨。把超时、重试、幂等、降级、审计日志、可观测性全加上。做到这一步您手上就是一个能在生产环境稳定运行的 Java AI 应用了。走完这五步再回头看“系统重塑”这件事它就不再是一个抽象口号而是一张清晰的地图您的业务系统抽象成一层又一层稳定的工具能力LLM 成为调度大脑Java 负责把所有决定安全、可靠、可控地落地。我个人在实际项目里最大的体会是Java 做人工智能最大的优势不是某个具体框架而是您过去积累的工程素养——类型安全、事务控制、幂等设计、可观测性这些东西在 AI 应用时代不但没过时反而因为模型的不确定性变得更加重要。模型随时会变工具随时会加但一套稳健的 Java 执行底座能让整个系统在变化中始终稳得住。如果您现在还在犹豫要不要用 Java 做 AI我的建议是别犹豫从一个小工具调用开始把它跑通再慢慢往外扩。这条路我走过确实走得通。
返回列表