构建企业级智能应用平台:从LLM集成到RAG工作流编排
引言
在大语言模型(LLM)技术飞速发展的今天,如何将AI能力真正落地到企业应用场景中,成为众多开发团队面临的挑战。本文将深入介绍一个基于Spring Boot 3.4构建的企业级智能应用平台,展示从底层LLM客户端封装、向量检索、知识切片到可视化工作流编排的完整技术栈。
一、项目概览
这是一个模块化设计的企业级智能知识管理平台,采用微服务架构思想,将不同功能拆分为独立模块:
核心模块
- farm2-llm: 大语言模型客户端抽象层
- farm2-ai-flow: AI工作流引擎与组件化编排
- farm2-skc-know: 知识库管理系统
- farm2-lucene: 全文检索与向量混合检索
- farm2-auth: 统一认证与权限管理
- farm2-base: 基础工具与通用组件
技术栈
- 后端框架: Spring Boot 3.4 + JDK 17
- 持久层: MyBatis 3.0.4 + MySQL 8.0
- 搜索引擎: Apache Lucene 9.9.2
- 缓存: Ehcache 2.10.8
- JSON处理: Fastjson2 2.0.53
- 文档处理: Apache POI + PDFBox
- AI SDK:
- PlexPT ChatGPT SDK 5.1.1
- 阿里云 DashScope SDK 2.21.16
- Ollama 原生HTTP客户端
二、LLM客户端抽象层:统一多模型接入
设计思路
在实际应用中,企业可能需要同时使用多个大模型提供商的服务(如阿里云通义千问、Ollama本地部署、OpenAI等)。为了屏蔽不同API的差异,我们设计了统一的客户端接口体系。
核心接口定义
public interface Farm2LlmClientInter extends Farm2ClientInter {
/**
* 初始化客户端配置
*/
void init(LlmClient client);
/**
* 获取最大Token数量
*/
int getTokenSize();
/**
* 发送消息(支持流式响应)
*/
LlmSendInfo sendMsg(AiTextPrompt msg, Farm2LlmMessageHandleInter handle);
}
支持的模型类型
系统通过LlmFuncKeyEnum枚举区分不同类型的模型:
- 对话模型 (TALK): 用于智能问答、文本生成
- 向量模型 (EMBEDDING): 用于文本向量化,支持语义检索
- 重排序模型 (RERANK): 用于检索结果的相关性排序
客户端实现示例
1. Ollama原生HTTP客户端
@Component
public class Farm2LlmOllamaClientImpl implements Farm2LlmClientInter {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public LlmSendInfo sendMsg(AiTextPrompt msg, Farm2LlmMessageHandleInter handle) {
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", paras.getModelkey());
requestBody.put("messages", buildMessages(msg));
requestBody.put("stream", true); // 启用流式输出
// 发送HTTP请求并处理SSE流式响应
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url(paras.getBaseurl() + "/api/chat")
.post(RequestBody.create(
MediaType.parse("application/json"),
objectMapper.writeValueAsString(requestBody)
))
.build();
// 异步处理流式响应
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
try (BufferedReader reader = new BufferedReader(
response.body().charStream())) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("data: ")) {
String jsonStr = line.substring(6);
JsonNode node = objectMapper.readTree(jsonStr);
String content = node.path("message")
.path("content").asText();
handle.onMsg(content); // 回调处理
}
}
}
}
});
}
}
2. 阿里云DashScope客户端
使用PlexPT SDK封装阿里云通义千问模型,支持更高级的功能如函数调用、思维链等。
动态客户端管理
系统支持在运行时动态切换和选择模型:
// 自动选择最优可用客户端
LlmClient client = llmClientService.getAutoClient(
LlmFuncKeyEnum.TALK, true);
// 根据ID获取指定客户端
Farm2LlmClientInter llmClient =
llmClientService.getTalkClientById(clientId);
这种设计使得:
- ✅ 可以无缝切换不同的模型提供商
- ✅ 支持模型的灰度发布和A/B测试
- ✅ 实现负载均衡和故障转移
三、RAG架构:检索增强生成
什么是RAG?
RAG(Retrieval-Augmented Generation)是一种结合信息检索和文本生成的技术架构。它通过以下步骤提升大模型的回答质量:
- 检索: 从知识库中查找相关文档
- 增强: 将检索到的内容作为上下文注入提示词
- 生成: 大模型基于增强后的上下文生成回答
系统架构
用户提问
↓
查询理解与改写
↓
┌─────────────┬──────────────┐
│ 向量检索 │ 全文检索 │
│ (Semantic) │ (Keyword) │
└──────┬──────┴──────┬───────┘
↓ ↓
混合检索结果融合
↓
重排序 (Rerank)
↓
上下文组装
↓
LLM生成回答
↓
流式返回前端
1. 知识切片策略
高质量的切片是RAG系统的基石。系统提供了多种切片器:
LLM增强切片器 (LLMEnhancedChunker)
这是最智能的切片方式,利用大模型进行语义理解和结构化提取:
public class LLMEnhancedChunker implements ChunkerInter {
@Override
public List<SkcChunk> chunk(String text, Map<String, Object> params) {
// 分批处理长文本
int batchSize = (Integer) params.get("batch_size");
// 调用LLM进行智能切片
String prompt = buildPrompt(text, batchSize);
String response = AiSyncUtils.sendMsgSync(llmClient,
new AiTextPrompt(prompt, systemPrompt, LlmCommandModelEnum.NONE),
timeout, handler);
// 解析JSON格式的切片结果
List<SkcChunk> chunks = FarmJsons.toList(response, SkcChunk.class);
// 每个切片包含:
// - content: 清洗后的正文
// - keywords: 核心关键词
// - search_aliases: 同义词扩展
// - summary: 一句话摘要
// - full_path: 分类路径
return chunks;
}
}
优势:
- 🎯 语义完整性:避免在句子中间切断
- 🔍 元数据丰富:自动生成关键词、摘要
- 📝 代词消解:将"本条规定"替换为具体实体名
- 🌐 别名扩展:增加搜索命中率
其他切片器
- OverlapWindowChunker: 滑动窗口切片,保留重叠部分以维持上下文
- EmbeddingLoadChunker: 基于嵌入向量相似度动态调整切片边界
2. 混合检索实现
系统同时支持向量检索和全文检索,并通过加权融合提升召回率:
public List<SkcChunkIndexDoc> search(RagQuery ragQuery) {
// 1. 向量检索
float[] vector = embedClient.getEmbedding(ragQuery.getWord());
DocumentResult vectorResult = searchService.search(
new DocumentRuleByVector(
"embedding", vector, topK, similarityThreshold
)
);
// 2. 全文检索(Lucene)
String keywordQuery = buildLuceneQuery(ragQuery.getSearchKey());
DocumentResult textResult = luceneService.search(keywordQuery);
// 3. 结果融合(RRF算法)
List<SkcChunkIndexDoc> merged = reciprocalRankFusion(
vectorResult.getDocs(),
textResult.getDocs(),
k = 60 // RRF参数
);
// 4. 重排序
if (rerankClientId != null) {
merged = rerankWithLLM(merged, ragQuery.getWord());
}
return merged;
}
关键技术点:
- Reciprocal Rank Fusion (RRF): 一种无需训练的融合算法,通过倒数排名加权合并多路检索结果
- 权限过滤: 检索时自动过滤用户无权访问的知识
- 多字段Boost: 标题、标签、正文赋予不同权重
3. 上下文补充机制
针对切片导致的上下文丢失问题,系统实现了智能上下文注入:
public class FarmAppComponentInjectLlmTalkImpl
implements FarmAppComponentInter {
@Override
public void run(Map<String, Object> params,
AiAppRunnerContext context) {
// 1. 获取当前检索到的chunks
List<SkcChunkIndexDoc> docs = context.getParas("search_results");
// 2. 使用LLM判断哪些chunk需要补充上下文
for (SkcChunkIndexDoc doc : docs) {
String judgment = llmJudgeNeedContext(doc.getContent());
if ("NEED".equals(judgment)) {
// 3. 从数据库加载相邻chunks
List<AiflowChunkInfo> adjacentChunks =
chunkInfoDao.findAdjacent(
doc.getAppId(),
doc.getPartNo(),
forwardCount = 2,
backwardCount = 2
);
// 4. 合并到原文档
doc.setContent(mergeWithContext(doc, adjacentChunks));
}
}
// 5. 更新上下文变量
context.setParas("enhanced_docs", docs);
}
}
这种机制有效解决了:
- ❓ 指代不明:"该政策"指的是什么?
- 📖 逻辑断裂:前提条件在前一个chunk中
- 🔗 关联缺失:相关概念分散在多个chunk
四、AI工作流引擎:可视化编排智能应用
设计理念
传统的AI应用开发需要将业务逻辑硬编码在代码中,缺乏灵活性。我们设计了基于组件化的工作流引擎,允许用户通过拖拽方式编排复杂的AI业务流程。
核心概念
1. 应用 (AiApp)
一个完整的AI应用,由多个组件节点组成,例如:
- 智能客服助手
- 文档摘要生成器
- 代码审查机器人
2. 组件 (AppComponent)
工作流的基本执行单元,每个组件完成特定功能:
| 组件类型 | 功能描述 | 示例 |
|---|---|---|
| RAG检索 | 从知识库检索相关内容 | 查询相关政策条款 |
| LLM对话 | 调用大模型生成文本 | 生成回答、总结 |
| 布尔判断 | LLM返回true/false | 判断是否敏感内容 |
| 变量转换 | 数据处理与格式转换 | JSON解析、字符串拼接 |
| 消息发送 | 向客户端推送消息 | 流式输出、进度提示 |
| 上下文注入 | 补充缺失的上下文 | 加载相邻文档片段 |
3. 流程定义 (Flow Graph)
使用JSON格式存储有向无环图(DAG):
{
"nodes": [
{
"id": "start",
"type": "start",
"position": {"x": 100, "y": 100}
},
{
"id": "node_rag_search",
"type": "TYPE-APPCOM-RAGSEARCH",
"data": {
"clientId": "emb_client_001",
"topK": 5,
"textLength": 2000
},
"position": {"x": 300, "y": 100}
},
{
"id": "node_llm_talk",
"type": "TYPE-APPCOM-LLMTALK-TEXT",
"data": {
"llmClientId": "llm_client_001",
"prompt": "基于以下资料回答问题:\n{{search_results}}\n\n用户问题:{{user_msg}}",
"sysPrompt": "你是一个专业的知识助手"
},
"position": {"x": 500, "y": 100}
},
{
"id": "end",
"type": "end",
"position": {"x": 700, "y": 100}
}
],
"edges": [
{"source": "start", "target": "node_rag_search"},
{"source": "node_rag_search", "target": "node_llm_talk"},
{"source": "node_llm_talk", "target": "end"}
]
}
工作流执行引擎
@Service
public class FarmAiAppRunnerImpl implements FarmAiAppRunnerInter {
@Override
public void runApp(String appId, AiAppRunnerContext context) {
// 1. 加载应用配置
AiflowApp app = aiflowAppService.getById(appId);
GraphData graph = FlowJsonUtils.parseGraph(app.getFlowJson());
// 2. 从开始节点出发
Node currentNode = FlowGraphUtils.getStartNode(graph);
// 3. 遍历执行节点
while (currentNode != null && !currentNode.isEnd()) {
// 执行当前节点
executeNode(currentNode, context);
// 根据边关系找到下一个节点
currentNode = FlowGraphUtils.getNextNode(
graph, currentNode, context
);
}
// 4. 完成处理
context.complete();
}
private void executeNode(Node node, AiAppRunnerContext context) {
// 获取组件实现类
String componentType = node.getType();
FarmAppComponentInter component =
AppComponentUtils.getComponent(componentType);
// 解析组件参数
Map<String, Object> params = node.getData();
// 执行组件
try {
component.run(params, context);
logSuccess(node.getId());
} catch (Exception e) {
logError(node.getId(), e);
context.sendToClientMsg("执行失败: " + e.getMessage(),
AiAppMsgTypeEnum.ERROR);
throw e;
}
}
}
上下文管理
工作流中的节点通过AiAppRunnerContext共享数据:
public class AiAppRunnerContext {
private final String id; // 会话ID
private SseEmitter emitter; // SSE连接
private List<AiMsgDto> hisMsgs; // 历史消息
private Map<String, Object> paras; // 参数存储
// 设置变量(供后续节点使用)
public void setParas(String key, Object value) {
paras.put(key, value);
}
// 获取变量(支持模板替换)
public String getParaString(String key) {
return (String) paras.get(key);
}
// 向客户端推送消息
public void sendToClientMsg(String msg, AiAppMsgTypeEnum type) {
AiMsgDto aiMsg = new AiMsgDto();
aiMsg.setType(type);
aiMsg.setContent(msg);
emitter.send(SseEmitter.event().data(aiMsg));
}
}
变量引用语法:
在组件配置中可以使用{{variable_name}}引用上下文变量:
{
"prompt": "请总结以下内容:\n{{rag_results}}"
}
执行时会自动替换为实际值。
典型应用场景
场景1:智能问答机器人
[开始]
↓
[RAG检索] → 检索相关知识
↓
[上下文注入] → 补充相邻文档
↓
[LLM生成] → 基于知识生成回答
↓
[发送消息] → 流式返回给用户
↓
[结束]
场景2:文档审核工作流
[开始]
↓
[LLM判断] → 是否包含敏感内容?
↓
[条件分支]
├─ YES → [标记违规] → [通知管理员]
└─ NO → [LLM摘要] → [存档]
↓
[结束]
场景3:多轮对话记忆
[开始]
↓
[加载历史] → 从数据库读取对话历史
↓
[RAG检索] → 检索相关知识
↓
[上下文组装] → 合并历史+知识+当前问题
↓
[LLM对话] → 生成回复
↓
[保存历史] → 持久化对话记录
↓
[发送消息] → 返回给用户
↓
[结束]
五、实战案例:构建智能客服系统
需求分析
某企业需要构建一个智能客服系统,能够:
- 自动回答产品相关问题
- 从知识库中检索准确的政策条款
- 支持多轮对话,记住上下文
- 对于不确定的问题转人工
系统设计
1. 知识库准备
-- 知识分类表
CREATE TABLE SKC_TYPE (
ID VARCHAR(32) PRIMARY KEY,
NAME VARCHAR(256),
PARENT_ID VARCHAR(32),
CODE_PATH VARCHAR(1024) -- 分类路径,如:产品/手机/iPhone
);
-- 知识表
CREATE TABLE SKC_KNOW (
ID VARCHAR(32) PRIMARY KEY,
TITLE VARCHAR(512),
CONTENT TEXT,
TYPE_ID VARCHAR(32),
STATE VARCHAR(2) -- 1:草稿 2:待审 3:发布
);
-- 切片表
CREATE TABLE AIFLOW_CHUNK_INFO (
ID VARCHAR(32) PRIMARY KEY,
APP_ID VARCHAR(32), -- 关联的知识ID
PART_NO INT, -- 切片序号
CONTENT TEXT, -- 切片内容
EMBEDDING BLOB, -- 向量数据
KEYWORDS VARCHAR(1024), -- 关键词
SUMMARY VARCHAR(512) -- 摘要
);
2. 切片处理流程
// 重建所有知识的切片索引
@PostMapping("/reRagChunk")
public FarmResponseResult reRagChunk(@RequestBody KeyValueDto key) {
new Thread(() -> {
if ("KNOW".equals(key.getKey())) {
// 遍历所有已发布的知识
DataResult result = skcKnowService.searchSkcKnow(
DataQuery.getInstance()
.addRule(new DBRule("STATE", "3", "="))
);
for (SkcKnow know : result.getData()) {
// 读取知识内容
String text = FileUtils.readText(know.getContentFile());
// 执行切片流程
AiflowChunkFlow flow = chunkFlowService.getFlow(
new SkcChunkApp(F2EObjectT.KNOW, know.getId(), ...)
);
// 调用配置的切片器
List<SkcChunk> chunks = chunkerService.executeFlow(
flow.getId(), text
);
// 生成向量并存储
for (SkcChunk chunk : chunks) {
float[] embedding = embedClient.getEmbedding(
chunk.getContent()
);
chunkInfoService.saveChunk(know.getId(), chunk, embedding);
}
}
}
}).start();
return FarmResponseResult.success();
}
3. 工作流配置
创建一个名为"智能客服"的应用,配置如下工作流:
节点1:RAG检索
{
"type": "TYPE-APPCOM-RAGSEARCH",
"data": {
"clientId": "emb_qwen_v3",
"topK": 5,
"textLength": 3000,
"searchIndexWordVarKey": "user_msg",
"searchVectorWordVarKey": "user_msg"
}
}
节点2:上下文注入
{
"type": "TYPE-APPCOM-LLMTALK-INJECT",
"data": {
"llm_client_id": "llm_qwen_plus",
"max_check_chunks": 5,
"forward_count": 1,
"backward_count": 1,
"prompt": "判断以下文本是否需要补充上下文才能完整理解..."
}
}
节点3:LLM生成回答
{
"type": "TYPE-APPCOM-LLMTALK-TEXT",
"data": {
"llmClientId": "llm_qwen_max",
"sysPrompt": "你是专业的客服助手,基于提供的资料准确回答问题。如果资料中没有相关信息,请明确告知用户。",
"prompt": "参考资料:\n{{enhanced_docs}}\n\n用户问题:{{user_msg}}\n\n请给出专业、准确的回答:"
}
}
节点4:发送消息
{
"type": "TYPE-APPCOM-SENDMSG",
"data": {
"msgVarKey": "llm_response",
"msgType": "ANSWER"
}
}
4. 前端调用
// 发起智能对话
const eventSource = new EventSource(
`/api/aiflowrunner/run?appId=${appId}&userMsg=${encodeURIComponent(question)}`
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'ANSWER') {
// 流式显示回答
appendToChatBox(data.content);
} else if (data.type === 'SYS_MSG') {
// 显示系统消息
showSystemMessage(data.content);
} else if (data.type === 'COMPLETE') {
// 对话完成
eventSource.close();
}
};
eventSource.onerror = (error) => {
console.error('SSE错误:', error);
eventSource.close();
};
效果展示
用户提问:
iPhone 15的保修期是多久?
系统执行过程:
-
RAG检索:从知识库中找到3个相关切片
- 切片1:iPhone系列产品保修政策概述
- 切片2:iPhone 15具体保修条款
- 切片3:保修范围说明
-
上下文注入:LLM判断切片2需要补充上下文,加载了前后相邻的切片
-
LLM生成:
根据苹果官方政策,iPhone 15的保修期为: 1. 整机保修:自购买之日起1年 2. 主要部件(主板、显示屏等):2年 3. 电池:如果电池容量低于原始容量的80%,且在保修期内,可免费更换 温馨提示: - 保修需要提供购买凭证 - 人为损坏不在保修范围内 - 建议购买AppleCare+延长保修服务 -
流式返回:文字逐字显示,用户体验流畅
六、性能优化与实践建议
1. 向量检索优化
问题:随着知识库增长,向量检索速度下降
解决方案:
- ✅ 使用HNSW索引加速近似最近邻搜索
- ✅ 对热门知识预计算向量缓存
- ✅ 分页批量处理,避免一次性加载过多数据
// 批量生成向量
int batchSize = 100;
for (int i = 0; i < chunks.size(); i += batchSize) {
List<SkcChunk> batch = chunks.subList(i,
Math.min(i + batchSize, chunks.size()));
// 并行生成向量
batch.parallelStream().forEach(chunk -> {
float[] embedding = embedClient.getEmbedding(chunk.getContent());
chunk.setEmbedding(embedding);
});
// 批量入库
chunkInfoService.batchSave(batch);
// 更新进度
FarmProcessUtils.setProcess(processKey,
i * 100 / chunks.size(), "处理中...");
}
2. LLM调用成本控制
策略:
- 🎯 小模型优先:简单任务使用低成本模型
- 💾 结果缓存:相同问题直接返回缓存答案
- ⏱️ 超时控制:避免长时间等待
- 📊 Token监控:统计每次调用的Token消耗
// 智能选择模型
LlmClient selectModel(String taskComplexity) {
if ("SIMPLE".equals(taskComplexity)) {
// 简单任务:使用快速便宜的模型
return llmClientService.getClientByLevel(1);
} else if ("COMPLEX".equals(taskComplexity)) {
// 复杂任务:使用高质量模型
return llmClientService.getClientByLevel(3);
}
return llmClientService.getAutoClient(LlmFuncKeyEnum.TALK, true);
}
3. 并发与异步处理
场景:大量用户同时发起AI请求
方案:
- 使用线程池隔离不同任务
- SSE流式输出减少等待焦虑
- 后台异步处理耗时操作(如重建索引)
// 异步重建索引
@Async("taskExecutor")
public void rebuildIndexAsync(List<String> knowIds) {
String processKey = UUID.randomUUID().toString();
FarmProcessUtils.setProcess(processKey, 0, "开始重建");
try {
for (String knowId : knowIds) {
// 处理单个知识
processKnowledge(knowId);
// 更新进度
updateProgress(processKey);
}
FarmProcessUtils.setProcessEnd(processKey, "完成");
} catch (Exception e) {
FarmProcessUtils.setProcessError(processKey, e.getMessage());
}
}
4. 安全性考虑
- 🔐 API密钥管理:加密存储,不硬编码
- 🛡️ 输入验证:防止提示词注入攻击
- 👤 权限控制:基于角色的知识访问控制
- 📝 审计日志:记录所有AI交互行为
// 权限过滤
Set<String> userReadKnowIds = permissionService.getUserReadKnowIds(currentUser);
// 检索时自动过滤
query.addRule(new WhereInRule("APP_ID", userReadKnowIds));
七、未来展望
技术演进方向
-
多模态支持
- 图片理解与生成
- 语音交互
- 视频内容分析
-
Agent智能体
- 自主规划与决策
- 工具调用(搜索、计算、API)
- 多Agent协作
-
个性化推荐
- 基于用户行为的智能推荐
- 自适应学习路径
-
实时知识更新
- 增量索引构建
- 热更新机制
- 版本管理
最佳实践总结
✅ 模块化设计:清晰的职责划分,便于维护和扩展
✅ 抽象接口:屏蔽底层差异,提高可移植性
✅ 可视化编排:降低使用门槛,提升开发效率
✅ 混合检索:结合向量与关键词,提升召回率
✅ 流式输出:改善用户体验,减少等待焦虑
✅ 权限控制:保障数据安全,符合企业规范
结语
本文详细介绍了一个企业级智能应用平台的核心架构与实现细节。通过LLM客户端抽象层、RAG检索增强、可视化工作流编排等技术,我们能够快速构建灵活、高效的AI应用。
关键成功因素:
- 良好的架构设计:分层清晰,职责明确
- 灵活的扩展机制:插件化组件,易于定制
- 完善的工程实践:性能优化、安全控制、监控告警
希望这篇文章能为正在探索AI应用落地的团队提供一些参考和启发。AI技术仍在快速发展,保持学习和创新的心态,才能在变革中抓住机遇。
产品地址: 添加链接描述
技术栈: Spring Boot 3.4 + JDK 17 + MyBatis + Lucene
更多推荐
所有评论(0)