news 2026/10/17 8:45:24

Java应用接入本地大模型:VLLM+langchain4j实现工具调用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java应用接入本地大模型:VLLM+langchain4j实现工具调用

先说下我为什么折腾这个事。团队里有个 Java 后端项目,需要接入大模型做智能对话和自动化操作,但业务数据全部要求留在内网,云端 API 这条路直接堵死。我们就在内网 GPU 机器上部署了本地推理服务,模型推理用的是 VLLM,因为它在吞吐和显存管理上确实比同类方案强出一截;Java 侧我选了 langchain4j,不需要额外起一套 Python 服务,直接嵌进现有工程。整个链路最核心的部分,是把 VLLM 当成 OpenAI 兼容的服务接入 langchain4j,并且让大模型在对话过程中自主决定调用我们预先注册的 API 工具。这套流程跑通之后,相当于把"会说话的大模型"变成了"能干活的后端助手",可扩展性比纯对话高太多了。

如果你正好也在做类似的事——Java 应用接本地大模型、需要 function calling / tools 插件调用、数据不想出内网——那这篇文章应该能帮你少走不少弯路。我会把部署参数、代码细节、工具调用机制,以及我在实测中遇到过的几个真实问题全部写清楚,不是网上那种只给结论不解释原因的文章。

1. 为什么是 VLLM + langchain4j,而不是其他组合

先聊选型。这个组合不是随手抓的,我在动手之前确实对比过几套方案。

1.1 VLLM 在本地推理里的优势

本地跑大模型,推理服务的选择无非 VLLM、TGI、llama.cpp 这几个主流方案。VLLM 用的是 PagedAttention 显存管理机制,能把显存碎片利用起来,同等显卡下能支撑的并发请求明显更多。实际测试中,我们用一张 24GB 显存的卡跑 7B 量级的开源对话模型,VLLM 的吞吐大概是 llama.cpp 的几倍,而且它对 OpenAI 兼容协议的实现非常完整,/v1/chat/completions 这个接口直接就有,意味着客户端可以像调云端 API 一样调它。

这一点对接下来的 langchain4j 接入非常关键。因为 langchain4j 内置的 OpenAi 模块就是按 OpenAI 协议写的,VLLM 把协议兼容性做足了,两边就能无缝对上,不需要自己写一层适配。

1.2 langchain4j 在 Java 生态里的位置

项目是纯 Java 技术栈,如果为了接大模型单独引一套 Python 服务,运维成本和数据链路都会变复杂。langchain4j 的定位就是给 Java 开发者用的 LLM 集成框架,它对接 OpenAI 兼容服务只需要几个依赖加几行配置,内部还封装好了聊天会话、消息历史、工具调用这些逻辑。

对比过 Spring AI 和直接 HTTP 调用,最终选 langchain4j 的核心原因有两个:

  • AiServices 这套抽象对工具调用支持得很完整,我用@Tool注解写一个 Java 方法,框架自动把方法名、参数、描述序列化成模型能读懂的 schema,不需要手工拼 JSON。
  • 它支持模型响应直接映射到 Java 对象,对后端开发来说心智成本低。

如果你只是做一次性的接口测试,那直接 HTTP 请求也行,但一旦涉及多轮对话、工具循环这些工程化问题,还是框架省心。

1.3 和其他方案的取舍对照

方案优点缺点适用场景
Python LangChain生态最大、社区资料多需要跨语言维护,Java 服务要额外起 Python 进程无 Java 约束的全 Python 项目
Spring AI和 Spring Boot 集成自然工具调用等高级能力早期支持不够细只用简单对话的场景
langchain4jJava 原生、工具调用封装好、协议兼容强社区比 Python 生态小一些Java 后端需要 function calling 的场景
直接 HTTP 调 VLLM零依赖、完全可控多轮对话、工具循环全部要自己写内部私有协议或简单 demo

我的结论是:Java 业务系统接 VLLM,langchain4j 是最均衡的选择。接下来就是实际部署和接线。

2. 本地 VLLM 服务部署:命令、参数和最容易踩的坑

在开始写 Java 代码之前,得先把 VLLM 服务跑起来,并且保证它对外的接口形态是"OpenAI 兼容"的。这一步很多人不在意,到后面排查代码问题排查半天,结果发现是服务启动参数配错了。

2.1 硬件选型和显存估算

如果是 7B 参数量的开源对话模型,FP16 精度下权重大概占 14GB 显存,再加上 KV Cache 和激活值,一张 24GB 的卡(比如常见的 4090 或 A5000)基本够用,并发请求数量不要调太高就行。如果是 13B 模型,建议直接上 48GB 或 80GB 的卡,或者用 AWQ 量化把显存占用压下来。我们当时就是一张 24GB 卡跑 7B 模型,--gpu-memory-utilization设置到 0.9,实测下来单卡并发 8 个请求没有明显 OOM。

2.2 启动命令实战

vllm serve /data/models/Qwen2.5-7B-Instruct \ --served-model-name local-model \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --host 0.0.0.0 \ --port 8000 \ --api-key local-demo-key \ --enable-auto-tool-choice \ --tool-call-parser hermes \ --max-model-len 8192

这条命令里的参数,每个都不是随便加的:

  • --served-model-name local-model:这是对外暴露的模型名。客户端请求里model字段必须写local-model,否则 VLLM 会报模型不存在的错。我们内部统一约定这个值,避免每台机器路径不一样导致的混乱。
  • --api-key local-demo-key:给服务加个静态密钥。VLLM 要求客户端在 Authorization 头里带上这个值,langchain4j 配置里的apiKey会和它对应。
  • --enable-auto-tool-choice+--tool-call-parser hermes:这两个参数是工具调用能否生效的前提。第一个参数让 VLLM 自动决定返回工具调用还是普通文本;第二个参数告诉 VLLM 用哪个解析器来解析模型输出的工具调用格式。不同模型家族对应的解析器不一样,Qwen 系列一般用hermes,Llama 系列可能要用mistral。如果解析器选错,模型输出的工具调用就会被 VLLM 强行往下走默认生成逻辑,你收到的是一个普通文本回复而不是结构化的工具调用指令。

2.3 验证服务是否就绪

启动日志里看到类似Application startup complete的字样后,先用 curl 打一发确认接口通不通:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Authorization: Bearer local-demo-key" \ -H "Content-Type: application/json" \ -d '{ "model": "local-model", "messages": [{"role": "user", "content": "你好,介绍一下你自己"}], "max_tokens": 128 }'

如果返回了正常的choices数组,说明 VLLM 基本可用。这里有个细节:网络上很多教程在客户端填baseUrl时不带/v1,服务端虽然做了兼容,但为了少出问题,建议统一把/v1写在客户端地址里,VLLM 的--api路由默认就是/v1开头。

2.4 部署时容易忽略的两个点

第一,--max-model-len不要贪大。序列长度越长,KV Cache 占用越高,并发能力越低。如果只是做对话和工具调用,8192 一般够用;硬拉到 32K 反而会因为显存不够导致请求排队甚至失败。

第二,如果你要用 docker 启动 VLLM,记得加端口映射,并且确认容器内外的--api-key是同一个。我见过有人 docker 启动时忘了映射 8000 端口,Java 代码连到的是宿主机上另一个旧服务,排查了很久才发现打错了端口。

3. langchain4j 接入 VLLM:OpenAI 兼容接口的配置细节

服务端准备好了,接下来就是 Java 侧的对接。这里我把过程拆成依赖引入、模型构建、通信验证三步,每一步都有明确的执行依据。

3.1 依赖引入

pom.xml 里加两个核心依赖:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.35.0</version> </dependency>

这里需要解释一下为什么是langchain4j-open-ai而不是langchain4j-vllm。VLLM 对外提供的接口是 OpenAI 兼容协议,langchain4j 的 OpenAi 模块就是用来对接这类服务的。换句话说,在我们这个场景里,客户端眼里 VLLM 就是一个"私有化的 OpenAI 服务",协议兼容意味着所有用 OpenAi 模块写的代码可以直接跑。

如果你拿的是比较新的 langchain4j 版本,部分模块包名和版本号会有调整,建议以 Maven 仓库里实际能拉到的版本为准。选版本的时候尽量选稳定版,不要追最新的快照版,工具调用这类功能在不同版本之间行为差异挺大。

3.2 构建模型连接

OpenAiChatModel model = OpenAiChatModel.builder() .baseUrl("http://192.168.1.100:8000/v1") .apiKey("local-demo-key") .modelName("local-model") .temperature(0.3) .timeout(Duration.ofSeconds(60)) .logRequests(true) .logResponses(true) .build();

这一段有四个字段需要额外注意:

  • baseUrl必须带/v1。VLLM 对不带/v1的路径有时会返回 404,这算是最常见的接入报错之一。
  • apiKey不需要是真实云端密钥,但必须和 VLLM 启动时传的--api-key保持一致。VLLM 不认识你自定义的 key 时会直接返回 401。
  • modelName对应--served-model-name设置的名字,这里写local-model。
  • logRequests(true)和logResponses(true)是我强烈建议开启的配置。刚开始接入时你需要看清楚实际发送给 VLLM 的请求体长什么样、返回体长什么样,这两个开关能省掉很多抓包的时间。生产环境记得关闭,否则日志会涨得很快。

先写一小段验证代码:

String answer = model.generate("你好,请简单介绍下你自己"); System.out.println(answer);

能正常返回内容,说明 langchain4j 和 VLLM 的网络链路已经通了。

3.3 一个容易误解的地方:聊天历史

很多人在这个阶段会问,为什么同样的提问,多轮对话后模型好像"忘了"之前的内容。其实 VLLM 本身是无状态的,每次请求都是独立的,对话的上下文完全靠客户端把历史消息一起发过去。langchain4j 里ChatMemory的作用就是帮你在内存里维护这份历史消息列表,每次调用时自动携带。

ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(20) .build();

个人实践经验是:20 条历史消息对大多数业务对话够用,但如果你还会让模型"记忆"工具调用的中间结果,建议把这个值控制在 20 到 30 之间,太长会让 token 消耗快速增加,而且模型容易把早先的无关信息混进当前决策里。

到这里,普通对话已经没问题了。但我们的目标是让模型主动调用工具,所以接下来才是重头戏。

4. API tools 插件调用的核心:从 @Tool 注解到动态 ToolSpecification

工具调用(function calling)的完整链路是:用户提问 → langchain4j 把可用工具的 JSON Schema 发给 VLLM → VLLM 判断需要调用某个工具时,返回结构化的tool_calls→ langchain4j 解析后反射调用你的 Java 方法 → 把方法返回值回传给 VLLM → VLLM 结合工具结果生成最终回复。

这个链路里,VLLM 只负责"决策",真正的执行发生在 Java 进程里。所以工具调用的可靠性,一半取决于模型对工具描述的理解,一半取决于注册工具时描述写得够不够清楚。

4.1 最简单的工具注册:@Tool 注解

先定义一个工具类:

public class MoneyTools { @Tool("查询指定用户的账户余额") public BigDecimal getBalance(@P("用户ID") String userId) { // 这里调内部账户服务的接口 return accountService.getBalance(userId); } @Tool("根据用户ID查询最近交易记录,limit代表返回条数") public List<Transaction> getTransactions(@P("用户ID") String userId, @P("返回条数,默认5") int limit) { return transactionService.listByUser(userId, limit); } }

@Tool里的描述文字非常关键,它会变成模型决策时的依据。比如getTransactions的注释里明确写了limit的含义,模型才知道该传什么值。如果你写的是"查询交易"这种模糊描述,模型很可能不知道该在什么场景下调用它,或者参数填错。

组装服务:

interface Assistant { String chat(String userMessage); } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .tools(new MoneyTools()) .build(); String result = assistant.chat("帮我查一下用户 A1001 的账户余额");

AiServices会自动把MoneyTools里的所有@Tool方法序列化成工具列表,并在每一轮对话时附加到请求里。langchain4j 处理了工具调用的循环,你不需要手动解析tool_calls再手动回传结果。

4.2 工具参数的类型支持

@P注解里可以补充更细的描述,比如枚举范围、格式示例。模型读懂了参数的约束,填错参数的几率会小很多:

@Tool("按日期范围查询订单") public List<Order> queryOrders(@P("开始日期,格式为 yyyy-MM-dd") String startDate, @P("结束日期,格式为 yyyy-MM-dd") String endDate) { return orderRepository.findByDateRange(startDate, endDate); }

实践出来的经验是:参数描述一定要把"格式"写清楚,比如日期就是yyyy-MM-dd,金额就是数字。VLLM 虽然能理解自然语言,但日期这种格式不写清楚,它真会给你生成五花八门的写法。

4.3 动态工具:ToolSpecification 的适用场景

@Tool适合工具固定不变的场景。如果你的工具列表是动态的,比如不同用户身份能看到不同的操作权限,就需要用ToolSpecification动态构建:

ToolSpecification queryOrderSpec = ToolSpecification.builder() .name("query_order") .description("根据订单号查询订单状态和详情") .addParameter("orderId", JsonObjectSchema.builder() .stringSchema() .description("订单号") .build()) .build();

然后在真正发消息时,把这份 specification 传给模型:

OpenAiChatRequest request = OpenAiChatRequest.builder() .messages(...) .toolSpecifications(queryOrderSpec) .build(); Response<AiMessage> response = model.generate(request);

动态工具的价值在于:你可以根据登录用户、当前上下文、业务开关来组装不同的工具集合。比如普通用户只注册query_order,管理员额外注册refund_order,模型能调用的边界就被精确控制住了,而不是一股脑把所有工具暴露给所有会话。

需要注意一点:无论用@Tool还是ToolSpecification,最终发给 VLLM 的都是一样的 JSON Schema 结构。langchain4j 在这两种方式下做了统一的序列化处理,所以你不需要担心动态方式会破坏协议兼容性。

4.4 VLLM 端必须满足的前提条件

服务端启动时如果没加--enable-auto-tool-choice,你会发现模型收到带 tools 的请求后,大概率直接忽略工具,输出一段普通文本。排查时第一件事就是回去看启动命令里有没有这个参数。

另外,VLLM 版本对工具调用的支持迭代很快。老版本可能只支持部分解析器,或者对tools字段的校验很严格,遇到奇怪的报错先升级 VLLM 到较新版本再试。

4.5 一个完整的最小可复现示例

把上面的内容串成一段可直接运行的逻辑:

public String runAssistant(String userMessage) { // 1. 构建模型连接 OpenAiChatModel model = OpenAiChatModel.builder() .baseUrl("http://192.168.1.100:8000/v1") .apiKey("local-demo-key") .modelName("local-model") .build(); // 2. 构建带工具调用的服务 Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .tools(new MoneyTools()) .build(); // 3. 调用并返回结果 return assistant.chat(userMessage); }

执行这句话:

runAssistant("帮我查一下用户 A1001 最近 3 笔交易记录");

正常情况下会经历:模型认为需要调用getTransactions→ Java 执行方法 → 结果返回模型 → 模型基于结果生成回答。整个过程日志里能看到tool_calls的相关输出。

5. 实测踩坑记录:VLLM 工具调用兼容性问题与排查路径

代码能跑通不意味着稳了。我在这个环节踩过好几个坑,这里按"现象 — 排查 — 解决"的方式记录,方便你对照排查。

5.1 VLLM 返回普通文本,不触发工具调用

现象:请求里带了 tools,但模型回复是一段纯文字,并没有走工具逻辑。

排查步骤:

第一步,看 VLLM 启动命令里是否包含--enable-auto-tool-choice。我当时第一版部署命令没加,模型完全无视工具。

第二步,确认模型本身是否支持 tool calling。并不是所有开源模型都原生支持 function calling。Qwen 2.5 系列、Llama 3.1 系列都支持,但一些较小的模型或早期版本模型可能不支持,即便 VLLM 有解析器也没用。验证方法是直接拿 curl 发一个带tools字段的请求,观察返回内容里有没有tool_calls。

第三步,检查请求里工具描述是否过于含糊。模型不调用工具的另一个常见原因是它"看不出该用哪个工具",把@Tool描述写得更具体、参数约束写得更清楚后,触发率会明显提升。

5.2 返回了 tool_calls,但 langchain4j 解析失败或参数为空

现象:VLLM 确实返回了tool_calls,但 Java 侧收到的AiMessage里工具列表为空,或者方法执行时参数缺失。

排查步骤:

先用logRequests(true)和logResponses(true)打开日志,看完整的请求响应。我遇到过一次的情况是:VLLM 返回的tool_calls里function.arguments字段是 JSON 字符串,内部包含参数,但 langchain4j 版本与返回结构不完全兼容,导致解析不出参数。升级 langchain4j 到更新版本后解决。

另一个常见问题是--tool-call-parser选错。VLLM 的解析器要按模型家族选。用 Qwen 系列却配了mistral解析器,确实会出现工具调用数据解析错乱的问题。我的建议是先去 VLLM 的文档里确认所选模型该搭配哪个 parser,不要一律抄网上的hermes。

5.3 工具执行结果回传后,模型"装没看见"

现象:Java 方法执行成功了,结果也回传给了 VLLM,但最终回复是"我暂时无法查询订单信息"之类的车轱辘话,明明结果已经在上下文里了。

这种情况多半是历史消息被截断,或者工具结果消息格式出了问题。查一下ChatMemory的maxMessages设置,如果设置太小,早期消息会被挤掉,模型看不到工具调用的输入和输出,自然给不出正确的最终回答。另外记得把工具执行结果作为user或tool角色消息回传,这一层 langchain4j 在AiServices里会自动处理,但如果你用的是底层model.generate()手写循环,就一定要检查消息角色拼装得对不对。

5.4 并发压测时出现偶发超时和 500

VLLM 在高并发下会先进请求队列,如果你客户端请求 timeout 设置太短,比如默认 10 秒,很容易在模型思考超时的那一瞬间抛异常。代码里把timeout加到 60 秒甚至 120 秒,同时服务端用--max-num-seqs控制最大并发序列数。我这里推荐的做法是客户端加一层超时重试逻辑,但不能让重试导致重复提交写操作类工具。

5.5 curl 直连测试是排错的第一利器

遇到任何工具调用相关的问题,不要先怀疑代码,先用 curl 手工模拟一次完整的对话过程,原始 JSON 返回能告诉你 80% 的事实。下面是一个带 tools 的请求示例:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Authorization: Bearer local-demo-key" \ -H "Content-Type: application/json" \ -d '{ "model": "local-model", "messages": [ {"role": "user", "content": "查一下用户 A1001 的余额"} ], "tools": [ { "type": "function", "function": { "name": "getBalance", "description": "查询指定用户的账户余额", "parameters": { "type": "object", "properties": { "userId": {"type": "string", "description": "用户ID"} }, "required": ["userId"] } } } ], "tool_choice": "auto" }'

如果这个请求直接返回tool_calls,说明 VLLM 链路是通的,问题在 Java 侧;如果返回普通文本,问题就在 VLLM 部署参数或模型选型。这个分流判断能节省大量排查时间。

6. 最后想说的

整套方案跑起来之后,我最大的感受是:VLLM + langchain4j 这套组合完全可以在 Java 业务线里承担生产级别的 AI 自动化任务。工具调用的稳定性虽然受模型本身能力影响,但在选对模型、描述清晰、参数合理的前提下,实测的成功率足够支撑内部系统使用。如果你准备在生产环境推进,建议先从只读类工具开始,比如查询订单、查余额,等模型调用链路彻底稳定了,再逐步放开写操作类工具,比如审批、下单、退款,并给这类工具加上人工确认机制。

最后分享一个小技巧:把 VLLM 的启动命令、模型Serving 名称、默认解析器、API Key 统一记在项目 README 里。代码可以随时改,但环境配置一旦多人协作就很容易出现"代码一样、环境不一致"的问题。把这组信息固化下来,后面的人接到这个项目时,环境复现会顺畅得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 8:45:22

diagram-design:把架构图当作代码管理的工程化绘图工具

很多人刚开始接触“diagram-design”这个名字时&#xff0c;第一反应可能是&#xff1a;这不又是一个画图工具吗&#xff1f;市面上的流程图工具、架构图工具、在线白板多得数不过来&#xff0c;为什么还要自己折腾一个&#xff1f;但真正在技术团队里待过几年、被各种制图协作…

作者头像 李华
网站建设 2026/10/17 8:45:00

XblGameSaveTask.exe是什么?别乱下载!系统组件识别与修复全攻略

先说结论&#xff1a;XblGameSaveTask.exe 不是一款需要单独下载的第三方软件&#xff0c;它是 Windows 系统里游戏存档同步服务的一部分。前段时间有朋友拿着一张截图来找我&#xff0c;说启动器提示找不到 XblGameSaveTask.exe&#xff0c;网上搜了一圈全是“高速下载”链接&…

作者头像 李华
网站建设 2026/10/17 8:44:40

Android GPS底层驱动开发:HAL、JNI与串口NMEA解析实战

简介&#xff1a;围绕GPS Android底层驱动的中文资料包&#xff0c;面向Android系统开发者与驱动移植工程师&#xff0c;系统讲解JNI与HAL层的协同原理&#xff0c;并附关键源码与移植分析笔记。压缩包共5个文件&#xff0c;大小约9.28MB&#xff0c;内含2份Word讲解文档、1个R…

作者头像 李华
网站建设 2026/10/17 8:44:33

artcraft 工作流实战:从素材到成品的批量处理与配置复用

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个词&#xff0c;我脑子里蹦出来的不是某个具体工具&#xff0c;而是一种很朴素的组合感——art&#xff08;艺术&#xff09;加上 craft&#xff08;手艺、工艺&#xff09;。这两个词…

作者头像 李华
网站建设 2026/10/17 8:43:29

医学图像配准深度学习模型搭建:从变形场到损失函数的实战指南

我最早接触医学图像配准深度学习模型的搭建时&#xff0c;一度以为只要把固定图和浮动图拼在一起&#xff0c;扔进一个卷积网络&#xff0c;让它输出变形场就完事了。等真把模型搭出来跑起训练&#xff0c;才发现最耗精力的根本不是什么“更好的结构”&#xff0c;而是那几个容…

作者头像 李华
网站建设 2026/10/17 8:43:29

用纯bat脚本实现Windows系统清理:从临时文件到缓存的安全设计

帮同事A清一台老办公电脑的时候&#xff0c;我盯着系统盘剩余空间看了半天&#xff1a;回收站早就倒空了&#xff0c;桌面也干净&#xff0c;C盘却被塞得只剩几个G。逐个翻临时目录、缓存、日志&#xff0c;删到一半我就在想&#xff0c;这种事情为什么不写成一个脚本一次性解决…

作者头像 李华