我用 Vibe Coding 做了一个基于 Java 的 AI IM 系统:Spring Boot + LangChain4j + WebSocket + Skills + RAG 可商用的全栈落地实战

这不是一篇“让 AI 帮我生成了几个文件”的流水账,而是一篇完整的工程复盘。

这篇文章聚焦的是我最终落地的 Java 技术栈版本,核心工程是 java-im:基于 Spring Boot、Spring WebSocket、Spring Data JPA、Spring AI、LangChain4j、Redis 和 Qdrant 做出来的 AI IM 系统。 我把 AI 当成结对工程师,用 vibe coding 的方式,把这个 Java 项目逐步打磨成一个带有 AI Agent、Tools、Context Compression、Skills 路由和 RAG 优化能力的智能 IM 系统。 整个过程中,AI 不只是代码生成器,更像是实现加速器、重构搭子、Prompt 调参器、日志分析助手和测试补全器。


一、项目技术栈与框架总览

为了让读者一开始就知道这篇文章讲的到底是什么工程,我先把这个项目涉及到的核心技术栈和框架全部摆在最前面。

层次技术栈 / 框架在项目中的职责
后端语言与构建Java 17、Maven作为 java-im 的运行时与构建工具
应用框架Spring Boot 3.5.8提供整体应用启动、依赖注入、配置管理和组件装配能力
Web 接口层Spring Web MVC、spring-boot-starter-validation提供 REST API、参数校验、控制器层和统一请求处理
实时通信Spring WebSocket提供 /api/v1/ws 长连接能力,承担 IM 实时消息收发
流式输出Spring Web MVC + Flux<ServerSentEvent<?>>在 Spring MVC 控制器中返回响应式 SSE 事件流,承载 AI 增量输出与工具过程事件
数据持久化Spring Data JPA、Hibernate、MySQL 8持久化用户、消息、群组、群成员、上下文等核心业务数据
缓存与状态Redis、Spring Data Redis承担部分缓存与运行时辅助状态
安全认证JWT、JwtAuthFilter、spring-security-crypto(BCrypt)处理 HTTP 鉴权、密码加密、用户身份解析
WebSocket 鉴权HandshakeInterceptor在 WebSocket 握手阶段校验 token,并注入当前用户身份
AI 模型编排LangChain4j 1.13.0负责聊天消息结构、tool calling、模型请求封装和对话编排
AI 接入基础设施Spring AI 1.1.2提供 embedding、vector store、模型配置和生态集成能力
大模型接入OpenAI-Compatible API、qwen-turbo、glm-4.7-flash提供主备模型能力,支持普通问答与工具调用
RAG 向量库Qdrant存储 IM 业务知识向量,支撑检索增强
知识库形态Markdown、TXT作为 RAG 的源知识文档,沉淀业务规则、排障 runbook、产品策略
提示词与工具定义TXT Prompt、YAML Tool Schema、SnakeYAML管理系统提示词、摘要提示词、工具定义与规则约束
JSON/序列化Jackson处理请求体、WebSocket 消息、SSE 事件和上下文持久化结构
API 文档springdoc-openapi、Swagger UI提供接口调试与文档展示
测试Spring Boot Test、JUnit 5、Mockito验证 skills 路由、WebSocket 断链清理、关键服务逻辑
前端Vue 3、TypeScript、Vite、Pinia、Element Plus提供聊天界面、会话列表、AI 面板、管理端交互
工程方法Vibe Coding不是框架,但它是我推动这套系统快速迭代和重构的核心工作方式

如果把这张表压缩成一句话,这个项目本质上是:

一个基于 Spring Boot + Spring WebSocket + LangChain4j + Spring AI + Qdrant + MySQL + Redis 的 Java AI IM 系统。


二、为什么我要做这个项目

我想做的不是一个“能发几条消息”的聊天 Demo,而是一个足够接近真实业务的 IM 系统:

  • 有用户体系、JWT 认证、联系人、群聊、消息 ACK、离线补拉、会话列表。

  • 有基于 Spring WebSocket 的长连接,也有 HTTP 和 SSE。

  • 有用户侧接口,也有管理侧接口和管理员 AI 能力。

  • 有 AI,不只是普通聊天,而是能执行工具、能理解上下文、能基于业务知识回答问题。

更重要的是,这个项目不是一次性设计完再照着写,而是在持续迭代里长出来的。

我最终选择的是 Java 技术栈实现,也就是现在这套 java-im 工程。 整个实现过程中,我持续用 vibe coding 的方式把它往更工程化的方向推进,尤其是:

  • 让 AI 不再死板地绑定单一 skill,而是能自动判断:

    • 只是普通回答;

    • 需要单 skill;

    • 还是需要多 skill 协同。

  • 让 RAG 不再只是“查一下向量库”,而是有查询扩展、历史感知、skill 感知、去重、重排、多样化、预览和可观测能力。

这篇文章会把这条演进路径讲清楚。


三、什么是我在这个项目里说的 Vibe Coding

我这里说的 vibe coding,不是“给 AI 一句需求,等它自动把整个系统写完”。

我真正采用的方式更像这样:

  1. 我先确定架构边界和关键语义。

  2. 把一个复杂问题拆成可验证的小任务,让 AI 分段实现。

  3. 用日志、测试、接口行为和真实对话去验证结果。

  4. 发现不对,就继续让 AI 帮我重构、补测试、修 Prompt、加约束。

  5. 我始终保留最终决策权,AI 提高的是实现速度,不替代工程判断。

这套方法特别适合做这种跨层系统:

  • 一头连着 Spring WebSocket、MySQL、Redis、SSE。

  • 另一头连着大模型、Tool Calling、Prompt、RAG、上下文压缩。

如果没有 AI 辅助,很多“试错成本很高但又必须亲手打磨”的细节会推进得非常慢。 而在 vibe coding 模式下,我能很快做出第一版,再不断把它从“能跑”打磨到“工程上说得过去”。


四、项目背景与定位:本文讲的是 Java 实现,不是 Go WebSocket 原型

这里我要先说清楚一件事,避免读者误解:

  • 我早期确实参考和尝试过 Go 版 IM 原型,用它验证过一些 IM 核心语义,比如 ACK、离线消息、群未读。

  • 但本文不是在写 Go WebSocket 项目复盘。

  • 本文真正讲述、也是最终落地发布的版本,是 java-im 这套 Java / Spring Boot 技术栈实现。

也就是说,读这篇文章时,你可以直接把它理解成:

  • 一个基于 Spring Boot 的 IM + AI 系统;

  • WebSocket 连接层是 Spring WebSocket;

  • HTTP 接口和 SSE 流式输出都在同一个 Java 服务里;

  • AI 编排基于 LangChain4j + Spring AI + Qdrant;

  • Skills、RAG、上下文压缩、工具调用都围绕这套 Java 工程展开。

如果要说项目演进,那更准确的表达应该是:

1. 先把 IM 核心语义做对

  • 私聊消息的 ACK 语义;

  • 离线消息补拉;

  • 群成员未读水位;

  • 会话列表和消息持久化。

2. 再把 AI 从“能聊”做成“能落地”

  • Prompt 文件化;

  • Tool Calling;

  • Skills 自动规划;

  • RAG 检索增强;

  • 上下文压缩;

  • 可观测与预览接口;

  • 安全边界和失败兜底。


五、整体架构:基于 Java / Spring Boot 的 IM + AI 系统


Vue3 / TypeScript Frontend

HTTP API

Spring WebSocket (/api/v1/ws)

SSE Streaming (/api/v1/ai/*/stream)

Java IM Service (Spring Boot)

MySQL + JPA/Hibernate

Redis

LangChain4j / OpenAI-Compatible LLM

Spring AI VectorStore + Qdrant

RAG Markdown Knowledge Base

如果把它简化成一句话:

  • 这不是一个 Go WebSocket 服务外加一个 Java AI 服务的拼接方案;

  • 而是一套以 java-im 为核心的 Java 单体工程;

  • IM 主链路、AI 对话、SSE、Tools、Skills、RAG 都在同一个 Spring Boot 项目里完成。

这也是我最终选择 Java 来承载这套系统的原因:

  • Spring 生态在 Web、WebSocket、JPA、Filter、配置管理这类基础设施上非常成熟;

  • Spring AI 和 LangChain4j 让模型接入、向量库接入、工具调用编排更顺手;

  • 对这种“业务系统 + AI 编排”混合场景来说,Java 的工程化优势非常明显。


六、Java 版 IM 的核心技术实现

这一部分是整个系统最基础、也最不能出错的地方。

1. 技术栈选择

当前 Java 版的核心栈大致是:

  • Spring Boot 3.5.x:作为整体应用框架。

  • Spring Web MVC:承担 HTTP API。

  • Spring WebSocket:承担长连接。

  • Spring Data JPA + Hibernate + MySQL:承担消息、群组、成员、上下文等持久化。

  • Spring Data Redis:缓存与辅助状态。

  • Spring Web MVC + Flux<ServerSentEvent<?>>:承担 AI 流式输出。

  • Spring AI + Qdrant:承担向量检索基础设施。

  • LangChain4j:承担模型调用、tool calling 和消息结构编排。

  • 自定义 OpenAiCompatibleClient:统一对接 DashScope、智谱等 OpenAI-Compatible 模型。

  • JwtAuthFilter + HandshakeInterceptor:分别处理 HTTP 和 WebSocket 鉴权。

2. 分层设计与依赖注入

Java 版没有把逻辑都堆在 controller 或 handler 里,而是走了明确的分层:

  • controller:只负责参数和协议层。

  • service:承载业务语义。

  • repository:只做数据访问和聚合查询。

  • websocket:单独维护握手鉴权、会话注册、消息分发和断链清理。

  • prompt + rag + skill + ai client:单独维护 AI 编排层。

实际实现上,这套结构的一个直接好处是:

  • WebSocket 收到 @AI 时走的是同一个 AiServiceImpl;

  • HTTP /api/v1/ai/chat/stream 走的也是同一个 AiServiceImpl;

  • 管理侧 AI 走的仍然是同一套技能规划和检索逻辑,只是 adminMode 不同。

这件事很小,但很能说明一个现实: AI 功能接进系统以后,依赖注入不只是“代码优雅”,而是直接影响用户对“记忆”和“连续对话”的感知。

3. WebSocket 长连接模型

这里必须特别说明一下: 当前对外发布和本文聚焦的实现,不是 Go 里的 gorilla/websocket Hub 模型,而是 Java / Spring WebSocket 的 Handler 模式。

当前 Java 版的实时链路是:

  • @EnableWebSocket + WebSocketConfigurer 注册 /api/v1/ws

  • ImHandshakeInterceptor 在握手前校验 JWT

  • ImWebSocketHandler extends TextWebSocketHandler

  • WebSocketSessionRegistry 用 ConcurrentHashMap 维护在线会话

  • afterConnectionEstablished / handleTextMessage / afterConnectionClosed 分别处理建连、消息和断链

换句话说,这里的设计更贴近 Spring 生态的习惯:

  • 握手鉴权交给 HandshakeInterceptor

  • 连接生命周期交给 TextWebSocketHandler

  • 在线会话管理交给 SessionRegistry

这种实现方式的优点是:

  • 结构更贴近 Spring Boot 应用整体;

  • 和 HTTP Filter、Controller、Service 层的协作更自然;

  • 更容易跟 JPA、AI Service、系统通知服务做集成。

4. 消息 ACK 与离线语义

我没有把消息状态简单做成“成功 / 失败”,而是拆成了更贴近 IM 语义的状态:

  • delivered:对方在线并收到;

  • stored:对方离线,但消息已经落库,等待补拉。

这样做的意义是:

  • 发送端能知道消息到底是“没发出去”,还是“已存储待送达”;

  • 用户体验上不会把离线误判成消息丢失;

  • 后续客服和 AI 排障也有真实依据。

这套语义在 Java 版里直接体现在 ImWebSocketHandler 的处理流程里:

  • 私聊先落库;

  • 如果目标用户在线,就立即投递并标记 delivered;

  • 如果不在线,就保留为 stored;

  • 然后给发送方回 ACK。

5. 群聊离线补拉与未读水位

群聊是 IM 里最容易被低估的复杂点。

私聊的 stored/delivered 语义并不能直接复用到群聊,因为群聊不是一对一状态,而是每个成员都有自己的阅读进度。

所以我最终采用的是:

  • group_members.joined_at

  • group_members.last_read_at

来表达成员级读水位。

核心规则是:

  • 只返回 created_at > COALESCE(last_read_at, joined_at) 的群消息;

  • 新成员没有 last_read_at 时,用 joined_at 兜底;

  • 这样不会收到入群前的旧消息。

这是一个很关键的产品语义修正: 如果这里做错,用户看到的不是“偶发 bug”,而是整个群聊未读逻辑都不可信。

而且这个规则也直接进入了后面的 AI 知识库。 也就是说,RAG 不是在讲“理想中的 IM 设计”,而是在讲这个 Java 系统自己真实采用的群未读语义。

6. 会话列表统一排序

我还专门修过一个会话列表的小坑:

  • 私聊和群聊会话原本分开查询;

  • 结果 append 到同一个切片后顺序错乱;

  • 最终必须按统一的 last_time 再排序一次。

这个问题很像真实业务里常见的那类 bug: 单个模块都没错,但一组合起来,最终表现就不对。

7. JWT 鉴权与 WebSocket 握手

Java 版里,HTTP 和 WebSocket 的鉴权不是混在一起处理的,而是各自走最适合自己的链路。

HTTP 侧
  • 用 JwtAuthFilter extends OncePerRequestFilter

  • 统一拦截 /api/*

  • 从 Authorization: Bearer xxx 里解析 token

  • 把当前用户写进 request attribute

WebSocket 侧
  • 浏览器原生 WebSocket 不方便自定义 Authorization Header

  • 所以握手阶段通过 query 参数拿 token

  • ImHandshakeInterceptor 在握手前解析 JWT

  • 解析成功后,把 username 和 role 写进 WebSocket session attributes

这两套链路并存,是因为:

  • HTTP 请求和 WebSocket 握手本来就不是同一个协议阶段;

  • 如果硬凑一套统一方式,最终只会让实现更绕。

8. SSE 流式输出与 AI 接入

除了 WebSocket,我还保留了 SSE 作为 AI 对话输出通道:

  • /api/v1/ai/chat/stream

  • /api/v1/ai/tools/chat/stream

  • /api/v1/ai/compress/stream

  • /api/admin/ai/chat/stream

这样做有两个好处:

  • IM 主消息链路仍然由 WebSocket 承担,协议边界更清晰;

  • AI 生成链路天然适合 text/event-stream,前端更容易做逐段渲染、取消生成和过程可视化。

这也是我比较认同的一种工程边界:

  • WebSocket 更适合 IM 主消息流;

  • SSE 更适合 AI 生成流;

  • 二者在同一个 Java 服务里共存,但各自只处理最擅长的职责。

在 Java 版里,这部分对应的是:

  • AiController 和 AdminController 直接返回 Flux<ServerSentEvent<Map<String, Object>>>;

  • OpenAiCompatibleClient.chatStream() 基于 LangChain4j 的 OpenAiStreamingChatModel + StreamingChatResponseHandler 透传上游增量输出;

  • 工具对话现在不只推 tool_call / tool_result / chunk / done,还新增了 status 事件,让前端先看到“规划中 / 生成中 / 整理工具结果中”;

  • chatWithToolsStream() 增加了“明显无需工具时直接流式回答”的快路径,避免用户在首个 token 前白等一轮 tool planning;

  • Vite 开发代理和后端响应头都会显式关闭缓存 / buffering,避免浏览器最终看到的是“伪流式”。

这里还有一个很重要的工程现实:

  • 当前版本采用的是 Spring Web MVC + Flux<ServerSentEvent<?>> 的响应式写法来做 SSE;

  • 它已经可以把 LangChain4j 的流式 chunk 和工具过程事件按 SSE 实时推给前端;

  • 但项目主运行时目前仍然是 spring-boot-starter-web + Tomcat,而不是整站切到 spring-boot-starter-webflux + Reactor Netty。

也就是说,我这次优先解决的是“真实流式链路”和“前端体感问题”,而不是为了追求名义上的 WebFlux,把整个应用一次性重构掉。

这比“只给最终答案”更适合做 AI 调试和用户体验反馈。


七、AI 工具链是怎么接进 IM 的

很多 AI IM 项目的问题是: 它们只有一个“聊天框”,并没有真正把模型能力和业务系统打通。

我这里做的是更偏 Agentic 的集成方式。

1. Prompt 从文件加载,不写死在代码里

在当前 Java 实现里,我没有把系统提示词直接硬编码在业务逻辑里,而是:

  • 用独立的 txt 文件管理系统 Prompt;

  • 用 yaml 文件管理工具定义;

  • 运行时读取并拼装。

这样做的价值很直接:

  • Prompt 调整不需要大改业务代码;

  • 工具 schema 更容易维护;

  • 用户侧和管理侧可以拥有不同约束。

2. 工具不是“口头调用”,而是真执行

我在工具 Prompt 里明确约束了模型:

  • 用户要求建群、邀请、踢人、解散群,必须真实调工具;

  • 不能没调工具就说“已经完成”;

  • 依赖返回值的工具必须串行;

  • 互不依赖的工具才允许并发;

  • group_id 必须来自真实工具返回值;

  • 批量操作必须走批量工具;

  • 生活服务问题要调用 get_weather / get_news / get_route。

这套约束的目标只有一个: 让模型不要假装自己有系统权限,而是变成一个严格遵守业务边界的执行代理。

3. 用户侧工具与生活服务工具

用户侧工具不只有 IM 操作,也有生活服务工具:

  • get_news

  • get_weather

  • get_route

这背后其实对应了一个设计取舍:

  • 我不希望 AI 只能回答“IM 说明书”;

  • 但也不希望生活服务问题被误送到 IM RAG 里乱检索。

这正好就引出了后面 skills 的设计。


八、为什么我必须引入 Skills,而不是继续堆 Prompt

项目做到一定阶段以后,我发现只靠一个大 Prompt 会越来越难控制。

典型问题有几个:

1. 同一个系统里,其实存在多种“思考视角”

比如这些问题,其实关注点完全不一样:

  • “为什么收不到私聊消息?” 这是客服 / 排障视角。

  • “怎么做群未读数才准确?” 这是产品 / 会话语义视角。

  • “帮我把这几个人拉进群里。” 这是工具执行 / 群组操作视角。

  • “怎么给 AI 助手接入 RAG 和上下文压缩?” 这是 AI 工程视角。

如果都让一个通用 Prompt 去同时处理,模型很容易:

  • 回答不聚焦;

  • 检索错知识;

  • 该调用工具时没调;

  • 不该调用时又乱调。

2. 不是所有问题都值得走 RAG

用户问一句:

我想去河北赵州桥玩,现在在广州天河智慧城,这两天天气适合去吗?

这其实是生活服务问题,应该优先走天气工具,而不是:

  • 去检索 IM 知识库;

  • 匹配什么群聊、消息、上下文;

  • 最后给出完全不相关的回答。

所以我后来把判断拆开了:

  • 先做 skill planning;

  • 再决定是否需要 RAG;

  • 工具调用和 skill 绑定也分离处理。

这一步是整个系统从“能用”走向“可控”的关键。


九、Skills 当前实现的原理、步骤和运行机制

这一部分是我现在最想讲清楚的,因为它正好回应了一个真实需求:

AI 不能死板地只走一个 skill,必须能自动判断是基础回答、单 skill、还是多 skill 协同。

当前实现核心在三个对象里:

  • ImSkillDefinition

  • SkillCatalogService

  • SkillPlan

1. Skill 定义层:把“领域关注点”结构化

每个 skill 目前都包含这些信息:

  • code

  • name

  • description

  • promptFragment

  • domains

当前注册了 6 个技能:

  1. im_customer_service

  2. im_group_operations

  3. im_admin_operations

  4. im_product_operations

  5. im_risk_control

  6. im_ai_assistant

注意这里的 skill 不是独立 Agent,也不是独立模型。 它本质上是一个“业务关注点包”:

  • 一部分用于路由匹配;

  • 一部分用于 Prompt 注入;

  • 一部分用于 RAG 查询扩展。

补充:当前 6 个 Skill 的职责矩阵

为了避免 skills 讲得过于抽象,我把当前 6 个 skill 的职责拆成一个更具体的矩阵。

Skill Code业务角色主要处理的问题典型关键词Prompt 注入后的关注点对 RAG 的影响
im_customer_service客服 / 排障收不到消息、登录异常、ACK 状态解释、会话异常客服、登录、异常、消息、已读、离线、撤回用客服口吻解释现象、定位故障、区分 stored / delivered / failed更偏向客服 runbook、消息投递链路、常见排障知识
im_group_operations群组操作建群、加群、邀请、踢人、退群、解散群群、建群、邀请、踢人、解散、group_id强调真实 group_id、群主权限、成员校验、批量工具规则更偏向群生命周期、成员关系、群操作规则
im_admin_operations管理后台封禁、解禁、删号、重置密码、统计、角色管理管理员、后台、封禁、统计、角色强调权限、审计、高风险操作、后台边界更偏向后台治理、审计、管理规则
im_product_operations产品 / 运营会话列表、未读数、通知策略、埋点指标、状态机产品、运营、未读、会话、通知、策略让回答更像产品经理,而不是通用客服更偏向会话体验、未读逻辑、投递指标、运营判断
im_risk_control安全 / 风控骚扰、刷屏、黑名单、权限滥用、审核风控、安全、审核、黑名单、刷屏、审计强调频控、最小权限、违规策略、审计留痕更偏向风控规则、风险边界、审核与滥用治理
im_ai_assistantAI 工程Prompt、RAG、tools、上下文压缩、模型路由、智能助手设计AI、RAG、tool、agent、prompt、上下文、skills强调 AI 在 IM 中的落地方法,而不是泛泛聊大模型更偏向 AI/RAG/skills/上下文治理相关知识

这张表的价值在于,它把 skill 从“一个代码名”变成了“一个有工程职责的路由单元”。

换句话说,当前的 skills 体系并不是为了炫技,而是为了回答下面这类现实问题:

  • 用户现在是在问功能解释、产品设计、后台操作、风控边界,还是 AI 工程?

  • 这个问题应该注入哪类 Prompt 关注点?

  • 这个问题应该优先检索哪类知识?

  • 这个问题到底要不要走 RAG?

2. SkillPlan:先规划模式,再决定是否启用 RAG

SkillPlan 当前有 3 种模式:

  • BASIC_RESPONSE

  • SINGLE_SKILL

  • MULTI_SKILL

同时它还带一个关键标记:useRag。

也就是说,当前实现把两个判断拆开了:

  1. 当前是普通回答、单技能还是多技能。

  2. 这一轮是否值得启用 RAG。

这是一个非常重要的设计点,因为:

  • BASIC_RESPONSE 不等于不能调工具;

  • 它只是表示“本轮不强行绑定 IM 业务 skill,不额外注入 skill prompt,不启用 IM RAG”。

例如天气、路线、新闻这些生活服务问题:

  • 在 /chat/stream 里会被当成普通回答处理;

  • 在 /tools/chat/stream 里仍然可以由工具 Prompt 驱动模型去调用 get_weather/get_route/get_news。

也就是说:

  • skill 负责业务视角与知识边界;

  • tool 负责是否真正执行动作;

  • 两者被有意识地解耦了。

3. SkillCatalogService 的路由步骤

当前的 skill 路由逻辑可以概括成下面这几步:

输入用户消息
  -> 如果前端显式指定 requestedSkillCode,直接尊重
  -> 否则先做 normalize
  -> 判断是不是非 IM 的生活服务问题
  -> 对所有 skill 做关键词打分
  -> 判断是否存在多意图
  -> 选出 1~3 个最合适的 skill
  -> 产出 SkillPlan(mode, skills, useRag)

如果把它写成更接近代码的伪代码,大概是这样:

SkillPlan plan(String requestedSkillCode, String userMessage, boolean adminMode) {
    if (requestedSkillCode is not blank) {
        return explicitSingleSkill();
    }
​
    normalized = normalize(userMessage);
    if (normalized is blank) {
        return BASIC_RESPONSE(no rag);
    }
​
    if (isLifeServiceQuery(normalized) && !looksImRelated(normalized)) {
        return BASIC_RESPONSE(no rag);
    }
​
    ranked = rankSkills(normalized, adminMode);
    if (ranked is empty) {
        if (!looksImRelated(normalized)) {
            return BASIC_RESPONSE(no rag);
        }
        return fallbackSingleSkill(with rag);
    }
​
    selected = selectSkills(ranked, normalized);
    if (selected.size() > 1) {
        return MULTI_SKILL(with rag);
    }
    return SINGLE_SKILL(with rag);
}

这个伪代码有两个很关键的点:

  • 生活服务问题优先被挡在 IM skill / IM RAG 之外。

  • 只有“看起来像 IM 领域问题”的请求,才会继续走 skill ranking 和 RAG。

4. 具体打分策略

当前并没有上一个专门的小模型来做 skill router,而是采用了“规则可控 + 结果可解释”的方案:

  • 每个 skill 注册一组关键词;

  • 如果命中关键词,就累计分数;

  • 某些场景再加额外 bonus:

    • 群操作相关词给 im_group_operations 加权;

    • AI/RAG/Prompt/上下文相关词给 im_ai_assistant 加权;

    • 管理端词汇在 adminMode 下给 im_admin_operations 加权。

这个方案的优点是:

  • 可解释;

  • 容易调;

  • 很适合早期迭代。

缺点也很明显:

  • 对隐式意图和复杂语义的泛化能力有限;

  • 仍然依赖人工维护关键词。

但在当前阶段,这是一种我认为非常务实的折中。

5. 多 Skill 判定逻辑

模型什么时候应该进多 skill?

当前实现里,多 skill 的触发主要依赖两类信号:

  • 文本中有明显的复合意图词:

    • “并”

    • “同时”

    • “以及”

    • “并且”

    • “还要”

    • “又要”

  • 排名靠前的 skill 分数足够接近,说明问题跨领域。

最终最多选择 3 个 skill 协同。

这其实非常贴近真实问法,比如:

怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩?

这类问题天然跨了:

  • 客服排障;

  • AI 工程;

  • 上下文治理。

如果硬压成单 skill,回答一定会变窄。

补充:用三个真实问题看当前 Skill 路由结果

为了让这套规则更直观,我用三个非常典型的问题来解释当前会怎么路由。

示例 1:纯生活服务问题

用户问题:

我想去河北赵州桥玩,现在在广州天河智慧城,这两天天气适合去吗?

当前规划结果:

  • mode = BASIC_RESPONSE

  • skills = []

  • useRag = false

为什么?

  • 它命中了“天气 / 适合出行 / 路线”这类生活服务词;

  • 但没有明显 IM 业务词;

  • 所以不应该把它送进 IM 知识库;

  • 如果用户走的是 tools 接口,那么后续仍然可以由工具 Prompt 驱动模型调用天气工具。

  • 当前实现里,chatWithToolsStream() 还会先用 shouldUseToolPlanning() 做快路径判断:明显只是普通问答时直接流式回答,明显是天气 / 新闻 / 路线 / 群操作时再进入工具规划循环。

这正是我前面说的:

  • BASIC_RESPONSE 不等于系统什么都不做;

  • 它只是明确告诉系统:别把这个问题当 IM 业务知识问答。

示例 2:典型单 Skill IM 问题

用户问题:

为什么我收不到私聊消息,登录也没报错?

当前规划结果:

  • mode = SINGLE_SKILL

  • primarySkill = im_customer_service

  • useRag = true

为什么?

  • “收不到私聊消息”明显是客服 / 排障问题;

  • “登录没报错”进一步强化了客服定位;

  • 这类问题通常不能只靠模型猜,需要检索排障 runbook 和消息状态规则。

示例 3:典型多 Skill AI 工程问题

用户问题:

怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩?

当前规划结果通常会是:

  • mode = MULTI_SKILL

  • skills 至少包含:

    • im_customer_service

    • im_ai_assistant

  • useRag = true

为什么?

  • “客服排障回答”是客服知识域;

  • “RAG / tools / 上下文压缩”又是 AI 工程域;

  • 文本里还有“同时”,说明这是典型复合问题;

  • 这时单 skill 反而会丢信息。

6. Skill 结果是怎么影响最终 Prompt 的

在 AiServiceImpl 里,skillPlan 并不是只用于“看一眼”,而是真正参与了系统 Prompt 的构造。

核心逻辑是:

  • 普通聊天走 buildChatSystemPrompt

  • 工具对话走 buildToolSystemPrompt

  • 两者都会把 buildSkillPlanPrompt(skillPlan) 拼进去

最终注入效果大概是:

  • 当前模式:基础回答 / 单 skill / 多 skill 协同

  • 如果是单 skill:

    • 注入该 skill 的关注点 promptFragment

  • 如果是多 skill:

    • 逐个列出多个 skill 的关注点

    • 告诉模型先拆解,再综合回答

    • 只有互不依赖的子任务才能并发

这一步的价值在于:

  • skill 不再是代码里一个“标签”;

  • 而是转化成了模型真正能感知的推理约束。

7. Skills 当前执行链路

把 skills 放进整个请求生命周期里,可以理解成下面这条链路:

用户请求
  -> SkillCatalogService.plan()
  -> 产出 SkillPlan
  -> chat / tool 系统 Prompt 注入 skill planning 规则
  -> 如果 useRag = true,则再做 skill-aware RAG 检索
  -> 模型生成回答或发起工具调用
  -> 工具结果回灌到消息历史
  -> 保存上下文 / 触发压缩

也就是说,skills 不是单独运行的一层,而是连接了:

  • Prompt;

  • RAG;

  • Tool Use;

  • Context。

这就是为什么我会说它不是“一个分类器”,而是当前 AI 编排层的入口。

补充:为什么我当前没有把 6 个 Skill 做成 6 个独立 Agent

很多人看到 skills 之后,第一反应会是:

那为什么不直接做 6 个子 Agent,让一个总控 Agent 去调度它们?

我当前没有这么做,原因主要有四个。

第一,当前问题的复杂度还没高到必须多 Agent

现在大多数问题,本质上还是:

  • 需要一个统一对话上下文;

  • 需要一个统一工具调用历史;

  • 需要一个统一的最终回答出口。

也就是说,当前更需要的是“单模型下的规划与约束”,而不是“多模型并发协作”。

第二,多 Agent 会放大上下文和状态同步成本

如果每个 skill 都变成独立 Agent,那么要解决:

  • 每个 Agent 看哪些历史;

  • 工具结果怎么共享;

  • RAG 命中结果谁负责解释;

  • 最终答案如何聚合;

  • 多 Agent 之间是否会互相矛盾。

这会让系统复杂度一下子从“工程可控”变成“调试地狱”。

第三,当前阶段我更重视可解释和可验证

现在 skills 的好处是:

  • 能直接看到为什么命中;

  • 可以通过规则快速修正;

  • 能在 preview 和日志里验证结果。

而多 Agent 一旦做得不够好,问题就会变成:

  • 路由错了?

  • 检索错了?

  • 子 Agent Prompt 错了?

  • 聚合错了?

定位成本会明显上升。

第四,当前这版更适合作为下一阶段多 Agent 的地基

我现在更愿意把当前 skills 看成一个“中间形态”:

  • 先把 skill 定义、路由、Prompt 注入、RAG 联动做扎实;

  • 之后再逐步演进到 planner + workers 的结构。

也就是说,我并不是否定多 Agent,而是有意把演进顺序排成:

规则化 skill 路由 -> 可解释的多 skill 协同 -> planner 化 -> 多 Agent 化

8. 当前 Skills 设计的意义

这一版 skills 虽然还不是最终形态,但已经解决了几个很实际的问题:

  • 避免所有问题都被死板地套进 IM RAG。

  • 避免所有问题都走一个大而全 Prompt。

  • 给多意图问题提供了一个可控拆解入口。

  • 让技能选择、检索选择、提示词选择之间形成联动。


十、RAG 当前实现原理、优化步骤和工程细节

我这里的 RAG,不是“把文档扔进向量库就完了”,而是一步一步做成现在这样的。

1. 为什么要做 RAG

AI 在 IM 场景里最怕的是“看起来很会说,但说的不是系统真实规则”。

比如这些问题:

  • 为什么消息显示 stored?

  • 为什么群未读不对?

  • 为什么解散群失败?

  • 后台封禁用户要注意什么?

这些都不是通用百科知识,而是系统特有的业务规则。 所以如果不用 RAG,模型很容易“讲得像那么回事,但跟系统实现不一致”。

2. 知识源怎么组织

我把知识源整理成了按主题拆分的 Markdown 文档,比如:

  • IM AI / RAG / Skills

  • 客服与排障 Runbook

  • 风控与审计

  • 产品指标与运营判断

这样做有两个好处:

  • 文档本身可读,适合长期维护;

  • 检索时能保留足够强的来源和标题语义。

3. 种库流程:先把知识切块再入向量库

当前的种库服务是 RagSeedService,核心步骤是:

  1. 从外部目录或 classpath 加载 .md/.txt 知识文件。

  2. 按标题和空行做分块。

  3. 控制单 chunk 大小,当前上限约 1200 字符。

  4. 为每个 chunk 打 metadata:

    • source

    • heading

    • domain

    • chunk_index

  5. 分批写入向量库,当前 batch size 为 10。

  6. 写入 marker,避免重复 seed。

这里我选择了 Spring AI VectorStore + Qdrant 的组合。

这很重要,因为它让我把系统分成了两层:

  • 上层是自己的 RAG 编排逻辑;

  • 下层是可替换的向量存储实现。

补充:为什么我按 Markdown 标题切块,而不是简单按固定长度切块

这一点其实对检索质量影响非常大。

当前 RagSeedService 的切块思路不是纯粹按长度硬切,而是:

  • 遇到标题优先切分;

  • 在段落自然结束处优先切分;

  • 只有 chunk 过长时,才在空行处截断;

  • 过短的片段直接过滤,不进向量库。

这样做的好处是:

1. 每个 chunk 更接近“一个完整业务语义单元”

比如:

  • “为什么群未读不对”

  • “为什么群操作失败”

  • “AI 回答不准确的常见原因”

这些标题本身就构成了很强的检索锚点。 如果按固定长度乱切,很容易把一个规则拆散,最终模型看到的是半截话。

2. heading metadata 变得非常有价值

因为切块围绕标题组织,所以后面重排时:

  • 标题命中可以额外加分;

  • 预览接口里也更容易读懂;

  • 最终定位问题时,人一眼就知道命中了什么知识块。

3. 更适合长期维护知识库

我希望知识库本身是“人能读、也适合检索”的。 Markdown 标题分层刚好满足这个需求。

补充:当前 RAG 的关键配置参数是怎么设计的

在 application.yml 里,我把当前这版 RAG 的关键参数显式配出来了,这一点我非常建议在工程里保留,因为参数不透明的 RAG 系统几乎没法调。

参数当前值作用我为什么这么设
top-k5最终返回给模型的命中数太少容易丢信息,太多会污染 Prompt
per-query-top-k4每个查询变体先召回多少条保持扩展 query 有探索空间,但不把噪声带太多
max-merged-results8去重前最大保留命中数给去重和多样化留出候选池
max-chunks-per-source2同一来源最多取几个 chunk防止某一个文档霸榜
similarity-threshold-1相似度阈值当前让系统不过早过滤,先依赖 rerank 再收敛
expand-querytrue是否开启查询扩展解决用户口语化提问和文档表达不一致的问题
history-awaretrue是否引入历史上下文解决“这个/刚才那个”之类的上下文依赖问题
history-query-messages3最近多少条消息参与历史提示太多容易引入噪声,3 条是当前比较平衡的值
history-query-max-chars160历史提示最大长度控制 query 膨胀,避免上下文干扰过大
max-query-variants4最多构造多少个查询变体控制检索成本和召回噪声
seed-on-startuptrue启动时是否自动种库开发环境和单机部署更方便

这些参数看起来都是小开关,但实际上它们共同决定了:

  • 检索召回的范围;

  • 检索噪声的大小;

  • Prompt 中证据的密度;

  • 每次请求的成本和延迟。

4. 检索不是只查一次,而是查询扩展

当前 LangChainRagRetriever 并不会只拿用户原句做一次向量检索。

它会按规则构造多组查询:

  • 原始 query;

  • normalize 后的 query;

  • 追加 “IM 即时通讯 业务规则” 之类的 query;

  • 追加 skill 名称和 domains 的 query;

  • 如果开启历史感知,再把 summary / recent history 拼进 contextHint;

  • 根据 admin/user 模式追加不同的领域词。

默认控制参数大致是:

  • perQueryTopK = 4

  • topK = 5

  • maxMergedResults = 8

  • maxQueryVariants = 4

这背后的思路是:

  • 不要指望用户每次都把问题问得足够标准;

  • 系统要主动把 query 拉回到“业务知识可以理解的表达”上。

补充:当前查询扩展具体扩了什么

为了让这块更具体,我把当前查询扩展逻辑拆开说一下。

如果原始问题是:

怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩?

系统可能构造出这些 query 变体:

  1. 原始 query 怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩

  2. normalize 后 query 去掉多余标点和空白,让表述更规整

  3. 加入 IM 业务提示词 原始 query + IM 即时通讯 业务规则

  4. 加入 skill 名称和领域 原始 query + IM智能助手 AI RAG tools 上下文

  5. 如果启用历史感知,再拼上最近历史摘要 contextHint + 原始 query

这背后有一个经验判断:

  • 向量检索擅长找语义近似;

  • 但业务知识文档往往更“规整”和“书面化”;

  • 所以 query 必须适度往知识表达方式上靠。

补充:当前重排为什么不直接上 Cross-Encoder

理论上更强的 reranker 当然可以提高排序质量,但我当前没有直接上更重的 cross-encoder,原因是:

  • 当前系统还在快速迭代阶段;

  • 我更需要一套足够轻、足够透明、足够容易调的方案;

  • 规则重排已经能解决不少“向量相似但业务不相关”的问题;

  • 真到下一阶段,再引入更强 reranker 会更合适。

这也是我做这个系统的一贯原则:

  • 先把可解释的基础设施做对;

  • 再逐步堆更贵、更复杂的能力。

5. 历史感知:RAG 不只看当前一句话

如果只看当前 query,很多问题其实是上下文依赖的,比如:

  • “这个群怎么处理?”

  • “刚才那个为什么失败?”

  • “这个情况要怎么答?”

所以当前实现里,RAG 会把两类历史信息用起来:

  • 最近若干条用户/助手消息;

  • 已经压缩好的历史摘要。

然后构造成 contextHint 参与检索。

这一步特别关键,因为它让 RAG 不再是“本轮问什么就查什么”,而是带着会话上下文去理解问题。

6. Skill 感知:先知道自己在哪个领域,再去检索

这也是当前实现里我很看重的一点:

  • SkillPlan 决定了当前涉及哪些业务领域;

  • RAG 在扩展 query 和做重排时会利用这些 skill 信息;

  • skill 的 name/code/domains 都会参与关键词加权。

这意味着同样一句“为什么失败了”,在不同 skill 上下文里,检索结果会不一样:

  • 群组问题偏向群成员、权限、group_id;

  • 客服问题偏向 ACK、登录、消息状态;

  • AI 工程问题偏向 Prompt、上下文压缩、tools、RAG。

这就是 skill 和 RAG 真正联动起来的地方。

7. 检索结果不是直接用,而是会去重、重排、多样化

向量检索的一个常见问题是:

  • 同一份文档的多个相邻 chunk 可能一起冲到前面;

  • 明明有更有价值的来源,却被重复内容挤掉了。

所以我在当前实现里做了三层优化:

第一层:合并去重

使用:

  • source

  • heading

  • chunk_index

组成唯一 key,把重复命中的 chunk 合并,并记录它命中了哪些 query 变体。

第二层:规则重排

不是只看向量分数,而是做了一个轻量 rerank:

  • 原始 vectorScore

  • 命中 query 关键词加分

  • 命中标题关键词再加分

  • 命中 skill 名称 / domains 再加分

  • admin / user 模式下再加不同 bonus

最终得到 rerankedScore。

这套方法很朴素,但很有效,因为它把“语义相似”进一步拉回“业务相关”。

第三层:来源多样化

我加了 maxChunksPerSource 限制,默认单来源最多取 2 个 chunk。

这样可以避免:

  • 前 5 条结果全来自同一篇文档;

  • 模型看到的信息视角过于单一。

这一步很像搜索里的 result diversification,只不过我用的是一种更轻量、可控的工程实现。

8. RAG 结果不是黑盒,我做了可预览和可观测

很多 RAG 系统有一个问题: 你只能看最终回答,但不知道中间到底检索到了什么。

所以我专门做了:

  • /api/v1/ai/rag/status

  • /api/v1/ai/rag/rebuild

  • /api/v1/ai/rag/preview

尤其 preview 很有价值,它可以直接看到:

  • 当前命中的 skillMode

  • 是否 useRag

  • 命中的 skills

  • contextHint

  • expandedQueries

  • 每个 hit 的:

    • source

    • heading

    • vectorScore

    • rerankedScore

    • matchedQueries

    • 预览文本

这使得 RAG 调试不再靠猜,而是可以直接看证据链。

9. RAG 与上下文压缩是配套设计,不是两套孤立机制

当前实现里,上下文不会无限增长。

做法是:

  • 普通聊天和工具对话分别维护上下文;

  • 超过窗口后,只保留最近若干条消息;

  • 更早的历史交给模型压缩成摘要;

  • 后续 RAG 检索再使用这个摘要作为 summaryHint。

这件事非常重要,因为它解决了两个问题:

  1. 上下文窗口成本失控。

  2. 早期关键历史完全丢失。

也就是说:

  • 历史摘要是“记忆压缩层”;

  • RAG 是“知识补充层”;

  • 两者一起工作,模型才能既记得会话,又不胡编系统规则。


十一、RAG 当前这版的完整执行步骤

为了更直观,我把当前链路整理成一个可执行的步骤图:

用户发起请求
  -> SkillCatalogService.plan()
  -> 得到 SkillPlan(mode / skills / useRag)
  -> 加载当前上下文 summary + recent history
  -> buildRagHistoryHints()
  -> LangChainRagRetriever.buildExpandedQueries()
  -> 对每个 query variant 做向量检索
  -> 合并重复 chunk
  -> 结合关键词 / skill / adminMode 做 rerank
  -> 按 source 做 diversification
  -> 把结果注入 system prompt
  -> 模型回答 / 调工具
  -> 保存历史上下文
  -> 超窗后做 context compression

这就是我现在理解的“工程上靠谱的 RAG”:

  • 它不是单一 API 调用;

  • 它是一条受控、可调、可观测、可验证的链路。

补充:一条真实请求在当前系统里是怎么跑完的

这里我用一个更贴近真实开发的问题,串起 skills、RAG、Prompt、tools 和上下文。

用户问题:

怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩?

第一步:Skill Planning

系统先做 SkillCatalogService.plan():

  • 命中“客服 / 排障”

  • 命中“RAG / tools / 上下文压缩”

  • 命中“同时”这样的复合意图词

因此得到:

  • mode = MULTI_SKILL

  • skills = [im_customer_service, im_ai_assistant]

  • useRag = true

第二步:加载上下文

系统从 AiContextEntity 里取出:

  • summary

  • recent

并从 recent 里抽出:

  • 最近用户消息

  • 最近助手消息

生成 historyHints。

第三步:构造检索词

LangChainRagRetriever 基于:

  • 原始问题

  • summaryHint

  • historyHints

  • skill 列表

  • 当前是否 adminMode

生成一组 expandedQueries。

第四步:向量召回

系统对每个 query variant 调一次向量检索,得到候选 chunk 列表。

这些 chunk 可能分别来自:

  • 05-im-ai-rag-skills.md

  • 10-im-support-diagnosis-runbook.md

  • 12-im-product-metrics-and-operations.md

第五步:去重、重排、多样化

系统会:

  • 合并重复命中的 chunk

  • 叠加 skill / keyword / heading bonus

  • 对来源做多样化限制

最后得到更适合注入 Prompt 的 hit 列表。

第六步:拼装系统 Prompt

系统 Prompt 最终不再只是原始 chat_system.txt,而是:

  • 基础系统规则

  • 历史摘要

  • skill planning 结果

  • RAG 检索结果

拼接之后的结果。

也就是说,模型此时看到的并不是“裸问题”,而是:

  • 你应该从哪些业务视角回答;

  • 你可以优先参考哪些系统知识;

  • 当前的会话历史重点是什么。

第七步:模型生成回答或发起工具调用

如果是普通聊天接口,模型会直接回答。 如果是工具接口,模型还可能:

  • 先决定要不要调工具;

  • 再根据工具执行结果生成最终回答。

第八步:写回上下文

最终用户消息、助手回复、工具调用记录、工具结果都会写回上下文:

  • chat 上下文

  • 或 tool / admin-tool 上下文

如果窗口超过阈值,就触发压缩。

这个过程看起来长,但每一步都是在为“最终回答更贴近真实系统”服务。

补充:上下文压缩的具体实现步骤

很多文章一写到上下文压缩,就只说一句“定期做 summary”。 但如果不讲具体步骤,这部分其实很容易被说虚。

我当前的实现是这样的:

1. 每个用户、每种上下文类型分别存

当前至少分了几类上下文:

  • chat

  • tool

  • admin-tool

这样做的原因是:

  • 普通问答历史和工具执行历史的结构不同;

  • 管理后台的问题又有不同的安全边界;

  • 如果混在一起,Prompt 很容易被污染。

2. 上下文结构不是一整段文本,而是 summary + recent

我没有直接存“一大段完整历史”,而是拆成:

  • summary:更早历史的压缩摘要

  • recent:最近还没被压缩的消息列表

这是非常经典但也非常有效的结构。

3. 超窗后不是直接截断,而是先切头尾

当 recent 超出窗口后:

  • 把较老的一段消息作为 head

  • 把最近的一段消息作为 tail

  • tail 直接保留

  • head 交给模型压缩

这样做的结果是:

  • 最新上下文原文不丢;

  • 旧上下文也不会彻底消失。

4. 不同类型上下文用不同的摘要 Prompt

当前实现里:

  • 普通对话走 summarize_chat.txt

  • 工具对话走 summarize_tools.txt

原因很简单:

  • 普通聊天更关心讨论主题和结论;

  • 工具对话更关心操作结果、真实 ID、失败原因、关键参数。

如果共用一个摘要 Prompt,很容易把工具历史压坏。

5. 压缩后的摘要不仅用于记忆,也用于 RAG

压缩结果不是只给模型“下次参考一下”,而是会进一步参与:

  • buildRagHistoryHints

  • summaryHint

  • contextHint

也就是说,摘要不仅服务于“记忆”,还服务于“检索理解”。

这是我觉得当前实现里非常关键的一点:

  • 上下文压缩不是独立附属功能;

  • 它已经进入了 RAG 的主链路。


十二、当前实现里我觉得比较关键的工程细节

除了主功能,我还很重视那些“看起来小,但真的会把系统打崩”的细节。

1. 断链清理不能把 WebSocket 收尾流程搞崩

我最近修了一个很典型的问题:

  • WebSocket 关闭后,服务端会尝试更新用户所在群组的 last_read_at;

  • 但如果清理过程中数据库查询报错,整个收尾逻辑会抛异常;

  • 最终在日志里出现一串 Unhandled exception after connection closed。

后来我把逻辑改成:

  • 断开连接先正常打印日志;

  • 如果用户名缺失,直接跳过清理;

  • 清理失败只记 warning,不再往外抛。

这个修复的本质不是“吞异常”,而是把清理流程从主链路故障里隔离出去。

2. groups 是 MySQL 保留字,表名要显式转义

还有一个很真实的坑:

  • JPA 查询 groups 表时报 SQL syntax error;

  • 根因是 groups 在 MySQL 里是保留字;

  • 最后通过 @Table(name = "\groups\") 解决。

这类问题特别适合用 vibe coding 配合日志排查:

  • AI 可以很快帮我定位“像保留字问题”;

  • 但最终还是要我结合实际 SQL 和 ORM 生成语句来确认。

3. AI 调用要有主备兜底

在当前 Java 实现里,我保留了主备模型切换能力:

  • 主模型失败先打日志;

  • 再切到备用模型;

  • 尽量保证对话不中断。

这件事在 AI 功能里非常重要,因为模型服务的不稳定性远高于传统数据库和缓存。

4. Tool Loop 必须限制轮数

当前工具调用轮数有上限,比如现在这版实现设置了 MAX_TOOL_ROUNDS = 5。

原因很简单:

  • 不限制,模型可能在异常情况下来回试探;

  • 一旦工具结果不符合预期,可能出现无穷循环调用。

所以工程上必须给 Agent 一个硬边界。

5. 需要测试,而不是只靠人工聊天验证

我补了几类很关键的测试:

  • SkillCatalogServiceTest

    • 验证生活服务问题不误入 IM skill / RAG;

    • 验证单 skill 和多 skill 路由是否符合预期。

  • ImWebSocketHandlerTest

    • 验证断链清理失败时不会继续抛异常。

AI 可以帮忙生成测试框架,但“测什么最值钱”仍然是人的判断。


十三、我如何用 Vibe Coding 推进这个项目

如果回头看整个实现过程,我觉得最有价值的不是某一段代码,而是这种协作方式本身。

我是怎么用 AI 的

我主要把 AI 用在这些地方:

  • 快速生成第一版模块骨架;

  • 帮我把自然语言需求翻成 Prompt 规则;

  • 根据日志反推问题可能出在哪一层;

  • 对已有代码做局部重构;

  • 补测试、补异常分支、补边界处理;

  • 帮我把“一个模糊目标”拆成一组可实现步骤。

补充:我实际采用的一轮 Vibe Coding 工作流

很多人会问:

你说你是用 vibe coding 做的,那一轮完整协作到底长什么样?

我把自己现在比较稳定的一轮协作方式总结成 8 步:

第 1 步:先定边界,不让 AI 自由发挥

我会先明确告诉 AI:

  • 现在要改的是哪一层;

  • 不能改什么;

  • 成功标准是什么;

  • 如果有旧实现,要优先兼容什么。

比如:

  • 这次只改 skills 路由,不要顺手改工具 schema;

  • 这次要让生活服务问题不进入 IM RAG;

  • 这次要把断链清理失败改成 warn 日志,不允许再抛异常。

第 2 步:让 AI 先读代码,再动手

我不会一上来就让 AI 直接写。 因为在这种项目里,“先理解代码现状”远比“直接生成一份看起来正确的新代码”重要。

第 3 步:让 AI 提一个局部实现方案

这一步通常不是为了听思路,而是为了看它是否真的理解了问题边界。 如果方案里开始飘,我就能及时把它拉回来。

第 4 步:让 AI 改一小段,立即验证

我更偏好:

  • 一次只改一块;

  • 一次只修一个逻辑问题;

  • 一次只验证一条主链路。

因为这种做法最适合快速回滚和快速定位。

第 5 步:拿真实日志和真实请求打它

这一点我特别依赖真实输入:

  • HTTP exchange 日志

  • WebSocket 断链日志

  • 模型请求体

  • 工具调用结果

  • RAG 命中结果

很多问题不是代码静态看不出来,而是必须用真实请求才能打出来。

第 6 步:如果结果不对,不是只改代码,还要改 Prompt 和规则

AI 系统和传统 CRUD 不一样。 很多问题不在业务代码本身,而在:

  • Prompt 约束不够强;

  • 工具 schema 不够清晰;

  • 路由规则不够细;

  • 检索查询不够稳定。

所以我经常会同时改:

  • Java 代码

  • Prompt 文件

  • YAML 工具定义

  • 测试样例

第 7 步:补一层可观测性

如果一个 AI 功能调不好,通常不是“模型太笨”,而是你根本看不见它中间发生了什么。

所以我会优先补:

  • 请求日志

  • 检索预览

  • 工具调用事件

  • 失败原因

  • 测试断言

第 8 步:把这轮经验沉淀成规则

比如最后沉淀出来的就可能是:

  • 生活服务问题直接 BASIC_RESPONSE,不进 IM RAG;

  • 群名必须先解析真实 group_id;

  • 批量操作必须走批量工具;

  • 断链清理失败只打 warning;

  • RAG 结果必须可 preview。

这一步很重要,因为它决定了系统是“修完一个问题”,还是“真的长出更强的工程能力”。

我没有把 AI 用成什么

我没有把 AI 当成:

  • 不看代码的自动提交机;

  • 不验证结果的万能工程师;

  • 一句需求就能替我负责系统正确性的黑盒。

因为这类项目一旦跨到:

  • WebSocket 状态语义;

  • 数据一致性;

  • 工具执行边界;

  • RAG 命中质量;

  • Prompt 安全性;

你就会很清楚地知道,人必须在环。

Vibe Coding 最适合解决什么问题

我认为它最适合:

  • 初版原型很复杂,但不想手敲每个样板的场景;

  • 系统有很多“半结构化知识”要转成代码和 Prompt;

  • 需要频繁试错、快速验证、持续重构的项目。

这个 AI IM 系统恰好就是这种典型场景。


十四、Skills 和 RAG 后续还能怎么继续优化

当前这版已经能跑、能控、能调,但离“更强的生产级智能层”还有不少空间。

我认为后面最值得继续做的方向有这些。

1. Skill Router 从规则路由升级到轻量模型路由

现在的 skill 选择还是关键词 + bonus。

下一步可以考虑:

  • 做一个小型 intent classifier;

  • 或者让一个便宜模型先做 route,再交给主模型回答。

这样可以提升对隐式意图和复杂语义的识别能力。

2. Skill 体系从内置注册升级成配置化 / 插件化

当前 skill 是代码内注册的。

未来可以把它演进成:

  • skill 配置文件;

  • skill 对应独立 prompt fragment;

  • skill 对应独立工具白名单;

  • skill 对应独立知识域。

这样扩展新领域时,不必改核心代码。

3. 多 Skill 从“并列协同”升级成分层规划

现在的多 skill 仍然偏平面。

后续可以做成更明确的 Planner:

  • 先拆子任务;

  • 再判断哪些串行、哪些并行;

  • 最后做结果汇总。

也可以把工具规划和 skill 规划进一步合并。

4. RAG 从纯向量检索升级到混合检索

当前还是以向量召回为主,辅以规则重排。

未来可以继续加:

  • BM25 / keyword hybrid retrieval;

  • metadata filter;

  • 按 skill / domain / role 分 collection;

  • 基于 query type 的动态 topK;

  • 更强的 reranker。

这会让检索质量更稳定。

5. 增加答案引用与证据对齐

现在模型已经能拿到 source / heading,但最终回答还没有强制输出引用。

未来如果做得更严格,可以:

  • 在回答里显式给出知识来源;

  • 或至少输出内部证据 ID;

  • 让“为什么这样回答”更容易审计。

6. 做离线评测集与回归测试

这会是后面非常值钱的一步。

比如建立一批标准问答:

  • 客服问题;

  • 群操作问题;

  • 管理后台问题;

  • AI 工程问题;

  • 生活服务问题。

每次改:

  • skill 路由规则;

  • RAG 参数;

  • Prompt;

  • 工具定义;

都跑一遍回归评测,才能避免“修一个点、坏一大片”。

7. 做语义缓存和分层成本控制

对高频重复问题,可以增加:

  • query normalization;

  • semantic cache;

  • 不同模型分层调用;

  • 低成本模型做 route / summarize,高成本模型做最终回答。

这对成本和延迟都会更友好。

8. 让工具调用再加一层风险控制

现在已经有不少规则约束了,但未来还可以继续加强:

  • 高风险工具需要二次确认;

  • 不同 skill 绑定不同工具白名单;

  • 根据用户角色动态裁剪工具集;

  • 工具失败后进入明确的降级策略,而不是让模型反复试。


十五、我对这个项目的最终理解

如果只看表面,这像是一个:

  • 基于 Java 技术栈实现的 IM 项目;

  • 在同一个 Spring Boot 工程里做了 AI 能力增强;

  • 接了向量库;

  • 加了几个 tools;

  • 能回答问题、能拉群、能做天气查询。

但如果从工程视角去看,它真正有价值的地方在于:

  1. 它把 IM 语义、AI 能力、知识系统和上下文治理放到了同一个设计里。

  2. 它不是用一个大 Prompt 硬撑,而是开始走向可规划、可观测、可维护。

  3. 它展示了 vibe coding 在复杂系统里的正确打开方式:

    • 不是替代工程判断;

    • 而是加速实现、加快验证、加深迭代。

我现在越来越相信一件事:

未来很多真正有价值的项目,都不是“AI 自动生成完毕”的产物,而是人和 AI 作为双人组,一起把一个系统从模糊想法推到工程成品。

这个 AI IM 系统,就是我用这种方式做出来的一个完整样本。


附:如果你只想快速看当前 Skills 和 RAG 的实现结论

Skills 当前结论

  • 不是独立 Agent,而是“领域关注点 + 路由规则 + Prompt 片段 + RAG 边界”的组合。

  • 当前自动判断三种模式:

    • BASIC_RESPONSE

    • SINGLE_SKILL

    • MULTI_SKILL

  • 最多协同 3 个 skill。

  • skill 决定业务视角和是否启用 RAG。

  • tool 决定是否真正执行动作。

RAG 当前结论

  • 采用 Markdown 知识库 -> chunk -> metadata -> Qdrant -> vector retrieval -> rerank -> diversify -> prompt grounding 的完整链路。

  • 不是单 query 检索,而是:

    • query expansion

    • history-aware retrieval

    • skill-aware retrieval

    • dedupe

    • rerank

    • source diversification

  • 提供 preview/status/rebuild,方便调试和回归。

下一步最值得做的事

  • skill router 小模型化;

  • 多 skill planner 化;

  • hybrid retrieval;

  • 答案引用;

  • 评测集;

  • 语义缓存;

  • 风险分级工具调用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐