资讯动态

LangChain4j 工具调用:Function Calling 让模型动手查数据,多工具连调实测

发布时间:2026/10/22 2:12:09 来源:尧图企业网站定制
LangChain4j 工具调用Function Calling 让模型动手查数据多工具连调实测作者鱼宵 LangChain4j实战精通营 · 第 6 篇上周帮朋友排查一个客服机器人问它订单 1001 发货了没它张口就来已发货物流单号 SF88881234预计明天送达。“我去后台一查这订单压根不存在单号是它现编的。这种一本正经地胡说八道”就是做 AI 应用最怕的幻觉。怎么治给模型装一双能查真实数据的手——这就是 Function Calling工具调用。这篇带你跑通一个问题同时问订单和库存模型自己决定调哪个方法、自己抽参数、连调两次再把内存表里的真实数据组织成人话。代码全在仓库lesson-06/clone 下来照着命令重跑你就能在控制台亲眼看到那行【工具被框架调用】。一、核心原理模型只会动嘴不会动手一句话大模型是个被关在房间里的实习生——知识渊博、口才极好但房间里没有电话、没有电脑、没有数据库钥匙。你问他订单到哪了他不是不会查是他根本没手。1. 为什么模型需要工具LLM 的本质是文字接龙给它一段上文它预测最可能接的下一段文字。它跑在沙箱里没有执行权——不能跑 SQL、不能发 HTTP、不能调你的 Java 方法。你问它今天库存多少它只能靠训练时记住的旧知识瞎猜。Function Calling工具调用就是给这个实习生塞一个对讲机他不离开房间但可以喊帮我查一下订单 1001你在外面查好对着对讲机念给他听他再转述给客户。2. 五步链路模型输出的是意图不是答案很多人第一次听 Function Calling 会以为模型能执行我的代码了——错。真正执行代码的是你的 Java 框架模型从头到尾只负责说。链路是这样的步骤谁干的干了什么① 你提问用户“订单 1001 什么状态库存还剩多少”② 模型输出意图大模型“我要调 getOrderStatus参数 orderId1001”注意它只是说要调没真调③ 框架反射调方法LangChain4j真的去跑CustomerTools.getOrderStatus(1001)④ 结果回填框架把方法返回值塞回给模型作为一条工具消息⑤ 模型组织回答大模型拿着真实数据说人话“订单已发货单号 SF…”一个问题里要查两个东西框架就把 ②③④ 循环跑两遍。你只调了一行assistant.chat(...)背后可能是多次模型往返。这就是多工具连调——顺序是模型自己排的不是你写死的 if-else。3. Tool 和 P贴给模型看的说明书你把工具挂墙上每个工具贴一张说明牌“这是查订单的要填纯数字单号比如 1001”。模型决定用哪个工具时读的就是这块牌子。Tool(...)标在方法上那句话会被翻译成 JSON Schema 发给模型。它是给模型读的不是给人读的。P(...)标在参数上告诉模型 orderId 长什么样。没有它模型可能把整句订单 1001 的状态当成 orderId 塞进去。牌子写得越清楚模型越不会乱按按钮、乱填参数。模型没调工具八成就是Tool描述太含糊。4. AiServices只写接口不写实现LangChain4j 最爽的一点你定义一个 Java 接口比如CustomerAssistant里面就一个chat(String)方法——不写实现类。运行时它用动态代理给你生成实现自动跑完调模型→识别工具意图→调工具→回填→再调模型整个循环。类比成签岗位合同你只管写清楚这个人负责 chat内部按几个按钮你不用管。二、动手编译 跑 fat jar环境Windows 11 PowerShell 5.1 JDK 17。这是控制台课没有 HTTP 接口跑完自动退出。第 1 步检查环境。java-version# 期望 True[bool][Environment]::GetEnvironmentVariable(DEEPSEEK_API_KEY)# 期望 True只看存在别打印值第 2 步编译打 fat jar。cd langchain4j-journey\lesson-06$env:JAVA_HOMEC:\Program Files\Java\jdk-17# 用户级 JAVA_HOME 指向 JDK8每条新窗口都要重设mvn clean install-DskipTests# 结尾看到 BUILD SUCCESS第 3 步跑起来真实调 DeepSeek。java-Dfile.encodingUTF-8-jar target\lesson-06-1.0.0.jar重点盯控制台里【工具被框架调用】那几行——那就是模型自主调工具的铁证。中文不乱码靠上面那个-Dfile.encodingUTF-8。三、关键代码四个文件逐行拆工程里真正要看的就是四个 Java 文件。模型配置LlmConfig和第 1 课同款OpenAiChatModel 从环境变量读 Key这里不重复贴重点看下面三个。第一段CustomerTools.java——两个被暴露给模型的工具方法。packagecom.langchain4j.lesson06;importdev.langchain4j.agent.tool.P;importdev.langchain4j.agent.tool.Tool;importorg.springframework.stereotype.Component;importjava.util.HashMap;importjava.util.Map;/** * 业务工具类一个普通 Java 类用 Tool 把方法暴露给大模型。 * 模型不会执行代码它只说要调哪个方法真正执行的是框架。 * 这里用内存 Map 模拟订单表/库存表生产换成 MyBatis 查真实库。 */ComponentpublicclassCustomerTools{/** 模拟订单表订单编号 - 订单状态 */privatestaticfinalMapString,StringORDER_DBnewHashMap();/** 模拟库存表商品名 - 剩余件数 */privatestaticfinalMapString,IntegerSTOCK_DBnewHashMap();static{ORDER_DB.put(1001,已发货物流单号 SF1234567890预计明天送达);ORDER_DB.put(1002,待付款订单已提交但未支付);ORDER_DB.put(1003,已完成已签收并评价);ORDER_DB.put(1004,已取消用户主动取消);STOCK_DB.put(机械键盘,35);STOCK_DB.put(无线鼠标,120);STOCK_DB.put(显示器支架,8);STOCK_DB.put(USB-C 数据线,0);}/** * Tool 里那句说明 写给模型看的工具说明书会被转成 JSON Schema 发给模型。 * P 是写给参数的说明书告诉模型 orderId 长什么样。 */Tool(根据订单编号查询订单当前状态例如是否发货、物流信息、是否完成或取消)publicStringgetOrderStatus(P(订单编号纯数字字符串例如 1001、1002)StringorderId){// 这行打印就是调用链记录仪它真被执行了说明框架真的调了这个方法System.out.println( 【工具被框架调用】getOrderStatus(orderIdorderId));StringstatusORDER_DB.get(orderId);if(statusnull){// 查不到也返回人话模型会拿着它去跟用户说查无此单而不是抛异常System.out.println( 【工具返回】订单 orderId 不存在);return未找到订单编号 orderId请确认单号是否正确。;}System.out.println( 【工具返回】status);return订单 orderId 的状态status;}Tool(根据商品名称查询当前库存还剩多少件)publicStringgetStock(P(商品名称例如机械键盘、无线鼠标)StringproductName){System.out.println( 【工具被框架调用】getStock(productNameproductName));IntegerstockSTOCK_DB.get(productName);if(stocknull){System.out.println( 【工具返回】商品 productName 不存在);return没有找到商品「productName」的库存记录。;}Stringresultstock0?商品「productName」当前缺货库存 0 件建议提醒用户补货。:商品「productName」当前库存剩余 stock 件。;System.out.println( 【工具返回】result);returnresult;}}盯三个细节方法返回String返回值要回填给模型别返回 void查不到也return一句人话而不是抛异常那行System.out.println是埋的记录仪它打了就证明框架真进来了。第二段CustomerAssistant.java——一个接口就是一个助手。packagecom.langchain4j.lesson06;/** * 客服助手接口只定义不写实现。 * AiServices 运行时用动态代理生成实现类把调模型调工具回填全封装掉。 */publicinterfaceCustomerAssistant{/** 给一句话拿回一句自然语言回答模型自己决定直接答还是先调工具 */Stringchat(StringuserMessage);}第三段ToolCallDemoApplication.java——装配 两个演示。packagecom.langchain4j.lesson06;importdev.langchain4j.model.chat.ChatModel;importdev.langchain4j.service.AiServices;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/** * 第 6 课启动类控制台课implements CommandLineRunnerrun 完即退出。 * 运行java -Dfile.encodingUTF-8 -jar target\lesson-06-1.0.0.jar */SpringBootApplicationpublicclassToolCallDemoApplicationimplementsCommandLineRunner{privatefinalChatModelchatModel;// 大模型客户端LlmConfig 配好的 BeanprivatefinalCustomerToolscustomerTools;// 业务工具Spring 扫描成 BeanpublicToolCallDemoApplication(ChatModelchatModel,CustomerToolscustomerTools){this.chatModelchatModel;this.customerToolscustomerTools;}publicstaticvoidmain(String[]args){SpringApplication.run(ToolCallDemoApplication.class,args);}Overridepublicvoidrun(String...args){// 关键三行模型大脑 工具类双手 会自己调工具的助手CustomerAssistantassistantAiServices.builder(CustomerAssistant.class).chatModel(chatModel)// 用哪个模型做大脑.tools(customerTools)// 交出去工具类模型就能看见里面所有 Tool 方法.build();System.out.println( 第 6 课工具调用 Function Calling );// 演示一一个问题同时问订单库存逼模型连续调两个工具Stringq1订单 1001 现在是什么状态另外机械键盘的库存还剩多少;System.out.println(\n你问q1);System.out.println(下面会依次打印框架调用了哪个工具、传了什么参数、返回了什么);Stringa1assistant.chat(q1);System.out.println(AI 答a1);System.out.println(---------- 演示一结束 ----------);// 演示二闲聊问题看模型克制地不调工具、直接回答Stringq2你好简单介绍一下你能帮我做什么一句话就好。;System.out.println(\n你问q2);Stringa2assistant.chat(q2);System.out.println(AI 答a2);System.out.println(---------- 演示二结束 ----------);System.out.println(\n 演示结束 );}}最值得盯的就是.chatModel(...)配大脑、.tools(...)配双手这两行——你一行实现类都没写助手就会自己调工具了。四、实测输出一个问题连调两个工具以下是 2026-10-05 本机真实运行DeepSeekdeepseek-chat已省略 Spring Boot banner。先看演示一——一个问题里同时问订单和库存 第 6 课工具调用 Function Calling 你问订单 1001 现在是什么状态另外机械键盘的库存还剩多少 下面会依次打印框架调用了哪个工具、传了什么参数、返回了什么 【工具被框架调用】getOrderStatus(orderId1001) 【工具返回】已发货物流单号 SF1234567890预计明天送达 【工具被框架调用】getStock(productName机械键盘) 【工具返回】商品「机械键盘」当前库存剩余 35 件。 AI 答查询结果如下 **订单 1001 状态** - 状态已发货 - 物流单号SF1234567890 - 预计送达明天 **机械键盘库存** - 当前剩余35 件 如需查询其他订单或商品随时告诉我。 ---------- 演示一结束 ----------逐行读证据getOrderStatus(orderId1001)——模型自己决定调它、自己从整句话里抽出参数1001紧接着getStock(productName机械键盘)——一个问题里连续规划两次调用先订单后库存顺序是模型自己排的。而回答里的已发货 / SF1234567890 / 35 件全是内存 Map 里的真实数据模型只是排版好看了。再看演示二同一个助手换个闲聊问题——这次一行工具调用都没有你问你好简单介绍一下你能帮我做什么一句话就好。 AI 答我可以帮你查询商品库存和订单状态比如某商品还剩多少件或某订单是否已发货。 ---------- 演示二结束 ----------看出差别了吗这一问没有打印任何【工具被框架调用】——模型判断闲聊不用查数据直接用自己的话答了。调不调工具是模型自己决策的不是你写死的。排查没看到【工具被框架调用】就查三处——Tool/P描述够不够具体、问题里有没有明确提订单/库存、模型名是不是deepseek-chatdeepseek-reasoner行为不同。报 401 是 Key 没生效。五、挑战题改一改看看会怎样⭐删掉参数说明书把getOrderStatus上的P(订单编号…)整行删掉重跑演示一看模型还能不能乖乖抽出1001会不会把整句话塞进 orderId。答案就在源码CustomerTools.java——删完跑一次你就明白P为什么叫防呆。⭐⭐问一个不存在的单号把 q1 改成订单 9999 现在什么状态观察模型怎么拿着工具那句未找到订单编号 9999去回复用户。重点看它会不会自己编一个状态糊弄你——这就是兜底的价值。⭐⭐加个条件判断的问题把 q1 换成USB-C 数据线库存还有多少如果已经是 0 件顺便帮我查下订单 1003 的状态。“看模型能不能先查库存、再根据结果决定要不要查订单。跑出来你就懂什么叫模型自己安排工作流”。六、生产环境进阶三个加分项1. 别把异常栈回传给模型。启动日志里那串ToolErrorHandlingNoticeWARN 是 1.21.0 的正常提示。但它提醒你默认工具一抛异常异常信息会被回填给模型。生产环境要加.toolExecutionErrorHandler(...)否则你的 SQL、服务器路径可能被原样塞进上下文既泄露又费 token。2. 工具方法返回 String。返回值要回填给模型组织语言返回 String 最省事返回复杂对象模型拿到的是一坨 JSON可读性差。3. Key 只从环境变量读。LlmConfig里 apiKey 走System.getenv(DEEPSEEK_API_KEY)代码、yml、日志里都不出现明文 Key。课后 grep 一遍只留环境变量名。七、面试回答模板面试官Function Calling 的完整链路是什么模型真的会执行你的 Java 代码吗一句话模型不执行代码它只输出结构化的调用意图真正执行的是框架。展开用户提问 → 模型输出调某工具、参数是啥 → LangChain4j 反射调你的 Java 方法 → 结果回填模型 → 模型组织最终回答。类比成关在房间里的实习生用对讲机喊你查数据他全程没离开房间。一次 chat 内部可能多轮往返、连调多个工具。指向本课第一节 lesson-06 主类追问Tool 和 P 注解是写给谁看的不写会怎样写给模型看的。Tool(...)的说明会被转成 JSON Schema 随请求发给模型决定它选哪个工具P(...)描述参数决定它填对参数。描述含糊模型就可能不调工具、或把整句话当 orderId 传进去——挑战题第一题就是删P复现这个坑。指向本课第三节CustomerTools追问模型凭什么知道这个问题该调工具、那个问题直接答它训练时学会了这个判断涉及实时/私有数据就倾向输出调用意图闲聊和知识问答就直接答。本课演示二就是铁证——问你能帮我做什么一行工具调用都没打。指向本课第四节实测输出八、总结表坑现象解法JAVA_HOME 指向 JDK8mvn 跑在 Java 8 报错每条构建命令前$env:JAVA_HOMEC:\Program Files\Java\jdk-17中文乱码工具返回中文变???运行加-Dfile.encodingUTF-8文件 UTF-8 无 BOM模型不调工具直接编AI 答得空泛把Tool/P描述写具体、带示例参数抽错整句被当 orderIdP写清纯数字例如 1001工具异常栈外泄WARN 一堆、上下文泄露生产加.toolExecutionErrorHandler(...)误加 langchain4j-tools版本对不齐1.21.0 的 Tool/AiServices 都在核心包不用额外加九、关于这个系列本文是「Java 后端实战精通营」系列第 6 篇原则实战驱动、由浅到深、面试向每篇文章的结论都可以亲手验证。LangChain4j 实战精通营10 课https://gitee.com/j67mk2/langchain4j-journey本文对应源码位置lesson-06/控制台工程内含CustomerTools两个 Tool 工具 CustomerAssistant接口 ToolCallDemoApplication装配主类系列文章一览按发布顺序篇主题1LangChain4j 第一个对话程序手写 LLM 调用链路看清模型怎么开口2LangChain4j 提示词工程模板 few-shot 温度提示词从写死到复用3LangChain4j 结构化输出让模型吐出 Java 对象JSON 不再手写解析4LangChain4j 对话记忆让模型记住聊过什么三种方式实测对比5LangChain4j 流式输出打字机效果 SSE回答不再干等三秒6LangChain4j 工具调用Function Calling 让模型动手查数据多工具连调实测7LangChain4j 向量检索文本怎么变成可搜索的坐标8LangChain4j RAG 流水线手写问答全流程给 AI 开卷考试9LangChain4j Agent多步推理自主调用放手让实习生自己安排工作10LangChain4j 企业智能客服RAG 工具 记忆 流式 兜底验收 5 项全过下一篇预告《LangChain4j 向量检索文本怎么变成可搜索的坐标》——今天模型学会了动手查数据库但企业里更常见的知识是文档产品手册、FAQ、规章制度没法给每个知识点写一个Tool。下一课给模型装另一双眼睛把文本变成可搜索的坐标按意思而不是关键词去大海捞针。跑完有任何报错把控制台输出发评论区一起排查。标签建议LangChain4j、FunctionCalling、大模型工具调用摘要建议≤256 字大模型只会文字接龙、没有执行权问它订单状态容易幻觉编造单号。本文用 LangChain4j 的 Tool P AiServices把查订单、查库存两个普通 Java 方法暴露给 DeepSeek一个问题连调两个工具实测可见模型自己决定调不调、抽什么参数。附删 P 对照、不存在单号兜底等 3 道挑战题与面试回答模板源码在 gitee lesson-06 可 clone 直接跑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑