LangChain4j自定义工具开发实战:从Function Calling到Java AI Agent构建

发布时间:2026/10/22 3:56:01
LangChain4j自定义工具开发实战:从Function Calling到Java AI Agent构建 很多人第一次接触LangChain4j时都会觉得它只是个封装好的ChatLLM调用库写两句Prompt就能聊天。但真正让它从对话玩具变成可用系统的恰恰是自定义工具这套机制。今天我就围绕自定义工具开发这件事把我踩过的坑和总结出来的思路完整写一遍希望能给正在用LangChain4j做Java AI应用的朋友一些实在的参考。这套东西适合谁如果你正在用Java生态做Agent应用或者已经跑通了基础的LLM对话但发现模型只会纸上谈兵、没法查数据库、没法调接口、没法做计算那这篇文章就是给你准备的。我会从机制原理讲到完整代码再到排查实录尽量让你看完就能直接动手。1. 为什么说自定义工具是LangChain4j的灵魂1.1 从一次失败的对话说起模型能力的边界先讲一个我实际遇到的场景。某次我让模型帮用户查询订单状态模型回答得头头是道您的订单正在发货中预计三天后到达。实际上它根本没查过任何订单系统这段回复完全是它根据上下文编出来的。这就是纯LLM应用最大的问题模型的知识有截止日期它不掌握你系统的实时数据也不具备调用外部服务的能力。很多初学者会把希望寄托在提示词上——你是一个订单查询助手请查询数据库并返回结果。但提示词再长模型也没有执行SQL的通道。它连你的数据库地址都不知道更别说连接、查询、返回结果了。所以LangChain4j这类框架才引入了工具机制本质上是给模型开了一扇门让它通过你预先定义好的方法去访问外部世界。1.2 工具的定位把LLM从聊天机器变成行动派在LangChain4j中工具的本质非常简单一个普通的Java方法上面加了Tool注解。但就是这么一层注解把模型的角色从信息复读机变成了行动派。模型在推理时会判断当前任务需不需要调用工具如果需要它会生成一个结构化的调用请求框架负责把请求路由到对应的方法方法执行完再把结果交还给模型继续推理。整个链路可以用八个字概括模型决策、框架执行。模型只负责决定该调用哪个工具、参数是什么真正干活的是你的Java代码。这意味着你能用Java做多少事Agent就能做多少事。查数据库、调第三方接口、算复杂公式、操作文件系统、甚至触发一条消息队列全都变得可行。1.3 与函数调用Function Calling的关系这里必须说清楚LangChain4j的自定义工具底层依赖的是大模型平台的函数调用能力。大多数主流模型在训练时就支持一种特殊格式把一组函数的名称、描述、参数结构发给模型模型在回答时会选择是否调用某个函数并返回结构化的参数。LangChain4j帮你屏蔽了这些细节。你不需要手动拼JSON Schema也不需要解析模型返回的原始调用指令。你只写Java方法框架会在背后完成方法签名到函数描述的转换、模型调用请求的生成、返回结果的解析。这层封装大大降低了开发门槛但也带来一个副作用很多人只关注代码本身忽略了函数调用的底层规则结果参数老是绑定不上、模型老是选错工具。所以我在后面的章节里会花大量篇幅讲底层逻辑。2. 自定义工具的核心机制与设计思路2.1 Tool注解Java方法如何变成AI技能Tool注解是整套机制的入口。它的用法很直白在方法上标注后这个方法的类如果被注册到AiServices上模型就能感知到它的存在。我贴一段最基础的用法Tool(计算两个数字的和) public double add(double a, double b) { return a b; }就这么简单。但你别小看这个注解它有几个关键属性值得研究。首先是name属性默认是方法名如果方法名足够语义化比如queryUserOrder可以不显式指定但如果方法名是handleData这种泛化名称一定要通过name属性改成更清晰的名字。其次是description属性这里的描述文本不是给你看的是给模型看的。模型没有任何常识来判断你的方法想干什么全靠这段描述来理解工具的用途、触发条件和使用限制。还有一个容易被忽略的细节Tool注解可以标注在类级别。当标注在类上时类里的所有公共方法都会被视为工具方法。这个特性适合工具较多的场景可以省去逐个标注的麻烦。但我个人的实践经验是能显式标注就显式标注因为类级别的全量暴露容易让模型看到一些不该暴露的内部方法。2.2 工具描述的艺术模型靠什么选择工具我见过太多人在工具描述上敷衍了事写个查询方法就完事了。这会导致什么后果模型在多个工具之间犹豫或者干脆调错工具。你要明白工具描述就是模型的使用说明书描述的质量直接影响工具选择的准确率。我给你一个实用的描述模板谁 在什么场景下 能做什么 需要注意什么。举个例子同样是查询用户信息的方法两种写法对比一下// 不推荐的写法 Tool(查询用户) public String getUser(String id) { ... } // 推荐的写法 Tool(根据用户ID查询用户详细信息包含姓名、手机号、等级、注册时间 仅在用户明确提供ID时调用不要自己猜测ID) public String getUser(String id) { ... }第二种写法把触发条件、参数来源、注意事项都写清楚了。模型看到这段描述时会把它跟当前对话的意图做匹配。如果你不给足信息模型就只能靠猜猜错就是必然的结果。另外一个经验是描述中要明确不要做什么。负向约束对模型很有效。比如你开发了一个会修改数据库的工具一定要在描述里写清楚仅当用户明确要求更新或删除时才能调用查询场景严禁调用。这类约束能有效降低模型的误判率。2.3 参数绑定的底层逻辑JSON Schema如何工作参数绑定是自定义工具最容易出问题的一环。模型本身不认识Java类型它只知道JSON。LangChain4j在处理工具注册时会把你方法的参数列表转换成JSON Schema。比如double add(double a, double b)会变成类似于{type:object,properties:{a:{type:number},b:{type:number}}}的结构。模型根据这个Schema生成参数JSON框架再把JSON反序列化回Java类型。理解这个机制之后就会明白参数的简单类型String、int、double、boolean绑定非常稳定但复杂对象就要格外小心。如果你定义了一个嵌套较多的POJO作为参数模型的JSON生成能力可能不足以精确还原所有字段尤其是嵌套层级超过两层时出错率会明显上升。这不是LangChain4j的Bug而是模型能力的客观限制。我实际开发中的经验是尽可能把参数保持在一层结构字段数量控制在五六个以内。如果工具需要接收的信息较多优先使用一个嵌套简单的POJO并且给每个字段加上明确的FieldDescription注解说明含义。这类注解会被LangChain4j合并进JSON Schema的描述信息能显著提高模型生成参数的准确率。public class OrderQueryParam { FieldDescription(订单ID必填) private String orderId; FieldDescription(查询类型base表示基础信息detail表示详细信息) private String queryType; }反过来返回值的设计同样重要。模型拿到的工具结果也是JSON格式的文本它会基于这段文本来组织最终回答。如果你返回的是一个格式混乱的复杂对象模型很可能理解不了最终给用户一段不知所云的回复。3. 实操从零开发一个可用工具链3.1 环境准备与依赖引入基础环境就不啰嗦了JDK17及以上、Maven即可。项目里需要引入LangChain4j的核心依赖以及对应的模型依赖。我这边以OpenAI兼容接口为例依赖如下dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.36.2/version /dependency版本号我用的是我实测稳定的版本建议你以当时的最新稳定版为准。这里有个容易踩的坑LangChain4j的不同版本之间API变动不小尤其是工具注册这块早期版本和0.30之后的版本差异很大。如果你网上搜到的是老教程很可能代码已经跑不通了这时候优先去官方文档查对应版本的迁移说明。3.2 示例一一个带状态的计算器工具从一个最简单的例子开始。计算器工具虽然业务价值不大但非常适合理解工具的生命周期。Tool(计算两个数的四则运算运算符支持、-、*、/) public double calculate(double a, double b, FieldDescription(运算符、-、*、/) String operator) { switch (operator) { case : return a b; case -: return a - b; case *: return a * b; case /: if (b 0) { throw new IllegalArgumentException(除数不能为0); } return a / b; default: throw new IllegalArgumentException(不支持的运算符: operator); } }注意这里我故意抛了异常。工具方法抛异常时LangChain4j会把异常信息返回给模型模型可以感知到错误并尝试换一种方式重新调用或者向用户说明。如果你在方法内部把异常吞掉并且返回一个无意义的字符串模型反而会困惑。关于异常处理后面我单独说。我建议你在跑通这个例子后观察一下模型的行为模式当你提问3乘以5等于多少模型会先输出一个函数调用请求框架执行后再基于结果生成最终回答。这个先思考、再调用、再回答的三段式过程是所有工具调用的通用模式。3.3 示例二对接外部API的天气查询工具工具最典型的应用场景是接外部API。我写一个对接天气接口的工具示例帮你理解如何把HTTP调用包装成模型可感知的功能。Tool(根据城市名称查询实时天气返回温度、湿度、风力、天气状况) public String getWeather(FieldDescription(城市中文名比如北京、上海) String city) { // 这里以某个公共天气API为例实际项目换成你自己的接口 String url https://api.example.com/weather?city URLEncoder.encode(city, StandardCharsets.UTF_8); HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); HttpRequest request HttpRequest.newBuilder(URI.create(url)) .timeout(Duration.ofSeconds(10)) .GET() .build(); try { HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { return 天气查询失败HTTP状态码: response.statusCode(); } // 这里假设返回的是JSON实际需要按你对接的API结构解析 return parseWeatherJson(response.body()); } catch (Exception e) { return 天气查询异常: e.getMessage(); } }这个例子有几个值得注意的点。其一工具方法里不要只返回一个状态码或布尔值模型需要的是能被它理解的信息。所以我在错误分支里返回了带上下文信息的字符串。其二超时必须设置。模型发起工具调用时是会等待结果的如果外部接口迟迟不返回整个对话流程就被卡死了。我在HTTP客户端上设置了连接超时和请求超时这样即使外部接口挂了工具也能在短时间内返回一个错误信息模型可以据此向用户解释。还有一点容易被忽略工具方法返回的字符串会被塞回给模型它是有长度消耗的。如果外部API返回了一个大JSON比如几千上万字的原始报文你最好在工具内部做好裁剪只把模型需要的核心字段拼成简短描述返回。这不仅是成本问题也关系到模型理解的准确性——一堆无关字段会干扰模型的注意力。3.4 示例三让Agent具备数据库查询能力数据库接入是自定义工具的高频需求也是最能体现工具价值的场景。我的建议是不要让模型直接生成SQL而是由你的代码接收模型解析好的查询参数再由代码内部拼装SQL或者调用预定义好的查询方法。这样做既能保证SQL的可靠性也能有效防止提示词注入导致的非法查询。Tool(根据用户ID查询最近订单列表返回订单号、商品名称、金额、下单时间 仅当用户明确请求查询订单时调用) public ListOrderSummary queryUserOrders(FieldDescription(用户ID必填) Long userId) { String sql SELECT order_no, product_name, amount, create_time FROM t_order WHERE user_id ? ORDER BY create_time DESC LIMIT 10; return jdbcTemplate.query(sql, new Object[]{userId}, (rs, rowNum) - { OrderSummary summary new OrderSummary(); summary.setOrderNo(rs.getString(order_no)); summary.setProductName(rs.getString(product_name)); summary.setAmount(rs.getBigDecimal(amount)); summary.setCreateTime(rs.getTimestamp(create_time)); return summary; }); }返回值我直接返回了ListOrderSummaryLangChain4j会自动把它序列化成JSON数组。这里有个性能上的经验工具内部尽量做分页或限量别一次性返回几百上千条记录。模型处理大量JSON结果时响应速度和准确率都会明显下降。我的经验是单次返回控制在20条以内如果有更多数据通过参数支持分页查询。另一个不得不提的话题是权限控制。把数据库暴露给模型是有风险的模型虽然有对齐机制但用户完全可以通过对话间接试探出你的查询能力。比如构造一个把张三的余额改成99999的请求如果你的工具只提供了查询能力还好但如果你做了更新工具且没有权限校验那就出大问题了。我的做法是在工具方法入口处统一做权限校验校验当前对话用户是否有权执行这个操作而不是只依赖模型自己去判断。3.5 接通AiServices工具注册与运行工具写好了怎么注册LangChain4j使用AiServices来实现。这是整个框架中最核心的接口。我贴一段完整代码Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherTool(), new OrderTool(), new CalculatorTool()) .build(); String answer assistant.chat(北京今天天气怎么样); interface Assistant { String chat(String message); }这里最需要注意的一点是Assistant接口的设计。你可以只定义一个简单的chat方法但如果你想接收更丰富的上下文可以在接口方法上添加MemoryId参数或者让方法返回TokenStream以支持流式输出。我实际项目中通常这样设计interface Assistant { String chat(MemoryId String userId, UserMessage String message); }加了MemoryId之后框架会按用户维度维护独立的对话记忆不同用户之间的上下文不会串。这个设计在工具调用场景下特别重要——没有记忆时模型每次调用工具都像失忆了一样前一轮查到的订单号下一轮对话就忘了。还要提一个很多人不知道的细节工具类建议使用无状态设计。如果工具类内部维护了可变状态比如把查询结果缓存到实例变量里在多会话并发时会出现数据串味的问题。每个会话请求会共享同一个工具实例你很难预料框架是否每次都创建新实例。我的实践是工具类里只放无状态方法需要传递的上下文信息全部通过参数传入需要外部服务就通过构造器注入。4. 进阶技巧让工具更可靠、更高效4.1 返回值结构设计模型友好的结果长什么样工具执行结果直接影响最终回复质量。我调试过很多次之后形成了一套自己的返回值设计原则说给你参考。第一长文本优先精简。返回给模型的信息是要点提取而非原文转述。比如你查了一篇文章的全文不要直接把全文丢给模型而是先提取摘要、核心观点用简洁的要点形式拼接成字符串。第二结构化信息用JSON保留结构。像订单列表、用户列表这类有清晰字段关系的数据直接返回POJO或Map让框架序列化成JSON。不要手动拼字符串手动拼的格式往往不完整模型解析起来也费劲。第三数值和状态信息要带单位、带上下文。温度23和温度23摄氏度对模型的理解天差地别。类似的余额100会让人摸不着头脑余额100元人民币就清晰得多。模型缺乏现实世界的常识校验它默认你给的信息是完整准确的所以信息残缺会导致误导。4.2 错误处理与容错设计我在最初开发时犯过一个错误把工具方法里可能出现的异常全部catch住然后返回一个操作失败。这个做法让模型非常难办它只知道自己失败了不知道怎么处理、怎么向用户解释。后来我调整了策略分三种情况处理异常。一种是可预期业务异常。比如上面的计算器工具里除数为0或者查询用户不存在这种情况下你抛出一个带有明确信息的异常或者直接返回未查询到xxx信息这类结果字符串。推荐后者因为模型可以直接基于这个结果组织回答不需要额外的错误理解过程。一种是外部服务异常。比如HTTP调用超时、第三方接口返回500这种应该返回包含错误类型和可能原因的信息让模型能够向用户解释当前查询服务暂时不可用。还一种是程序内部Bug级别的异常。这种异常不该隐藏让它往外抛你可以在外层通过框架的异常处理机制统一记录日志、追踪排查。工具方法里隐藏Bug会让问题变得极难定位——模型拿到一段莫名其妙的错误文本你在日志里却什么都看不到。4.3 异步与并发注意事项工具方法默认是阻塞执行的。如果你接到一个长时间运行的外部任务整个对话链路会阻塞在工具调用上。我的项目中有一个工具会触发生成一份PDF报告整个过程需要十来秒。直接同步调用会导致用户体验非常糟糕。LangChain4j对异步工具调用有支持但实现方式不是简单地把工具方法写成CompletableFuture就能解决的。更实用的一种方案是把耗时任务拆分。工具方法本身只负责提交任务并立即返回一个任务ID具体的耗时过程由后台线程池处理用户通过另一个查询工具来轮询任务状态。Tool(提交数据报表生成任务返回任务ID) public String submitReportTask(FieldDescription(报表类型) String reportType) { String taskId UUID.randomUUID().toString(); reportTaskExecutor.submit(() - generateReport(taskId, reportType)); return 任务已提交任务ID: taskId; } Tool(根据任务ID查询报表生成状态未完成返回进度完成返回下载地址) public String queryReportTask(FieldDescription(任务ID) String taskId) { return reportTaskExecutor.getStatus(taskId); }这个设计把长任务拆成了提交轮询两个工具既解决了阻塞问题又符合Agent分步执行的天然模式。模型会在调用提交工具后继续调用查询工具来获取结果整个过程表现得非常自然。并发方面还有一个隐藏问题工具方法需要保证线程安全。不要在方法里使用SimpleDateFormat这类非线程安全的工具类也不要用静态变量存储会话级数据。4.4 工具的动态注册与权限控制有些场景下不同用户应该能看到不同的工具集。比如管理员账号有数据删除工具普通用户没有。LangChain4j支持在构建AiServices时指定工具列表但这是静态的。如果需要动态控制我的做法是封装一层路由工具对外只暴露一个统一入口工具内部根据用户身份决定实际执行逻辑。Tool(执行指定的管理操作) public String executeAdminOperation( FieldDescription(操作类型deleteUser、updateRole、resetPassword) String operation, FieldDescription(操作参数JSON) String payload, FieldDescription(当前用户身份标识) String currentUser) { if (!adminChecker.isAdmin(currentUser)) { return 权限不足当前用户不是管理员; } return switch (operation) { case deleteUser - userService.deleteUser(payload); case updateRole - userService.updateUserRole(payload); default - 未知操作: operation; }; }这个做法的优点是权限控制集中在一个入口逻辑清晰缺点是需要自己维护一套操作分发处理。如果你的项目同时兼顾灵活性和安全性建议在工具层做操作白名单校验绝不能让用户传入的任意操作字符串直接映射到内部方法。5. 常见问题与排查技巧实录5.1 模型总是选错工具怎么办这是出现频率最高的问题。模型面对多个工具时选了错误的那个原因九成出在工具描述上。我之前遇到过一件事系统里有查询天气和查询空气质量两个工具描述都写得很模糊——根据城市查询天气信息和根据城市查询空气质量信息。结果用户问北京适合跑步吗模型反而调了天气工具而不是空气质量工具。后来我把空气质量工具的description改成了根据城市查询空气质量指数AQI、PM2.5浓度、首要污染物当用户询问是否适合户外运动、空气是否清新时调用。再测试时选择准确率明显提升。这个案例想说明的是描述不仅要说明工具是什么还要说明什么时候该用我。如果改完描述仍然选错还有一个偏门排查方向检查工具方法的参数名和描述。模型在选择工具时也会参考参数名如果你的方法参数叫str、obj这种无意义的名字模型的判断也会更困难。5.2 参数绑定失败或JSON解析报错模型返回的参数JSON无法绑定到Java方法通常会表现为框架抛反序列化异常或者方法执行时收到null值。我总结出三个高频原因。第一个是参数类型过于复杂。前面提到了嵌套层级深、字段多的对象类型模型很难一次生成准确。解决方法是拆解参数——把复杂的POJO拆成多个简单参数或者增加更多FieldDescription说明字段含义。第二个是必填参数缺失。模型有时会忽略某些参数尤其是非基础类型的参数。我的做法是在工具方法入口做显式校验缺失时直接返回参数不完整缺少xxx而不是让框架在绑定阶段就报错。返回给模型一个可理解的错误比抛一个生硬的序列化异常好得多。第三个是类型泛化问题。比如你定义参数类型是ListString模型可能生成一个字符串而不是数组定义Map类型模型可能生成一个数组。这种现象在弱模型上更常见。解决方案是尽量避免Map、Object这类宽泛类型换成具体的POJO。5.3 工具执行结果乱码或信息丢失工具返回的中文内容在最终回答中变成了乱码或者部分字段消失这个问题我在早期也踩过。排查方向一字符编码。HTTP调用外部接口时如果服务端返回的是GBK编码而工具代码按UTF-8解析中文必然乱码。处理方式是调用时显式指定字符集HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))。排查方向二JSON序列化。返回的POJO里如果存在null值字段序列化后会体现为字段缺失。模型不知道这些字段是没有值还是被丢弃了有时会编造一个看起来合理的默认值来补全信息。如果你希望模型明确知道某个字段不存在可以在返回值里直接给一个空字符串或无而不是null。排查方向三结果截断。框架内部对工具调用的返回结果有一定长度限制超长结果会被截断。这个截断是静默的不会报错但模型拿到的就是不完整信息。我遇到过排查了很久的模型回答到一半突然跳话题最后发现是工具返回的文档正文太长被截断了。5.4 循环调用与成本失控工具调用最隐蔽的坑是模型反复调用同一个工具始终得不到满意结果。比如你提供一个查询工具返回了未找到记录模型不甘心换个参数又查来回好几次。这个过程会连续消耗多次模型的token额度成本成倍上升。LangChain4j对这类循环有内置的一定防护但我自己引入了更严格的控制。一个有效的经验是工具返回值中明确给出终止条件。比如返回未找到用户ID为123的记录请确认ID是否正确不要重复查询模型看到这个信息后通常会停止重复尝试。另一个控制手段是限制单轮对话中的工具调用总次数。你可以在业务层面统计一次对话里的函数调用数量超过阈值就强制给模型一个已达到查询次数上限请基于已有信息回答的提示。这个做法对控制成本非常实用。5.5 一个排查工具问题的通用套路最后分享一套我常用的排查流程尤其适合在线上环境快速定位工具相关的问题。第一步开启框架的日志输出。把工具调用相关的日志级别调到DEBUG通常会看到模型返回的原始函数调用指令、参数JSON、工具执行结果。绝大多数问题在这一步就能暴露。第二步单独测试工具方法本身。把工具从Agent里摘出来直接构造参数调用一遍。如果工具方法本身就有问题那不管模型那边怎么调用都会出问题。先排除工具自身的问题再去排查模型交互。第三步用固定Prompt复现。在线上环境用户的表述五花八门模型选工具的行为会有随机性。排查时可以用一个固定的问题反复测试同一个场景观察失败是否稳定复现。如果时好时坏大概率是模型对工具描述的理解存在模糊地带需要优化描述如果稳定复现大概率是工具逻辑或参数绑定有确定性问题。写在最后的一点体会做了一段时间LangChain4j自定义工具之后我最大的感受是这套机制的门槛不在写代码而在翻译——把业务能力翻译成模型能理解的语言再把模型的决定翻译回精确的Java调用。工具描述写得好不好、参数设计得规不规范、错误信息可不可读这些细节的权重远超代码本身的逻辑复杂度。我在实际迭代中反复体会到一个原则永远假设模型是个聪明但缺乏常识的新同事。你得把工具的用途、触发条件、边界约束、错误提示都写到它一眼能看明白的程度。同时也要时刻记住工具是暴露给用户的入口权限校验、参数校验、频率控制这些安全底线一个都不能少。如果你刚接触这部分内容建议先拿计算器那种最简工具跑通全流程理解框架日志里每次函数调用的流转过程再逐步接入真实业务API。每一步都要观察模型的行为变化慢慢你会建立起一种直觉什么样的工具设计模型用着顺手什么样的设计迟早出问题。这种直觉比记住任何API细节都值钱。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询