LangChain4j用AiServices拼Tool

LangChain4j AiServices:用接口声明式串起 Tool、Memory 与按需 RAG

写 LLM 应用时,最容易膨胀的是「手写消息列表 + 自己拼 tool call + 自己管会话」。LangChain4j 的 AiServices 换了个思路:先声明一个 Java 接口,再用 builder 把 ChatModel、记忆、工具、检索插进去,调用侧只剩一次方法调用。下面按官方 AiServices 教程与入门页落地四件事:接口与 builder、多用户 Memory、@Tool、以及把 RAG 做成按需 Tool。版本以文档示例的 langchain4j / langchain4j-open-ai / langchain4j-bom = 1.21.0 为准;bom 下不少模块仍可能是 1.21.0-beta31 一类,升级时要当潜在 breaking 看待。这里不涉及 Agentic 的 @Agent / sequence。

一、接口 + AiServices:声明式入口

JDK 要求 ≥ 17 。最小形态是:定义助手接口,用 AiServices.create 或 AiServices.builder 绑定模型。教程里最简单的接口甚至可以只有一个 String chat(String userMessage);需要模板变量或与 @MemoryId 并存时,再给参数加上 @UserMessage(否则构建 AiServices 时会抛 IllegalConfigurationException,1.21.0 实测)。

java 复制代码
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

public class AiServicesBasics {

    interface Assistant {
        String chat(String userMessage);
    }

    public static void main(String[] args) {
        ChatModel model = OpenAiChatModel.builder()
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .modelName("gpt-4o-mini")
                .build();

        Assistant assistant = AiServices.create(Assistant.class, model);
        System.out.println(assistant.chat("用一句话解释何为声明式 AI 服务"));
    }
}

OpenAiChatModel.builder().apiKey(...).modelName("gpt-4o-mini").build() 与入门文档一致。需要同时挂记忆、工具、检索时,改用 builder 更清晰:

java 复制代码
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;

public class AiServicesWithMemory {

    interface Assistant {
        String chat(String userMessage);
    }

    static Assistant create(ChatModel model) {
        return AiServices.builder(Assistant.class)
                .chatModel(model)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
                .build();
    }
}

声明式的好处是边界清楚:接口是「对业务暴露的能力」,builder 是「运行时拼装」。模型供应商、窗口大小、工具集合都可以在不动接口签名的前提下替换。文档还提醒:Quarkus / Spring Boot 扩展可以自动装配助手 Bean,那时甚至不必手写 AiServices.create,但核心心智模型不变,仍然是「接口 + 低层组件」。

Chains 在文档里已标 legacy (目前主要是 ConversationalChain / ConversationalRetrievalChain),新项目不要再把 ConversationalChain 当默认方案。需要编排多个助手时,用普通 Java 把多个 AiServices 串起来即可,别回到旧 Chain API。

上线前建议把「接口方法签名」和「builder 里挂了什么」分成两份清单:前者进 API 评审,后者进运维/成本评审(模型、窗口、是否每次检索)。两边分开,改 prompt 或换模型时才不会误伤调用方。

二、ChatMemory:单窗、多用户与驱逐

单会话可以直接 .chatMemory(MessageWindowChatMemory.withMaxMessages(10)),所有调用共享同一块记忆,适合演示或单人后台任务。多用户 / 多会话要用 chatMemoryProvider + @MemoryId:

java 复制代码
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;

public class MultiUserMemoryDemo {

    interface Assistant {
        String chat(@MemoryId String memoryId, @UserMessage String userMessage);
    }

    public static Assistant create(ChatModel model) {
        return AiServices.builder(Assistant.class)
                .chatModel(model)
                .chatMemoryProvider(memoryId ->
                        MessageWindowChatMemory.withMaxMessages(10))
                .build();
    }
}

文档写明两条硬约束,生产里值得写进评审清单:

  1. 同一 @MemoryId 不要并发调用 ,会损坏 ChatMemory;目前没有内置防并发。需要的话在业务侧对 memoryId 加串行(锁、队列、单线程执行器),不要指望框架替你挡。
  2. 方法上没有 @MemoryId 时,provider 收到的 memoryId 默认是字符串 "default"。所有「忘了传会话号」的调用会挤进同一个窗口,排查时很迷惑。

会话结束或用户注销时,让助手接口 继承 ChatMemoryAccess ,即可使用 getChatMemory / evictChatMemory,避免进程内记忆无限涨:

java 复制代码
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.memory.ChatMemoryAccess;

public class EvictMemoryDemo {

    interface Assistant extends ChatMemoryAccess {
        String chat(@MemoryId String memoryId, @UserMessage String userMessage);
    }

    static void endSession(Assistant assistant, String memoryId) {
        assistant.evictChatMemory(memoryId);
    }
}

记忆窗口大小没有万能值:withMaxMessages(10) 是教程常用示例。MessageWindowChatMemory 是按条数的滑动窗口,超出上限时先挤掉最旧的消息,SystemMessage 一旦加入就会保留(1.21.0 实测:上限 3,先放 SystemMessage 再放 4 条用户消息,最后剩 system, u3, u4)。它和 evictChatMemory 不是一回事:前者在窗口里淘汰旧消息,后者是把整段会话的记忆删掉。短客服会话可以更小,长文档讨论可能要更大,但别指望单靠加大窗口替代检索,那是下一节 RAG 的职责。窗口过大时,每次请求都会把历史消息再送给模型,延迟和费用一起涨;窗口过小则「用户刚说过的偏好」转眼忘掉。结合产品会话长度做压测,比拍脑袋抄 10 更靠谱。

还有一个实操细节:@MemoryId 的类型可以是 int、String 等;关键是 业务侧保证同一会话始终用同一个 id ,并且结束时 evict。把 HTTP 会话、用户 id、工单号直接当 memoryId 都可以,但要防止「未登录共享 default」和「登出后 id 复用却未 evict」两类串话。

三、@Tool:把可执行能力交给模型

工具就是普通类上的 @Tool 方法,再通过 .tools(...) 注册。教程示例很直白:加减乘除一类纯函数,模型会在需要时发起调用,AiServices 负责执行并回填。

java 复制代码
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;

public class ToolDemo {

    static class Tools {
        @Tool
        int add(int a, int b) {
            return a + b;
        }

        @Tool
        int multiply(int a, int b) {
            return a * b;
        }
    }

    interface Assistant {
        String chat(String userMessage);
    }

    public static Assistant create(ChatModel model) {
        return AiServices.builder(Assistant.class)
                .chatModel(model)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
                .tools(new Tools())
                .build();
    }
}

工具描述(注解文案或方法名语义)写清楚「做什么、参数含义」,比堆很多模糊工具更不容易误调。工具内部若有副作用(下单、改库),务必在业务层做鉴权与幂等,框架不会替你审计。多工具时保持每个 @Tool 单一职责;把「查天气」和「改配置」塞进同一个巨型方法,既难测也难让模型选对。

成本视角上,每一次暴露给模型的工具都会占用 prompt 配额。文档在「Chaining multiple AI Services」(串联多个 AiServices)一节也强调:用户只是打招呼时,不必把几十个工具一并塞进上下文。能拆成「轻助手 / 重助手」就拆,比一个上帝助手挂满工具更安全,也更省钱。

四、RAG:每次都搜,还是做成 Tool 按需搜

AiServices 支持 .contentRetriever(...) 或 .retrievalAugmentor(...)。默认行为是:每次对话都会走检索。对「知识库问答」很合适;对「你好」「谢谢」这类招呼,则白白多打一轮向量库 / 搜索。

java 复制代码
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.service.AiServices;

public class AlwaysRagDemo {

    interface Assistant {
        String chat(String userMessage);
    }

    static Assistant create(ChatModel model, ContentRetriever retriever) {
        return AiServices.builder(Assistant.class)
                .chatModel(model)
                .contentRetriever(retriever)
                .build();
    }
}

需要查询改写、重排等高级能力时,换成 .retrievalAugmentor(...)(例如文档中的 DefaultRetrievalAugmentor)会更灵活;这里的焦点仍是「挂不挂、何时挂」,不展开改写链路的每个子组件。

文档给出的另一条路叫 RAG as a Tool :把 ContentRetriever 包进一个 @Tool,注册进 .tools(...),由模型决定要不要检索。问候可以不搜,真正的知识问题再搜。官方示例的核心写法如下(ContentRetriever 实例由你的嵌入存储提供):

java 复制代码
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.query.Query;
import dev.langchain4j.service.AiServices;

import java.util.stream.Collectors;

public class RagAsToolDemo {

    static class SearchTool {
        private final ContentRetriever contentRetriever;

        SearchTool(ContentRetriever contentRetriever) {
            this.contentRetriever = contentRetriever;
        }

        @Tool("Search for technical information about LangChain4j and RAG configurations")
        public String search(String query) {
            return contentRetriever.retrieve(new Query(query)).stream()
                    .map(content -> content.textSegment().text())
                    .collect(Collectors.joining("\n\n"));
        }
    }

    interface Assistant {
        String chat(String userMessage);
    }

    public static Assistant create(ChatModel model, ContentRetriever retriever) {
        return AiServices.builder(Assistant.class)
                .chatModel(model)
                .tools(new SearchTool(retriever))
                .build();
    }
}

文档描述的预期行为也很直观:输入「Hello, how are you today?」时,模型可不调工具;输入「How do I configure a ContentRetriever?」这类技术问题时,再触发 search()。工具上的描述文案会影响「何时搜」,值得认真写,别随手写个 search。

选型可以记成:

策略 挂法 适合
总是 RAG .contentRetriever / .retrievalAugmentor 几乎句句都要查库
RAG as Tool @Tool + .tools 招呼与闲聊多、知识问答少

不要同时「每次检索」又「同库再挂一个搜索 Tool」却不加说明。每多一次检索,上下文变长、响应变慢、费用变高(官方在 Chaining multiple AI Services 一节谈到 RAG 的成本);两层检索会不会互相重复,文档没有专门讨论,这是经验判断,上线前用真实问题抽样看一下。选定一种主路径,在系统提示里写清「何时应调用搜索工具」,比叠两层检索更干净。

五、版本与依赖注意点

入门文档示例版本:

  • langchain4j
  • langchain4j-open-ai
  • langchain4j-bom

均为 1.21.0 。用 bom 统一管理时,许多模块仍可能是 1.21.0-beta31 一类 ,API 可能 breaking。升级前看发行说明,别默认「同主版本就二进制兼容」。例如文档 Streaming 一节里的 langchain4j-reactor 就示例为 1.21.0-beta31。

xml 复制代码
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>dev.langchain4j</groupId>
      <artifactId>langchain4j-bom</artifactId>
      <version>1.21.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

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

示例里的类名、注解均来自官方教程。Agentic 的 @Agent / sequence 不在上面这些依赖结论的范围内。你若已经在用 Agentic 编排,那是工作流层;AiServices 仍适合作为「单助手 + Tool + Memory」的声明式内核,两者不必互相取代。

依赖冲突排查时,优先看 bom 导入是否唯一、是否有旧版 langchain4j 被传递依赖拉进来。锁定 1.21.0 核心工件后,再单独评估仍带 beta 后缀的模块是否本迭代必需。注意 beta 不只出现在你显式声明的模块里:实测 langchain4j-open-ai:1.21.0 自己就传递依赖 langchain4j-reactive-streaming:1.21.0-beta31。

六、小结与评审清单

若你把 AiServices 接到 HTTP 层,建议在过滤器里生成或解析会话 id,再传入 @MemoryId,别让控制器随手拼字符串。超时、熔断、限流仍按普通下游依赖治理:模型调用是 I/O,工具调用可能是更重的 I/O,两者超时要分开配置。联调时先关掉 RAG、只留一个无副作用工具,确认记忆与并发约束无误,再打开检索;分层打开,问题更好定位。

AiServices 把「怎么调模型」收成接口,把「用什么模型、什么记忆、什么工具、是否检索」留给 builder。落地时优先记住:

  1. 多用户用 chatMemoryProvider + @MemoryId;同 ID 不要并发;
  2. 接口继承 ChatMemoryAccess,用 evictChatMemory 收尾,防泄漏;
  3. @Tool 描述写清楚,副作用自己鉴权;工具过多就拆成多个助手;
  4. RAG 默认每次检索;招呼多就改成 RAG-as-Tool(ContentRetriever 包进 @Tool);
  5. 版本与 beta 模块的注意事项见第五节,升级前先看发行说明。

按这五条过一遍 PR,比先纠结 prompt 措辞更能减少会话错乱和无效检索。声明式接口稳定之后,再迭代系统提示与检索质量,节奏会顺很多。把「记忆是否会串 / 是否会漏 evict」和「检索是每次还是按需」写成验收项,比只看 Demo 能否跑通更接近生产。

相关阅读:《LangChain4j用Agent拼工作流》。AiServices 是 Tool、Memory、RAG 这一层的声明式接口;那篇讲的是单独发版、带 beta 后缀的 langchain4j-agentic 模块(版本形如 1.20.2-beta30,官方标注为实验性),用来做工作流和 Agent 编排。

封面建议:白底示意图,上方是一块写着 interface Assistant 的面板,下方并排三块,分别是 ChatModel、Memory 加 @MemoryId、@Tool 与 RAG as a Tool,整体以绿色作强调,右下角有「LangChain4j 1.21.0」的小字。

相关推荐
vx_Biye_Design1 小时前
springboot高校选课系统82776-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
Sweet锦2 小时前
不调 Python,不装向量库:我用纯 Java 写了一套以图搜图引擎
java·人工智能·开源·图搜索
如意猴3 小时前
【C++】007 C/C++ 内存管理机制、malloc与new的区别及模板初阶
java·c++·算法
可乐鸡翅yeah_3 小时前
HLS 分片过期清理,直播旧 TS 分片磁盘爆满问题处理
java·后端·spring·m3u8·m3u8在线·音视频在线播放
专业程序开发源3 小时前
SSM校园拍摄交流服务平台36936-计算机课程设计、毕业设计
java·spring boot·后端·python·elasticsearch·php·课程设计
用户094248568033 小时前
第23章:OpenJDK逃逸分析、标量替换与锁优化
java·jvm
wuminyu3 小时前
LockStack在虚拟线程Mount和Unmount拷贝过程剖析
java·linux·c语言·jvm·c++
樱花落木兰4 小时前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存
HAHAXX84 小时前
电商RPA批量上架通用方案:一套流程如何同时跑通拼多多、抖店、淘宝和跨境平台
java·运维·rpa