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

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

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


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

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

层次 技术栈 / 框架 在项目中的职责
后端语言与构建 Java 17Maven 作为 java-im 的运行时与构建工具
应用框架 Spring Boot 3.5.8 提供整体应用启动、依赖注入、配置管理和组件装配能力
Web 接口层 Spring Web MVCspring-boot-starter-validation 提供 REST API、参数校验、控制器层和统一请求处理
实时通信 Spring WebSocket 提供 /api/v1/ws 长连接能力,承担 IM 实时消息收发
流式输出 Spring Web MVC + Flux<ServerSentEvent<?>> 在 Spring MVC 控制器中返回响应式 SSE 事件流,承载 AI 增量输出与工具过程事件
数据持久化 Spring Data JPAHibernateMySQL 8 持久化用户、消息、群组、群成员、上下文等核心业务数据
缓存与状态 RedisSpring Data Redis 承担部分缓存与运行时辅助状态
安全认证 JWTJwtAuthFilterspring-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 APIqwen-turboglm-4.7-flash 提供主备模型能力,支持普通问答与工具调用
RAG 向量库 Qdrant 存储 IM 业务知识向量,支撑检索增强
知识库形态 MarkdownTXT 作为 RAG 的源知识文档,沉淀业务规则、排障 runbook、产品策略
提示词与工具定义 TXT PromptYAML Tool SchemaSnakeYAML 管理系统提示词、摘要提示词、工具定义与规则约束
JSON/序列化 Jackson 处理请求体、WebSocket 消息、SSE 事件和上下文持久化结构
API 文档 springdoc-openapiSwagger UI 提供接口调试与文档展示
测试 Spring Boot TestJUnit 5Mockito 验证 skills 路由、WebSocket 断链清理、关键服务逻辑
前端 Vue 3TypeScriptVitePiniaElement 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 AILangChain4j 让模型接入、向量库接入、工具调用编排更顺手;

  • 对这种“业务系统 + 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

  • WebSocketSessionRegistryConcurrentHashMap 维护在线会话

  • 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

  • 解析成功后,把 usernamerole 写进 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 版里,这部分对应的是:

  • AiControllerAdminController 直接返回 Flux<ServerSentEvent<Map<String, Object>>>

  • OpenAiCompatibleClient.chatStream() 基于 LangChain4jOpenAiStreamingChatModel + 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_assistant AI 工程 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-k 5 最终返回给模型的命中数 太少容易丢信息,太多会污染 Prompt
per-query-top-k 4 每个查询变体先召回多少条 保持扩展 query 有探索空间,但不把噪声带太多
max-merged-results 8 去重前最大保留命中数 给去重和多样化留出候选池
max-chunks-per-source 2 同一来源最多取几个 chunk 防止某一个文档霸榜
similarity-threshold -1 相似度阈值 当前让系统不过早过滤,先依赖 rerank 再收敛
expand-query true 是否开启查询扩展 解决用户口语化提问和文档表达不一致的问题
history-aware true 是否引入历史上下文 解决“这个/刚才那个”之类的上下文依赖问题
history-query-messages 3 最近多少条消息参与历史提示 太多容易引入噪声,3 条是当前比较平衡的值
history-query-max-chars 160 历史提示最大长度 控制 query 膨胀,避免上下文干扰过大
max-query-variants 4 最多构造多少个查询变体 控制检索成本和召回噪声
seed-on-startup true 启动时是否自动种库 开发环境和单机部署更方便

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

  • 检索召回的范围;

  • 检索噪声的大小;

  • 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 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐