从零到一理解RAG完整链路:代码逐行拆解+实战解析
在大模型应用爆发的当下,RAG(Retrieval-Augmented Generation,检索增强生成)早已不是陌生概念——它解决了大模型“知识过期”“幻觉生成”的核心痛点,让AI能基于实时、精准的知识库内容生成回答,广泛应用于智能客服、文档问答、企业知识库等场景。
但很多开发者面对RAG代码时,常常陷入“看得懂单个接口,却串不起完整链路”的困境:为什么要分8个步骤?每段代码在链路中承担什么角色?异步处理、流式输出这些细节到底有什么用?
今天,我们就用最通俗的类比、最细致的代码拆解,带你吃透RAG完整链路,从问题接收到底层输出,每一行代码的作用都讲得明明白白,还会补充实战中的扩展技巧,帮你真正把RAG落地到项目中。
一、先搞懂:RAG到底是什么
很多教程一上来就讲代码,容易让人懵圈。其实RAG的核心逻辑特别简单,用两个生活场景就能讲透:
场景1:去图书馆找资料。你想写一篇关于“年假政策”的文章,第一步是去书架上找到相关的书籍、章节(这就是「检索」);第二步是读完这些内容,用自己的话总结出答案(这就是「生成」)。RAG的本质,就是让AI模仿这个过程,避免“凭记忆答题”(也就是大模型的幻觉)。
场景2:点外卖。这个类比更贴合代码链路,我们可以把RAG的8个步骤对应成点外卖的全流程,先建立整体认知:
用户下单(提问)→ 商家接单(系统接收问题)→ 骑手取餐(加载对话历史)→ 厨房备菜(问题重写、改写)→ 按菜单炒菜(意图识别)→ 多路送餐(多通道检索)→ 装盘打包(Prompt 构建)→ 骑手送餐(LLM 生成)→ 送到你手上(流式输出)
记住这个类比,后续拆解代码时,你就能快速对应到每个步骤的核心作用。先明确核心公式:RAG = Retrieval(检索)+ Augmented(增强)+ Generation(生成)——检索是基础,增强是核心,生成是结果。
二、RAG完整链路代码拆解
本次拆解基于Java语言(Spring Boot框架),代码是企业级实战版本,包含异步处理、流式输出、故障转移等生产环境必备特性。每个阶段我们遵循「是什么→生活类比→代码逐行解释→扩展补充」的逻辑,确保你不仅懂“代码写了什么”,还懂“为什么这么写”。
阶段1:问题接收(入口层)—— 商家接单,建立连接
👉 这是什么?
用户请求的入口,系统接收用户问题,并建立流式传输连接,确保后续回答能实时推送给用户(避免用户长时间等待)。
👉 代码
@GetMapping(value = "/rag/v3/chat", produces = "text/event-stream;charset=UTF-8")
public SseEmitter chat(@RequestParam String question,
@RequestParam(required = false) String conversationId,
@RequestParam(required = false, defaultValue = "false") Boolean deepThinking) {
SseEmitter emitter = new SseEmitter(0L);
ragChatService.streamChat(question, conversationId, deepThinking, emitter);
return emitter;
}
用表格清晰拆解每段代码的作用:
| 代码部分 | 详细解释 |
|---|---|
| @GetMapping | 定义GET请求接口,访问地址为/rag/v3/chat,是用户提问的入口 |
| produces = "text/event-stream" | 核心关键:采用SSE(Server-Sent Events)协议,实现“服务器向客户端实时推送数据”,也就是我们常说的“流式输出”,避免一次性返回完整回答导致的等待 |
| String question | 用户的提问内容,必填参数(比如“入职不满一年有年假吗?”) |
| String conversationId | 会话ID,可选参数。新对话可不传,继续对话时传入之前的ID,用于加载历史记录(比如用户追问“那年假怎么申请?”,需要关联上一轮对话) |
| Boolean deepThinking | 是否开启深度思考模式(类似o1模型的推理能力),默认关闭,开启后会调用更擅长推理的模型,适合复杂问题 |
| new SseEmitter(0L) | 创建SSE连接,0L表示连接永不超时(生产环境可根据需求设置超时时间,比如300000L=5分钟) |
| ragChatService.streamChat(...) | 调用服务层方法,传入用户问题、会话ID、深度思考开关和SSE连接,开始处理整个RAG链路 |
| return emitter | 返回SSE连接,Spring框架会自动维护这个连接,后续有数据就实时推送给客户端 |
👉 类比理解
就像打电话:SSE连接就是“电话线路”,streamChat就是“开始通话”,流式输出就是“对方说话你实时听到”,而不是等对方把所有话都说完再一次性听。
阶段2:会话记忆补全——骑手取餐,回顾过往
👉 这是什么?
加载用户之前的对话历史,让AI知道上下文,避免“答非所问”。比如用户先问“年假有多少天”,再问“怎么申请”,AI需要知道上一轮问的是年假,才能准确回答申请流程。
👉 生活类比
你找客服投诉:“上次你们说3天内处理,但已经5天了还没处理”。客服需要先查看你之前的投诉记录(对话历史),才能理解你现在的诉求——这就是会话记忆的作用。
👉 代码
public List<ChatMessage> load(String conversationId, String userId) {
// 步骤1:并行加载摘要和历史记录
CompletableFuture<ChatMessage> summaryFuture = CompletableFuture.supplyAsync(
() -> loadSummaryWithFallback(conversationId, userId)
);
CompletableFuture<List<ChatMessage>> historyFuture = CompletableFuture.supplyAsync(
() -> loadHistoryWithFallback(conversationId, userId)
);
// 步骤2:等待所有任务完成后合并结果
return CompletableFuture.allOf(summaryFuture, historyFuture)
.thenApply(v -> {
ChatMessage summary = summaryFuture.join(); // 获取摘要
List<ChatMessage> history = historyFuture.join(); // 获取历史
return attachSummary(summary, history); // 合并在一起
})
.join(); // 等待所有任务完成
}
| 代码部分 | 详细解释 |
|---|---|
| CompletableFuture | Java中的异步任务容器,相当于“同时派两个骑手去取货”,不用等一个完成再去做另一个,提升效率 |
| summaryFuture | “骑手1”:加载对话摘要(当对话很长时,会将历史压缩成摘要,避免历史记录过多导致模型上下文溢出) |
| historyFuture | “骑手2”:加载完整的对话历史记录(近期的对话,未被压缩的内容) |
| supplyAsync | 异步执行任务,两个加载操作同时进行,不用串行等待 |
| CompletableFuture.allOf(...) | 等待两个异步任务(加载摘要、加载历史)都完成,相当于“等两个骑手都回来” |
| thenApply | 任务完成后,执行合并操作,将摘要和历史记录整合在一起 |
| summaryFuture.join() / historyFuture.join() | 获取两个异步任务的执行结果(摘要和历史记录) |
| attachSummary(...) | 将摘要放在历史记录的前面,确保模型先看到整体摘要,再看详细历史,避免上下文混乱 |
👉 关键优势:并行加载的意义
为什么要并行加载摘要和历史?看一组对比就懂了:
- 串行加载(慢):加载摘要2秒 → 加载历史3秒 → 总计5秒
- 并行加载(快):加载摘要2秒(同时加载历史3秒) → 总计3秒
生产环境中,对话历史可能很多,并行加载能显著提升响应速度,改善用户体验。
👉 实战扩展
可给异步任务指定专用线程池(比如intentClassifyExecutor),避免占用主线程;同时添加降级策略(loadSummaryWithFallback中的Fallback),当加载摘要失败时,直接加载历史记录,避免整个链路中断。
阶段3:问题重写——厨房备菜,规范问题
👉 这是什么?
用户的提问往往是口语化、模糊的(比如“咋整”“怎么弄”),直接用于检索会导致“搜不到相关内容”。问题重写就是把口语化问题,改写成更适合检索的“标准问题”,同时拆分复杂问题(比如“请假和出差有什么区别”拆成两个子问题)。
👉 生活类比
你问朋友:“那个...就是...上次说的那个...怎么弄来着?” 朋友帮你重写:“你问的是上周提到的报销流程怎么申请。” —— 朋友的作用,就是“问题重写”。
👉 代码
public RewriteResult rewriteWithSplit(String userQuestion, List<ChatMessage> history) {
// 步骤1:检查是否启用了 LLM 重写
if (!ragConfigProperties.getQueryRewriteEnabled()) {
// 如果没启用,就用简单的规则处理
String normalized = queryTermMappingService.normalize(userQuestion);
List<String> subs = ruleBasedSplit(normalized);
return new RewriteResult(normalized, subs);
}
// 步骤2:使用 LLM 进行智能重写
String normalizedQuestion = queryTermMappingService.normalize(userQuestion);
return callLLMRewriteAndSplit(normalizedQuestion, userQuestion, history);
}
| 代码部分 | 详细解释 |
|---|---|
| queryRewriteEnabled | 配置开关,控制是否启用AI(LLM)重写。开发环境可关闭,用简单规则测试;生产环境开启,提升重写效果 |
| queryTermMappingService.normalize | 术语归一化,把口语化词汇转换成标准词汇(比如“咋整”→“怎么办”“医保咋用”→“医保卡使用”) |
| ruleBasedSplit | 基于规则的拆分(比如按标点、“和”“或”等连词拆分),比如“请假和出差有什么区别”拆成“请假制度规定”“出差管理规范” |
| RewriteResult | 重写结果对象,包含“改写后的标准问题”和“拆分后的子问题”,供后续意图识别和检索使用 |
| callLLMRewriteAndSplit | 调用大模型进行智能重写和拆分,结合对话历史,让重写更精准(比如用户之前问过“年假”,现在说“怎么休”,会重写成“年假怎么休”) |
👉 实际效果示例
| 用户原始问题 | 改写后的标准问题 | 拆分的子问题 |
|---|---|---|
| “医保怎么用” | “医保卡的使用方法和报销流程” | ["医保卡的使用方法", "医保报销流程"] |
| “报销咋整” | “公司费用报销申请流程” | ["公司费用报销申请流程"] |
| “请假和出差有什么区别” | “请假制度和出差规定的区别” | ["请假制度规定", "出差管理规范"] |
👉 实战扩展
可维护一个“术语映射表”,把常见的口语化词汇、简称(比如“年假”→“年休假”“OA”→“办公自动化系统”)统一映射,提升归一化效果;同时给LLM重写添加模板,明确重写要求(比如“改写后的问题要简洁、准确,适合检索,不添加多余内容”)。
阶段4:意图识别——按菜单炒菜,精准定位
👉 这是什么?
判断用户的问题属于哪个领域、哪个类目,然后去对应的知识库检索(避免“大海捞针”)。比如用户问“年假怎么休”,要识别出属于“人事领域→请假类目→年假话题”,再去人事知识库的请假模块检索,而不是去财务、IT知识库。
👉 生活类比
你去医院挂号:分诊台护士问“你哪里不舒服?”,你说“头疼、发烧”,护士判断“挂内科发烧门诊”——护士的作用,就是RAG系统的“意图识别”。
👉 代码
public List<SubQuestionIntent> resolve(RewriteResult rewriteResult) {
// 步骤1:从重写结果中提取子问题
List<String> subQuestions = CollUtil.isNotEmpty(rewriteResult.subQuestions())
? rewriteResult.subQuestions() // 有子问题就用子问题
: List.of(rewriteResult.rewrittenQuestion()); // 没有就用改写后的问题
// 步骤2:并行识别每个子问题的意图
List<CompletableFuture<SubQuestionIntent>> tasks = subQuestions.stream()
.map(q -> CompletableFuture.supplyAsync(
() -> new SubQuestionIntent(q, classifyIntents(q)),
intentClassifyExecutor
))
.toList();
// 步骤3:收集所有识别结果
List<SubQuestionIntent> subIntents = tasks.stream()
.map(CompletableFuture::join)
.toList();
// 步骤4:限制意图数量,防止检索太多
return capTotalIntents(subIntents);
}
| 代码部分 | 详细解释 |
|---|---|
| rewriteResult.subQuestions() | 上一步拆分出的子问题列表(比如“请假和出差有什么区别”拆成两个子问题) |
| CollUtil.isNotEmpty | 判断子问题列表是否为空(Apache Commons Collections工具类,简化空判断) |
| 并行识别子问题意图 | 用CompletableFuture异步并行处理每个子问题的意图识别,提升效率(比如两个子问题同时识别,不用串行) |
| classifyIntents(q) | 调用AI或规则模型,识别单个子问题的意图(比如“年假怎么休”识别为“人事领域→请假→年假”) |
| intentClassifyExecutor | 意图识别专用线程池,避免占用主线程,提升系统并发能力 |
| capTotalIntents | 限制意图数量(比如最多保留3个),避免识别出太多意图,导致后续检索范围过大、效率降低 |
👉 树形意图分类示意
[系统根节点]
|
┌───────────────┼───────────────┐
↓ ↓ ↓
[人事领域] [财务领域] [IT领域]
| | |
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
↓ ↓ ↓ ↓ ↓ ↓
[请假] [考勤] [报销] [发票] [网络] [设备]
用户问:"年假怎么请"
系统识别:人事领域 → 请假类目 → 年假话题(置信度 0.95)
👉 置信度过滤(关键优化)
识别意图时,会给每个意图打一个“置信度分数”,过滤掉匹配度太低的意图,避免检索无关内容:
private List<NodeScore> classifyIntents(String question) {
List<NodeScore> scores = intentClassifier.classifyTargets(question);
return scores.stream()
.filter(ns -> ns.getScore() >= INTENT_MIN_SCORE) // 过滤低于 0.35 分的
.limit(MAX_INTENT_COUNT) // 最多保留 3 个
.toList();
}
| 分数范围 | 含义 | 处理方式 |
|---|---|---|
| 0.95+ | 高度匹配 | ✅ 精准检索,优先匹配该意图对应的知识库 |
| 0.6-0.95 | 中度匹配 | ✅ 参与检索,作为补充 |
| 0.35-0.6 | 低度匹配 | ⚠️ 可选参与,根据业务需求调整 |
| < 0.35 | 几乎不匹配 | ❌ 直接过滤,不参与检索 |
阶段5:多通道检索——多路送餐,广泛召回
👉 这是什么?
根据意图识别结果,从知识库中找到与问题相关的文档片段(核心步骤,检索的质量直接决定AI回答的准确性)。采用“多通道”检索,兼顾“精准匹配”和“广泛召回”,避免漏检或误检。
👉 生活类比
你在图书馆找书:① 精准检索:知道是《哈利波特》,直接去J.K.罗琳的书架找;② 模糊检索:不知道书名,只记得“魔法师戴眼镜”,在所有书架搜索——多通道检索就是结合这两种方式,确保能找到所有相关书籍。
👉 检索架构图
用户问题
↓
┌─────────────────────────────────────────┐
│ MultiChannelRetrievalEngine │
│ 多通道检索引擎 │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐
│ │ 意图定向检索通道 │ │ 向量全局检索通道 │
│ │ (精准匹配) │ │ (广泛召回) │
│ └────────┬────────┘ └────────┬────────┘
│ │ │
│ └─────────┬─────────┘
│ ↓
│ ┌───────────────────────┐
│ │ 后置处理器链 │
│ │ ① 去重 │
│ │ ② Rerank 重排序 │
│ └───────────────────────┘
│ ↓
│ 检索结果列表
└─────────────────────────────────────────┘
👉 代码
public RetrievalContext retrieve(List<SubQuestionIntent> subIntents, int topK) {
// 步骤1:确定最终返回的数量
int finalTopK = topK > 0 ? topK : DEFAULT_TOP_K; // 默认返回 10 条
// 步骤2:并行处理每个子问题的检索
List<CompletableFuture<SubQuestionContext>> tasks = subIntents.stream()
.map(si -> CompletableFuture.supplyAsync(
() -> buildSubQuestionContext(
si,
resolveSubQuestionTopK(si, finalTopK)
),
ragContextExecutor // 专门的线程池
))
.toList();
// 步骤3:等待所有检索完成并合并结果
List<SubQuestionContext> contexts = tasks.stream()
.map(CompletableFuture::join)
.toList();
return mergeContexts(contexts);
}
| 代码部分 | 详细解释 |
|---|---|
| finalTopK | 检索结果的数量,用户传入topK就用传入的值,否则用默认值10(DEFAULT_TOP_K为常量),避免返回太多结果导致模型上下文溢出 |
| 并行处理子问题检索 | 每个子问题对应一个检索任务,异步并行处理(比如两个子问题同时检索),提升检索效率 |
| buildSubQuestionContext | 为单个子问题构建检索上下文,结合意图信息,确定检索的知识库和范围 |
| resolveSubQuestionTopK | 计算每个子问题应返回的检索结果数量(比如重要子问题返回更多结果,次要子问题返回更少) |
| ragContextExecutor | 检索专用线程池,避免检索操作(可能耗时)阻塞主线程 |
| mergeContexts | 合并所有子问题的检索结果,去重、重排序后,形成最终的检索上下文 |
👉 检索流程补充(核心细节)
private KbResult retrieveAndRerank(SubQuestionIntent intent,
List<NodeScore> kbIntents,
int topK) {
// 步骤1:使用多通道检索
List<RetrievedChunk> chunks = multiChannelRetrievalEngine
.retrieveKnowledgeChannels(subIntents, topK);
if (CollUtil.isEmpty(chunks)) {
return KbResult.empty(); // 没找到任何相关文档
}
// 步骤2:按意图节点分组
Map<String, List<RetrievedChunk>> intentChunks = new ConcurrentHashMap<>();
for (NodeScore ns : kbIntents) {
intentChunks.put(ns.getNode().getId(), chunks);
}
// 步骤3:格式化上下文
String groupedContext = contextFormatter.formatKbContext(
kbIntents, intentChunks, topK);
return new KbResult(groupedContext, intentChunks);
}
关键细节:多通道检索后,会经过“去重”(避免重复的文档片段)和“Rerank重排序”(根据与问题的相似度,重新排序检索结果,把最相关的放在前面),确保后续模型能优先使用最精准的文档。
👉 检索结果示例
| 文档 ID | 文档内容 | 相似度分数 |
|---|---|---|
| doc_001 | “员工年假标准:入职满1年享受5天年假,满3年享受10天,满5年享受15天” | 0.92 |
| doc_002 | “年假申请流程:员工通过OA系统提交年假申请,部门负责人审批后生效” | 0.88 |
| doc_003 | “请假制度包括:事假、病假、年假、婚假等,其中年假需满足入职年限要求” | 0.75 |
| doc_004 | “法定节假日安排:根据国家规定,春节放假7天,国庆节放假7天” | 0.45 |
注:相似度分数越高,说明文档与问题的相关性越强,后续会优先作为模型生成回答的依据。
阶段6:Prompt 构建——装盘打包,清晰呈现
👉 这是什么?
把检索到的文档、对话历史、用户问题,组装成一个完整的“Prompt(提示词)”,让大模型能清晰了解“上下文+问题+参考资料”,从而生成准确、不跑偏的回答。
👉 生活类比
你去问老师问题:只说“年假怎么休?”,老师可能不清楚你是哪个公司、入职多久;但你说“老师好,我是XX公司新入职员工,想了解公司年假政策,比如入职满一年有多少天年假?申请流程是怎样的?”,老师就能精准回答——Prompt构建就是做这种“说清楚上下文”的工作。
👉 代码
public List<ChatMessage> buildStructuredMessages(PromptContext context,
List<ChatMessage> history,
String question,
List<String> subQuestions) {
List<ChatMessage> messages = new ArrayList<>();
// 步骤1:添加系统提示词(告诉 AI 怎么回答)
String systemPrompt = buildSystemPrompt(context);
if (StrUtil.isNotBlank(systemPrompt)) {
messages.add(ChatMessage.system(systemPrompt));
}
// 步骤2:添加 MCP 工具调用的结果(如有)
if (StrUtil.isNotBlank(context.getMcpContext())) {
messages.add(ChatMessage.system(
formatEvidence(MCP_CONTEXT_HEADER, context.getMcpContext())
));
}
// 步骤3:添加知识库检索结果
if (StrUtil.isNotBlank(context.getKbContext())) {
messages.add(ChatMessage.user(
formatEvidence(KB_CONTEXT_HEADER, context.getKbContext())
));
}
// 步骤4:添加历史对话
if (CollUtil.isNotEmpty(history)) {
messages.addAll(history);
}
// 步骤5:添加用户问题
messages.add(ChatMessage.user(question));
return messages;
}
| 代码部分 | 详细解释 |
|---|---|
| systemPrompt(系统提示词) | 告诉大模型“怎么回答”,比如“你是XX公司的HR助手,请根据提供的文档内容准确回答用户问题,文档中没有的信息请明确告知,不要编造”,避免模型生成幻觉 |
| MCP_CONTEXT | MCP工具调用的实时数据(比如实时查询员工的入职年限、考勤记录),补充知识库中没有的动态信息 |
| KB_CONTEXT | 上一步检索到的知识库文档内容,格式化后添加到Prompt中,作为模型回答的参考依据 |
| formatEvidence | 格式化检索结果和工具结果,加上标题(比如“## 知识库文档”“## 动态数据”),让Prompt结构清晰,模型更容易识别 |
| 添加历史对话 | 将之前的对话历史添加到Prompt中,确保模型了解上下文(比如用户上一轮问了“年假有多少天”,这一轮问“怎么申请”,模型能关联起来) |
| 添加用户问题 | 最后添加用户当前的问题,让模型明确“需要回答什么”,避免答非所问 |
👉 最终Prompt结构(示例)
【消息 1 - 系统提示词】
你是XX公司的HR助手,请根据提供的文档内容准确回答用户问题。如果文档中没有相关信息,请明确告知用户,不要编造。
【消息 2 - 知识库文档】
## 文档内容
doc_001:员工年假标准:入职满1年享受5天年假,满3年享受10天,满5年享受15天。
doc_002:年假申请流程:员工通过OA系统提交年假申请,部门负责人审批后生效。
【消息 3 - 历史对话】
用户:年假有多少天?
助手:根据公司规定,入职满1年享受5天年假,满3年享受10天,满5年享受15天。
【消息 4 - 当前问题】
用户:入职不满一年有年假吗?
这样的Prompt结构清晰,模型能快速找到参考资料、理解上下文,生成准确的回答。
阶段7:模型调用——骑手送餐,生成回答
👉 这是什么?
调用大语言模型(LLM),传入构建好的Prompt,让模型基于检索到的文档和上下文,生成回答。同时实现“多模型路由”和“故障转移”,确保模型调用稳定(一个模型失败,自动切换到下一个)。
👉 代码
public StreamCancellationHandle streamChat(ChatRequest request,
StreamCallback callback) {
// 步骤1:选择合适的模型
List<ModelTarget> targets = selector.selectChatCandidates(
request.getThinking());
if (CollUtil.isEmpty(targets)) {
throw new RemoteException("无可用大模型提供者");
}
// 步骤2:尝试每个模型
for (ModelTarget target : targets) {
ChatClient client = resolveClient(target, label);
if (client == null) {
continue; // 这个模型不可用,跳过
}
try {
// 步骤3:调用模型
return client.streamChat(request, callback, target);
} catch (Exception e) {
// 步骤4:失败了,标记并尝试下一个
healthStore.markFailure(target.id());
log.warn("模型 {} 调用失败,切换下一个", target.id());
continue;
}
}
// 所有模型都失败了
throw new RemoteException("大模型调用失败,请稍后再试");
}
| 代码部分 | 详细解释 |
|---|---|
| ModelTarget | 目标模型对象,包含模型名称、提供商、调用地址等信息(比如DeepSeek、阿里云百炼、Ollama本地模型) |
| selectChatCandidates | 根据请求特性选择候选模型(比如开启深度思考模式,优先选择DeepSeek-o1;普通问答优先选择阿里云百炼) |
| resolveClient | 根据模型对象,获取对应的模型调用客户端(不同模型的调用方式不同,比如DeepSeek有专属客户端,阿里云百炼有对应的SDK) |
| client.streamChat(...) | 调用模型的流式生成接口,传入请求、回调函数和模型对象,模型会实时返回生成的内容(流式输出) |
| 故障转移逻辑 | 如果当前模型调用失败(比如网络异常、模型服务宕机),标记失败状态,自动尝试下一个候选模型,确保整个链路不中断 |
👉 故障转移机制(生产环境必备)
请求进来 → 尝试 DeepSeek → 成功✅→ 返回结果
↓ 失败❌
尝试 阿里云百炼 → 成功✅→ 返回结果
↓ 失败❌
尝试 Ollama 本地模型 → 成功✅→ 返回结果
↓ 失败❌
返回错误信息
👉 模型选择策略(实战参考)
| 场景 | 首选模型 | 备选模型 | 说明 |
|---|---|---|---|
| 深度思考(复杂问题) | DeepSeek-o1 | - | 擅长推理,适合需要分析、计算的复杂问题(比如“年假和事假的薪资区别”) |
| 普通问答(简单问题) | 阿里云百炼 | DeepSeek | 响应速度快、成本低,适合简单的政策查询(比如“年假怎么申请”) |
| 本地部署(无外网) | Ollama | - | 可本地部署,无外网需求,适合涉密场景 |
阶段8:流式输出——送到手上,实时反馈
👉 这是什么?
把模型生成的回答,通过之前建立的SSE连接,实时推送给用户,就像“打字机”一样,逐字逐句显示,避免用户长时间等待(尤其是复杂问题,模型生成回答需要几秒,流式输出能提升用户体验)。
👉 代码
@Override
public void onContent(String chunk) {
// 步骤1:检查是否被用户取消
if (taskManager.isCancelled(taskId)) {
return; // 用户取消了,直接返回
}
// 步骤2:过滤空内容
if (StrUtil.isBlank(chunk)) {
return; // 空内容,不处理
}
// 步骤3:累积回答内容
answer.append(chunk);
// 步骤4:分块发送
sendChunked(TYPE_RESPONSE, chunk);
}
@Override
public void onComplete() {
// 步骤1:保存回答到历史记录
Long messageId = memoryService.append(
conversationId,
UserContext.get
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)