这不是一篇“让 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
>
在 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 协同。
这篇文章会把这条演进路径讲清楚。
我这里说的 vibe coding,不是“给 AI 一句需求,等它自动把整个系统写完”。
我真正采用的方式更像这样:
- 我先确定架构边界和关键语义。
- 把一个复杂问题拆成可验证的小任务,让 AI 分段实现。
- 用日志、测试、接口行为和真实对话去验证结果。
- 发现不对,就继续让 AI 帮我重构、补测试、修 Prompt、加约束。
- 我始终保留最终决策权,AI 提高的是实现速度,不替代工程判断。
这套方法特别适合做这种跨层系统:
- 一头连着 Spring WebSocket、MySQL、Redis、SSE。
- 另一头连着大模型、Tool Calling、Prompt、RAG、上下文压缩。
如果没有 AI 辅助,很多“试错成本很高但又必须亲手打磨”的细节会推进得非常慢。 而在 vibe coding 模式下,我能很快做出第一版,再不断把它从“能跑”打磨到“工程上说得过去”。
这里我要先说清楚一件事,避免读者误解:
- 我早期确实参考和尝试过 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 检索增强;
- 上下文压缩;
- 可观测与预览接口;
- 安全边界和失败兜底。
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 的工程化优势非常明显。
这一部分是整个系统最基础、也最不能出错的地方。
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:承担 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/wsImHandshakeInterceptor在握手前校验 JWTImWebSocketHandler extends TextWebSocketHandlerWebSocketSessionRegistry用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_atgroup_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;>> OpenAiCompatibleClient.chatStream()基于LangChain4j的OpenAiStreamingChatModel + StreamingChatResponseHandler透传上游增量输出;- 工具对话现在不只推
tool_call / tool_result / chunk / done,还新增了status事件,让前端先看到“规划中 / 生成中 / 整理工具结果中”; chatWithToolsStream()增加了“明显无需工具时直接流式回答”的快路径,避免用户在首个 token 前白等一轮 tool planning;- Vite 开发代理和后端响应头都会显式关闭缓存 / buffering,避免浏览器最终看到的是“伪流式”。
这里还有一个很重要的工程现实:
- 当前版本采用的是
Spring Web MVC + Flux的响应式写法来做 SSE;> - 它已经可以把 LangChain4j 的流式 chunk 和工具过程事件按 SSE 实时推给前端;
- 但项目主运行时目前仍然是
spring-boot-starter-web+ Tomcat,而不是整站切到spring-boot-starter-webflux+ Reactor Netty。
也就是说,我这次优先解决的是“真实流式链路”和“前端体感问题”,而不是为了追求名义上的 WebFlux,把整个应用一次性重构掉。
这比“只给最终答案”更适合做 AI 调试和用户体验反馈。
很多 AI IM 项目的问题是: 它们只有一个“聊天框”,并没有真正把模型能力和业务系统打通。
我这里做的是更偏 Agentic 的集成方式。
1. Prompt 从文件加载,不写死在代码里
在当前 Java 实现里,我没有把系统提示词直接硬编码在业务逻辑里,而是:
- 用独立的
txt文件管理系统 Prompt; - 用
yaml文件管理工具定义; - 运行时读取并拼装。
这样做的价值很直接:
- Prompt 调整不需要大改业务代码;
- 工具 schema 更容易维护;
- 用户侧和管理侧可以拥有不同约束。
2. 工具不是“口头调用”,而是真执行
我在工具 Prompt 里明确约束了模型:
- 用户要求建群、邀请、踢人、解散群,必须真实调工具;
- 不能没调工具就说“已经完成”;
- 依赖返回值的工具必须串行;
- 互不依赖的工具才允许并发;
group_id必须来自真实工具返回值;- 批量操作必须走批量工具;
- 生活服务问题要调用
get_weather / get_news / get_route。
这套约束的目标只有一个: 让模型不要假装自己有系统权限,而是变成一个严格遵守业务边界的执行代理。
3. 用户侧工具与生活服务工具
用户侧工具不只有 IM 操作,也有生活服务工具:
get_newsget_weatherget_route
这背后其实对应了一个设计取舍:
- 我不希望 AI 只能回答“IM 说明书”;
- 但也不希望生活服务问题被误送到 IM RAG 里乱检索。
这正好就引出了后面 skills 的设计。
项目做到一定阶段以后,我发现只靠一个大 Prompt 会越来越难控制。
典型问题有几个:
1. 同一个系统里,其实存在多种“思考视角”
比如这些问题,其实关注点完全不一样:
- “为什么收不到私聊消息?” 这是客服 / 排障视角。
- “怎么做群未读数才准确?” 这是产品 / 会话语义视角。
- “帮我把这几个人拉进群里。” 这是工具执行 / 群组操作视角。
- “怎么给 AI 助手接入 RAG 和上下文压缩?” 这是 AI 工程视角。
如果都让一个通用 Prompt 去同时处理,模型很容易:
- 回答不聚焦;
- 检索错知识;
- 该调用工具时没调;
- 不该调用时又乱调。
2. 不是所有问题都值得走 RAG
用户问一句:
我想去河北赵州桥玩,现在在广州天河智慧城,这两天天气适合去吗?
这其实是生活服务问题,应该优先走天气工具,而不是:
- 去检索 IM 知识库;
- 匹配什么群聊、消息、上下文;
- 最后给出完全不相关的回答。
所以我后来把判断拆开了:
- 先做
skill planning; - 再决定是否需要 RAG;
- 工具调用和 skill 绑定也分离处理。
这一步是整个系统从“能用”走向“可控”的关键。
这一部分是我现在最想讲清楚的,因为它正好回应了一个真实需求:
AI 不能死板地只走一个 skill,必须能自动判断是基础回答、单 skill、还是多 skill 协同。
当前实现核心在三个对象里:
ImSkillDefinitionSkillCatalogServiceSkillPlan
1. Skill 定义层:把“领域关注点”结构化
每个 skill 目前都包含这些信息:
codenamedescriptionpromptFragmentdomains
当前注册了 6 个技能:
im_customer_serviceim_group_operationsim_admin_operationsim_product_operationsim_risk_controlim_ai_assistant
注意这里的 skill 不是独立 Agent,也不是独立模型。 它本质上是一个“业务关注点包”:
- 一部分用于路由匹配;
- 一部分用于 Prompt 注入;
- 一部分用于 RAG 查询扩展。
补充:当前 6 个 Skill 的职责矩阵
为了避免 skills 讲得过于抽象,我把当前 6 个 skill 的职责拆成一个更具体的矩阵。
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_RESPONSESINGLE_SKILLMULTI_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) 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) 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 的触发主要依赖两类信号:
- 文本中有明显的复合意图词:
“并”
“同时”
“以及”
“并且”
“还要”
“又要”
最终最多选择 3 个 skill 协同。
这其实非常贴近真实问法,比如:
怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩?
这类问题天然跨了:
- 客服排障;
- AI 工程;
- 上下文治理。
如果硬压成单 skill,回答一定会变窄。
补充:用三个真实问题看当前 Skill 路由结果
为了让这套规则更直观,我用三个非常典型的问题来解释当前会怎么路由。
示例 1:纯生活服务问题
用户问题:
我想去河北赵州桥玩,现在在广州天河智慧城,这两天天气适合去吗?
当前规划结果:
mode = BASIC_RESPONSEskills = []useRag = false
为什么?
- 它命中了“天气 / 适合出行 / 路线”这类生活服务词;
- 但没有明显 IM 业务词;
- 所以不应该把它送进 IM 知识库;
- 如果用户走的是 tools 接口,那么后续仍然可以由工具 Prompt 驱动模型调用天气工具。
- 当前实现里,
chatWithToolsStream()还会先用shouldUseToolPlanning()做快路径判断:明显只是普通问答时直接流式回答,明显是天气 / 新闻 / 路线 / 群操作时再进入工具规划循环。
这正是我前面说的:
BASIC_RESPONSE不等于系统什么都不做;- 它只是明确告诉系统:别把这个问题当 IM 业务知识问答。
示例 2:典型单 Skill IM 问题
用户问题:
为什么我收不到私聊消息,登录也没报错?
当前规划结果:
mode = SINGLE_SKILLprimarySkill = im_customer_serviceuseRag = true
为什么?
- “收不到私聊消息”明显是客服 / 排障问题;
- “登录没报错”进一步强化了客服定位;
- 这类问题通常不能只靠模型猜,需要检索排障 runbook 和消息状态规则。
示例 3:典型多 Skill AI 工程问题
用户问题:
怎么给客服排障回答接入 RAG 和 tools,同时保留上下文压缩?
当前规划结果通常会是:
mode = MULTI_SKILLskills至少包含:
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 不再是代码里一个“标签”;
- 而是转化成了模型真正能感知的推理约束。
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,不是“把文档扔进向量库就完了”,而是一步一步做成现在这样的。
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
10。 这里我选择了 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 = 4topK = 5maxMergedResults = 8maxQueryVariants = 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 可能一起冲到前面;
- 明明有更有价值的来源,却被重复内容挤掉了。
所以我在当前实现里做了三层优化:
第一层:合并去重
使用:
sourceheadingchunk_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
contextHintexpandedQueries- 每个 hit 的:
source
heading
vectorScore
rerankedScore
matchedQueries
预览文本
这使得 RAG 调试不再靠猜,而是可以直接看证据链。
9. RAG 与上下文压缩是配套设计,不是两套孤立机制
当前实现里,上下文不会无限增长。
做法是:
- 普通聊天和工具对话分别维护上下文;
- 超过窗口后,只保留最近若干条消息;
- 更早的历史交给模型压缩成摘要;
- 后续 RAG 检索再使用这个摘要作为
summaryHint。
这件事非常重要,因为它解决了两个问题:
- 上下文窗口成本失控。
- 早期关键历史完全丢失。
也就是说:
- 历史摘要是“记忆压缩层”;
- 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_SKILLskills = [im_customer_service, im_ai_assistant]useRag = true
第二步:加载上下文
系统从 AiContextEntity 里取出:
summaryrecent
并从 recent 里抽出:
- 最近用户消息
- 最近助手消息
生成 historyHints。
第三步:构造检索词
LangChainRagRetriever 基于:
- 原始问题
summaryHinthistoryHints- skill 列表
- 当前是否 adminMode
生成一组 expandedQueries。
第四步:向量召回
系统对每个 query variant 调一次向量检索,得到候选 chunk 列表。
这些 chunk 可能分别来自:
05-im-ai-rag-skills.md10-im-support-diagnosis-runbook.md12-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. 每个用户、每种上下文类型分别存
当前至少分了几类上下文:
chattooladmin-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
压缩结果不是只给模型“下次参考一下”,而是会进一步参与:
buildRagHistoryHintssummaryHintcontextHint
也就是说,摘要不仅服务于“记忆”,还服务于“检索理解”。
这是我觉得当前实现里非常关键的一点:
- 上下文压缩不是独立附属功能;
- 它已经进入了 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 可以帮忙生成测试框架,但“测什么最值钱”仍然是人的判断。
如果回头看整个实现过程,我觉得最有价值的不是某一段代码,而是这种协作方式本身。
我是怎么用 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 系统恰好就是这种典型场景。
当前这版已经能跑、能控、能调,但离“更强的生产级智能层”还有不少空间。
我认为后面最值得继续做的方向有这些。
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 当前结论
- 不是独立 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;
- 答案引用;
- 评测集;
- 语义缓存;
- 风险分级工具调用。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/271857.html