LangChain4j AiService 实战:会话记忆与结构化输出

简介: LangChain4j AiService 实战:只写接口,框架自动生成实现。讲清会话记忆与三种结构化输出。

大家好,我是晚安code。

上一篇我们把 ChatModel 跑通了,单次对话很爽。可一旦要写个真正能用的机器人,事情就没那么简单了:多轮记忆要自己存、输出 JSON 要自己解析、系统提示词要每个请求手动拼一遍,随便哪步写岔了都在线上炸给你看。这一篇要聊的 LangChain4j AiService,就是专门把这堆脏活收走的。点个收藏,我们开始。

一、为什么需要 AiService:从手写胶水到声明式

直接拿 ChatModel 写业务,代码会越写越肿。以「客服机器人」为例,你要自己维护一个消息列表、自己把历史拼进每次请求、自己从回复里抠出要用的字段。这些活跟业务没关系,却占掉一半代码量。

AiService 换了个思路:你只管定义一个 Java 接口、把需求用注解写清楚,实现类由框架在运行时生成。跟 Spring Data JPA 一个套路——你写 interface UserRepository,框架帮你生成数据库操作;这里你写 interface Assistant,框架帮你生成模型调用。

能力 ChatModel 直调 AiService
多轮记忆 自己存消息、自己拼 配一个 ChatMemory 自动管
系统提示词 每次请求手动塞 SystemMessage @SystemMessage 注解
结构化输出 自己写 JSON 解析 返回类型化对象,自动转换
工具调用 自己实现调用循环 @Tool 注解自动接线

在 AI 应用里,这个对比的意义是:AiService 是 LangChain4j 所有高级能力的统一入口——对话、记忆、工具、知识库,都在这一个接口上装配,别再用低层 API 手工拼。

二、AiService 基础用法:一个接口跑通对话

AiService:LangChain4j 里用 Java 接口声明的 AI 服务。你定义接口、加几个注解,框架运行时生成实现类,替你完成消息组装、模型调用、结果转换。可以理解成「Spring Data JPA 的 Repository,只不过接的是大模型」。示例按 1.17.x 编写(2026 年 8 月),发布前记得核对官方文档。

最小用法,一个接口加一行创建:

public interface Assistant {
   
    @SystemMessage("你是贴心的小助手,用简体中文回复")
    String chat(String userMessage);
}

Assistant assistant = AiServices.create(Assistant.class, model);

String answer = assistant.chat("你好,介绍一下你自己");
System.out.println(answer);

AiServices.create() 是简版,要配记忆、工具的时候换 builder:

Assistant assistant = AiServices.builder(Assistant.class)
        .chatModel(model)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .build();

把接口定义出来,剩下的交给框架生成的代理——这就是声明式开发的甜头。我个人的体会(示例场景):第一次看到 AiServices.create 直接返回接口的实例,真有「写了个空接口它就跑起来了」的魔幻感。背后的动态代理做了三件事:组装消息、调模型、解析返回。

LangChain4j AiService 内部原理:接口注解 → 动态代理 → ChatModel 调用

左边只画接口,中间代理包办一切,右边拿到结果

三、Prompt 提示词模板:把动态内容塞进提示词

写业务时提示词几乎都是带变量的:翻译的语言、总结的文本、要提取的字段,每次都不一样。LangChain4j 用 PromptTemplate 处理:

PromptTemplate template = PromptTemplate.from(
    "把下面这段话翻译成{
   {language}},只输出译文:\n{
   {text}}"
);

Prompt prompt = template.apply(Map.of(
    "language", "英文",
    "text", "你好,世界"
));

ChatResponse response = model.chat(prompt.toUserMessage());
System.out.println(response.aiMessage().text());

在 AiService 里更省事,注解里直接写模板,方法参数自动填进 { {变量}}

public interface Translator {
   
    @UserMessage("把下面这段话翻译成{
   {language}},只输出译文:\n{
   {text}}")
    String translate(@V("language") String language, @V("text") String text);
}

@V 是给变量起名,编译器开了 -parameters 的话可以省略(Spring Boot、Quarkus 默认带)。还有个偷懒写法:变量只有一个时用 { {it}},方法参数自动顶进去。提示词模板的价值在于把「prompt 工程」沉淀成可维护的配置,而不是散落在代码字符串里。

四、ChatMemory:让 AI 记住你说过的话

ChatMemory(会话记忆):保存多轮对话历史的数据结构。大模型本身不记上下文,每次调用都是独立的,是记忆把之前聊过的内容带进下一次请求。

不加记忆的机器人,你上一句刚说完名字,下一句它就忘了,问「我叫什么」直接懵。加了记忆就不一样:

public interface Assistant {
   
    String chat(String userMessage);
}

Assistant assistant = AiServices.builder(Assistant.class)
        .chatModel(model)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .build();

assistant.chat("你好,我是程序员天天困");
assistant.chat("我叫什么名字?"); // 记得住

默认的 MessageWindowChatMemory 是消息窗口:只保留最近 N 条,老消息自动丢。想要更精确地控预算,用 TokenWindowChatMemory——按 token 数而不是条数截断,大模型按 token 计费,预算更好控。

可能有人会问:加了记忆是不是把历史全部塞给模型?

不是。窗口记忆只保留最近 N 条/N 个 token,超出就滚动淘汰。真正全部保留的是「持久化」的事:实现 ChatMemoryStore 接口,把消息落库,重启也不丢。本地 demo 用内存版够了,生产按用户维度隔离,一个用户一条记忆。

多用户场景必须做隔离,否则 A 用户的消息串到 B 用户那里去了。用 @MemoryId + ChatMemoryProvider

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

Assistant assistant = AiServices.builder(Assistant.class)
        .chatModel(model)
        .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
        .build();

assistant.chat("alice", "你好,我是 Alice");
assistant.chat("bob", "你好,我是 Bob");
assistant.chat("alice", "我叫什么名字?"); // 回答 Alice,不会串到 Bob

窗口记忆是默认选择,但生产上更推荐按用户建记忆——消息窗口看条数、Token 窗口看预算,两者都别让单条记忆无限增长。

五、结构化输出三种方式:纯提示词 / JSON Mode / JSON Schema

从模型拿到的是自然语言,业务却要结构化数据:提取客户信息、解析天气、生成卡片 JSON。LangChain4j 给了三条路,严格度递增,对应不同场景。

方式一:纯提示词约束(最通用,最不稳)

在提示词里说清楚「只输出 JSON」,然后自己用 Jackson/Gson 解析:

String answer = model.chat("""
    从下面这段文本里提取客户姓名、手机号和城市,只输出 JSON:
    {
   "name": "...", "phone": "...", "city": "..."}

    文本:张三是杭州的用户,电话 13812345678
    """);

// 自己用 Jackson 解析
Customer customer = objectMapper.readValue(answer, Customer.class);

通用性最强,所有模型都吃这一套。问题也明显:模型可能不老实,多解释一句、加个 Markdown 代码块、字段名跑偏,你的解析器当场崩。

方式二:JSON Mode(保证是合法 JSON)

responseFormat 指定 JSON 类型,模型被约束输出合法 JSON:

OpenAiChatModel model = OpenAiChatModel.builder()
        .apiKey(System.getenv("OPENAI_API_KEY"))
        .modelName("gpt-4o-mini")
        .responseFormat(ResponseFormat.builder()
                .type(ResponseFormatType.JSON)
                .build())
        .build();

JSON Mode 只承诺「是 JSON」,不承诺「长什么样」——字段名、嵌套结构还得靠提示词兜底,解析仍要自己做。适合「只要合法 JSON 就行」的场景,能挡住一半的解析崩溃。

方式三:JSON Schema / 类型化对象(最严格)

把返回定义成一个 record,AiService 直接返回类型化对象,框架自动按 schema 解析、反序列化:

record Customer(
        @Description("客户姓名") String name,
        @Description("手机号") String phone,
        @Description("所在城市") String city
) {
   }

public interface CustomerExtractor {
   
    Customer extract(String text);
}

CustomerExtractor extractor = AiServices.create(CustomerExtractor.class, model);
Customer customer = extractor.extract("张三是杭州的用户,电话 13812345678");
System.out.println(customer.name()); // 张三

框架会把 record 转成 JSON Schema 描述给模型,模型严格按结构输出,连字段含义都通过 @Description 交代清楚。部分供应商(Gemini、Mistral 等)还要在构建模型时声明 supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA) 才开启。

方式 严格度 输出保证 自己解析 兼容性
纯提示词 所有模型
JSON Mode 合法 JSON 多数商业模型
JSON Schema 字段结构 不要 看供应商支持

我的取舍(示例场景):能用 JSON Schema 就别用纯提示词,解析崩溃的排查成本远高于配 schema 的成本;但本地部署的模型经常不支持严格模式,这时候降级到提示词约束最稳。三种方式不是替代关系,是按模型能力从高往低降级的关系

六、小结

这一篇我们把 LangChain4j AiService 这个核心抽象拆开了:一个接口,提示词模板、会话记忆、结构化输出全自动装配。到这儿,一个「有记忆、能稳定吐 JSON」的对话服务已经成型。

下一篇是生产篇,也是系列最后一篇:RAG 知识库、工具调用、Guardrail 护轨、可观测性、SSE 流式输出,把 demo 真正推进生产。


我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你让 AI 吐结构化数据时,被格式坑过几次?

目录
相关文章
人工智能 缓存 前端开发
12026 63
人工智能 JavaScript 开发工具
4812 17
Web App开发 人工智能 API
1385 1
人工智能 Java BI
1472 1
开发工具 Swift git
1974 6
人工智能 JavaScript 测试技术
2406 2
人工智能 自然语言处理 安全
992 0
人工智能 JavaScript 测试技术
1200 4
缓存 JavaScript Shell
2102 3

热门文章

最新文章