基于LangChain4j官方文档-AI零代码生成平台的RAG扩展与Skill集成

AI零代码生成平台的RAG扩展与Skill集成

一、前言

AI零代码生成平台初始版本只实现了"用户输入 → AI 输出代码"的简单流程。随着项目复杂度的提升,遇到了两个核心问题:

  1. 知识复用问题:每次代码生成都是"从零开始",AI 无法参考历史生成的优质代码。同样的设计模式、组件结构在不同项目中反复生成,质量参差不齐。

  2. 规范约束问题:代码生成的风格、规范完全依赖系统提示词,但提示词越长,AI 越容易忽略关键约束。需要一种更灵活的机制来按需注入规范。

针对这两个问题,我查阅了 LangChain4j 官方文档,在项目中实现了 RAG(检索增强生成)Skill(技能) 两套扩展机制。

因为 Langchain4j 更新的很快,鱼皮做这个项目的时候好像使用的是1.1.0版本,现在最新版本是1.16.1。我是项目完成后更新到1.15.0后才去做的改造,更新后立马就是大片的红,所以自己扩展的时候请慎重,如果自己改造过程中出现什么问题,很可能就是版本问题,建议自己多去看看官方文档。接下来我在介绍时,也会贴出官方文档对应的网站地址。

下面我主要介绍我项目中 RAG 和 Skill 的实现,其他的改造欢迎查看我的 GitHub 仓库或者去线上地址体验~~~

线上地址:http://www.icodeplay.site/

GitHub:https://github.com/20223309-zhou/ai-code-platform-backend


二、RAG 扩展

2.1 实现目标

在本项目中,RAG 的核心目标不是让 AI"搜索资料",而是让 AI 在生成代码时参考已部署的优质代码块,因为只有构建和部署成功的项目才能保证代码的正确性和可用性。当用户生成"电商网站"时,AI 能从已部署的模板库中检索到风格相似的导航栏、商品卡片等代码片段,作为生成参考,从而保持平台输出的一致性。

2.2 向量嵌入模型选型

官方文档基于本地运行的嵌入模型介绍 在这里 本地嵌入模型完整列表 可在此处找到

LangChain4j提供的本地嵌入模型简单对比:

维度BgeSmallZhV15e5-small-v2AllMiniLmL6V2
开发者智源(BAAI)微软(intfloat)sentence-transformers
向量维度512384384
语言侧重中文英文英文(中文较弱)
参数量~37M~38M~22M
MTEB/基准表现C-MTEB 优异BEIR 49.0MTEB 56.3
最佳场景中文 RAG/语义搜索英文高质量检索原型/边缘部署

所以本项目选择了 BAAI/bge-small-zh-v1.5(BgeSmallZhV15) 作为嵌入模型,原因是:

  • 中文优化:项目的中文 Prompt、代码注释、模板描述多,bge-small-zh 中英双语效果好
  • 维度均衡:512 维,精度高于 MiniLM 的 384 维,又远低于 text-embedding-3-large 的 3072 维,存储和检索效率高
  • 本地部署(主要原因):LangChain4j 官方支持 ONNX 格式,模型约 30MB,零成本零延迟,离线可用

2.3 向量数据库选型

一开始我选择的是Milvus,但是Milvus需要使用Docker进行集群部署,相对复杂,所以我去让AI为我进行选型;

AI对比了四种方案:

方案结论
Qdrant✅ 选用。Docker 单节点部署,LangChain4j 官方提供 QdrantEmbeddingStore,集成代码仅需几行
Milvus❌ 太重。适合大规模集群,小项目杀鸡用牛刀
PGvector❌ 需要绑定 PostgreSQL,项目未使用 PG
Chroma❌ Python 生态为主,Java 集成不成熟

最终我选择了 Qdrant,余弦距离 + HNSW 索引,当前几千条代码向量检索毫秒级响应。

官方文档的Qdrant介绍和示例在这里

2.4 代码切分策略

代码切分不能像切散文一样按 token 数机械截断——那会从中间切开一个函数体,检索到的片段毫无意义。我实现了 SplitExecutor 策略模式,通过标签的正则匹配按代码的语义结构切分:

text
复制代码
QdrantDocumentLoader 扫描 _deployed/ 目录 → LanguageTool 识别代码类型(Vue / HTML / 多文件) → SplitExecutor 路由到对应的 Splitter → VueSplitter:按 <template> / <script> / <style> 三大块拆分 → HtmlSplitter:按 <section> 顶层语义容器拆分 → MultiSplitter:保持文件完整性,每个文件为一个单元 → BgeSmallZhV15 嵌入 → Qdrant 存储

切分流程:

text
复制代码
loadDocuments(filePath) │ ▼ splitExecutor.chunk(项目路径, 文件内容) │ ├─ 从路径提取 codeGenType:"multi_file_1" → "multi" │ ├─ switch(codeGenType) │ │ │ ├─ "vue" → VueSplitter.chunk() │ ├─ "html" → HtmlSplitter.chunk() │ └─ "multi" → MultiSplitter.chunk() │ ▼ ┌─────────────────────────────────────────────────────┐ │ splitter.chunk() │ │ │ │ 文件内容 ≤ 200 字符 → 整个文件作为一个 TextSegment │ │ │ │ 文件内容 > 200 字符 → 按正则匹配语义标签拆分 │ │ ┌──────────┬────────────────────┐ │ │ │ Vue │ <template> │ │ │ │ │ <script> │ │ │ │ │ <style> │ │ │ ├──────────┼────────────────────┤ │ │ │ HTML │ <header> │ │ │ │ │ <section.hero> │ │ │ │ │ <footer> │ │ │ ├──────────┼────────────────────┤ │ │ │ Multi │ 整个文件作为一个块 │ │ │ └──────────┴────────────────────┘ │ │ │ │ 无匹配 → 整个文件作为一个 TextSegment │ │ │ └──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ supplementChunk(chunks) │ │ │ │ 对每个 TextSegment的text,调用 AI 模型: │ │ "用一句话描述以下代码的功能(不超过30字):" │ │ │ │ 将AI描述拼接为新的 TextSegment, | | text格式为"描述:代码内容",例如: │ │ "这是一个深蓝风格的导航栏组件:<nav>...</nav>" │ │ │ └──────────────────────────────────────────────────────┘ │ ▼ List<TextSegment> → embeddingModel.embedAll() → Qdrant存储

TextSegment 的组成:

text
复制代码
TextSegment { String text; // "导航栏组件:<nav>...</nav>"(描述:原始代码) Metadata metadata; // 元数据 } Metadata { "file_name" : "D:/tmp/code_output/vue_project_1/src/App.vue" // 文件完整路径 "tag_type" : "template" // template / script / style / header / full "project_type" : "vue" // vue / html / multi }

存储流程:

1、用户点击部署 → 后端调用deployApp()

java
复制代码
// ① 复制到部署目录(供访问) FileUtil.copyContent(sourceDir, new File(deployDirPath), true); // 在tmp目录下创建一个目录用来复制所有部署后的项目,作为写入Qdrant的语料库 // ② 复制到知识库目录(供检索) String filePath = RAG_LOAD_DIRECTORY_PATH + "/" + codeGenType + "_" + appId; if (!new File(filePath).exists()) { // 只处理首次部署 Thread.startVirtualThread(() -> { // 异步执行,不阻塞部署返回 FileUtil.copyContent(new File(sourceDirPath), new File(filePath), true); qdrantDocumentLoader.loadDocuments(filePath); // 核心:加载到 Qdrant }); }

2、loadDocuments(dirPath) 内部流程

text
复制代码
loadDocuments(RAG_LOAD_DIRECTORY_PATH/multi_file_1) │ ├─ ① getExistingFilePaths() │ └─ 零向量搜索 Qdrant(maxResults=10000, minScore=0.0) │ └─ 提取所有已有文件的 metadata.file_name → Set<String> │ ├─ ② Files.walk(dirPath) 扫描目录 │ ├─ 排除: node_modules / dist / .git │ ├─ 只保留: .vue / .html / .js / .css │ └─ 得到待处理文件列表 │ └─ ③ 逐文件处理 ├─ 检查 metadata.file_name 是否在 Qdrant 中已存在 │ ├─ 已存在 → skip │ └─ 不存在 → │ ├─ splitExecutor.chunk(filePath, content) // 按语义结构切分 │ ├─ embeddingModel.embedAll(segments) // BGE 嵌入(512维) │ └─ embeddingStore.addAll(embeddings, segments) // 存入 Qdrant │ └─ 日志: 新增/跳过 统计

3、关键设计点

环节说明
去重机制getExistingFilePaths() 用零向量搜索查出 Qdrant 中所有已有文件的完整路径,新文件才处理。已部署的项目再次部署不会重复索引
异步执行Thread.startVirtualThread(),不阻塞部署接口返回
目录隔离部署目录 deployKey/ 和知识库目录 codeGenType_appId/ 分离。用户删除部署目录并不会删除知识库目录对应的项目,即使项目重部署,也不会重复导入到Qdrant中
首次部署if (!new File(filePath).exists()) 只检查目录是否存在,不检查内容是否更新。如需强制重新索引需要手动删目录
增量而非全量永远只添加新文件,不清理已删除的旧文件(旧文件的向量仍留在 Qdrant 中)

2.5 RAG 执行链路

LangChain4j官方文档的的RAG实现 在这里

上面已经通过标签的语义切分将部署的项目全部存入了Qdrant,那拥有语料后LangChain4j是如何去实现RAG检索的呢?

完整链路:

text
复制代码
┌─────────────────────────────────────────────────────────────┐ │ 用户启用 RAG 的完整检索链路 │ └─────────────────────────────────────────────────────────────┘ 前端 POST /chat/gen/code ?useRag=true │ ▼ ┌─────────────────────────────────┐ │ AppController.chatToGenCode() │ │ RagSwitchHolder.set(true) │ ← 使用ThreadLocal记录用户是否开启了RAG按钮 └──────────────────────────────────┘ │ ▼ ┌──────────────────────────────────┐ │ AiCodeGeneratorFacade │ │ 获取 AiService 实例 │ │ (已注入 retrievalAugmentor) │ └──────────────────────────────────┘ │ ▼ AiServices 处理用户消息 │ ▼ ┌──────────────────────────────────┐ │ DefaultRetrievalAugmentor │ │ 拦截用户消息,准备检索 │ └──────────────────────────────────┘ │ ▼ ┌──────────────────────────────────┐ │ ConditionalContentRetriever │ ← 装饰器 │ RagSwitchHolder.isEnabled() │ ← 从ThreadLocal获取boolean,判断是否需要开启RAG │ ├─ true → 继续检索 │ │ └─ false → 返回空列表,跳过 │ └──────────────────────────────────┘ │ true ▼ ┌──────────────────────────────────┐ │ EmbeddingStoreContentRetriever │ │ │ │ ① query.text() → embeddingModel │ │ BgeSmallZhV15.embed(userMsg) │ │ → 将用户消息转为 512 维向量 │ │ │ │ ② dynamicFilter(query) │ │ 如果 query 含 "vue" → 过滤 │ │ metadata.project_type = vue │ │ │ │ ③ embeddingStore.search() │ │ Qdrant 余弦相似度检索 │ │ maxResults=5, minScore=0.75 │ │ → 返回 Top-5 相似代码块 │ └──────────────────────────────────┘ │ ▼ ┌──────────────────────────────────┐ │ DefaultContentInjector │ │ 将检索结果注入用户消息: │ │ │ │ === 知识库参考 === │ │ 导航栏组件:<nav>...</nav> │ │ 商品卡片:<div>...</div> │ │ ... │ │ === 用户问题 === │ │ 帮我生成一个电商网站 │ │ │ │ → 返回增强后的 UserMessage │ └──────────────────────────────────┘ │ ▼ 发送给 LLM (DeepSeek / Qwen 等) │ ▼ 生成带参考的代码 │ ▼ ┌──────────────────────────────────┐ │ RagSwitchInterceptor │ │ afterCompletion() │ │ RagSwitchHolder.clear() │ ← 清理 ThreadLocal └──────────────────────────────────┘

在这个链路中,LangChain4j 提供了四个关键扩展点:

组件类名角色
ContentRetrieverEmbeddingStoreContentRetriever将用户 query 向量化 → 搜索 Qdrant → 返回匹配的 TextSegment
ContentInjectorDefaultContentInjector把检索到的代码块按模板拼接到用户消息中
RetrievalAugmentorDefaultRetrievalAugmentor编排整个流程:调用 Retriever → 调用 Injector → 返回增强消息
ContentRetriever(自定义)ConditionalContentRetriever装饰器模式,通过 ThreadLocal 动态开关检索

EmbeddingStoreContentRetriever 做了什么?

text
复制代码
// 配置 EmbeddingStoreContentRetriever.builder() .embeddingModel(embeddingModel) // BgeSmallZhV15,将 query 转向量 .embeddingStore(embeddingStore) // Qdrant,执行向量搜索 .dynamicFilter(query -> ...) // 可以根据TextSegment的元数据标签实现动态过滤 .maxResults(5) // 返回 Top-5 .minScore(0.75) // 相似度阈值 .build();

核心逻辑:

text
复制代码
用户消息"帮我生成一个电商网站" → BgeSmallZhV15.embed("帮我生成一个电商网站") → 得到 [0.123, -0.456, ..., 0.789] (512维向量) → Qdrant.search(向量, limit=5, score_threshold=0.75) → 返回 5 个最相似的代码 TextSegment → ContentInjector 按模板格式化后拼入用户消息 → LLM 收到:"参考这段代码的风格,帮我生成..."

三、Skill 集成

3.1 背景

随着视觉规范、组件设计规范、响应式断点规则不断增加,系统提示词越来越长。长提示词的问题:AI 在长文本中容易"迷失",尤其在需要专注特定任务时,无关的约束反而会干扰输出。

3.2 Skill 设计

LangChain4j 提供了 Skills.toolProvider() 机制——每个 Skill 是一个包含 YAML 元信息的 Markdown 文件,注册为 AI 可调用的"技能工具"。AI 在生成过程中根据当前任务自主决定是否激活某个技能。

我的项目共实现了 9 个 Skill,放置在项目根目录的 skills/ 下:

技能触发条件
form-validation-patterns页面包含表单
table-list-patterns需要展示列表/表格
api-call-pattern涉及 API 请求
responsive-breakpoints响应式布局
design-tokens定义颜色/间距/阴影
micro-interactions动画/过渡反馈
ui-reference用户上传了参考图片或 URL
code-audit代码生成完成后质量检查
responsive-check验证多断点布局表现

3.3 集成方式

官方文档的 Skills 集成 看这里

根据官方的推荐,使用工具模式去集成 Skills,需要提前将 Skill 资源加载到内存中去:

image.png

每个 SKILL.md 文件结构:

yaml
复制代码
--- name: form-validation-patterns description: 智能表单校验:联动规则、动态表单项、实时反馈 trigger: 当前页面包含表单时 --- ## 表单校验实现规范 ...

提前准备好需要的 Skills ,采用ClassPathSkillLoader去将Skills 文档加载到内存中去:

image.png

image.png

java
复制代码
@Configuration @Slf4j public class SkillsInitConfig { @Bean public Skills skills() { log.info("初始化 Skills ..."); return Skills.from(ClassPathSkillLoader.loadSkills("skills")); } }
java
复制代码
@Slf4j @Configuration public class AiCodeGeneratorServiceFactory { @Resource private Skills skills; ~~~ AiServices.builder(AiCodeGeneratorService.class) .toolProvider(skills.toolProvider()) // 一行代码注册所有 Skill .chatMemoryProvider(memoryId -> chatMemory) .tools(toolManager.getAllTools()) .build();

AI 在生成代码时,如果判断当前任务触发了某个技能,会主动激活对应的 SKILL.md 获取完整规范,而不是让所有规范完整写在系统提示词里。这种按需加载的方式大幅降低了无效 token 消耗,同时规范的执行率更高。


四、多模态支持

4.1 图片与文本文件上传

对话生成支持上传图片文本文件作为生成参考:

java
复制代码
List<Content> contents = new ArrayList<>(); // 用户提示词 TextContent textContent = new TextContent(message); contents.add(textContent); // 图片 → ImageContent ImageContent imageContent = new ImageContent(imageUrl); contents.add(imageContent); // 文本文件 → TextContent TextContent textContent = new TextContent("=== 上传的文件内容 ===\n" + fileText); contents.add(textContent); // 构造多模态 UserMessage UserMessage userMessage = UserMessage.from(contents);
  • 图片:上传后被存储到腾讯云 COS,返回imageUrl转换成 ImageContent 送入 AI
  • 文本文件:支持 .md.txt.markdown,读取内容后作为 TextContent 送入 AI
  • 文件大小限制 5MB,前端通过 MIME 类型和后缀双重校验

4.2 网页检索工具(WebFetchTool)

除了上传本地文件,还提供了 WebFetchTool,AI 可以根据用户提供的 URL 访问目标网站,提取其配色方案、字体、布局结构作为设计参考。

结合 ui-reference Skill,流程如下:

text
复制代码
用户上传参考图片或提供参考网站 URL → 激活 ui-reference Skill → 分析参考素材的配色/字体/布局 → 调用 WebFetchTool 抓取目标网站样式 → 将分析结果融入代码生成

4.3 图片搜索与 Logo 生成

  • SearchImageTool:通过 Bing 搜索可商用图片,用于填充网页中的配图
  • GenerateLogoSvg:从 Iconify 图标库搜索 SVG 图标拼入 Logo 布局,搜不到时自动降级为首字母圆角方块 Logo

五、其他

5.1 用户体验改进

5.1.1 流式中断 — 从源头停止 AI 输出

官方文档的流式输出取消 看这里

前端的停止按钮调用后端的取消接口:

text
复制代码
前端点击停止 → POST /app/cancel/{appId} → CancelGenerationManager.cancel(appId) → processTokenStream 检测到 isCancelled(appId) == true → context.streamingHandle().cancel() // LangChain4j 从源头截断 TokenStream → sink.complete() // 结束 SSE 流

关键点在于使用 context.streamingHandle().cancel() 而非 sink.complete()——前者真正通知 AI 停止生成,后者只是关闭了前端的连接,后端仍在空转。

5.1.2 页面导航安全

当用户在生成过程中跳转到其他页面时:

  • 前端onUnmounted 中调用 cancelAppGeneration 接口,同时浏览器自动关闭 EventSource 连接
  • 后端CancelGenerationManager.remove(appId) 清理状态

返回对话页时,组件重新挂载,检查是否有已有对话历史,有则加载、没有则重新发送初始 Prompt——不会自动重复生成。

5.1.3 深度思考流式展示

官方文档支持推理思考过程的流式输出 看这里

使用 DeepSeek 模型的流式推理能力,前端同时展示推理过程(thinking)生成结果(content) 两个区域:

javascript
复制代码
// thinking:展示推理过程 onPartialThinking → SSE 事件 → inner.type === 'thinking' → messages[index].thinking += inner.data // ai_response:展示生成结果 onPartialResponse → SSE 事件 → inner.type === 'ai_response' → messages[index].content += inner.data

两个流独立渲染,互不干扰。用户可以看到 AI 的推理链的同时,逐步看到生成的代码内容。

5.1.4 自动滚动与浮层按钮

text
复制代码
用户位于消息底部 → 新内容自动滚动到底部(autoScrollIfNearBottom) 用户向上翻阅 → 停止自动滚动,右下角出现浮层▼按钮 点击按钮 → 滚动到底部 → 按钮消失

使用 nextTick 确保 DOM 更新完毕后再判断是否在底部,防止内容追加时滚动位置判断错误。

5.2 业务优化

5.2.1 流式输出的演进

三种模式最初都采用流式输出,但能力不同:

  • HTML / 多文件模式:最初使用 Flux<String> 流式输出代码文本,前端逐字渲染,但不支持工具调用。每次修改只能让 AI 全量重新生成代码,再通过 CodeParserExecutor 解析代码块 → CodeFileSaverExecutor 保存到文件。效率低,无法精准修改。
  • Vue 工程模式:从一开始就使用 TokenStream + 工具调用。AI 通过 FileWriteTool 逐个写入文件,通过 FileReadTool / FileModifyTool 完成修改,天然支持精准操作。

我将 HTML 和多文件模式统一升级为 TokenStream,注册了 FileReadToolFileModifyToolSearchImageToolGenerateLogoSvg 等工具,实现了与 Vue 模式一致的工具调用能力。

总结

以上就是我基于官方文档,对AI零代码生成平台的扩展和改造。错误和不足的地方感谢指正。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
我爱爪哇
下载 APP