Vibe Coding 做了一个基于 Java 的 AI IM 系统:Spring Boot + LangChain4j + WebSocket + Skills + RAG 可商用的全栈落地实战
我用 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 一句需求,等它自动把整个系统写完”。
我真正采用的方式更像这样:
-
我先确定架构边界和关键语义。
-
把一个复杂问题拆成可验证的小任务,让 AI 分段实现。
-
用日志、测试、接口行为和真实对话去验证结果。
-
发现不对,就继续让 AI 帮我重构、补测试、修 Prompt、加约束。
-
我始终保留最终决策权,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 个技能:
-
im_customer_service -
im_group_operations -
im_admin_operations -
im_product_operations -
im_risk_control -
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。
也就是说,当前实现把两个判断拆开了:
-
当前是普通回答、单技能还是多技能。
-
这一轮是否值得启用 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,核心步骤是:
-
从外部目录或 classpath 加载
.md/.txt知识文件。 -
按标题和空行做分块。
-
控制单 chunk 大小,当前上限约
1200字符。 -
为每个 chunk 打 metadata:
-
source -
heading -
domain -
chunk_index
-
-
分批写入向量库,当前 batch size 为
10。 -
写入 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 变体:
-
原始 query
怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩 -
normalize 后 query 去掉多余标点和空白,让表述更规整
-
加入 IM 业务提示词
原始 query + IM 即时通讯 业务规则 -
加入 skill 名称和领域
原始 query + IM智能助手 AI RAG tools 上下文 -
如果启用历史感知,再拼上最近历史摘要
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。
这件事非常重要,因为它解决了两个问题:
-
上下文窗口成本失控。
-
早期关键历史完全丢失。
也就是说:
-
历史摘要是“记忆压缩层”;
-
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;
-
能回答问题、能拉群、能做天气查询。
但如果从工程视角去看,它真正有价值的地方在于:
-
它把 IM 语义、AI 能力、知识系统和上下文治理放到了同一个设计里。
-
它不是用一个大 Prompt 硬撑,而是开始走向可规划、可观测、可维护。
-
它展示了
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;
-
答案引用;
-
评测集;
-
语义缓存;
-
风险分级工具调用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)