前言:本篇文章带你用Langchain4j开发一个属于自己的AI编程助手  

需求分析

我们要实现؜一个 AI 编程小⁠助手,可以帮助用户‏答疑解惑,并且给出‌编程学习的指导建议

要实现这个需求,我们首先要能够调用 AI 完成 基础对话,而且要支持实现 多轮对话记忆。此外,如果想进一步增强 AI 的能力,需要让它能够 使用工具 来联网搜索内容;还可以让 AI 基于我们自己的 知识库回答,给用户提供我们在编程领域沉淀的资源和经验。

ai-coder-helper

如果要从 ؜0 开始实现上述功⁠能,还是很麻烦的,‏因此我们要使用 A‌I 开发框架来‏提高效率。

什么是 LangChain4j

LangChain4j 是一个专为 Java 生态系统设计的开源库,旨在简化将大语言模型(LLM)(如 OpenAI GPT、Anthropic Claude、Google Gemini 等)集成到 Java 应用程序中的过程。它是 Python 热门库 LangChain 的 Java 移植版本,专为 Java 开发者设计。

    接下来学习有关LangChain4j相关的重要内容

  • 对话记忆
  • 结构化输出
  • RAG 知识库
  • 工具调用
  • MCP
  • SSE 流式输出

1. AI 对话 - ChatModel

ChatModel 是 LangChain4j 中的核心接口,代表能与用户进行对话交互的大型语言模型。它封装了调用各种大语言模型(如 GPT、Claude、Gemini 等)进行文本生成的能力,是构建智能对话系统的基石。

1.首先需要创建一个SpringBoot项目(jdk>=17)

2.引入至少一个AI大模型的依赖

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
    <version>1.1.0-beta7</version>
</dependency>

3.需要到 阿里云百炼平台 获取大模型调用 key

4.回到项目,؜在配置文件中添加大⁠模型配置,指定模型‏名称和 API K‌ey

langchain4j:
  community:
    dashscope:
      chat-model:
        model-name: qwen-max
        api-key: <You API Key here>

5.创建一个 AiCo⁠deHelper 类,引入自动注‏入的 qwenChatModel‌,编写简单的对话代码

@Service
@Slf4j
public class AiCodeHelper {

    @Resource
    private ChatModel qwenChatModel;

    public String chat(String message) {
        UserMessage userMessage = UserMessage.from(message);
        ChatResponse chatResponse = qwenChatModel.chat(userMessage);
        AiMessage aiMessage = chatResponse.aiMessage();
        log.info("AI 输出:" + aiMessage.toString());
        return aiMessage.text();
    }
}

6.创建测试类(快捷键 CTRL+shift + T)

@SpringBootTest
class AiCoderApplicationTests {

    @Resource
    private AiCodeHelper aiCodeHelper;

    @Test
    void contextLoads() {
        String chat = aiCodeHelper.chat("介绍一下你自己");
        System.out.println(chat);
    }

}

7.以 Debug 模式运行单元测试

注意:报错处理

如果遇到找不到符号的 lombok 报错:

可以修改 ؜IDEA 的注解处⁠理器配置,改为使用‏项目中的 lomb‌ok:

2. 系统提示词 - SystemMessage

系统提示词在 LangChain4j 中是用来定义 AI 助手角色、行为准则和任务上下文的核心指令。

  1. 角色设定:固定 AI 的身份(如专家/助手/翻译)

  2. 行为约束:限制回答范围或风格(如"仅返回 JSON")

  3. 任务引导:明确当前对话的目标(如"分析以下代码漏洞")

例如:
"你是一位严谨的 Java 架构师,用专业术语解释技术概念,并给出可执行的代码示例"

代码示例:

    @Resource
    private ChatModel qwenChatModel;

    private static final String SYSTEM_MESSAGE = """
        你是编程领域的小助手,帮助用户解答编程学习的问题,并给出建议。
        """;

    public String chat(String message) {
        SystemMessage systemMessage = SystemMessage.from(SYSTEM_MESSAGE);
        UserMessage userMessage = UserMessage.from(message);
        ChatResponse chatResponse = qwenChatModel.chat(systemMessage, userMessage);
        AiMessage aiMessage = chatResponse.aiMessage();
        log.info("AI 输出:" + aiMessage.toString());
        return aiMessage.text();
    }

编写测试类,查看结果:

@SpringBootTest
class AiCodeHelperTest {

    @Resource
    private AiCodeHelper aiCodeHelper;

    @Test
    void chat(String message) {
    aiCodeHelper.chat("我想学习算法");
    }
}

AI回答:

"学习算法是一个非常棒的选择,它不仅能帮助你更好地理解编程的本质,还能大大提升解决实际问题的能力。下面是一些建议,希望能帮助到你:

  1. 基础知识:首先确保你已经掌握了至少一种编程语言(如Python、Java或C++),因为算法的学习通常需要通过编程来实现。如果你还不熟悉任何编程语言,建议先从基础开始学起。

  2. 选择合适的学习资源

    • 书籍:《算法导论》(Introduction to Algorithms) 是一本经典的教科书,适合有一定数学背景的人阅读;《算法图解》则更适合初学者入门。

    • 在线课程:Coursera、edX等平台上有很多优质的算法相关课程,比如斯坦福大学的“Algorithms Specialization”系列课程。

    • 网站与博客:LeetCode、Codeforces提供了大量的算法题目供练习,并且有详细的解答分析;还有许多个人博客分享了丰富的学习经验和技巧。

  3. 实践是关键:理论知识很重要,但更重要的是能够将所学应用于实际问题中。尝试解决一些简单的算法挑战题开始,随着能力的增长逐渐挑战更难的问题。记得在解决问题时思考不同方法之间的效率差异。

  4. 加入社区交流:加入相关的论坛或社交媒体群组(如Reddit上的r/learnprogramming),与其他学习者交流心得,可以让你保持动力并且学到更多。

  5. 持续学习与更新:技术领域变化迅速,新的算法和技术层出不穷。即使成为了一名熟练的程序员之后,也不要停止学习的脚步。

希望这些建议对你有所帮助!记住,学习算法是一个长期的过程,不要急于求成,保持耐心和持续的努力是非常重要的。祝你学习顺利!

3. AI 服务 - AI Service

LangChain4j核心开发模式——AI Service

  • 高层API抽象:将AI能力封装为Java服务接口,通过注解声明行为

  • 声明式开发:用@SystemMessage/@UserMessage定义提示词,无需底层调用

  • 动态代理实现AiServices.create()自动生成代理类,处理消息转换和模型交互

  • 服务化集成:支持Spring注入,像调用普通Service一样使用AI功能

本质:用写接口的方式开发AI应用,大幅降低集成复杂度

方法调用处理流程

消息转换细节

1.首先引入 langchain4j 依赖

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.1.0</version>
</dependency>

2.然后创建一个编程助手 AI Service 服务,采用声明式开发方法,编写一个对话方法,然后可以直接通过 @SystemMessage 注解定义系统提示词

public interface AiCodeHelperService {

    @SystemMessage("你是一位编程小助手")
    String chat(String userMessage);
}

还可以在 resources 目录下新建文件 system-prompt.txt 来存储系统提示词

public interface AiCodeHelperService {

    @SystemMessage(fromResource = "system-prompt.txt")
    String chat(String userMessage);
}

3.编写工厂类,用于创建 AI Service:

@Configuration
public class AiCodeHelperServiceFactory {

    @Resource
    private ChatModel qwenChatModel;

    @Bean
    public AiCodeHelperService aiCodeHelperService() {
        return AiServices.create(AiCodeHelperService.class, qwenChatModel);
    }
}

4.编写测试类

@SpringBootTest
class AiCodeHelperServiceTest {

    @Resource
    private AiCodeHelperService aiCodeHelperService;

    @Test
    void chat() {
        String result = aiCodeHelperService.chat("你好,我是程序员鱼皮");
        System.out.println(result);
    }
}

AI回答:

你好!学习算法是一个很好的决定,它不仅能够帮助你更好地理解计算机科学的基础知识,还能提高解决问题的能力。下面是一些建议,希望能帮到你:

  1. 基础知识:首先确保自己对数据结构(如数组、链表、栈、队列、树、图等)有足够的了解。因为很多算法都是基于这些基础的数据结构来实现的。

  2. 选择合适的学习资源

    • 书籍:《算法导论》(Introduction to Algorithms) 是一本非常经典的教材;对于初学者来说,《算法图解》可能更容易上手。

    • 在线课程:Coursera、edX 上有很多关于算法的优质课程,比如斯坦福大学提供的《算法专项课程》。

    • 网站与博客:LeetCode、GeeksforGeeks 等网站提供了大量的练习题和详细的解答分析,非常适合实践练习。

  3. 动手实践:理论学习之后一定要通过编程实践来加深理解。可以从简单的排序算法开始尝试编写代码,然后逐步挑战更复杂的题目。使用 Python 或者 C++ 这样广泛应用于算法实现的语言会比较好。

  4. 参与讨论:加入相关的论坛或社区(例如 Stack Overflow, GitHub 等),与其他学习者交流心得,遇到问题时也可以寻求帮助。

  5. 持续学习与复习:算法领域博大精深,需要长期积累。定期回顾所学内容,并关注最新的研究成果和技术发展是非常重要的。

希望这些建议对你有所帮助!如果你有任何具体的问题或者想要进一步探讨某个话题,请随时告诉我。

Spring Boot 项目中使用

如果你觉得手؜动调用 create ⁠方法来创建 Servi‏ce 比较麻烦,在 S‌pring Boot ‏项目中可以引入依赖:

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>1.1.0-beta7</version>
</dependency>

然后给 AI Service 加上 @AiService 注解,就能自动创建出服务实例了:

@AiService
public interface AiCodeHelperService {

    @SystemMessage(fromResource = "system-prompt.txt")
    String chat(String userMessage);
}

记得注释掉؜之前工厂类的 @C⁠onfigurat‏ion 注解,否则‌会出现 Bean ‏冲突

4. 会话记忆 - ChatMemory

LangChain4j会话记忆核心机制

  • 作用:持久化对话上下文,实现多轮连贯交互

  • 实现类MessageWindowChatMemory 自动维护消息窗口

  • 淘汰策略:保留最近N条消息(如10条),超限自动移除非关键历史

  • 集成方式:创建AI Service时注入chatMemory实例

本质:滑动窗口式记忆管理,让AI拥有短期对话记忆

@Configuration
public class AiCodeHelperServiceFactory {

    @Resource
    private ChatModel qwenChatModel;

    @Bean
    public AiCodeHelperService aiCodeHelperService() {
        // 会话记忆
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
        AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
                .chatModel(qwenChatModel)
                .chatMemory(chatMemory)
                .build();
        return aiCodeHelperService;
    }
}

编写单元测试,测试会话记忆是否生效:

@Test
    void chatWithMemory() {
        String result = aiCodeHelperService.chat("你好,我想学习算法");
        System.out.println(result);
        result = aiCodeHelperService.chat("你好,我忘记了我问的上一个问题,帮我完整复述一遍");
        System.out.println(result);
    }

记忆生效:

5. 结构化输出 - Structured

结构化输出؜是指将大模型返回的文⁠本输出转换为结构化的‏数据格式,比如一段 ‌JSON、一个对象、‏或者是复杂的对象列表。

结构化输出有 3 种实现方式:

  • 利用大模型的 JSON schema
  • 利用 Prompt + JSON Mode
  • 利用 Prompt

    默认是 Prompt 模式,也就是在原本的用户提示词下 拼接一段内容 来指定大模型强制输出包含特定字段的 JSON 文本。

    你是一个专业的信息提取助手。请从给定文本中提取人员信息,
    并严格按照以下 JSON 格式返回结果:
    
    {
        "name": "人员姓名",
        "age": 年龄数字,
        "height": 身高(米),
        "married": true/false,
        "occupation": "职业"
    }
    
    重要规则:
    1. 只返回 JSON 格式,不要添加任何解释
    2. 如果信息不明确,使用 null
    3. age 必须是数字,不是字符串
    4. married 必须是布尔值
    

    比如我们增加一个 让 AI 生成学习报告 的方法,AI 需要输出学习报告对象,包含名称和建议列表:

    @SystemMessage(fromResource = "system-prompt.txt") Report chatForReport(String userMessage); // 学习报告 record Report(String name, List<String> suggestionList){}

    编写单元测试:

    @Test 
    void chatForReport() { 
    String userMessage = "你好,我是程序员鱼皮,学编程两年半,请帮我制定学习报告"; AiCodeHelperService.Report report = aiCodeHelperService.chatForReport(userMessage); System.out.println(report); 
    }

    6. 检索增强生成 - RAG

    RAG(检索增强生成)实时知识库 + 大模型生成

    1. 核心作用

      • 破时效限制:从最新资料检索 → 动态更新AI知识

      • 防幻觉:用真实数据锚定生成 → 答案可追溯来源

    2. 企业级应用

      • 智能客服:用私有知识库精准回答

      • 行业顾问:基于内部文档生成专业分析

    RAG 的完整工作流程如下:

    流程图 1:整体工作流

    流程图 2:索引构建细节

    流程图 3:检索生成细节       流程图 4:优化检索流程

    流程图 5:端到端 RAG 系统

    RAG 的‏实现方式

    LangC؜hain 提供了 ⁠3 种 RAG 的‏实现方式,我把它称‌为:极简版、标准版‏、进阶版。

    极简版 RAG

    极简版适合快速查看效果,首先需要引入额外的依赖,里面包含了内置的离线 Embedding 模型,开箱即用:

    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-easy-rag</artifactId>
        <version>1.1.0-beta7</version>
    </dependency>
    

    示例代码如下,使用内؜置的文档加载器读取文档,然后利用内置的⁠ Embedding 模型将文档转换成‏向量,并存储在内置的 Embeddin‌g 内存存储中,最后给 AI Serv‏ice 绑定默认的内容检索器。

    // RAG
    // 1. 加载文档
    List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/docs");
    // 2. 使用内置的 EmbeddingModel 转换文本为向量,然后存储到自动注入的内存 embeddingStore 中
    EmbeddingStoreIngestor.ingest(documents, embeddingStore);
    // 构造 AI Service
    AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
            .chatModel(qwenChatModel)
            .chatMemory(chatMemory)
            // RAG:从内存 embeddingStore 中检索匹配的文本片段
            .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
            .build();
    

    标准版 RAG

    下面来试试标准版 RAG 实现,为了更好地效果,我们需要:

    • 加载 Markdown 文档并按需切割
    • Markdown 文档补充文件名信息
    • 自定义 Embedding 模型
    • 自定义内容检索器

    在 Spring Boot 配置文件中添加 Embedding 模型配置,使用阿里提供的 text-embedding-v4 模型:

    langchain4j:
      community:
        dashscope:
          chat-model:
            model-name: qwen-max
            api-key: <You API Key here>
          embedding-model:
            model-name: text-embedding-v4
            api-key: <You API Key here>
    

    新建 rag.RagConfig,编写 RAG 相关的代码,执行 RAG 的初始流程并返回了一个定制的内容检索器 Bean:

    /**
     * 加载 RAG
     */
    @Configuration
    public class RagConfig {
    
        @Resource
        private EmbeddingModel qwenEmbeddingModel;
    
        @Resource
        private EmbeddingStore<TextSegment> embeddingStore;
    
        @Bean
        public ContentRetriever contentRetriever() {
            // ------ RAG ------
            // 1. 加载文档
            List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/docs");
            // 2. 文档切割:将每个文档按每段进行分割,最大 1000 字符,每次重叠最多 200 个字符
            DocumentByParagraphSplitter paragraphSplitter = new DocumentByParagraphSplitter(1000, 200);
            // 3. 自定义文档加载器
            EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                    .documentSplitter(paragraphSplitter)
                    // 为了提高搜索质量,为每个 TextSegment 添加文档名称
                    .textSegmentTransformer(textSegment -> TextSegment.from(
                            textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),
                            textSegment.metadata()
                    ))
                    // 使用指定的向量模型
                    .embeddingModel(qwenEmbeddingModel)
                    .embeddingStore(embeddingStore)
                    .build();
            // 加载文档
            ingestor.ingest(documents);
            // 4. 自定义内容查询器
            ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
                    .embeddingStore(embeddingStore)
                    .embeddingModel(qwenEmbeddingModel)
                    .maxResults(5) // 最多 5 个检索结果
                    .minScore(0.75) // 过滤掉分数小于 0.75 的结果
                    .build();
            return contentRetriever;
        }
    }
    

    然后在构建 AI Service 时绑定内容检索器:

    @Resource
    private ContentRetriever contentRetriever;
    
    @Bean
    public AiCodeHelperService aiCodeHelperService() {
        // 会话记忆
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
        // 构造 AI Service
        AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
                .chatModel(qwenChatModel)
                .chatMemory(chatMemory)
                .contentRetriever(contentRetriever) // RAG 检索增强生成
                .build();
        return aiCodeHelperService;
    }
    

    编写单元测试:

    @Test
    void chatWithRag() {
        Result<String> result = aiCodeHelperService.chatWithRag("怎么学习 Java?");
        System.out.println(result.content());
        System.out.println(result.sources());
    }
    

    AI回答:

    进阶版 RAG

    这就是一套标准的 R؜AG 实现了,大多数时候,使用标准版就够⁠了。进阶版会更加灵活,额外支持查询转‏换器、查询路由、内容聚合器、内容注入器‌等特性,将整个 RAG 的流程流水线化

    智能查询处理流程 = 查询优化 + 多路检索 + 内容增强

    流程图 1:整体工作流

    流程图 2:查询优化细节

    流程图 3:智能路由机制

    流程图 4:内容处理流程

    流程图 5:增强生成阶段

    流程图 6:错误处理机制

    定义好 R؜AG 流程后,最后⁠通过 Retrie‏valAugmen‌tor 提供给 A‏I Service:

    AiServices.builder(xxx.class)
        ...
        .retrievalAugmentor(retrievalAugmentor)
        .build();
    

    7. 工具调用 - Tools

    工具调用(Tool Calling)可以理解为让 AI 大模型 借用外部工具 来完成它自己做不到的事情。

    跟人类一样؜,如果只凭手脚完成⁠不了工作,那么就可‏以利用工具箱来完成‌。

    工具可以是؜任何东西,比如网页⁠搜索、对外部 AP‏I 的调用、访问外‌部数据、或执行特定‏代码等。

    比如用户提؜问 “帮我查询江苏最⁠新的天气”,AI 本‏身并没有这些知识,它‌就可以调用 “查询天‏气工具”,来完成任务。

    需要注意的是,工具调用的本质 并不是 AI 服务器自己调用这些工具、也不是把工具的代码发送给 AI 服务器让它执行,它只能提出要求,表示 “我需要执行 XX 工具完成任务”。而真正执行工具的是我们自己的应用程序,执行后再把结果告诉 AI,让它继续工作。

    我们需要的网络搜索能力,就可以通过工具调用来实现。这里我们细化下需求:让 AI 能够通过我的 面试鸭刷题网站 来搜索面试题。

    实现方案很简单,因为面试鸭网站的搜索页面 支持通过 URL 参数传入不同的搜索关键词,我们只需要利用 Jsoup 库 抓取面试鸭搜索页面的题目列表就可以了。

    <dependency>
        <groupId>org.jsoup</groupId>
        <artifactId>jsoup</artifactId>
        <version>1.20.1</version>
    </dependency>
    

    然后在 tools 包下编写工具,通过 @Tool 注解就能声明工具了,注意 要认真编写工具和工具参数的描述,这直接决定了 AI 能否正确地调用工具。

    @Slf4j
    public class InterviewQuestionTool {
    
        /**
         * 从面试鸭网站获取关键词相关的面试题列表
         *
         * @param keyword 搜索关键词(如"redis"、"java多线程")
         * @return 面试题列表,若失败则返回错误信息
         */
        @Tool(name = "interviewQuestionSearch", value = """
                Retrieves relevant interview questions from mianshiya.com based on a keyword.
                Use this tool when the user asks for interview questions about specific technologies,
                programming concepts, or job-related topics. The input should be a clear search term.
                """
        )
        public String searchInterviewQuestions(@P(value = "the keyword to search") String keyword) {
            List<String> questions = new ArrayList<>();
            // 构建搜索URL(编码关键词以支持中文)
            String encodedKeyword = URLEncoder.encode(keyword, StandardCharsets.UTF_8);
            String url = "https://www.mianshiya.com/search/all?searchText=" + encodedKeyword;
            // 发送请求并解析页面
            Document doc;
            try {
                doc = Jsoup.connect(url)
                        .userAgent("Mozilla/5.0")
                        .timeout(5000)
                        .get();
            } catch (IOException e) {
                log.error("get web error", e);
                return e.getMessage();
            }
            // 提取面试题
            Elements questionElements = doc.select(".ant-table-cell > a");
            questionElements.forEach(el -> questions.add(el.text().trim()));
            return String.join("\n", questions);
        }
    }
    

    给 AI Service 绑定工具:

    // 构造 AI Service
    AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
            .chatModel(qwenChatModel)
            .chatMemory(chatMemory)
            .contentRetriever(contentRetriever) // RAG 检索增强生成
            .tools(new InterviewQuestionTool()) // 工具调用
            .build();
    

    编写单元测试,验证工具的效果:

    @Test
    void chatWithTools() {
        String result = aiCodeHelperService.chat("有哪些常见的计算机网络面试题?");
        System.out.println(result);
    }
    

    可以通过 ؜Debug 看到 ⁠AI Servic‏e 加载了工具:

    8. 模型上下文协议 - MCP

    MCP(Model Co؜ntext Protocol,模型上下文协议)是⁠一种开放标准,目的是增强 AI 与外部系统的交互‏能力。MCP 为 AI 提供了与外部工具、资源和‌服务交互的标准化方式,让 AI 能够访问最新数据‏、执行复杂操作,并与现有系统集成。

    可以将 MCP ؜想象成 AI 应用的 USB 接⁠口。就像 USB 为设备连接各种‏外设和配件提供了标准化方式一样,‌MCP 为 AI 模型连接不同的‏数据源和工具提供了标准化的方法。

    简单来说,通؜过 MCP 协议,AI ⁠应用可以轻松接入别人提供‏的服务来实现更多功能,比‌如查询地理位置、操作数据‏库、部署网站、甚至是支付等等。

    首先从 MCP 服务市场搜索 Web Search 服务,推荐 智谱AI开放平台,因为它提供了 SSE 在线调用服务,不用我们自己在本地安装启动,很方便。

    引入依赖:

    <!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-mcp -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-mcp</artifactId>
        <version>1.1.0-beta7</version>
    </dependency>
    

    在配置文件中新增 API Key 的配置:

    bigmodel:
      api-key: <Your Api Key>
    

    新建Config包,按照官方推荐,初始化和 MCP 服务的通讯,并创建 McpToolProvider 的 Bean:

    @Configuration
    public class McpConfig {
    
        @Value("${bigmodel.api-key}")
        private String apiKey;
    
        @Bean
        public McpToolProvider mcpToolProvider() {
            // 和 MCP 服务通讯
            McpTransport transport = new HttpMcpTransport.Builder()
                    .sseUrl("https://open.bigmodel.cn/api/mcp/web_search/sse?Authorization=" + apiKey)
                    .logRequests(true) // 开启日志,查看更多信息
                    .logResponses(true)
                    .build();
            // 创建 MCP 客户端
            McpClient mcpClient = new DefaultMcpClient.Builder()
                    .key("yupiMcpClient")
                    .transport(transport)
                    .build();
            // 从 MCP 客户端获取工具
            McpToolProvider toolProvider = McpToolProvider.builder()
                    .mcpClients(mcpClient)
                    .build();
            return toolProvider;
        }
    }
    

    注意,上面我们؜是通过 SSE 的方式调用 MC⁠P。如果你是通过 npx‏ 或 uvx 本地启动 MC‌P 服务,需要先安装对应的工‏具,并且利用下面的配置建立通讯:

    McpTransport transport = new StdioMcpTransport.Builder()
        .command(List.of("/usr/bin/npm", "exec", "@modelcontextprotocol/server-everything@0.6.2"))
        .logEvents(true) // only if you want to see the traffic in the log
        .build();
    

    在 AI Service 中应用 MCP 工具:

    @Resource
    private McpToolProvider mcpToolProvider;
    
    // 构造 AI Service
    AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
            .chatModel(qwenChatModel)
            .chatMemory(chatMemory)
            .contentRetriever(contentRetriever) // RAG 检索增强生成
            .tools(new InterviewQuestionTool()) // 工具调用
            .toolProvider(mcpToolProvider) // MCP 工具调用
            .build();
    

    编写单元测试:

    @Test
    void chatWithMcp() {
        String result = aiCodeHelperService.chat("什么是程序员鱼皮的编程导航?");
        System.out.println(result);
    }
    

    9. 护轨 - Guardrail

    其实我感觉护轨这个名字起的؜不太好,其实我们把它理解为拦截器就好了。分为输入护轨⁠(input guardrails)和输出护轨(ou‏tput guardrails),可以在请求 AI ‌前和接收到 AI 的响应后执行一些额外操作,比如调用‏ AI 前鉴权、调用 AI 后记录日志。

    让我们小试؜一把,在调用 AI⁠ 前进行敏感词检测‏,如果用户提示词包‌含敏感词,则直接拒‏绝。

    新建 guardrail.SafeInputGuardrail,实现 InputGuardrail 接口:

    /**
     * 安全检测输入护轨
     */
    public class SafeInputGuardrail implements InputGuardrail {
    
        private static final Set<String> sensitiveWords = Set.of("secret", "hidden");
    
        /**
         * 检测用户输入是否安全
         */
        @Override
        public InputGuardrailResult validate(UserMessage userMessage) {
            // 获取用户输入并转换为小写以确保大小写不敏感
            String inputText = userMessage.singleText().toLowerCase();
            // 使用正则表达式分割输入文本为单词
            String[] words = inputText.split("\\W+");
            // 遍历所有单词,检查是否存在敏感词
            for (String word : words) {
                if (sensitiveWords.contains(word)) {
                    return fatal("Sensitive word detected: " + word);
                }
            }
            return success();
        }
    }
    

    LangCh؜ain4j 提供了几种⁠快速返回的方法,简单来‏说,想继续调用 AI ‌就返回 success‏、否则就返回 fatal。

    修改 AI Service,使用输入护轨:

    @InputGuardrails({SafeInputGuardrail.class})
    public interface AiCodeHelperService {
    
        @SystemMessage(fromResource = "system-prompt.txt")
        String chat(String userMessage);
    
        @SystemMessage(fromResource = "system-prompt.txt")
        Report chatForReport(String userMessage);
    
        // 学习报告
        record Report(String name, List<String> suggestionList) {
        }
    }
    

    编写单元测试,写一个包含敏感词的提示词:

    @Test
    void chatWithGuardrail() {
        String result = aiCodeHelperService.chat("kill the game");
        System.out.println(result);
    }
    

    10. AI 服务化

    至此,AI؜ 的能力基本开发完成⁠,但是目前只支持本地‏运行,需要编写一个接‌口提供给前端调用,让‏ AI 能够成为一个服务。

    我们平时开发的大多数接口都؜是同步接口,也就是等后端处理完再返回。但是对于 A⁠I 应用,特别是响应时间较长的对话类应用,可能会让‏用户失去耐心等待,因此推荐使用 SSE(Serve‌r-Sent Events)技术实现实时流式输出,‏类似打字机效果,大幅提升用户体验。

    SSE 流式接口开发

    LangC؜hain 提供了 ⁠2 种方式来支持流‏式响应(注意,流式‌响应不支持结构‏化输出)。

    TokenStream(第一种)

    interface Assistant {
    
        TokenStream chat(String message);
    }
    
    StreamingChatModel model = OpenAiStreamingChatModel.builder()
        .apiKey(System.getenv("OPENAI_API_KEY"))
        .modelName(GPT_4_O_MINI)
        .build();
    
    Assistant assistant = AiServices.create(Assistant.class, model);
    
    TokenStream tokenStream = assistant.chat("Tell me how to study");
    
    tokenStream.onPartialResponse((String partialResponse) -> System.out.println(partialResponse))
        .onRetrieved((List<Content> contents) -> System.out.println(contents))
        .onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
        .onCompleteResponse((ChatResponse response) -> System.out.println(response))
        .onError((Throwable error) -> error.printStackTrace())
        .start();
    

    使用 Flux(第二种)

    interface Assistant {
    
      Flux<String> chat(String message);
    }
    

    11. 开发AI编程小助手

    首先需要引入响应式包依赖:

    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-reactor</artifactId>
        <version>1.1.0-beta7</version>
    </dependency>
    

    然后给 A؜I Service⁠ 增加流式对话方法‏,这里顺便支持下多‌用户的会话记忆:

    // 流式对话
    Flux<String> chatStream(@MemoryId int memoryId, @UserMessage String userMessage);
    

    由于要用到流式模型,需要增加流式模型配置:

    langchain4j:
      community:
        dashscope:
          streaming-chat-model:
            model-name: qwen-max
            api-key: <Your Api Key>
    

    构造 AI؜ Service ⁠时指定流式对话模型‏(自动注入即可),‌并且补充会话记忆提‏供者:

    @Resource
    private StreamingChatModel qwenStreamingChatModel;
    
    AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
            .chatModel(myQwenChatModel)
            .streamingChatModel(qwenStreamingChatModel)
            .chatMemory(chatMemory)
          //  .chatMemoryProvider(memoryId ->
          //   MessageWindowChatMemory.withMaxMessages(10)) // 每个会话独立存储
          //  .contentRetriever(contentRetriever)  RAG 检索增强生成
          //  .tools(new InterviewQuestionTool())  工具调用
          //  .toolProvider(mcpToolProvider)  MCP 工具调用
            .build();
    

    注释的内容可以根据自己要求选或者不选

    最后,编写؜ Controll⁠er 接口。为了方‏便测试,这里使用 ‌Get 请求:

    @RestController
    @RequestMapping("/ai")
    public class AiController {
    
        @Resource
        private AiCodeHelperService aiCodeHelperService;
    
        @GetMapping("/chat")
        public Flux<ServerSentEvent<String>> chat(int memoryId, String message) {
            return aiCodeHelperService.chatStream(memoryId, message)
                    .map(chunk -> ServerSentEvent.<String>builder()
                            .data(chunk)
                            .build());
        }
    }
    

    增加服务器配置,指定后端端口和接口路径前缀:

    server:
      port: 8081
      servlet:
        context-path: /api
    

    启动服务器,用 CURL 工具测试调用:

    curl -G 'http://localhost:8090/api/ai/chat' \
      --data-urlencode 'message=你好,我想知道怎么学算法' \
      --data-urlencode 'memoryId=1'
    

    后端支持跨域

    为了让前端؜项目能够顺利调用后端⁠接口,我们需要在后端‏配置跨域支持。在 c‌onfig 包下创建‏跨域配置类,代码如下:

    /**
     * 全局跨域配置
     */
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            // 覆盖所有请求
            registry.addMapping("/**")
                    // 允许发送 Cookie
                    .allowCredentials(true)
                    // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
                    .allowedOriginPatterns("*")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowedHeaders("*")
                    .exposedHeaders("*");
        }
    }
    

    注意,如果 .allowedOrigins("*").allowCredentials(true) 同时配置会导致冲突,因为出于安全考虑,跨域请求不能同时允许所有域名访问和发送认证信息(比如 Cookie)

    12. 用Cursor生成前端

    由于这个项目不需要很复杂的页面,我们可以利用 AI 来快速生成前端代码,极大提高开发效率。这里使用 主流 AI 开发工具 Cursor,挑战不写一行代码,生成符合要求的前端项目。

    提示词

    首先准备一؜段详细的 Prom⁠pt,一般要包括需‏求、技术选型、后端‌接口信息,还可以提‏供一些原型图、后端代码等。

    你是一位专业的前端开发,请帮我根据下列信息来生成对应的前端项目代码。
    
    ## 需求
    
    应用为《AI 编程小助手》,帮助用户解答编程学习和求职面试相关的问题,并给出建议。
    
    只有一个页面,就是主页:页面风格为聊天室,上方是聊天记录(用户信息在右边,AI 信息在左边),下方是输入框,进入页面后自动生成一个聊天室 id,用于区分不同的会话。通过 SSE 的方式调用 chat 接口,实时显示对话内容。
    
    ## 技术选型
    
    1. Vue3 项目
    2. Axios 请求库
    
    ## 后端接口信息
    
    接口地址前缀:http://localhost:8081/api
    
    ## SpringBoot 后端接口代码
    
    @RestController
    @RequestMapping("/ai")
    public class AiController {
    
        @GetMapping("/chat")
        public Flux<ServerSentEvent<String>> chat(int memoryId, String message) {
            return aiCodeHelperService.chatStream(memoryId, message)
                    .map(chunk -> ServerSentEvent.<String>builder()
                            .data(chunk)
                            .build());
        }
    }
    

    在项目根目录下创建新的前端项目文件夹 ai-coder-front,使用 Cursor 工具打开该目录,输入 Prompt 执行。注意要选择 Agent 模式、Thinking 深度思考模型(推荐 Claude):


    大功告成!

    (出处:编程导航)

    Logo

    汇聚全球AI编程工具,助力开发者即刻编程。

    更多推荐