写Java的同学,最近应该都感受到了这股大模型应用开发的风。身边的同事要么在用Python写Agent,要么在折腾各种LangChain,搞得好像不会点大模型开发就落伍了一样。我用LangChain4j这个Java版框架做了一个月的内部知识库问答系统,今天把整个实战过程完整复盘一遍。这个框架把Java生态和LLM应用开发衔接得很自然,不用换语言,不用重新学一套技术栈,就能把大模型能力接进业务系统。
这篇内容会从选型思路、环境搭建、核心功能开发,到RAG多路召回实践、踩坑记录做一次完整拆解。所有代码都来自我这一个月的实际开发,贴出来的是可以直接抄作业的版本。适合已经在用Java做后端开发、想接触大模型应用、但被Python技术栈劝退的同学,也适合想用私有知识库做问答、做文档分析、做客服机器人这类场景的团队参考。
1. 项目整体设计与选型思路
1.1 为什么在Java生态里选择LangChain4j
先聊点实际的。大模型应用开发圈子确实被Python主导,但国内大量业务系统是Java写的,尤其是金融、电商、企业服务这类领域。为了一个AI功能去引入Python微服务,意味着运维要管两套环境、团队要学新语言、代码要跨语言调用,这一步的成本很多团队根本接受不了。
LangChain4j是专门给Java开发者准备的LLM应用开发框架,设计思路和Python那边的LangChain对齐,但实现上更贴合Java语言习惯。它把大模型调用、提示词模板、对话记忆、文档加载、向量存储这些零零碎碎的事情抽象成一套统一API。我用下来最直接的感受是:不用关心底层走的是OpenAI协议还是本地大模型,代码里切一个配置就行。
这框架的定位不是要做Java版的LangChain复刻,而是把Java生态里成熟的池化、并发、泛型、类型安全这些特性用起来。比如它自带的流式调用返回的是RxJava的Flowable,配合Spring WebFlux做SSE推送非常顺。这点和Python版那种异步回调的方式差别挺大,写惯Java的同学接受度会高很多。
1.2 技术方案取舍对照
我团队成员有长期用Spring Boot + MyBatis的经验,所以选型时对比了几条路线:
| 对比维度 | LangChain4j | Spring AI | 纯手写HTTP调用 |
|---|---|---|---|
| 模型适配层 | 内置多种模型Provider | 内置多种模型Provider | 需要自己封装 |
| 提示词模板 | 类型安全,支持占位符校验 | 基础支持 | 无 |
| 对话记忆 | 内置多种存储方案 | 基础支持 | 无 |
| 结构化输出 | 基于类型自动映射 | 基础映射 | 需要手写解析 |
| RAG组件 | AiServices + EmbeddingStore | VectorStore | 全部手写 |
| Java生态熟悉度 | 高 | 高 | 无 |
手写HTTP调用看着灵活,但一旦涉及流式输出、函数调用、上下文管理、向量检索这些复杂功能,代码量会爆炸,而且每个功能都要自己去趟坑。Spring AI是Spring官方出的,和Spring Boot集成最好,但组件成熟度和生态丰富度目前还比不上LangChain4j。LangChain4j胜在功能全、设计灵活、文档更新快,更适合当主框架用。
提示:如果你团队已经深度使用Spring生态,Spring AI依然值得关注,但当前生产环境下LangChain4j的稳定性和周边支持更靠谱。
2. 环境准备与核心概念
2.1 依赖引入和基础配置
JDK最低要求是17,我这边直接用21。Maven项目加一个依赖就行:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency>这只是核心库。如果对接不同模型服务,还需要加对应依赖。我项目里同时接了OpenAI兼容协议和本地部署的模型,两个依赖都加上了:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>0.35.0</version> </dependency>OpenAI兼容协议这个特别重要,国内很多模型服务商都提供OpenAI兼容的HTTP接口,比如通义千问的兼容模式、智谱的开放平台,还有自己用vLLM部署的开源模型,都可以走这个依赖接入。其实不用太纠结供应商,只要是兼容OpenAI格式的,统统用同一个类。
基础配置用代码写或者配在application.yml里都行。我建议用yml方式,环境切换方便:
langchain4j: open-ai: chat-model: base-url: ${LLM_BASE_URL:https://api.your-provider.com/v1} api-key: ${LLM_API_KEY:sk-xxx} model-name: ${LLM_MODEL:gpt-4o-mini} temperature: 0.7 timeout: 120s2.2 核心抽象概念速览
LangChain4j有一套清晰的核心抽象,第一次看会有点懵,但抓一个主线就通了:一切围绕ChatLanguageModel展开。
- ChatLanguageModel:大模型对话的统一入口,管输入输出,不管是OpenAI还是Ollama,都实现这个接口。平时用OpenAiChatModel.builder()或者OllamaChatModel.builder()创建实例。
- ChatMessage:对话消息的抽象,包含SystemMessage、UserMessage、AiMessage、ToolMessage几种类型,对应不同角色。
- ChatMemory:对话记忆管理器,负责把历史消息拼到下一次请求里。
- EmbeddingModel:负责把文本转成向量,做语义检索用。
- EmbeddingStore:向量数据库的抽象接口,存向量和原始内容。
- AiServices:框架里最强大的类,把大模型、记忆、工具、向量检索全部串成一个带类型的安全接口。
- Tool:或者叫Function Calling,允许让大模型输出结构化调用指令,然后由你的Java代码执行具体操作。
理解这些概念最关键的一点:大模型本身是无状态的API调用,LangChain4j的作用是做编排,把记忆、工具、检索这些能力织成一张网。想通了这个,后面看代码就顺了。
3. 核心功能开发实战
3.1 三步跑通第一个对话
不要从复杂功能开始,先打通最简单的对话链路。第一步创建模型实例,第二步组织消息,第三步发起调用并处理返回。我用OpenAI兼容模式演示,代码非常简单:
ChatLanguageModel model = OpenAiChatModel.builder() .apiKey("sk-xxx") .modelName("gpt-4o-mini") .build(); String response = model.generate("用一句话介绍LangChain4j"); System.out.println(response);就这么短。generate方法可以接受字符串或List ,框架帮你完成请求构造、鉴权、超时重试这些琐事。跑通这一步,后面的复杂功能都是在这个基础上加东西。
从上面这个简单的调用可以看到:调用大模型的成本极低,真正的成本在怎么设计提示词、怎么组织业务逻辑、怎么控制输出质量。我见过很多初学者把大模型当数据库用,问啥答啥完全不设边界,最后做出来的东西根本没法上生产。第一行代码跑通之后,必须考虑工程化的问题。
3.2 带上下文的多轮对话
第一个坑很快就会出现:大模型不记得你上一句说了什么。多轮对话必须自己维护历史消息,把之前的对话内容一并传过去。
用ChatMemory最简单。它会在内存里维护一个消息列表,每次调用前自动注入历史:
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10); ChatLanguageModel model = OpenAiChatModel.builder() .apiKey("sk-xxx") .modelName("gpt-4o-mini") .build(); String firstResponse = model.generate(chatMemory, "我的名字叫李明,请记住"); System.out.println(firstResponse); String secondResponse = model.generate(chatMemory, "我叫什么名字?"); System.out.println(secondResponse);注意这里第二问不需要再传历史,ChatMemory自己把第一轮消息拼进去了。MessageWindowChatMemory是滑动窗口式,只保留最近N条消息,防止Token长度爆掉。如果是超长对话场景,可以考虑用向量库做持久化记忆,按相似度召回相关内容,而不是无脑全量带上。这是我做客服机器人时总结的经验:消息窗口固定20条,再加一个知识库检索模块,两套机制配合使用,既有短期上下文又有长期知识。
那个记忆丢失的坑我踩过一次:MessageWindowChatMemory是默认只有最近消息,当我需要用户画像的时候发现早期的信息已经被挤掉了。后来改造方案是拆成两个记忆层:短期记忆走窗口式,长期用户画像单独存Redis,需要时作为SystemMessage注回。这样的架构在真实业务场景下才站得住脚。
3.3 用AiServices做结构化输出
大模型返回内容是不可控的,它对你说"好的",也可能给你一段废话。真实业务系统里,我希望它直接返回一个对象,比如判断用户意图并解析出参数。
AiServices就是干这个的。先定义一个接口,方法返回值直接写业务对象:
interface CustomerServiceAgent { @SystemMessage("你是客户服务助手,根据用户问题提取意图和参数") Intent detectIntent(String userMessage); } @Data public class Intent { private String type; private String productName; private Integer quantity; private Map<String, String> params; }然后用AiServices.builder()创建代理实例:
CustomerServiceAgent agent = AiServices.builder(CustomerServiceAgent.class) .chatLanguageModel(model) .build(); Intent intent = agent.detectIntent("我要买3个华为手机壳");框架会自动构造提示词,告诉模型:"请提取用户意图,结果用JSON输出并映射到Intent类"。然后解析JSON、类型校验、反序列化,全部内部完成。返回的Intent对象直接可以喂给下游订单系统,不用再写一堆正则解析。
实际开发时,这招比让大模型输出纯文本再解析可靠得多。关键点在于字段名设计要贴近业务,类型要简单,嵌套不要太深。太复杂的结构容易让模型产生幻觉,输出非法JSON,框架内置了解析失败重试逻辑,但重试多次也会抛异常。现在项目里所有和大模型交互的接口,响应全是强类型对象,干干净净。
注意:结构化输出依赖模型的JSON生成能力,太老的模型效果很差。用新一些的模型基本没问题,但如果遇到反复解析失败,先检查是不是模型版本太旧。
3.4 让大模型调用业务方法
结构化输出解决了数据提取问题,Function Calling解决的是行动问题。比如用户说"帮我查一下订单物流",大模型本身不会查库,但它可以输出一段工具调用指令,由你的Java代码真正执行查询。
先用@Tool注解写工具方法:
public class OrderService { @Tool("根据订单号查询物流状态") public String trackOrder(String orderId) { // 在这里查数据库或调外部接口 return "订单" + orderId + "正在派送中,预计今日送达"; } }然后把工具交给AiServices:
interface Assistant { String chat(String message); } OrderService orderService = new OrderService(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(orderService) .build(); String answer = assistant.chat("帮我查一下订单20240815001的物流信息"); System.out.println(answer);内部发生了什么?第一步大模型看到"查物流"相关描述,返回一个ToolCall请求,框架解析后调用OrderService.trackOrder方法拿到结果,再把结果作为ToolMessage回传给模型,最后模型整理成自然语言答案。
这一步是把大模型从"聊天机器人"变成"业务入口"的关键。我实际项目里,用@Tool接了订单查询、库存查询、退换货申请、优惠券发放五个业务方法,客服问答系统直接跑通闭环。工具方法命名要清晰,参数要加注释,因为接口名和参数描述都会被塞进提示词,直接影响模型的选择准确率。工具方法一定要幂等,别让模型反复调一个扣费接口把事情搞重复。
3.5 流式输出体验优化
还是一次性返回文本,用户等待体验很差,一个简单问题转圈好几秒,看着就是卡住了。生产级应用必须做流式输出。
ChatLanguageModel model = OpenAiChatModel.builder() .apiKey("sk-xxx") .modelName("gpt-4o-mini") .build(); TokenStream tokenStream = model.chat("讲一个程序员的笑话"); tokenStream .onPartialResponse(System.out::print) .onCompleteResponse(ignored -> System.out.println("\n---END---")) .start();TokenStream是流式调用的入口,onPartialResponse拿到增量token,可以实时推给前端。如果用的Spring WebFlux,可以配合SseEmitter做服务端推送:
@GetMapping("/chat/stream") public SseEmitter streamChat(@RequestParam String message) { SseEmitter emitter = new SseEmitter(); TokenStream tokenStream = model.chat(message); tokenStream .onPartialResponse(chunk -> { try { emitter.send(chunk); } catch (IOException e) { emitter.completeWithError(e); } }) .onCompleteResponse(r -> emitter.complete()) .onError(e -> emitter.completeWithError(e)) .start(); return emitter; }流式输出上线之后,用户的等待感大大降低。实测首token差不多300ms就能出来,体感和打字机一样。唯一的注意点:流式响应的token计数和用量统计要在complete阶段处理,不要用partial累加,因为重试或中断会导致数据不准。
4. 私域知识库问答(RAG)与多路召回实战
4.1 RAG核心流程
企业里的知识库问答,核心难点不在对话,而在怎么让大模型回答"我们自己文档里的内容"。RAG(检索增强生成)的思路是:把文档切碎、转成向量、存进向量库,用户提问时先检索最相关的片段,再拼进提示词让大模型基于片段作答。
LangChain4j做这件事特别顺。第一步加载文档:
Document document = Document.from("我们的退款政策是:用户可以在购买后7天内申请退款..."); TextSplitter splitter = DocumentSplitters.recursive(300, 30); List<TextSegment> segments = splitter.split(document);第二步建Embedding模型和向量库:
EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey("sk-xxx") .modelName("text-embedding-3-small") .build(); EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();第三步把所有segment转成向量存储:
for (TextSegment segment : segments) { Embedding embedding = embeddingModel.embed(segment.text()).content(); embeddingStore.add(embedding, segment); }第四步,构建RetrievalAugmentor并接入AiServices:
RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder() .contentRetriever(EmbeddingContentRetriever.builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .minScore(0.7) .maxResults(3) .build()) .build(); interface KnowledgeAssistant { String ask(String question); } KnowledgeAssistant assistant = AiServices.builder(KnowledgeAssistant.class) .chatLanguageModel(model) .retrievalAugmentor(augmentor) .build(); String answer = assistant.ask("退款政策是什么?");用户提问时会先向量检索,找出最相关的几个片段,塞进提示词,让大模型只根据片段回答。这样回答就基于私域知识,不会乱编。整个链路跑通后,知识库问答系统就成型了。
4.2 多路召回优化
基础RAG能用,但效果一般。最大的问题在召回质量:只靠向量相似度,关键词匹配不敏感;用户口语化提问和文档书面用语之间存在语义鸿沟,单纯向量检索经常漏召回。
我项目里把召回链路从单路改成了多路召回,效果提升非常明显。第一路是向量检索,做语义召回;第二路是BM25算法做关键词匹配,专门命中专有名词和精确术语;第三路是对用户问题做实体抽取,拿抽出来的实体名去数据库或文档索引做精确匹配。三条结果进一个融合策略,按分数加权取TopN。
多路召回在LangChain4j里实现并不复杂,自带的ContentRetriever接口支持自定义,可以写一个CompositeRetriever把多个检索器组合起来:
public class HybridRetriever implements ContentRetriever { private final EmbeddingContentRetriever vectorRetriever; private final KeywordRetriever keywordRetriever; @Override public List<Content> retrieve(Query query) { List<Content> vectorResults = vectorRetriever.retrieve(query); List<Content> keywordResults = keywordRetriever.retrieve(query); return fuse(vectorResults, keywordResults); } }融合策略这块我没有用太复杂的算法,就是先按来源加权、再按分数排序、最后做一遍去重。实测在内部2000多篇技术文档的知识库上,top5命中率从单路向量检索的62%提升到了79%。加了多路召回后,之前问"接口超时怎么办"这种口语化问题也能精确定位到《服务超时排查手册》了。
提示:向量检索的minScore参数建议从0.7开始调。设太高召回率会骤降,设太低无关内容会大量混入。实际调试时用一批典型问题做回归测试,边调边看效果。
4.3 向量库与文档切分细节
文档切分直接影响检索质量。我之前用过固定长度切分,按200字一刀切,结果把完整段落内容切断了,里面提到的"该服务""此方法"这类指代词孤零零挂在段落末尾,检索出来模型也读不懂。
后来改用递归切分策略,按标题、段落、句子三个层次切:优先按自然段边界切,段太长再按句号拆。每个分块尽量保持语义完整,并且给分块加上文档标题作为元数据。检索的时候标题和正文一起返回,大模型就能理解内容的上下文了。
向量库的选择上,如果数据量在一万条以下,直接用InMemoryEmbeddingStore就够了。它轻量、零部署、进程内存取,适合做原型和小型工具。数据量大了之后,建议换独立的向量数据库。我生产环境用的是开源方案,部署一个单机服务,通过REST API做向量存取。LangChain4j官方有适配器,切换成本很低。
另外一个坑是Embedding模型必须固定。测试时如果换了Embedding模型,之前存的向量全部需要重新生成,否则向量空间不一致,检索结果会完全乱掉。这个我在切换模型时没注意,排查了两个小时才发现线上检索效果断崖式下降,最后跑了一夜的脚本重新灌库才恢复。
5. 与Spring Boot整合的工程化实践
5.1 封装为Spring Bean
前面示例代码都是在main方法里演示,真实项目必须整合Spring Boot。把模型、Agent、工具类都注册成Bean,用配置类统一管理。这样业务代码只需要依赖注入,不需要关心实例化过程。
@Configuration public class LangChain4jConfig { @Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .apiKey(config.getApiKey()) .modelName(config.getModelName()) .timeout(Duration.ofSeconds(120)) .build(); } @Bean public EmbeddingModel embeddingModel() { return OpenAiEmbeddingModel.builder() .apiKey(config.getApiKey()) .modelName("text-embedding-3-small") .build(); } @Bean public CustomerServiceAgent customerServiceAgent(ChatLanguageModel model) { return AiServices.builder(CustomerServiceAgent.class) .chatLanguageModel(model) .tools(orderService, refundService) .build(); } }有了Bean之后,业务代码就清爽了,比如写一个Controller:
@RestController @RequestMapping("/api/chat") public class ChatController { @Resource private CustomerServiceAgent agent; @PostMapping public R<Intent> chat(@RequestBody ChatRequest request) { Intent intent = agent.detectIntent(request.getMessage()); return R.ok(intent); } }5.2 并发与性能调优
大模型API调用是典型的IO密集型操作,一个请求可能阻塞几十秒。千万别在主线程里直接调用模型,会直接把线程池打满。我在网关层和业务层都做了异步化改造。
长耗时请求用CompletableFuture异步编排,配合虚拟线程(JDK 21)效果最好:
@Bean(name = "llmExecutor") public Executor llmExecutor() { return Executors.newVirtualThreadPerTaskExecutor(); } public CompletableFuture<Intent> detectIntentAsync(String message) { return CompletableFuture.supplyAsync(() -> agent.detectIntent(message), llmExecutor); }另一个重点是超时与重试。Model API偶尔会抖动,单个请求卡30秒,全部请求都在等,体验直接崩掉。LangChain4j在模型层面有超时设置,建议设120秒上限。我还在外层加了一个带重试的请求包装器,第一次失败重试一次,间隔两秒,最多重试两次,超过就快速失败降级。核心经验:流式接口的超时时间和普通接口要分开设,流式超时不只是首字节时间,还要考虑整个流的读取周期。
5.3 数据一致性保障
知识库更新场景最容易出问题。业务侧可能同时更新文档和删除文档,向量库里必须保持同步,否则用户会检索到旧内容。
我用了事件驱动方案:文档变更时发一条MQ消息,消费者收到后重新加载对应文档内容,删除旧向量并插入新向量,保证两边数据一致性。消息带上文档ID、版本号,消费端按版本号判断是否需要处理,避免乱序。这套方案上线后,再也没有出现过"文档已经更新但问答还是旧答案"的投诉。中间件上,Redis放会话状态,MySQL放订单业务数据,向量库存知识片段,各自职责明确,通过消息组件解耦。
唯一要强调的是,向量库的更新和业务库更新不是强事务关系,做不到原子提交。现阶段业界普遍接受"最终一致":业务数据更新成功后发消息,异步刷新向量库。这条链路的延迟大概几秒钟,用户无感知。
6. 常见问题与排查技巧实录
6.1 高频踩坑速查表
按这一个月实操下来的频率,把坑整理成一张排查表:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 调用返回401 | API Key配错或已过期 | 检查yml配置,确认环境变量覆盖 |
| 请求超时 | 模型负载高、网络链路慢 | 调大timeout,实现流式输出 |
| 返回内容被截断 | maxTokens设置过小 | 调大maxTokens,排查对话窗口被塞满 |
| JSON解析失败 | 模型幻觉输出非法JSON | 升级模型,简化字段结构,使用AiServices强类型返回 |
| 召回内容不相关 | minScore太高或Embedding模型不匹配 | 调低minScore,固定Embedding模型重新灌库 |
| 多轮对话"失忆" | ChatMemory窗口太小或未持久化 | 调大窗口,或启用持久化记忆 |
| 工具反复被调用 | 工具描述不清或提示词引导不够 | 精炼@Tool的name和description |
| 响应速度奇慢 | 同步阻塞调用 | 改流式,改异步线程池 |
| 内存激增 | 对话消息无限堆积 | 用窗口式ChatMemory限制条数 |
6.2 一个典型的"知识库不回答"实战排查
某天运营反馈:知识库明明有《发票开具流程》,用户问"发票怎么开"时系统答非所问。
第一轮排查先看召回:在日志里把检索到的内容片段打出来,发现返回的是《报销流程》里的句子,分数0.62,低于minScore阈值,被过滤了。原来文档里写的是"开发票"而非"开票",用户口语"发票怎么开"和文档表述的向量距离比较远。这就是单路向量检索的典型短板。
第二轮排查是对策:把BM25关键词检索加进去。BM25对"发票"这种精确词命中很好,直接在《发票开具流程》里匹配到了相关段落。融合后得分通过了阈值,问答恢复正常。从这个案例能看出,多路召回不是炫技,是真实业务环境下的刚需。
另外一个案例是关于工具调用的。有一次用户问"订单在哪里",系统把trackOrder工具调用了一遍又一遍,返回结果还是错误的。打开日志发现模型把"订单号"参数传成了用户ID,工具方法要求String类型的订单ID,模型就从用户问题里随手抓了个数字塞进去。这个问题的根源是@Tool的参数描述不够具体,模型不知道如何正确提取参数。我把参数描述改成了"用户提供的完整订单号,格式为纯数字字符串,例如20240815001",误调用率立刻下降。
6.3 成本控制与Token优化技巧
Token直接等于钱。上线后第一周账单比我预想的高了不少,查日志发现大部分请求的输入Token都很大。罪魁祸首是系统提示词太长,加了很多无关紧要的背景设定,每次请求都带着跑。
优化方案:
- 系统提示词精简到最短能表达意图的程度
- 历史消息只保留最近的几轮
- 长文档检索结果控制在3段以内
- 不是所有请求都需要高精度模型,简单意图识别用便宜模型,复杂推理用高配模型
- 夜间的离线批处理任务,改用异步低优先级队列
综合下来,在不影响问答质量的情况下,API成本大约降了40%。这里面最大的收益来自"按场景选模型",聊天问答用轻量模型,文档分析用推理强的模型,成本差异好几倍。
7. 进阶扩展与个人经验
7.1 从Demo到生产还要补什么
跑通Demo只是第一步,上生产之前还有一堆工程化事情要补:
- 模型API密钥不能硬编码,用配置中心或KMS管理
- 所有模型调用都要有trace日志,记录输入、输出、Token消耗
- 加一层安全过滤,防止用户通过提示词注入让模型输出违规内容或绕过业务限制
- 线上模型和本地模型之间做降级切换,比如主模型不可用时自动切到备用模型
- 评估集自动化回归,每隔一段时间跑一遍典型问题集,验证答案质量没有退化
这些看起来不酷,但都是生产事故的救援绳。我见过有团队没做降级,模型服务商一次故障,整个客服系统直接瘫痪四小时。
7.2 后续可以扩展的方向
基础问答跑通后,可以往更复杂的方向延伸:对接企业内部的数据库,让模型根据用户问题自动生成SQL查询并解释结果;做多Agent协作,让规划Agent拆解任务、执行Agent调用工具、检查Agent审核结果;把工作流引擎和模型编排结合起来,把多步骤业务逻辑用可视化或代码方式定义清楚。
我自己的下一步计划是把代码评审Agent做实:让模型读Git diff,结合团队规范做自动审查。这个场景对结构化输出和工具调用要求高,但价值非常大。后续做完我会再写一篇详细的实战记录分享出来。
最后分享一个我个人的体会:用LangChain4j做AI应用,本质上做的是"大模型与工程体系的桥接"。模型的能力是天花板,工程的质量是地板,地板塌了,天花板再高也没用。框架给了你脚手架,但每个业务场景的边界、安全、成本、体验,还是要靠自己一点点打磨。这个项目做下来,最大的收获不是会调几个API,而是对整个AI应用工程化的理解深了一整个层次。