LangChain4j核心能力:AiService、Prompt注解与结构化输出实战

导读 :如果你用过 Spring Data JPA,一定对"只定义接口就能查数据库"这种体验印象深刻。LangChain4j 的 @AiService 做的事情如出一辙------只定义接口,就能调大模型。本文将从 @AiService 的设计理念出发,深入讲解 Prompt 注解体系(@SystemMessage@UserMessage@V)和结构化输出能力,帮你彻底掌握 LangChain4j 中最核心、最优雅的开发范式。读完本文,你会发现:与 Spring AI 相比,LangChain4j 在接口抽象层面确实做到了更少的代码、更清晰的语义。


一、@AiService:用接口定义 AI 能力

1.1 一个类比搞懂核心思想

LangChain4j 最有特色的设计就是 @AiService。它的思路和 Spring Data JPA 一模一样:

框架 你做的事 框架帮你做的事
Spring Data JPA 定义 Repository 接口 自动生成 SQL、执行查询、返回实体
LangChain4j 定义 AI Service 接口 自动构建 Prompt、调用大模型、解析返回值

换句话说,JPA 是"定义接口自动查数据库",@AiService 是"定义接口自动调大模型"

1.2 最简示例:一个接口搞定 AI 对话

来看项目中的 SimpleAssistantcom.jichi.langchain4j.service.SimpleAssistant):

复制代码
package com.jichi.langchain4j.service;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;

@AiService  // 告诉 LangChain4j:这是一个 AI 服务,帮我生成实现
public interface SimpleAssistant {

    @SystemMessage("你是一个友好的 AI 助手,用简洁的语言回答问题")
    String chat(String userMessage);
}

Controller 中直接注入调用(com.jichi.langchain4j.controller.aiservice.AssistantController):

复制代码
@RestController
@RequestMapping("/assistant")
public class AssistantController {

    private final SimpleAssistant assistant;

    public AssistantController(SimpleAssistant assistant) {
        this.assistant = assistant;
    }

    @GetMapping
    public String ask(@RequestParam String question) {
        return assistant.chat(question);
    }
}

没有 chatModel.call(),没有 getResult().getContent(),一切都被代理层屏蔽了。你写的代码看起来就像在调一个普通的 Java 方法。

1.3 工作原理

Spring Boot 启动时,LangChain4j 会扫描所有带有 @AiService 注解的接口,为它们动态生成代理对象并注入到 Spring 容器。当你调用接口方法时,代理内部做了以下几件事:

  1. 读取角色设定 :解析 @SystemMessage 注解,构建 System Prompt
  2. 包装用户消息:将方法参数封装为 User Message
  3. 构建消息列表:如果有历史对话(多轮记忆),会一并构建完整的 Message 列表
  4. 调用大模型:通过 ChatModel 发起实际请求
  5. 解析返回值:将模型的响应自动转换为方法声明的返回类型

这正是代理模式的经典应用------把所有"脏活累活"封装在代理层,开发者只需关注接口契约。

1.4 一个接口挂载多个能力

一个 @AiService 接口可以定义多个方法,每个方法拥有独立的 System Prompt。来看项目中的 MultiCapabilityAssistantcom.jichi.langchain4j.service.MultiCapabilityAssistant):

复制代码
package com.jichi.langchain4j.service;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;

@AiService
public interface MultiCapabilityAssistant {

    @SystemMessage("你是一个 Java 技术助手,专注于代码质量和性能优化")
    String reviewCode(String code);

    @SystemMessage("""
            你是一个技术文档写作专家。
            把技术内容转化为清晰易懂的文档,有条理,有示例。
            """)
    String writeDoc(String techContent);

    @SystemMessage("你是一个 SQL 专家,帮助优化数据库查询")
    String optimizeSql(String sql);
}

对应的 Controller(com.jichi.langchain4j.controller.aiservice.DevAssistantController):

复制代码
@RestController
@RequestMapping("/dev")
public class DevAssistantController {

    private final MultiCapabilityAssistant assistant;

    public DevAssistantController(MultiCapabilityAssistant assistant) {
        this.assistant = assistant;
    }

    @PostMapping("/review")
    public String reviewCode(@RequestBody String code) {
        return assistant.reviewCode(code);
    }

    @PostMapping("/doc")
    public String writeDoc(@RequestBody String techContent) {
        return assistant.writeDoc(techContent);
    }

    @PostMapping("/sql")
    public String optimizeSql(@RequestBody String sql) {
        return assistant.optimizeSql(sql);
    }
}

调用不同方法时,会自动走对应的 System Prompt,互不干扰。一目了然地知道这个接口有哪些 AI 能力,不需要翻看实现细节------这就是接口抽象的魅力。

1.5 多模型切换:让贵的干复杂活,便宜的干简单活

生产环境中,我们通常会配置多个模型------功能强但成本高的主模型,以及速度快、价格低的备用模型。LangChain4j 支持在 @AiService 级别指定使用哪个模型。

首先,将备用模型注册为 Spring Bean:

复制代码
@Configuration
public class ModelConfig {

    @Bean("cheapModel")
    public ChatLanguageModel cheapModel() {
        return OpenAiChatModel.builder()
                .baseUrl("https://api.example.com")
                .apiKey("your-api-key")
                .modelName("gpt-3.5-turbo")
                .build();
    }

    @Bean("mainModel")
    public ChatLanguageModel mainModel() {
        return OpenAiChatModel.builder()
                .baseUrl("https://api.example.com")
                .apiKey("your-api-key")
                .modelName("gpt-4")
                .build();
    }
}

然后在 @AiService 中通过 chatModel 属性指定:

复制代码
@AiService(chatModel = "cheapModel")
public interface SimpleTaskService {
    @SystemMessage("你是一个简单问答助手")
    String answer(String question);
}

@AiService(chatModel = "mainModel")
public interface ComplexTaskService {
    @SystemMessage("你是一个资深架构师,擅长分析复杂技术问题")
    String analyze(String topic);
}

注意 :当容器中存在多个 ChatModel Bean 时,所有 @AiService 都必须显式指定 chatModel,否则 Spring 会因为无法确定注入哪个 Bean 而报错。

1.6 手动构建:AiServices.builder()

除了注解方式,LangChain4j 也提供了编程式的构建方式,适合需要动态配置的场景。来看项目中的 TenantChatAssistantcom.jichi.langchain4j.service.TenantChatAssistant),它是一个不加 @AiService 的纯接口,由 AiServices.builder() 编程式构建:

复制代码
package com.jichi.langchain4j.service;

import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;

// TenantChatAssistant 是不加 @AiService 的纯接口,由 AiServices.builder() 编程式构建
public interface TenantChatAssistant {
    String chat(@MemoryId String sessionId, @UserMessage String message);
}

构建方式(摘自 TenantAwareAssistantService):

复制代码
AiServices.builder(TenantChatAssistant.class)
        .chatModel(chatModel)
        // @MemoryId 场景必须用 chatMemoryProvider,不能用 chatMemory
        .chatMemoryProvider(memId -> MessageWindowChatMemory.withMaxMessages(10))
        .systemMessageProvider(memId -> systemPrompt)
        .build();

这种方式和注解方式功能完全一致,只是从声明式变成了命令式,在需要运行时动态创建 AI Service 的场景下更加灵活。

1.7 对比 Spring AI:代码量少一半

实现同样的"代码审查 + 文档生成 + SQL 优化"三个能力,Spring AI 需要:

复制代码
// Spring AI 写法 - 每个能力都要手写调用逻辑
String result = chatClient.prompt()
        .system("你是一个Java开发工程师...")
        .user(code)
        .call()
        .content();

而 LangChain4j 只需要定义接口:

复制代码
// LangChain4j 写法 - 定义即能力(摘自 MultiCapabilityAssistant)
@SystemMessage("你是一个 Java 技术助手,专注于代码质量和性能优化")
String reviewCode(String code);

代码量几乎减半,而且接口本身就是能力清单,可读性极强。


二、Prompt 注解体系:@SystemMessage、@UserMessage 与 @V

有了 @AiService 的基础,接下来我们深入 Prompt 注解体系。LangChain4j 提供了三个核心注解来管理 Prompt:@SystemMessage@UserMessage@V

2.1 @SystemMessage:角色设定

@SystemMessage 用于设定 AI 的角色和行为规则,加在方法(或接口)上。

单行写法 (摘自 SimpleAssistant):

复制代码
@SystemMessage("你是一个友好的 AI 助手,用简洁的语言回答问题")
String chat(String userMessage);

多行写法 ------使用文本块(摘自 MultiCapabilityAssistant):

复制代码
@SystemMessage("""
        你是一个技术文档写作专家。
        把技术内容转化为清晰易懂的文档,有条理,有示例。
        """)
String writeDoc(String techContent);

2.2 @V 注解:Prompt 变量注入

很多时候 System Prompt 不能写死,需要动态传入参数。@V 注解就是做这件事的------将方法参数绑定到 Prompt 模板中的占位符。来看项目中的 FileBasedAssistantcom.jichi.langchain4j.service.FileBasedAssistant):

复制代码
@AiService
public interface FileBasedAssistant {

    @SystemMessage(fromResource = "prompts/customer-service.txt")
    String chat(@V("companyName") String company,
                @V("serviceScope") String scope,
                @UserMessage String message);
}

调用时:

复制代码
service.chat("鸡哥商城", "商品、订单、售后", "我的订单在哪?");

实际发送给模型的 System Prompt 就变成了:"你是"鸡哥商城"的智能客服助手。服务范围:商品、订单、售后..."。

@V 的使用规则很简单:@V("name") 中的 name 必须与 Prompt 模板中 {``{name}} 一一对应

2.3 @UserMessage:控制用户消息格式

默认情况下,方法中的 String 参数就是 User Message。但如果你想对用户消息做更精细的控制,比如组合多个变量,就需要 @UserMessage 注解。

方式一:模板化用户消息 (摘自 Translatorcom.jichi.langchain4j.service.Translator):

复制代码
@AiService
public interface Translator {

    @SystemMessage("你是一个专业翻译")
    @UserMessage("将以下文字翻译成{{language}}:\n{{text}}")
    String translate(@V("language") String language, @V("text") String text);
}

方式二:标注在参数上 (摘自 TenantChatAssistant):

复制代码
public interface TenantChatAssistant {
    String chat(@MemoryId String sessionId, @UserMessage String message);
}

此时 message 参数会被明确标记为用户消息,语义更加清晰。

2.4 @V 支持对象映射

当变量很多时,逐个用 @V 标注显得冗余。LangChain4j 支持直接传入对象,框架会自动将对象的字段映射到 Prompt 模板的占位符。来看项目中的 CodeReviewercom.jichi.langchain4j.service.CodeReviewer):

首先定义请求对象(com.jichi.langchain4j.model.CodeReviewRequest):

复制代码
package com.jichi.langchain4j.model;

public record CodeReviewRequest(
    String language,
    String code,
    String focusAreas  // "性能、空指针、事务"
) {}

然后在接口中直接使用对象参数:

复制代码
@AiService
public interface CodeReviewer {

    @SystemMessage("你是代码审查专家")
    @UserMessage("""
            请 review 以下 {{language}} 代码:

            ```{{language}}
            {{code}}
            ```

            重点关注:{{focusAreas}}
            """)
    String review(CodeReviewRequest request);
}

只要对象的字段名与 {``{}} 中的变量名一致,就能自动映射,无需额外注解。

2.5 从文件加载 Prompt

当 Prompt 较长或需要非开发人员维护时,把 Prompt 写在代码里就不合适了------每次修改都要重新编译。LangChain4j 支持从资源文件加载 Prompt。

项目中 resources/prompts/customer-service.txt 的内容:

复制代码
你是"{{companyName}}"的智能客服助手。

服务范围:{{serviceScope}}

约束:
- 只回答与{{companyName}}相关的问题
- 不确定的说"我帮您确认一下",不要猜测
- 回复不超过 150 字

然后在接口中通过 fromResource 引用(摘自 FileBasedAssistant):

复制代码
@AiService
public interface FileBasedAssistant {

    // fromResource 指定加载的文件路径
    @SystemMessage(fromResource = "prompts/customer-service.txt")
    String chat(@V("companyName") String company,
                @V("serviceScope") String scope,
                @UserMessage String message);
}

这样产品经理或运营人员可以直接修改 txt 文件来调整 AI 的行为,无需改代码、无需重新编译。

2.6 动态 System Prompt:多租户 SaaS 场景

在 SaaS 系统中,不同租户可能需要完全不同的 AI 人设------有的要严谨客服风格,有的要幽默助手风格,有的要推销员风格。这种场景下,静态的 @SystemMessage 就不够用了。

项目中通过 TenantPrompt 实体将 Prompt 存储在数据库中(com.jichi.langchain4j.model.TenantPrompt):

复制代码
@Entity
@Table(name = "tenant_prompt")
@Data
public class TenantPrompt {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String tenantId;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;
}

然后通过 TenantAwareAssistantServicecom.jichi.langchain4j.service.TenantAwareAssistantService)实现运行时动态切换:

复制代码
@Service
public class TenantAwareAssistantService {

    private final ChatModel chatModel;
    private final TenantPromptRepository promptRepo;
    private final Map<String, TenantChatAssistant> assistantCache = new ConcurrentHashMap<>();

    public TenantAwareAssistantService(ChatModel chatModel, TenantPromptRepository promptRepo) {
        this.chatModel = chatModel;
        this.promptRepo = promptRepo;
    }

    public String chat(String tenantId, String sessionId, String message) {
        TenantChatAssistant assistant = assistantCache.computeIfAbsent(tenantId, id -> {
            String systemPrompt = promptRepo.findByTenantId(id)
                    .map(TenantPrompt::getContent)
                    .orElse("你是一个通用助手");

            return AiServices.builder(TenantChatAssistant.class)
                    .chatModel(chatModel)
                    // @MemoryId 场景必须用 chatMemoryProvider,不能用 chatMemory
                    .chatMemoryProvider(memId -> MessageWindowChatMemory.withMaxMessages(10))
                    .systemMessageProvider(memId -> systemPrompt)
                    .build();
        });

        return assistant.chat(sessionId, message);
    }
}

核心思路:将 Prompt 存储在数据库中,运行时根据租户 ID 动态查询并注入 。配合 @MemoryId 可以实现会话级别的隔离,每个租户拥有独立的对话记忆。

2.7 注解优先级

当多个注解同时存在时,优先级规则如下:

  1. 方法级 @SystemMessage > 类级 @SystemMessage(方法粒度更细,优先级更高)
  2. @UserMessage 注解 > 默认参数作为用户消息(显式声明优先于隐式约定)

记住一个原则:越具体的声明,优先级越高


三、结构化输出:告别手动 JSON 解析

3.1 直接返回 Java 对象

这是 LangChain4j 最让人惊喜的能力之一。在 @AiService 的方法中,返回值可以是任意 Java 类型------不需要手动解析 JSON,不需要调用 .entity() 方法。

来看项目中的情感分析。先定义返回类型(com.jichi.langchain4j.model.SentimentResult):

复制代码
package com.jichi.langchain4j.model;

import java.util.List;

public record SentimentResult(
        String sentiment,       // POSITIVE / NEGATIVE / MIXED / NEUTRAL
        List<String> reasons,   // 判断依据
        int score               // 1-10 分
) {}

然后定义 AI Service 接口(com.jichi.langchain4j.service.SentimentAnalyzer):

复制代码
@AiService
public interface SentimentAnalyzer {

    @SystemMessage("""
            你是情感分析专家。
            分析用户评论的情感,给出情感类别、判断依据和评分。
            """)
    SentimentResult analyze(String review);
}

Controller 中直接调用(com.jichi.langchain4j.controller.structed.SentimentController):

复制代码
@RestController
@RequestMapping("/structured/sentiment")
public class SentimentController {

    private final SentimentAnalyzer analyzer;

    public SentimentController(SentimentAnalyzer analyzer) {
        this.analyzer = analyzer;
    }

    @GetMapping
    public SentimentResult analyze(@RequestParam String review) {
        return analyzer.analyze(review);
    }
}

完全像调用一个普通方法一样------传入字符串,返回对象。你甚至感知不到背后有大模型在工作。

3.2 背后的魔法

框架在背后做了三件事:

  1. Schema 生成 :将 Java 类(如 SentimentResult record)转换为 JSON Schema
  2. Prompt 注入:将 Schema 注入到 Prompt 中,要求模型按照指定格式输出 JSON
  3. 反序列化:将模型返回的 JSON 字符串反序列化为 Java 对象

这个过程对开发者完全透明,你只需要定义好返回类型的类结构即可。

3.3 枚举类型:固定选项输出

当 AI 的输出是有限选项之一时,可以直接返回枚举。来看项目中的工单分类。先定义枚举(com.jichi.langchain4j.model.TicketCategory):

复制代码
package com.jichi.langchain4j.model;

public enum TicketCategory {
    BILLING, TECH_SUPPORT, FEATURE_REQUEST, ACCOUNT, OTHER
}

然后定义分类器接口(com.jichi.langchain4j.service.TicketClassifier):

复制代码
@AiService
public interface TicketClassifier {

    @SystemMessage("""
            对客户工单进行分类。
            BILLING:账单/付款问题
            TECH_SUPPORT:技术故障
            FEATURE_REQUEST:功能建议
            ACCOUNT:账号/权限问题
            OTHER:其他
            """)
    TicketCategory classify(String ticket);
}

TicketCategory category = classifier.classify("我的信用卡被扣了两次");
// category -> TicketCategory.BILLING

枚举输出在意图识别、状态分类等场景中非常实用。

3.4 复杂嵌套结构

LangChain4j 的结构化输出支持任意深度的嵌套。来看项目中的合同信息提取。涉及三个 record 类:

ContractInfocom.jichi.langchain4j.model.ContractInfo):

复制代码
public record ContractInfo(
    String contractNumber,
    List<ContractParty> parties,
    String signDate,
    Double amount,
    String currency,
    List<String> keyObligations,
    List<String> warnings
) {}

ContractPartycom.jichi.langchain4j.model.ContractParty):

复制代码
public record ContractParty(String role, String name, String contactPerson) {}

对应的提取器接口(com.jichi.langchain4j.service.ContractExtractor):

复制代码
@AiService
public interface ContractExtractor {

    @SystemMessage("""
            你是合同信息提取专家。
            只提取文本中明确表述的信息,不推断不猜测。
            日期统一转为 YYYY-MM-DD 格式。
            无法确定的字段填 null,列表无内容填空列表。
            """)
    ContractInfo extract(String contractText);
}

只要定义好类的层级关系,框架就能将模型的输出正确地映射到嵌套对象中。String、int、Double、List、嵌套 record,统统支持。

项目中还有联系人提取(返回 Optional<ContactInfo>)和命名实体提取(返回 List<NamedEntity>),展示了更多返回类型的灵活性:

复制代码
// ContactExtractor - 返回 Optional,找不到时为空
@AiService
public interface ContactExtractor {
    @SystemMessage("从文本中提取联系人信息,如果某个字段找不到,对应字段为 null")
    Optional<ContactInfo> extract(String text);
}

// EntityExtractor - 返回 List,提取多个实体
@AiService
public interface EntityExtractor {
    @SystemMessage("从文本中提取所有命名实体(人名、地名、公司名、产品名)")
    List<NamedEntity> extract(String text);
}

3.5 流式输出:TokenStream

对于需要实时展示生成过程的场景(如打字机效果),可以将返回类型改为 TokenStream。来看项目中的 StreamingAssistantcom.jichi.langchain4j.service.StreamingAssistant):

复制代码
package com.jichi.langchain4j.service;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.spring.AiService;

@AiService
public interface StreamingAssistant {

    @SystemMessage("你是一个写作助手")
    TokenStream write(String topic);
}

Controller 中通过 SSE 返回(com.jichi.langchain4j.controller.structed.StreamingWriteController):

复制代码
@RestController
@RequestMapping("/stream")
public class StreamingWriteController {

    private final StreamingAssistant streamingAssistant;

    public StreamingWriteController(StreamingAssistant streamingAssistant) {
        this.streamingAssistant = streamingAssistant;
    }

    @GetMapping(value = "/write", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter write(@RequestParam String topic) {
        SseEmitter emitter = new SseEmitter(60_000L);

        streamingAssistant.write(topic)
                .onPartialResponse(token -> {
                    try {
                        emitter.send(token);
                    } catch (Exception e) {
                        emitter.completeWithError(e);
                    }
                })
                .onCompleteResponse(response -> emitter.complete())
                .onError(emitter::completeWithError)
                .start();

        return emitter;
    }
}

只需将返回类型从 String 改为 TokenStream,其他调用方式完全不变。TokenStream 是 LangChain4j 内置的类型,开箱即用。

3.6 对比 Spring AI

Spring AI 的结构化输出需要显式调用 .entity() 并传入目标类:

复制代码
// Spring AI 写法
SentimentResult result = chatClient.prompt()
        .system("分析情感倾向")
        .user(review)
        .call()
        .entity(SentimentResult.class);

LangChain4j 则完全隐藏了这个过程:

复制代码
// LangChain4j 写法(摘自 SentimentAnalyzer)
SentimentResult result = analyzer.analyze(review);

一行代码搞定,类型安全由编译器保证,开发体验显著提升。


四、参数传递规则总结

LangChain4j 的参数传递遵循以下规则:

参数类型 行为
普通 String 参数 直接作为 User Message 内容
@UserMessage String 明确标记为 User Message
@V("name") String 绑定到 Prompt 模板中的 {``{name}} 变量
@MemoryId String 对话记忆隔离 ID(用于多轮对话)
对象参数 自动将字段映射到模板变量

一个综合示例------项目中的 MultiScenarioAssistantcom.jichi.langchain4j.service.MultiScenarioAssistant)完整展示了这些用法:

复制代码
@AiService
public interface MultiScenarioAssistant {

    // 场景一:简单问答
    @SystemMessage("你是一个 Java 技术助手,回答简洁")
    String techChat(String question);

    // 场景二:有格式要求的翻译
    @SystemMessage("你是专业翻译,保持原文风格,不添加解释")
    @UserMessage("将以下内容翻译成{{language}}:\n\n{{content}}")
    String translate(@V("language") String lang, @V("content") String content);

    // 场景三:从文件加载复杂 Prompt
    @SystemMessage(fromResource = "prompts/code-review.txt")
    String reviewCode(@V("language") String lang, @UserMessage String code);

    // 场景四:多变量 + 对象参数
    @SystemMessage("你是数据分析专家")
    @UserMessage("分析以下{{period}}的销售数据,重点关注{{focus}}:\n{{data}}")
    String analyzeData(@V("period") String period,
                       @V("focus") String focus,
                       @V("data") String data);
}

五、总结

本文围绕 LangChain4j 三大核心能力展开:

@AiService 是 LangChain4j 最具特色的设计。它借鉴了 Spring Data JPA "定义接口即实现功能"的思路,通过动态代理将大模型调用封装在接口背后。开发者只需关注接口契约,不需要关心调用细节。支持单接口多能力、多模型切换、手动构建等灵活用法。

Prompt 注解体系 提供了 @SystemMessage@UserMessage@V 三个核心注解,覆盖了从静态配置到动态注入、从单行到多行、从硬编码到文件加载的全部场景。在多租户 SaaS 场景下,还可以结合数据库实现运行时动态切换 System Prompt。

结构化输出 让 AI 的返回值和普通 Java 方法一样类型安全。支持 record、枚举、嵌套对象、Optional、List、流式 TokenStream 等多种返回类型,框架自动完成 JSON Schema 生成、Prompt 注入和反序列化的全流程。

一句话概括:LangChain4j 的核心哲学是"用接口描述意图,让框架处理细节" 。如果你的项目是 Java 技术栈,正在选型大模型集成框架,LangChain4j 的 @AiService 绝对值得一试。

相关推荐
RInk7oBjo2 小时前
spring boot3--自动配置与手动配置
java·spring boot·后端
lixia0417mul22 小时前
简单的RAG知识库问答
java
云烟成雨TD2 小时前
Spring AI 1.x 系列【25】结构化输出案例演示
java·人工智能·spring
鱼鳞_2 小时前
Java学习笔记_Day23(HashMap)
java·笔记·学习
hua_ban_yu2 小时前
新版本 idea 如何设置热部署
java·ide·intellij-idea
SimonKing2 小时前
免费!不限量!用opencode接入英伟达(NVIDIA)大模型,轻松打造你的 AI 编程助手
java·后端·程序员
odng2 小时前
拉取最新代码报错修复说明
java
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第十四期 - 享元模式】享元模式 —— 内外状态分离与对象共享实现、优缺点与适用场景
java·设计模式·软件工程·享元模式