java 调用 ollama api

接入与开发

给 Java 开发者的 Ollama 本地接口调用指南,讲清请求封装、流式处理、超时并发与排错顺序。

你用 Java 写后端,想在本地跑一个 Ollama 服务,让项目直接调它做补全或对话。真动手时会发现:HttpClient 怎么发流式请求、返回的一堆 JSON 行怎么切、读到一半卡住怎么办,这些都不是看两行示例能解决的。这篇按工程落地的顺序给你一套可照抄的封装思路和排查顺序,不涉及协议兼容层和客户端配置那部分内容。

你用 Java 写后端,想在本地跑一个 Ollama 服务,让项目直接调它做补全或对话。真动手时会发现:HttpClient 怎么发流式请求、返回的一堆 JSON 行怎么切、读到一半卡住怎么办,这些都不是看两行示例能解决的。这篇按工程落地的顺序给你一套可照抄的封装思路和排查顺序,不涉及协议兼容层和客户端配置那部分内容。

下面四个小节分别对应四件事:请求对象怎么建、流式怎么读、超时并发怎么设、出问题先看哪。每节都给了具体动作,你可以边看边改代码。如果你还在挑本地模型跑什么规格,可以先到 /tools/speed-test 测一下自己机器的实际表现,再回来定并发和超时参数。

先把请求形状对齐:Java 侧要准备什么

动手写 Java 之前,先用命令行工具对着本地服务打一次真实请求,把返回的原始 JSON 存下来。这一步花不了几分钟,但能省掉后面在代码里反复猜字段名的来回。Java 侧第一版建议直接用 JDK 自带的 HttpClient,别急着引第三方 SDK,先把请求跑通再考虑封装层次。

  • 确认本地服务在监听、端口能通,命令行打一次拿到真实响应体,再回 Java 里照着写,避免一上来就在代码里猜字段
  • 把 URL、模型名、messages 数组、是否流式这几个字段抽成一个请求对象,后续换模型只改这个对象,不改调用点
  • 请求体字段名要和本地服务实际返回的对上,拿不准就先发一次非流式请求把原始 JSON 打出来,逐字段核对
  • 鉴权按你本地服务的实际配置来,没开鉴权就不要硬塞 header;开了鉴权就统一走一个拦截器,别散落在各个调用点
准备项怎么确认不适用的情况
服务可连通命令行工具直接发一次请求,能拿到返回体服务部署在别的机器上时,还要确认防火墙和路由规则
请求字段名非流式请求打印原始 JSON,逐字段比对服务有多个接口版本时,要以你实际部署的那版为准
鉴权方式看本地服务配置里有没有开鉴权纯本机自用、只监听回环地址时通常不需要
模型名从服务返回的模型列表里取,不要手写猜换模型频繁时建议做成配置项而不是写死在代码里

流式处理:怎么读、怎么切、怎么收尾

流式返回是一条条独立 JSON,按行切分而不是按整包解析。Java 侧用 BufferedReader 逐行读,读到空行就跳过,读到结束标记就退出循环。关键点是:每行先判断是不是结束标志,再做反序列化,否则最后一行容易抛解析异常;解析失败的行直接丢弃,不要中断整个流。

  1. 建立连接并发起流式请求用 HttpClient 的 BodyHandlers.ofInputStream 拿到字节流,注意请求体里把流式开关打开。
  2. 按行读取用 BufferedReader 包一层 InputStreamReader,逐行读;空行跳过,不要当成一条事件处理。
  3. 判断结束标记先判断当前行是不是结束标志,是就跳出循环;不是再做反序列化,避免最后一行解析报错。
  4. 拼装并推给上层字节流读进来拼成完整行才算一条事件,业务侧拿到的是干净对象而不是裸字符串。
  5. 收尾释放循环结束后关闭流和连接,异常路径也要走 finally 或 try-with-resources,否则连接会泄漏。

把「拼文本」和「推给上层」分开,是为了让排查变简单:流读取层只负责拿到完整行,解析层只负责转对象,业务层只负责消费。某一层出问题时你不用把整条链路翻一遍。另外一定要给流式读加上整体超时和单行读取超时,模型卡住不吐字时能主动断开,否则线程会一直挂在那里。

try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { if (line.isBlank()) continue; if (isDone(line)) break; try { Event e = parse(line); sink.accept(e); } catch (Exception ignored) { } } }

超时、并发与降级怎么设

本地机器算力有限,并发拉高只会让每个请求都变慢。按你那张卡的实际压测结果定并发上限,宁可排队也别硬冲。连接超时设短、读取超时设长,两者分开配;流式场景下读取超时是「两次吐字之间的最长间隔」,不是整个请求的总时长,这个区别不搞清楚,很容易把正常的长回答误判成超时。

  • 并发上限按实际压测结果定,先跑一轮并发梯度测试,找到延迟开始明显抬升的那个点,再往回收一档
  • 连接超时和读取超时分开配,流式场景下读取超时按「两次吐字之间的间隔」来估,不要按总时长估
  • 调用失败要有退路:重试要限次数并加退避,退到最后要么降级到非流式,要么直接返回兜底文案,别让上游一直等
  • 批量任务用固定大小线程池加有界队列,队列满了走拒绝策略,避免任务堆积把内存吃满
  • 如果对延迟敏感又想省钱,可以参考 /price-watch 上的价格变化再决定要不要本地跑,不必一刀切
参数设置思路什么情况下不适用
连接超时设短一些,本地服务连不上就是连不上,等再久也没用服务在容器里冷启动时可能要放宽一点
读取超时流式按两次吐字间隔估,非流式按整体响应时间估回答特别长的场景要单独放宽
并发上限按压测结果定,延迟开始明显抬升就收一档多张卡或服务做了批处理时要重新测
重试次数限次数并加退避,退到最后降级或兜底写操作类请求不要盲目重试,先确认幂等性

日志与排错:出问题先看哪一步

每次调用至少记四个字段:请求耗时、是否流式、返回状态、错误摘要。排查慢和贵的时候全靠这几个。排错顺序建议是:命令行能不能打通、Java 里非流式能不能通、流式能不能通、并发下稳不稳,一步一步往上加,别跳步。跳步的代价是你会同时面对好几个变量,分不清是哪个环节出的问题。

  1. 命令行先打通用命令行工具直接发请求,能拿到返回体说明服务本身没问题,问题在 Java 侧。
  2. Java 非流式跑通在 Java 里先发一次非流式请求,打印原始响应,确认字段名和连接方式都对。
  3. 切到流式把流式开关打开,观察是否按行返回、结束标记是否被正确识别。
  4. 压并发逐步加并发,看延迟和错误率的变化,找到稳定运行的上限。
  5. 存回归样本把一次真实请求的原始响应存成文件做回归样本,改封装代码后拿它跑一遍,比凭印象判断可靠。

常见卡点依次是服务没起来、端口被占、请求体字段名写错、流式没按行切、超时设得不合理,按这个顺序过一遍基本能定位。如果你还在纠结用哪套中转或本地方案,可以到 /sites 看看各家的接入方式和可信度检测结果,再决定自己搭还是走中转。具体的延迟和价格数字会变,以站内当时的记录为准。

先让请求能通,再让流能切,最后才是并发和降级;顺序反了,排查成本会翻好几倍。
下一步把本地服务连通之后,先用一个最小请求验证字段名,再回来加流式和超时。想对比不同接入方式的稳定性与成本,去站内导航和价格追踪页核对当时的数字。

常见问题

Java 里调用本地 Ollama,必须引入官方 SDK 吗?

不一定。只要本地服务提供 HTTP 接口,用 JDK 自带 HttpClient 就够,反而少一层依赖、出问题好定位。SDK 的价值主要在封装好流式解析和重试,如果它帮你省了这些,用也行;但如果它版本更新慢、字段对不上,自己写一个薄封装更可控。

流式返回读到一半卡住不动,一般是什么原因?

大概率是读取超时没设或设得太大,模型那边不吐字了,Java 侧就一直等。先给单行读取设一个合理上限,超时后主动断开并记录日志。另外要检查是不是按行读的——如果按整包读,模型没结束响应你就拿不到数据,看起来也像卡住。

本地跑和换成远端接口,Java 代码要改多少?

如果一开始就把地址、模型名、鉴权信息抽成配置项,切换基本只改配置。真正要改的是超时和并发这两块:远端延迟高、限流严,重试策略和线程池大小都得重调。所以封装时把这两项也做成可配置的,比写死在代码里省事。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。