Cursor 技巧 1:Interactive Feedback MCP

在使用 Cursor 时,用户会面临着每月 500 次 “快车道” 请求的限制,一旦用超,需要排队等待或者额外付费。

一个名为 “Interactive Feedback MCP” 的开源项目为Cursor用户提供了一种创新的节省快速请求的方式:旨在通过在单次Token消耗周期内实现无限轮次的追问和反馈,从而大幅提升付费额度的使用效率。其核心原理是在Cursor准备结束当前对话时拦截该信号,并允许用户输入新的反馈或指令,使对话在保持完整上下文的情况下继续进行,而不是开启新的、消耗额外Token的会话。

什么是 Interactive Feedback MCP

Interactive Feedback MCP (MCP: Model-Coordinator-Provider) 是一个本地运行的服务,它与Cursor的MCP协议集成,充当用户与AI模型之间的交互协调者。当AI完成一次响应后,此MCP会介入,询问用户是否需要进一步修改或有新的问题,并将用户的反馈无缝地融入当前对话流中。这意味着最初的一次请求所消耗的Token可以支持后续多轮的迭代优化,据称能将500次请求有效交互扩展数倍。

项目地址:https://github.com/noopstudios/interactive-feedback-mcp

安装步骤

依赖软件

  • Python 3.11 或更新版本。
  • uv(Python包管理器)。安装方法如下:
    • Windows: pip install uv
    • Linux/Mac: curl -LsSf https://astral.sh/uv/install.sh | sh

获取代码

clone interactive-feedback-mcp 仓库:

下载源代码也可以

下载完成后进入到代码目录。

安装 mcp

在 interactive-feedback-mcp 代码目录下执行如下命令:

该指令会创建一个虚拟环境并安装各种包(因此执行耗时也比较长)。

执行完成后,执行如下指令运行 MCP 服务器(还是在interactive-feedback-mcp 代码目录下):

在 Cursor 中配置 MCP

通过 Settings > Tools & MCP > Add Custom MCP 添加 MCP :

我这里安装后MCP没能立即启动,将 Cursor 退出重启后才会执行成功。执行成功后会有绿色状态标识。

在 Cursor 中配置 Rule

因为期望 Cursor 在每次请求结束前自动调用 MCP 服务,需要配置下 Cursor 的 Rule (Settings > Rules, Skills, Subagents > Rules > New User Rule ):

总结

通过拦截 Cursor 工作结束信号,在结束时调用 Interactive Feedback MCP 服务,允许用户在 Interactive Feedback 的交互窗口进行追问。

使得追问和修改都在同一个请求会话中进行,达到减少请求 Cursor 次数的效果。

注意: 随着会话上下文拉长,Cursor 会变得很蠢,需要适时的关闭交互窗口结束对话。

END!!!

Spring AI 智能体模式 2 : Anthropic 智能体 Skill

Spring AI 新增了对 Anthropic 智能体 Skill 的支持 — 这类模块化能力可让 Claude 直接生成实际文件,而非单纯的文本描述。启用该 Skill 后,Claude 能生成可直接下载使用的 Excel 电子表格、PowerPoint 演示文稿、Word 文档和 PDF 文件。

局限性说明

Anthropic Skill 的实现方案仅适用于其自研的 Claude 系列模型,存在以下局限性:

  1. 无移植性: 该 Skill 依赖 Anthropic 的代码执行能力和 Files API 基础设施,无法在其他大模型平台(OpenAI、 Gemini 等)使用;
  2. 专属类依赖: 需使用 AnthropicChatOptions 、 AnthropicSkill 、 AnthropicSkillsResponseHelper 等 Anthropic 专属类,而非 Spring AI 的通用接口;
  3. 模型限制: 仅 Claude Sonnet 4、Sonnet 4.5 和 Opus 4 模型支持该 Skill ;
  4. 文件有效期: 通过 Anthropic Files API 生成的文件,24 小时后会自动过期;
  5. 公测功能属性: Skill API 需携带公测版本请求头,且接口规范仍可能持续迭代。

Anthropic Skill 与通用智能体 Skill 的选型建议

Spring AI 支持两种不同的智能体 Skill 实现方案,可根据业务需求选择:

选择 Anthropic 原生Skill API 的场景

  • 需使用预构建的文档生成能力,支持 Excel、PowerPoint、Word、PDF 等格式;
  • 希望 Skill 在沙箱化的安全云端环境中运行;
  • 需实现团队共享的、工作空间级别的 Skill;
  • 业务已确定基于 Claude 系列模型开发;
  • 希望由 Anthropic 负责管理 Skill 的执行基础设施

选择通用智能体 Skill (spring-ai-agent-utils) 的场景

  • 需要让 Skill 适配多款大模型(OpenAI、 Anthropic、 Gemini 等);
  • Skill 需要访问本地资源、网络或自定义软件包;
  • 希望将 Skill 与应用代码一起打包,进行版本控制;
  • 需要对 Skill 的执行环境拥有更高的控制权;
  • 优先考虑方案的可移植性,避免厂商锁定。

能否同时使用两种方案?

可以。在同一应用中,可通过 Anthropic 原生 Skill 实现文档生成,同时借助通用智能体 Skill 完成其他可移植的业务能力开发。二者定位不同、功能互补,可协同使用。

为何基于 Spring AI 使用 Anthropic Skill ?

直接调用 Anthropic 原生 API 使用其 Skill ,需要手动完成多个繁琐步骤:

  • 为每个请求添加 code_execution 工具(Skill运行的必备依赖);
  • 携带三个公测版本请求头:skills-2025-10-02code-execution-2025-08-25files-api-2025-04-14
  • 解析多层嵌套的 JSON 响应,提取文件 ID;
  • 发起独立的 HTTP 请求,下载生成的文件。

而基于 Spring AI 开发时,上述操作均可由框架自动完成。当在请求中添加 Skill 后,Spring AI 会自动执行以下操作:

  1. 自动注入代码执行工具: Skill 在服务端运行 Python 脚本,框架会自动添加所需的工具配置;
  2. 统一管理公测请求头: 自动添加全部三个必备请求头,并与提示词缓存等其它功能实现无缝融合;
  3. 通过类型安全的枚举类: 通过 AnthropicSkill.XLSX 调用预构建的 Skill ,替代无类型提示的魔法字符串;
  4. 构建时校验规则: 会在构建配置时校验请求的 Skill 数量是否超出上限(8 个),而非在 API 调用失败后才抛出异常;
  5. 自动提取文件 ID: 通过 AnthropicSkillResponseHelper 递归检索嵌套的响应结构,精准提取文件 ID;
  6. 深度集成 Files API: 直接通过 anthropicApi.downloadFile() 方法下载生成的文件。

预构建 Skill

Anthropic 提供了四种预构建的 Skill:

Skill标识 输出结果
XLSX 包含数据、公式、图表的 Excel 电子表格
PPTX 包含幻灯片、版式设计的 PowerPoint 演示文稿
DOCX 带格式的 Word 文档
PDF PDF 格式的报告与文档

启用对应 Skill 后, Claude 会自动完成内容设计和技术实现,接口响应结果中会包含文件 ID,指向 Anthropic Files API 中生成的文档。

基础使用方法

通过 AnthropicChatOptions 类的统一 skill() 方法,即可快速启用 Anthropic Skill:

底层实现中,Spring AI 会自动添加代码执行工具和必备的公测请求头。Claude 处理请求并生成电子表格后,会返回包含文件 ID 的响应结果。

启用多个Skill

针对复杂的业务流程,可同时启用多个 Skill:

也可使用便捷的 skills() 方法批量添加:

claude 会根据输出内容的不同部分,自动匹配合适的文档格式。

注意: 每个请求最多可启用 8 个 Skill ,该限制会在构建时校验,并抛出清晰的错误提示。

结合 ChatClient API 使用

Anthropic Skill 可与 Spring AI 流式 ChatClient API 无缝协同使用:

下载生成的文件

生成的文件会在 Anthropic Files API 中保留 24 小时。Spring AI 提供了 AnthropicSkillsResponseHelper 工具类用于提取文件 ID,同时通过 AnthropicApi 提供文件操作的相关方法。

claude 生成文件后,文件 ID 会嵌入多层嵌套的响应结构中(最深可达 4 层),AnthropicSkillsResponseHelper 可通过递归检索,从任意深度提取文件 ID。

单个文件提取与下载

代码如下:

多文件批量下载

针对返回多个文件的响应,可使用批量下载方法:

自定义 Skill

自定义 Skill 支持将企业自有业务规则封装为可复用的模块,让 Claude 在文档生成过程中自动应用,典型应用场景包括:

  • 企业品牌规范(页眉、页脚、水印、企业 logo);
  • 合规要求(免责声明、保密通知);
  • 文档模板(标准结构、格式规范);
  • 领域专业知识(行业术语、专属计算公式)。

自定义 Skill 以 SKILL.md 文件的形式上传至 Anthropic 工作空间,上传后团队所有成员均可使用,且可与任意预构建 Skill 组合搭配。

Skill 文件结构

所有自定义 Skill 均需包含一个带 YAML 前置元数据的 SKILL.md 文件,示例如下:

元数据中的 description 字段用于告知 Claude 何时应用该Skill,建议表述明确,使用 “必须“ “强制“ 等词汇,确保 Skill 被稳定触发。

自定义 Skill 元数据字段的规范要求:

字段 要求
name 最长 64 个字符,仅允许小写字母、数字、连字符
description 最长 1024 个字符,不能为空

上传自定义Skill

使用 Anthropic API 上传自定义 Skill 文件,示例请求如下:

注意请求格式: 文件参数需使用 files[]= 格式,且 filename 必须包含与 YAML 前置元数据中 name 字段一致的目录名。

接口响应示例:

响应结果中的 id 即为该自定义 Skill 的唯一标识。在 Spring AI 中通过该 ID 调用 Skill 。

使用自定义Skill

通过统一的 skill() 方法,传入自定义 Skill ID 即可启用,支持与预构建 Skill 组合使用:

上述代码中,Claude 会同时应用两款 Skill: XLSX 预构建 Skill 实现电子表格生成,自定义水印 Skill 自动添加企业品牌标识。

Skill 版本固定

生产环境部署时,建议将自定义 Skill 固定为指定版本,避免因为 Skill 更新引发业务异常:

建议生产环境使用固定版本,开发环境使用默认的最新版本。

示例:文档生成服务

以下是一个完整的服务示例,整合了 Anthropic 预构建 Skill 和 自定义 Skill ,实现可配置的文档生成能力:

该实现模式让自定义 Skill 成为可选能力 — 当未配置 Skill ID 时,应用仅通过预构建 Skill 完成文档生成,保证了业务的兼容性。

自定义 Skill 开发技巧

在开发水印 Skill 的实践中,总结了几个实用技巧,列在下面以供参考:

  1. 使用强语气表述: 希望 Claude 稳定执行某条规则时,使用“必须“、“强制“等词汇能提升执行效果,具体可根据 Skill 场景调整;
  2. 按格式拆分指令: 自定义 Skill 可与任意预构建 Skill 组合,建议为 Excel、PowerPoint、Word、PDF 分别编写专属执行指令;
  3. 添加校验清单: 在 Skill 中增加校验环节,要求 Claude 完成文档生成前检查规则执行情况,可有效避免指令遗漏。

快速开始

步骤一:引入 Spring AI Anthropic 启动依赖

将以下依赖添加到 pom 文件:

步骤二:配置 Anthropic 密钥

在配置文件中添加如下配置:

速查指南

关键参数

特性 取值
支持的模型 Claude Sonnet 4、Sonnet 4.5、Opus 4
单请求最大Skill数 8 个
文件有效期 24 小时
推荐最大令牌数 生成复杂文档建议设置为 4096 及以上

核心方法

方法 描述
.skill(AnthropicSkill) 添加预构建 Skill(XLSX、PPTX、DOCX、PDF)
.skill(String) 通过 ID 或名称添加Skill,自动识别预制 / 自定义类型
.skill(String, String) 添加Skill并固定指定版本
.skills(String...) 批量添加多个Skill
.skills(List<String>) 通过列表批量添加Skill

示例应用

这里是Spring 提供的一个示例应用:document-forge。通过这个示例应用可体验上述介绍的所有 Anthropic Skill 功能,该应用提供可视化网页界面,支持:

  1. 选择文档格式(Excel、 PowerPoint、 Word、 PDF),通过自然语言描述生成需求;
  2. 上传源文件实现内容转换,例如将文本文件转为格式化的电子表格、将会议记录转为演示文稿;
  3. 启用自定义品牌标识,查看自定义 Skill 为生成文档添加页眉、页脚和水印的效果;
  4. 通过 Files API 集成,直接下载生成的文档;
  5. 针对演示文稿等耗时较长的请求,支持异步生成。

该示例应用底层实现了本文的核心设计模式:通过 AnthropicChatOptions 配置 Skill、使用 AnthropicSkillsResponseHelper 提取文件 ID、通过 AnthropicApi 下载文件。

相关拓展:Spring AI 通用智能体 Skill

上面介绍的 Anthropic Skill 是厂商专属的功能,核心能力为生成 Excel、PowerPoint 等实际文档文件;而 Spring AI 同时提供了通用智能体 Skill,可适配任意大模型平台。

Spring AI 智能体相关的模式都封装在 spring-ai-agent-utils 工具包中,可与这里介绍的 Anthropic 专属 Skill 互补使用。

核心选型原则: 需要生成实际文档时,使用 Anthropic Skill;需要可移植、独立于大模型的智能体能力时,使用通用智能体 Skill。

总结

Spring AI 为 Anthropic 智能体 Skill API 提供了简洁高效的集成方案,通过统一的 skill() 方法即可调用预构建 Skill 和 自定义 Skill ,框架自动处理代码执行工具、公测请求头、文件提取等复杂细节。

如需查看完整的 API 文档,可参考 Spring AI Anthropic Chat reference 。

关于自定义 Skill 的开发,可参考 Anthropic 官方的 Anthropic Skill文档 以及 Skill 开发指南 。

END!!!

Spring AI 智能体模式 1 : Agent Skills – 模块化,可复用的能力

独立于大模型,可在自有环境运行的 Skills。

Agent Skills 是由 指令、脚本和资源构成的模块化文件夹。 AI Agent 可以发现并按需加载 Skills。相较于将知识硬编码到 prompts 中 或者为每个任务开发专用工具,Skills 提供了一种灵活扩展智能体能力的实现方式。

Spring AI 的相关实现将 Agent Skills 引入到了 java 生态,实现了大模型的可移植性 — 只需定义一次 Agent Skills,即可在 OpenAI、Anthropic、Google Gemini 及其他所有得到支持的大模型中复用。

什么是 Agent Skills?

Agent Skills 是模块化的能力,表现为以 yaml 格式封装的 markdown 文件。 每个 Skill 对应一个文件夹,文件夹中需要包含一个 SKILL.md 文件。在 SKILL.md 中至少需要配置 名称描述 两类元数据,同时还包含指导 Agent 完成特定任务的指令。此外 Skills 文件夹中还可以整合脚本(scripts)、模板(templates)和参考文档(references)等内容。 其中的前置元数据不仅支持简单的字符串值,还支持复杂的 yaml 结构(列表和嵌套对象等)以支持高级使用场景。

Skills 采用渐进式披露机制实现上下文的高效管理:
  1. 发现阶段: Agent 启动时,仅加载所有可用 Skill 的名称和描述,保留能够判断 Skill 相关性的核心信息即可;
  2. 激活阶段: 当待执行任务与某一 Skill 的描述匹配时,Agent 才会将 Skill 的 SKILL.md 文件中的全部指令加载到上下文中;
  3. 执行阶段: Agent 按照指令完成任务,并根据具体情况按需加载引用文件或者执行内置代码

通过这个机制,即使注册上百个 Skill , 也能保证上下文窗口的轻量化,避免资源冗余。

Tips: 关于 Agent Skills 的更多内容可以参考 Skill 官方指南

为什么要在 Spring AI 中使用 Agent Skills

无缝集成

只需要注册少量工具,即可将 Agent Skill 集成到现有 Spring AI 应用中 — 且无需对架构做任何修改。

可移植、独立于模型,无厂商锁定

与绑定特定大模型平台的实现方案不同,Spring AI 的 Agent Skill 可以适配多家大模型服务商,切换模型时无需重写代码或重构 Skill。

可复用、可组合

Skill 可以在不同的项目间共享,与业务代码一起进行版本控制。多个 Skill 可以组合实现复杂工作流,还能通过辅助脚本和参考文档进行功能扩展。同时,Spring AI 的 Skill 还可以无缝兼容已有的 Claude Code Skill。

Spring AI 配套工具

Agent Skill 可与 Spring AI 的其它基于工具的功能完美协同,例如用于高效工具选择的动态工具发现,以及在 Skill 执行过程中捕获大模型推理逻辑的工具参数增强功能

Spring AI Skill 是怎么工作的

Spring AI 采用基于工具的集成方案,通过实现专属工具,让任意大模型都能触发 Skill 并访问其内置资源。该实现严格遵循 Claude Code 的针对 Skill、Base 脚本和文件读取的工具规范。

核心工具集包含:SkillsTool(必选)、ShellTools(可选)和 FileSystemTools(可选)。其中SkillsTool 提供 Skill 函数以支持 AI 模型按需发现并加载指定 Skill,同时也能与 FileSystemTools(用于读取参考文件)和 ShellTools(用于执行辅助脚本)协同工作。

Skill 的运行分为三个核心阶段:

  1. 发现阶段(启动时): 初始化过程中,SkillsTool 会扫描配置好的 Skill 目录(如 .claude/skills/),解析每个 SKILL.md 文件的前置元数据,提取名称和描述字段,构建轻量级的 Skill 注册表。该注册表会直接嵌入 SKill 工具的描述信息中,让大模型能够识别所有 Skill,且不会占用对话 context 资源。

  2. 语义匹配阶段(对话中): 用户发起请求后,大模型会检索嵌入在工具定义中的所有 Skill 描述;若模型判定用户请求与某个 Skill 的描述在语义上匹配,会以该Skill名称为参数,调用 Skill 工具。

  3. 执行阶段(Skill调用时): Skill 工具被调用后,SkillsTool 会从磁盘加载对应 SKILL.md 的完整内容,并将内容与 Skill 的基础目录路径一并返回给大模型。大模型随后按照 SKILL.md 中的指令执行操作。若Skill 引用了其它文件或辅助脚本,大模型会通过 FileSystemTools的 Read 函数或 ShellTools 的 Bash 函数按需访问。

Skill 应用实例

在这一节将通过一个实际场景示例演示 Skill 是如何工作的。

示例: 包含参考资料(References)和脚本(Scripts)的 Skill

当 Skills 整合了各类附加资源时,上述第三阶段的按需加载机制将发挥巨大作用。Skill 中可包含带有补充指令的参考文件,以及用于数据处理的可执行脚本 — 这些资源均在需要时才会加载。

以下是一个名为“my-skill“的 Skill 示例,该 skill 集成了 YouTube 视频字幕提取辅助脚本,以及一个补充指令文件 research_methodology.md 。

Skill 目录结构:

SKILL.md 文件部分内容:

当用户发起请求:“解释这个视频中的核心概念:https://www.youtube.com/watch?v=SGLjdkahoMk ,并遵循文档中的研究方法。“,AI 将按以下步骤执行:

  1. 调用 my-skill,加载其 SKILL.md 文件的完整内容;
  2. 识别到需要参考研究方法,调用Read函数加载 research_methodology.md 文件的完整内容;
  3. 识别到 YouTube 视频链接,通过 ShellTools 调用 Bash 函数执行辅助脚本;
  4. 基于提取的视频字幕,遵循研究方法中的指令,完成核心概念的解释。

脚本代码本身永远不会进入上下文窗口,只有脚本的输出结果会被加载,这一设计会让该方案的 token 使用效率大幅提升。

可以参考spring 官方提供的 Skills-Demo 项目,该项目完整实现了上述工作流。

安全提示: 示例脚本会直接在本地机器上执行,无沙箱隔离保护;使用前需要提前安装所有必要的运行时环境(Python、NodeJs 等)。为提升运行安全性,建议在容器环境中部署智能体应用。

快速开始

准备好在 Spring AI 项目中集成 Agent Skill 了么?按以下 4 个步骤即可实现:

步骤 1:引入依赖

在 pom 中添加如下依赖:

说明:如需获取最新稳定版本,请查看 GitHub release 页面。 环境要求:Spring-AI 版本需要为 2.0.0-M2+

步骤 2:配置智能体

生产环境建议: 对于打包后的应用,可通过 Spring 资源加载机制从类路径加载 Skill,示例如下:

该方式在将 Skill 作为 JAR/WAR 包部署的一部分进行分发时尤为实用。

步骤 3:创建首个 Skill

通过以下命令可以实现一个代码审查的 Skill:

步骤 4:结合 Skill 使用智能体

代码如下:

运行上述代码后,大模型将按以下逻辑执行:

  1. 将“审查该 Controller 类“的请求与 code-reviewer Skill 的描述进行匹配;
  2. 调用 SKill 工具,加载该 Skill 的 SKILL.md 文件中的全部指令;
  3. 调用 FileSystemTools 中的 Read 工具,访问 UserController.java 文件;
  4. 遵循审查指令对代码进行审查并输出详细反馈。

Skill 中的指令直接指导大模型的行为,无需将审查逻辑硬编码到 prompts 中。若要调整审查规则,只需要修改 Skill 文件即可,无需做代码改动。

当前局限性

尽管 Spring AI 智能体 Skill 的实现方案强大且灵活,但目前仍存在一些需要注意的局限性:

脚本执行的安全性问题

通过 ShellTools 执行的脚本会直接在本地机器运行,无沙箱隔离保护,这意味着存在安全风险的代码可能会访问本地文件系统、网络或系统资源。因此使用前务必审查所有 Skill 脚本,尤其是第三方提供的脚本;建议在容器化环境(Docker、Kubernetes)中部署智能体应用,降低安全风险。

无人工介入校验机制

目前暂无内置机制, 可在Skill或脚本执行前要求人工审批,大模型可自动调用所有已注册的 Skill 并执行其内置脚本。对于处理敏感操作的生产环境,建议采用基于 Spring AI 的工具回调机制实现自定义审批工作流,例如封装 ToolCallback 回调函数

Skill 版本控制能力有限

目前暂无内置的 Skill 版本控制系统。若修改某一 Skill 的行为,所有使用该 Skill 的应用都会自动加载新版本。对于生产环境,建议通过目录结构实现自定义的版本控制策略(如 .claude/skills/v1/.claude/skills/v2/)。

相关拓展:Anthropic原生 Skill API

Spring AI 同时集成了 Anthropic 的原生 Skill API,该方案采用与通用智能体不同的实现思路:

  1. Skill 在 Anthropic 的沙箱化云容器中运行(无网络访问权限,仅支持预安装的软件包);
  2. 提供预制的文档生成能力,支持 Excel、PowerPoint、Word、PDF 等格式;
  3. Skill 需上传至 Anthropic 服务器,可在企业工作空间内共享;
  4. 仅支持 Claude 系列模型(Sonnet 4、 Sonnet 4.5、 Opus 4)

核心差异: Anthropic 原生 Skill 运行在 Anthropic 的云基础设施中,而 Spring AI 通用智能体 Skill 运行在用户自有环境中。

方案选择建议

  • 若需要安全的沙箱化执行环境,或依赖预制的文档生成能力,选择 Anthropic 原生Skill ;
  • 若需要实现大模型可移植性、访问本地资源,或希望将 Skill 与应用打包部署,选择 Spring AI 通用智能体 Skill 。

能否同时使用两种方案?

可以。在同一应用中,可通过 Anthropic 原生 Skill 实现文档生成,同时通过 Spring AI 通用智能体 Skill 实现其他可移植的能力,二者定位不同,优势互补。

结论

智能体 Skill 为 Spring AI 带来了模块化、可复用的能力,且实现了无厂商锁定的特性。通过按需提供领域知识,无需修改代码即可更新智能体的行为,Skill可在不同项目间共享,还能无缝切换各大模型服务商。

spring-ai-agent-utils 工具包通过简单的、基于工具的实现方案,让 java 开发者能够轻松使用这一模式。无论是开发代码助手、文档生成工具,还是面向特定领域的智能体,Skill 都为梳理智能体的知识体系提供了可扩展的知识体系。

End !!!

SpringAI 02 – Chat Client API

ChatClient 提供了与AI模型交互的fluent API。它同时支持同步和流式编程模型。

ChatClient fluent API拥有构建传递给AI模型的提示(Prompt)的组成部分的方法。提示包含指导AI模型输出和行为的指令文本。从API的角度来看,提示由一系列消息组成。

AI模型处理两种主要类型的消息:

  • 用户消息,即用户的直接输入。
  • 系统消息,由系统生成以指导对话。

这些消息通常包含占位符,这些占位符在运行时会根据用户输入进行替换,以自定义AI模型对用户输入的响应。

还可以指定一些提示选项,例如:

  • AI模型的名称,即要使用的AI模型的名称。
  • 温度设置,控制生成输出的随机性或创造性。

这些功能使得ChatClient成为一个强大的工具,允许开发者以灵活的方式与AI模型进行交互,并通过定制化的提示和消息来优化AI模型的响应。

创建 ChatClient

使用 ChatClient.Builder 对象创建 ChatClient。你可以为任何 ChatModel SpringBoot 自动配置获取自动配置的 ChatClient.Builder 实例,或者手动创建一个。

使用自动配置的 ChatClient.Builder

在最简单的用例中,Spring AI 通过 SpringBoot 自动配置创建了一个 ChatClient.Builder 实例原型,使用时可以将其注入到类中。以下是一个简单的示例,用于检索对简单用户请求的字符串响应。

在这个简单的例子中,用户输入设置了用户消息的内容。call() 方法向 AI 模型发送请求,content() 方法将 AI 模型的响应作为字符串返回。

手动创建 ChatClient

可以通过设置属性 spring.ai.chat.client.enabled=false 来禁用 ChatClient.Builder 自动配置。当同时使用多个聊天模型时,这个配置会很有用。然后,为每个需要的 ChatModel 手动创建一个 ChatClient.Builder 实例:

ChatClient Fluent API

ChatClient fluent API 允许使用重载的 prompt() 方法以三种不同的方式创建提示(Prompt),以启动fluent API:

  • prompt(): 此方法不接受任何参数就可以开始使用fluent API,允许您构建用户、系统和其它提示(prompt)部分。
  • prompt(Prompt prompt): 此方法接受一个 Prompt 实例作为参数,这个参数可以是一个非fluent API 创建的 Prompt 实例。
  • prompt(String content): 这是一个快捷方法,类似于之前重载的方法,它接受用户文本内容作为参数。

ChatClient 响应

ChatClient API 提供了几种格式化 AI 模型响应内容的方法。

返回 ChatResponse

AI 模型的响应是一个由类型 ChatResponse 定义的复杂结构。ChatResponse中包含关于生成响应的元数据,并且还可以包含多个响应,称为 Generations,每个Generation都有自己的元数据。元数据还包括用于创建响应的token数量(每个token大概是 3/4 个单词)。这个信息很重要,因为托管的 AI 模型会根据每个请求中使用的token数量收费。

以下示例展示了如何获取包含元数据的 ChatResponse 对象的过程,ChatResponse实例是在 call() 方法后执行 chatResponse() 方法获得:

返回Entity

有时会希望将返回的字符串映射为某个特定的实体类的对象。entity() 方法提供了这个功能。

比如下面的 Java record类:

可以使用 entity() 方法轻松地将 AI 模型的输出结果映射为这个record类,如下所示:

还有一个重载的 entity() 方法,方法签名为 entity(ParameterizedTypeReference type),让您可以指定更复杂的类型,如List泛型:

流式响应

使用 stream() 方法可以像下面这样获得异步响应:

也可以使用 Flux<ChatResponse> chatResponse() 方法流式传输 ChatResponse 对象结果。

Spring AI将提供一个更便捷的方法,让开发者能够使用反应式 stream() 方法返回 Java Entity结果。与此同时,还可以使用结构化输出转换器( Structured Output Converter )来显式转换聚合的响应结果,就跟下面的例子一样。这个例子里也展示了fluent API 中参数的使用,具体将在文档的后续部分详细讨论。

call() 方法返回值

在 ChatClient 上指定 call() 方法后,有如下几种不同的响应类型选项。

  • String content(): 返回字符串格式的响应结果
  • ChatResponse chatResponse(): 返回包含多个Generation和响应元数据的 ChatResponse 对象,例如用于创建响应的token数量。
  • entity() 返回指定 Java 类型的返回结果
    • entity(ParameterizedTypeReference<T> type): 用于返回集合类型的结果(支持泛型)。
    • entity(Class type): 用于返回特定类型的结果。
    • entity(StructuredOutputConverter structuredOutputConverter): 可以指定 StructuredOutputConverter 实例,将字符串转换为需要的类型。

也可以使用 stream() 方法来替换 call()方法。

stream() 方法返回值

在 ChatClient 上指定 stream() 方法后,有几种响应类型选项:

  • Flux<String> content(): 返回 AI 模型生成的字符串Flux对象。
  • Flux chatResponse(): 返回包含响应的额外元数据的 ChatResponse 对象的 Flux对象。

使用默认值

使用默认系统文本在 @Configuration 类中创建ChatClient可以 简化运行时代码。通过设置默认值,在调用 ChatClient 时只需要指定用户文本,这样在运行时代码路径中就不需要再为每个请求设置系统文本了。

默认系统文本

以下面的例子中,我们将系统文本配置为始终以海盗的声音回复。为了避免在运行时代码中重复系统文本,我们将在@Configuration类中创建一个ChatClient实例:

以及一个调用这个ChatClient实例的 @RestController接口:

通过 curl 命令来调用这个接口,收到的返回结果是:

带有参数的默认系统文本

以下示例中,我们将在系统文本中通过占位符在运行时指定声音类型而不是设计时指定声音类型。

通过 httpie 调用应用程序接口的结果如下:

 

其他默认值

ChatClient.Builder 级别,可以指定默认的提示(prompt)配置:

  • defaultOptions(ChatOptions chatOptions): 传入在 ChatOptions 类中定义的快捷选项,或特定于模型的选项,如 OpenAiChatOptions。有关特定于模型的 ChatOptions 实现的更多信息,请参考 JavaDocs。
  • defaultFunction(String name, String description, java.util.function.Function<I, O> function)name用于在用户文本中指向函数。description解释了函数的作用,并帮助 AI 模型选择正确的函数以获得准确的响应。function参数是模型在需要时执行的 Java 函数实例。
  • defaultFunctions(String… functionNames): 在应用程序上下文中定义的 java.util.Function 的 bean 名称。
  • defaultUser(String text)defaultUser(Resource text)defaultUser(Consumer<UserSpec> userSpecConsumer): 这些方法用于定义用户文本。Consumer<UserSpec> 用于使用 lambda 来指定用户文本和任何默认参数。
  • defaultAdvisors(Advisor… advisor)Advisors 允许修改用于创建**提示(Prompt)**的数据。QuestionAnswerAdvisor 通过将提示与用户文本相关的上下文信息附加在一起实现了启用检索增强生成(RAG)模式。
  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer): 此方法支持使用 AdvisorSpec 配置多个AdvisorAdvisor可以修改用于创建最终**提示(Prompt)**的数据。Consumer<AdvisorSpec> 通过指定一个 lambda 来添加顾问,如 QuestionAnswerAdvisor,它通过将提示与基于用户文本的相关上下文信息附加在一起来支持检索增强生成(RAG)。

可以使用没有 default 前缀的相应方法在运行时覆盖这些默认值。

  • options(ChatOptions chatOptions)
  • function(String name, String description, java.util.function.Function<I, O> function)
  • functions(String… functionNames)
  • user(String text)user(Resource text)user(Consumer userSpecConsumer)
  • advisors(Advisor… advisor)
  • advisors(Consumer advisorSpecConsumer)

Advisor

Advisor API 提供了一种灵活而强大的方式,用于拦截、修改和增强 Spring 应用程序中的AI驱动交互。

使用用户文本调用 AI 模型时的通用模式是用上下文数据追加或增强提示。

这些上下文数据可以是不同类型的。常见类型包括:

  • 您自己的数据: 这是 AI 模型未经训练的数据。即使模型已经看到过类似的数据,追加的上下文数据在生成响应时仍会被优先考虑。
  • 对话历史记录: 聊天模型的 API 是无状态的。如果你告诉 AI 模型你的名字,它在后续交互中也不会记住它。必须将对话历史记录与每个请求一起发送,以确保在生成响应时考虑之前的交互。

ChatClient 中的 Advisor 配置

ChatClient 的 fluent API 提供了一个AdvisorSpec 接口来配置Advisor。这个接口提供了添加参数、一次设置多个参数和向调用链中添加一个或多个Advisor的方法。

向链中添加Advisor的顺序至关重要,因为它决定了它们的执行顺序。每个Advisor以某种方式修改提示或上下文,一个Advisor所做的更改会传递给链中的下一个。

在此配置中,MessageChatMemoryAdvisor 将首先执行,将对话历史记录添加到提示中。然后QuestionAnswerAdvisor 将根据用户的问题和添加的对话历史记录执行搜索,这样就可能会提供更相关的结果。

点此了解更多关于问题回答Advisor相关的内容

检索增强生成(RAG)

向量数据库存储了 AI 模型不知道的数据。当用户问题发送到 AI 模型时,QuestionAnswerAdvisor 会为与用户问题相关的文档查询向量数据库。

向量数据库的响应被追加到用户文本中,为 AI 模型生成响应提供上下文。

假设已经将数据加载到 VectorStore 中,就可以通过向 ChatClient 提供 QuestionAnswerAdvisor实例来执行检索增强生成(RAG)。

在此示例中,SearchRequest.defaults() 将在向量数据库中对所有文档执行相似性搜索。要限制搜索的文档类型,SearchRequest 接受一个 SQL 样式的过滤表达式,该表达式在所有 VectorStores 中都是可移植的。

动态过滤表达式

使用 FILTER_EXPRESSION Advisor上下文参数在运行时更新 SearchRequest 过滤表达式:

FILTER_EXPRESSION 参数允许根据提供的表达式动态过滤搜索结果。

聊天记忆

ChatMemory 接口表示对聊天对话历史的存储。它提供将消息添加到对话、从对话中检索消息和清除对话历史记录的方法。

目前有两种实现,InMemoryChatMemory 和 CassandraChatMemory,分别提供基于内存和TTL持久存储的聊天对话历史记录存储支持。

创建TTL的 CassandraChatMemory

以下Advisor的实现使用 ChatMemory 接口与对话历史记录来建议提示,它们在如何将内存添加到提示的细节上有所不同:

  • MessageChatMemoryAdvisor:内存被检索并作为一系列消息添加到提示中
  • PromptChatMemoryAdvisor:内存被检索并添加到提示的系统文本中。
  • VectorStoreChatMemoryAdvisor:构造函数 VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize, int order) 允许您:
    1. 指定用于管理和查询文档的 VectorStore 实例。
    2. 设置如果在上下文中未提供,则使用的默认对话 ID。
    3. 定义以token数量为单位的聊天历史记录检索窗口规格。
    4. 提供用于聊天Advisor系统的系统文本建议。
    5. 设置Advisor在链中的优先级顺序。

VectorStoreChatMemoryAdvisor.builder() 方法用于指定默认对话 ID、聊天历史记录窗口大小和要检索的聊天历史的顺序。

以下是一个使用多个Advisor的 @Service 实现示例:

日志记录

SimpleLoggerAdvisor 是一个记录 ChatClient 请求和响应数据的Advisor。这对于调试和监控 AI 交互很有用。

Spring AI 支持对 LLM 和向量存储交互的可观察性。有关更多信息,请参考可观测性指南。

要启用日志记录,在创建 ChatClient 实例时需要将 SimpleLoggerAdvisor 添加到Advisor链中。建议将其添加到链的末尾:

要查看日志,将顾问包的日志级别设置为 DEBUG

将这行配置添加到 application.properties 或 application.yam 文件中。

可以使用以下构造函数自定义记录的 AdvisedRequest 和 ChatResponse 数据:

示例用法:

这允许根据特定需求定制日志信息。

在生产环境中记录敏感信息时要小心。

END!!

SpringAI 01 – AI概念

模型 Model

模型是旨在处理和生成信息的算法,通常模仿人类认知功能。通过从大型数据集中学习模式和洞察力,这些模型可以进行预测、生成文本、图像或其他输出,增强各行业的应用。

当前有许多不同类型的 AI 模型,每种模型会适配特定的用例。虽然 ChatGPT 及其生成式 AI 功能通过文本输入和输出吸引了用户,但许多模型和公司提供了多样化的输入和输出。在 ChatGPT 之前,许多人对 Midjourney 和 Stable Diffusion 等文本到图像的生成模型着迷。

下表根据输入和输出类型对几种模型进行了分类:

AI模型类型分类

Spring AI 目前支持将输入和输出处理为语言、图像和音频的模型。上表中的最后一行,即接受文本作为输入并输出数字的那行,通常被称为嵌入文本,代表 AI 模型中使用的内部数据结构。Spring AI 支持嵌入(Embedding)以支持更先进的用例。

像 GPT 这种模型的独特之处在于它们的预训练性质,正如 GPT(Chat Generative Pre-trained Transformer)中的 “P” 所表示的。这种预训练特性将 AI 转变为一种通用开发工具,但不需要更多的机器学习或模型训练背景。

提示 Prompt

提示是基于语言的输入的基础,用于引导 AI 模型产生特定输出。对于熟悉 ChatGPT 的人来说,可能提示看起来只是输入到对话框中并发送到 API 的文本。然而,它远不止于此。在许多 AI 模型中,提示的文本不仅仅是一个简单的字符串。

ChatGPT 的 API 在一个提示中会有多个文本输入,每个文本输入都被分配了一个角色。例如,有系统角色,它告诉模型如何表现并设置交互的上下文。还有用户角色,通常就是用户的输入。

创建有效的提示既是科学也是艺术。ChatGPT 是为人类对话而设计的。这与使用 SQL 等特定的数据库查询语言来进行 “提问” 有很大不同。与 AI 模型进行交流必须要像与另一个真实的人交谈一样。

这种交互方式非常重要,以至于出现了像 “提示工程” 这样类似一门学科的名词。有大量提高提示有效性的技术正在涌现。花时间精心设计提示可以极大地改善最终输出的结果。

共享提示已成为一种常用的做法,并且在这个主题上正在进行积极的学术研究。作为创建有效提示可能有多违反直觉的一个例子(例如,与SQL比较),最近的一篇研究论文发现,最有效的一个提示可以用这样的语句开头: “深呼吸,一步一步地做这个”。这应该可以让你了解为什么语言如此重要。不幸的是我们还不完全理解如何最有效地利用这项技术,即使是在之前的迭代版本中(如 ChatGPT 3.5),更不用说正在开发的新版本了。

提示模板 Prompt Template

创建有效提示涉及建立请求的上下文,并将请求中指定的部分内容替换为让用户输入的值。

这个过程使用传统的基于文本的模板引擎进行提示创建和管理。Spring AI 为此使用了 OSS 库中的 StringTemplate。

例如下面就是一个简单的提示模板:

Tell me a {adjective} joke about {content}.

在 Spring AI 中,提示模板可以类比为 Spring MVC 架构中的 “视图”。提供一个模型对象(通常是 java.util.Map)来填充模板中的占位符。这样“渲染” 后的字符串就成为提供给 AI 模型的提示内容。

发送给模型的提示的具体数据格式有很大差异。最初是简单的字符串,现在提示已经发展到包括多个消息,一条消息中的每个字符串代表模型的一个不同角色。

嵌入 Embedding

嵌入是文本、图像或视频的数值表示,用于捕获输入之间的关系。

嵌入通过将文本、图像和视频转换为浮点数数组(称为向量)来工作。这些向量被用来捕获文本、图像和视频的含义。嵌入数组的长度称为向量的维度。

通过计算两段文本的向量表示之间的数值距离,应用程序可以确定用于生成嵌入向量的对象之间的相似性。

AI Embedding

作为探索 AI 的 Java 开发人员,不需要理解这些向量表示(vector representations)背后的复杂数学理论或具体实现,只需要对它们在 AI 系统中的作用和功能有基本的了解就足够了,特别是当你将 AI 功能集成到应用程序中时。

嵌入在像检索增强生成(RAG Retrieval Augmented Generation)模式这样的实际应用中特别相关。它们使数据能够表示为语义空间中的点,这类似于欧几里得几何中的二维空间,但维度更高。这意味着就像欧几里得几何中平面上的点根据其坐标可以近或远一样,在语义空间中,点的接近程度反映了含义的相似性。对应相似主题的语句在这个多维空间中位置更接近,就像图形上彼此靠近的点一样。这种接近有助于进行文本分类、语义搜索甚至产品推荐等任务,因为它允许 AI 根据它们在这个扩展的语义景观中的 “位置” 来识别和分组相关概念。

你可以将这个语义空间视为一个向量。

Token

Token是 AI 模型工作的基本构成。在输入时,模型将单词转换为token。在输出时,模型将token转换回单词。

在英语中,一个token大约对应 75% 的单词。作为参考,莎士比亚的全部作品,总计约 90 万字,可以转换为大约 120 万个token。

AI Token

也许更重要的是,token = 费用。在托管 AI 模型的上下文中,消耗的费用由使用的令牌数量决定。输入和输出都会被计入使用的总token数。

此外,模型也会受到token的影响,因为它限制了在单个 API 调用中处理的文本量。这个阈值通常称为 “上下文窗口”。模型不会处理超过此限制的文本。

例如,ChatGPT3 的token限制为 4K,而 GPT4 提供不同的选项,如 8K、16K 和 32K。Anthropic 的 Claude AI 模型的token限制为 100K,另外Meta 最近的研究产生了一个 100 万token限制的模型。

要用 GPT4 总结莎士比亚的作品集,你需要设计软件工程策略来分割数据,并在模型的上下文窗口限制内呈现数据。Spring AI 项目可以帮助你完成这项任务。

结构化输出 Structured Output

AI 模型的输出传统上是 java.lang.String 类型 —— 即使你要求回复为 JSON 格式。它可能是一个正确的 JSON,但它不是一个 JSON 数据结构。它只是一个字符串。此外,在提示中要求 “为 JSON” 也不是 100% 准确的。

这种复杂性导致了一个专门领域的出现,涉及创建提示以产生预期输出,然后将生成的简单字符串转换为可用于应用程序集成的可用数据结构。

Structured Output

结构化输出转换使用精心设计的提示,通常需要与模型进行多次交互才能获得所需的格式。

将数据和 API 引入 AI 模型

如何让 AI 模型获得它未经过训练的信息?

请注意,GPT 3.5/4.0 数据集仅延伸到 2021 年 9 月(今天2025年2月22日,学习得完了 o(╥﹏╥)o)。因此,该模型会说它不知道需要该日期之后的知识相关问题的答案。一个有趣的小知识是这个数据集大约为 650GB。

有三种技术可以将自定义 AI 模型用于你的数据:

  • 微调(fine Tuning):这种传统的机器学习技术涉及调整模型并更改其内部权重。然而,即使对于机器学习专家来说这也是一个具有挑战性的过程——尤其是对于像 GPT 这样的大型的极其资源密集的模型。此外,一些模型可能不提供此选项。
  • 提示填充(Promt Stuffing):这是一种更实用的替代方法,涉及将你的数据嵌入到提供给模型的提示中。鉴于模型的token限制,在模型的上下文窗口内呈现相关数据时需要技术处理。这种方法通俗地称为 “填充提示”。Spring AI 库可帮助你实现基于 “填充提示” 技术(也称为检索增强生成(RAG))的解决方案。Prompt Stuffing
  • 函数调用:此技术允许注册自定义用户函数,将大型语言模型连接到外部系统的 API。Spring AI 极大地简化了实现自定义函数调用的过程。

检索增强生成 RAG

面对将相关数据纳入提示以获得准确 AI 模型响应的挑战,检索增强生成(RAG Retrieval Augmented Generation)技术是一种解决方案。
该方法涉及批处理风格的编程模型,其中Job用于从文档中读取非结构化数据,对其进行转换,然后将其写入向量数据库。从高层次来看,这是一个 ETL(提取、转换和加载)管道。向量数据库用于 RAG 技术的检索部分。
在将非结构化数据加载到向量数据库的过程中,最重要的转换之一是将原始文档分割成较小的部分。将原始文档分割成较小部分的过程有两个重要步骤:

  1. 在保留内容语义边界的同时将文档分割成片段。例如,对于包含段落和表格的文档,应避免在段落或表格中间分割文档。对于代码,避免在方法实现中间分割代码。
  2. 将文档分割后的片段进一步分割成大小满足 AI 模型token限制的碎片。

RAG 的下一个阶段是处理用户输入。当 AI 模型要回答用户的问题时,问题和所有 “相似” 文档部分都被放入发送给 AI 模型的提示中。这就是使用向量数据库的原因。它非常擅长查找相似内容。

AI RAG

  • ETL Pipeline 这一节中进一步解释了如何编排从数据源解析数据以及将数据保存到结构化向量存储的流程,以确保在将数据传递给AI模型时,数据处于最佳检索格式。
  • ChatClient – RAG 这一节介绍了如何使用 QuestionAnswerAdvisor 在应用程序中启用 RAG 功能。

函数调用 Tool Calling

大型语言模型(LLM)在训练后是固定的,这会导致知识变陈旧,并且它们也无法访问或修改外部数据。

函数调用机制解决了这些缺点。它允许你注册自己的函数,将大型语言模型连接到外部系统的 API。这些系统可以为 LLM 提供实时数据并代表它们执行数据处理操作。

Spring AI 极大地简化了你需要编写以支持函数调用的代码。它为你处理函数调用对话。你可以将你的函数作为 @Tool 注解的方法,然后在提示选项中激活。此外,还可以在单个提示中定义和引用多个函数。

Tool Calling

解释下上图中各个步骤:

  • ① 当我们想在模型中使用函数时,我们会将函数的定义放到对话请求中。每个函数的定义中包含了名称、描述(例如,解释模型何时应调用该函数)和输入参数(例如,函数的输入参数模式)的信息
  • ② 当模型决定调用函数时,它会按照定义格式将函数名称以及参数信息发送到响应信息中
  • ③ 应用程序负责确认函数并用收到的参数信息执行函数
  • ④ 应用程序处理函数的调用结果
  • ⑤ 应用程序将函数调用结果返回给模型
  • ⑥ 模型将函数调用结果作为补充上下文用于产出最终的结果

参考函数调用文档来获取更多的信息以在更多不同的AI模型中使用这个功能。

评估 AI 响应

有效地评估 AI 系统响应用户请求的输出对于确保最终应用程序的准确性和可用性非常重要。有几种新兴技术使预训练模型本身可用于此目的。

此评估过程涉及分析生成的响应是否与用户的意图和查询的上下文一致。诸如相关性、连贯性和事实正确性等指标用于衡量 AI 生成响应的质量。

一种方法涉及将用户的请求和 AI 模型的响应都呈现给模型,询问响应是否与提供的数据一致。

此外,利用存储在向量数据库中的信息作为补充数据可以增强评估过程,有助于确定响应的相关性。

Spring AI 项目提供了一个 Evaluator API,目前提供了评估模型响应的基本策略。参考 Evaluation Testing 文档获取更多的信息。

END!!

基于生成式注解为类添加toString方法

在组内讨论时,有同事提建议在把对象写到日志中时最好直接输出对象不要做任何加工,也就是尽量调用对象自己的toString()方法,不要用JsonKit.toJson(obj)这样先把对象转为json字符串再输出的写法。

这个建议不是没有道理的,jackson和fastjson这些json工具将对象序列化为字符串时会有一个自动检查推测的过程,在这个过程中做了如下事情:

  1. 所有public方法,带返回值,符合“getXxx”(或“isXxx”,如果返回boolean会被称为“isgetter”)命名约定的成员方法被推测存在名字为“xxx”的属性(属性名按照bean命名约定推测,即开头大写字母转成小写)。
  2. 所有public成员字段被推测为要显示的属性,使用字段名字来序列化。

也就是说,一个“getXxx()”方法中如果做了业务性的处理,在被调用的过程中也会被执行。如下面的伪代码:

在对FetchUserAction的实例进行序列化时,会得到下面的json:

得到这个json的时候意味着至少已经做了 “从应用上下文获得当前用户ID” 和 “从数据库查询用户信息” 两个动作。在输出日志的时候静默的执行了一次涉及到资源的操作,这不是一个合理的事情。

当然也可以为getCurrentUser()这个方法添加类似@JsonIgnore这样的注解以避免出现上面的情况。但问题不在这里,问题的关键在于我们应该只需要对model类的实例或其对应的集合做toJson的处理;其它的执行业务处理的对象不应该被输出到日志中,即使因为种种原因不得不将之输出到日志中也不应该做toJson的处理,以避免出现类似前面的例子中的情况。

如果model类的toString()方法的返回值就已经是经过json序列化的就好了,这样我们在输出日志时就不需要显式地再做这个toJson的操作了,也就不会误将不需要json序列化的对象给序列化了。我一开始想的是通过lombok来解决这个问题,因为model类一般是依赖lombok来生成toString方法的。不幸的是,经过调研,我发现虽然已经有人在lombok的相关issue里提过类似的问题,但是lombok现阶段还不支持这么做。要想解决就只能自己实现了。

期望实现的效果是能够根据类上的一个注解如@ToJson来在编译期自动生成类的toString方法。方法内容是下面这样的:

在toString方法中调用json序列化工具类实现了将当前对象转为json字符串的操作。

最开始我是想用bytebuddy或asm来做这个事情的,但是使用这些字节码工具的时候需要加上javaagent相关的配置,运维肯定是不允许的。后来我又接触到了生成式注解,感觉这应该是解决这个问题的一个出路。

先来看下什么是生成式注解:

生成式注解处理器是JSR-269中定义的API,该API可以在编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程,通过生成式注解处理器可以读取、修改、添加抽象语法树中的任意元素。

“可以修改添加抽象语法树中的任意元素”,这看起来很酷,正是我想要的。

关于如何修改语法树可以参考这篇文档:《Java 中的屠龙之术:如何修改语法树?》。

看下具体是如何实现的吧,在类中添加toString方法的代码如下:

上面这个方法实现了向类中添加 toString 方法的逻辑,注释是我用通义灵码生成的,还是挺精准的。 如通义生成的注释说明, makeToStringBody 负责生成toString 方法的具体逻辑,这个方法的逻辑如下:

核心代码就是这些。其他的代码可以看我这个项目 zhyea / lombok-ext 。

本来还可以展开说说lombok和mapstruct的,但就这样吧!

END!!!

如何实现一个mock平台

过去几年里一直在做产业平台订单业务相关的开发工作。随着上下游相关业务日渐增多,系统日趋复杂,在开发测试中经常会遇到如下两个问题:
  1. 要实现一个功能可能会需要协调几个甚至十来个业务方来完成联调测试数据的准备
  2. 多人或多团队合作时,开发进度不好协调,如果关键节点上的一环开发进度慢了就可能会影响到整个项目的进度

mock是解决以上问题的一个办法:

  1. 我们可以不找依赖方生产数据,需要什么数据我们自己mock一个就好了;
  2. 合作方没有开发完,那就约定好输入输出结构,先mock一个预期结果写死在代码中,自己凑合着开发。

很多时候我们都是这么做的。合作的测试同学甚至还提出过要求:让我在业务代码中插入一些造数据的代码以支持根据某个特定的传入参数返回指定的值,以便他们进行测试。

目前成熟的mock工具也着实有几个,比如mockito、gmock、spock等等。这几个mock工具各有其特点,但是使用场景通常只限于单元测试,在功能上算是一个开发校验工具,对测试同学的作用不大。此外不管是使用mock工具mock数据、还是使用硬编码mock数据都需要写代码(或者说存在代码侵入)。另外,因为是通过插入的硬编码来实现的数据mock,灵活性什么的是压根儿没法儿说的。

一个理想的mock工具该是什么样的呢?我觉得需要能满足如下的要求:

  1. 能够根据返回值类型mock数据(mock数据的基本要求)
  2. 在方法层mock数据,而不仅仅是在接口层mock数据
  3. 能够基于接口mock数据
  4. 能够根据不同的传入参数返回不同的值
  5. 可以根据不同的环境(dev,test,stg,prod)来开启或关闭mock能力
  6. 可以在方法层随时开启或关闭mock能力
  7. 可以界面化配置
  8. 低代码侵入或无代码侵入

其中第1、2项是mock数据的基础要求,第3项可以解决多个合作方开发进度不同步的问题,第4、5、6项是对mock能力的灵活性要求,第7、8项是易用性的要求。简而言之就是要灵活好用。

下面介绍一个实现这种mock工具的方案。在这个方案里我们需要如下几种组件:

  1. mock-server,独立部署的服务,提供可视化界面,管理方法元数据,维护mock信息等功能。
  2. mock-agent, 集成到项目里,负责实现方法代理,完成方法元数据解析,与mock-server通信等功能。
  3. 方法AOP,方法切面,用来标记要mock的方法,获取方法请求参数,反馈mock数据

三个组件的关系大致如下图:

通过这三个组件来mock数据的整体流程如下图:

看完流程后,期望中的mock工具(或者说mock平台)是什么样的已经很清晰了:

  1. 工程整体上可以分为两部分:一个独立的mock-server,以及可以嵌入到业务系统里的mock-agent。
  2. mock-server 是一个信息管理平台,可以提供界面化的方式来维护应用信息、方法信息、mock数据信息及mock规则信息、用户信息,这个还是相对比较简单的
  3. mock-agent 则承担了两种职责:实现方法切面、完成和mock-server的通信;在springboot生态中可以通过自定义的springboot starter来实现,core java系统中可以通过asm或者byte-buddy来实现;mock-agent的实现是比较麻烦的,因为被嵌入的项目各有不同,情况比较复杂,需要较高的兼容性,事实上我就是在解决了这一块儿后才确信了整体方案的可行性

至于再具体的功能细节和代码实现就不一一展开说了,如有兴趣可以参考我的开源项目 Mocko 。mocko这个项目就是基于以上思路来做的实现,目前仅是一个MVP(最小可行性产品)版本,待完善的地方还有很多,欢迎大家多提意见,也欢迎有兴趣的朋友一起合作开发。

END!!!

使用Spring AOP实现注解式的分布式锁

这里简单说一个springboot生态下基于redis实现的分布式锁方案。预期实现的效果是在要加锁的方法上添加一个注解,然后就能根据请求参数得到并加上锁,方法执行完后,也会自动释放锁。这样在实现方法时,开发者就可以只关注业务逻辑,不用考虑加锁解锁相关的事情。务求整个过程的丝滑程度类似Spring的 @Transactional 注解。

这个分布式锁暂时基于redis来实现,用来和redis交互的组件则是spring-redisson。当然,我也考虑过基于mysql来实现,以后有时间了也会写一个mysql的版本。不过不管是redis还是mysql,两者都只是实现分布式锁的一个基础中间件,对整体实现思路没有什么影响。

先来看下这个分布式锁的大致结构图:

 图中左侧的redis/redisson/RedisProperties是我们实现分布式锁的基础,它们的作用是不需要多说的;右下角的Business代表了各种业务需求及对应的方法,他们需要使用分布式锁来实现业务处理时的互斥性,这也不用解释;在下面的内容中会详细介绍下其他部分,也就是我们这个分布式锁的主要组成部分。

LockAdvisor

LockAdvisor 是分布式锁的基础组件,它由LockPointcut 和 LockAdvice 组成。当然,不只是当前这个分布式锁,spring aop的核心结构一般都是 AdvisorAdvisor中又有PointcutAdvice两个成员,两者作用大致如下图所示:

Pointcut发挥作用是在应用启动时Bean实例化的过程中,而Advice发挥作用是在目标方法被调用执行的过程中。

Spring在创建Bean实例时,会用所有的Advisor和Bean实例进行匹配,匹配成功了就会创建相应的代理。这其中,匹配是依赖Pointcut来实现的,创建出的代理要做什么则是由Advice决定的。可以说Advisor是Spring创建代理的一个起点。

spring创建代理的过程可以查看spring中的这个方法:AbstractAutoProxyCreator.wrapIfNecessary() 。

LockPointcut

前面也说了,Pointcut会根据方法定义中的信息决定要拦截哪些方法。在分布式锁这个case里, LockPointcut 会根据方法中是否存在 @RLock注解来完成拦截并创建代理。当然实际情况会比较复杂一点,这里在识别到@RLock注解后又对注解中的参数进行了解析和缓存以便进行复用,所以在实现的时候是继承的StaticMethodMatcherPointcut这个类。如果只是要匹配@RLock注解,完全可以依赖Spring提供的AnnotationMethodMatcher来实现。

在springboot中提供了多种PointcutMethodMatcher的实现类,在使用时可以根据自己的情况选择使用spring提供的实现,不必一定要自己造轮子。

LockAdvice

LockAdvice里记录了在目标方法执行前后获得锁及释放锁的详细过程。在这个分布式锁的实现中,我把LockAdvice分成了LockInterceptorLockAspectSupportSpelEvaluator三个部分,三者的关系如下图:

LockAdvice分成三部分的目的是为了划分职责,也就是遵循单一性原则。三部分的职责分别如下:

  1. LockInterceptor 只是简单定义了切面,仅是个入口,具体的锁处理还是需要父类LockAspectSupport来实现;
  2. LockAspectSupport 提供了锁能力的支持,在这个类里会完成与redis的通信从而实现锁获得和锁关闭的处理
  3. SpelEvaluator 提供了SpEL解析的能力,分布式锁的key一般不会是一个常量,需要根据方法参数动态组装,SpelEvaluator就提供了根据方法参数动态组装锁key的能力

LockAutoConfiguration

LockAutoConfiguration 看起来像是一个配置类,但实际上在这个类里完成的是分布式锁实现中需要的各种Bean的创建。这里我们我们可以直接看下代码:

从代码可以看到,LockAutoConfiguration中确实引入了一个配置类 RedisProperties,这个类对应着项目配置文件(application.yml)中redis的配置。通过@EnableConfigurationProperties(RedisProperties.class)可以获得redis的配置信息,从而创建RedisClient的Bean实例。

前面提到的几个类的实例都会在这里完成创建并最终被Spring得到并管理使用起来。

EnableRLock

前面提到在LockAutoConfiguration中创建了多个Bean,但是这些Bean该怎么注入到Spring容器中呢。

我们在写代码的时候应该都用过@ComponentScan注解,在启动类中通过@ComponentScan注解显示注入LockAutoConfiguration及其中的各种Bean确实是一个办法,但不好不优雅。

刷过springboot面试题的同学应该接触过spring.factories,这是一种springboot推荐的做法。springboot的应用在启动时会扫描所有依赖中的spring.factories文件,并自动注入文件中定义的AutoConfiguration类及类中定义的各种Bean。

在这个分布式锁的实现中采用的是另一种做法,是通过在项目启动类上定义的 @EnableRLock注解及@EnableRLock中的@Import注解注入LockAutoConfiguration及其中的各种Bean。体验上类似开启springboot事务支持能力的@EnbaleTransactional注解。(关于@Import注解可以参考 SpringBoot探索01 – @Import注解 )

这个分布式锁的具体实现可以看redis上的代码: zhyea / rlock-spring-boot-starter  

也可以直接添加如下依赖使用:

就这样。

END!!!

MapStruct属性多转一实现

在项目里遇到了需要使用mapstruct将source对象的多个属性转为target对象的一个属性的场景。针对这个问题研究了一段时间,发现想要解决得好一些还是挺让人头疼的。

先说结论吧:MapStruct支持将多个对象转为一个对象,但是不支持将多个属性转为一个属性。对,mapstruct是不支持这么做的。

最终的解决方案也非常简单:在使用mapstruct完成对象的简单转换后,再做一次加工就行。不过我想将这个事情做得优雅一些,目的是尽量不影响业务代码。

项目的代码不好拿出来,举个例子来说明下,在下面的代码中定义了一个产品的Entity类及相应的Item类。目标是将Entity类的实例通过mapstruct转为Item类的实例。

先看下类的定义:

产品Entity类 ProductEntity:

产品Item类 ProductItem :

ProductItem 比 ProductEntity 多了一个 status 属性,这个status属性可以由 生产日期 (manufactureDate)和保质期(qualityGranteeMonths)计算出来。计算逻辑可以看下 ProductStatusEnum 的定义及类中静态的analyze方法:

要实现基于 ProductEntity 的生产日期和保质期两个字段映射出Item的status的值,可以在完成 Entity和Item的转换后再做一次处理,类似下面的代码:

如上面的代码:在转换接口 ProductConverter 中,定义了一个default方法entity2Item,在这个方法中利用mapstruct生成的 entity2ItemSimply() 方法完成简单转换后又做了一次 生产日期、保质期和状态的映射。因为是在同一个转换接口中定义的,在使用时还是比较丝滑的。

不过有一个小问题,就是现在转换接口 ProductConverter 中存在两个将 Entity转为Item的方法,在处理相关Collection 转换时就会出现因为不知道该调用哪个方法而产生的报错,如下:

要解决这个问题也比较简单,使用 qualifyByName 进行标记即可, 最终代码如下:

对了,不要忘了给 entity2Item() 方法加上 @Name 注解,不然会报相关方法找不到的错的。

就这样。源码在这里: zhy-explore / mapstruct-explore

End !!!

springboot入门16 – 包装Controller返回值2

之前有整理过一次怎样包装SpringBoot Controller的做法。

最近在原有方案的基础上又升级了下,可以通过引入 spring-boot-starter 的形式对接口返回值进行封装。

具体做法如下:

1. 引入 zhy-spring-boot-starter 依赖

同时需要确认已引入 spring-boot-starter-web 依赖。这样 zhy-spring-boot-starter 中的返回值封装组件才会生效。

2. 包装返回值

要封装返回值,只需要在接口类或接口方法上添加@ResponseWrapper注解就可以了。

因为只需要对REST接口进行封装。所以组件只会对 接口类或接口方法 生效。

  • 接口类指存在@RestController注解的类
  • 接口方法指存在@RestController注解的类下的方法,或者存在@ResponseBody方法

对其它的接口封装没有意义,封装后还容易出错,所以加了如上限定。

比如对在 public boolean login(String username, String password) 这样一个登录接口封装后的返回结果大致如:

其中 data 是接口方法的返回值。msg 是存在故障时的异常信息提示。code 是返回结果的状态码,这个状态码可以自定义:

如果要调整接口返回值的code,可以在配置文件 application.yaml 中添加如上配置并做调整。

3. 包装异常信息

组件中提供了默认的异常封装能力。

其中自定义的业务异常需要继承 @RwException 或者直接使用 @RwException 这个异常类。封装后的返回值大致如:

除了业务异常外还对接口参数validator校验异常结果进行了封装。

参数校验失败时的code默认是 10000 。

以上两种异常是可控的业务异常,所以尽管接口返回json中的code值虽然不同,但是接口返回信息中的http status还是200。

此外还有可能会出现一些空指针异常之类的因为编程意外而产生的异常,此时的异常信息统一都是:

而且接口返回信息中的http status是500。

如果还想对异常做更进一步的处理,可以考虑禁用当前组件的异常封装能力,并自定义异常封装能力。

要实现禁用当前组件的异常封装能力可以在配置文件中添加如下配置:

目前就只需要做这些配置就够了。

如果想做些自定义的扩展可以参考源码:github / zhyea / zhy-spring-boot-starter

END!!