写在前面
做少儿跑酷教练知识库问答助手项目时,我接触到了一个很典型的 AI 应用场景:用户在小程序里提出和课程、等级、动作相关的问题,系统根据已有知识库返回回答。
这个模块的实现方式不是从零搭建一套向量数据库和 RAG 服务,而是基于扣子工作流完成知识库问答能力。业务侧主要负责把用户问题和会话信息传给工作流,再把工作流返回的结果通过 WebSocket 流式输出给前端。
一开始我对它的理解比较偏“调用一个 AI 工作流接口”。但如果想真正理解这类 AI 应用,不能只停在调用层,还要知道几个问题:
- 为什么知识库问答不能直接把问题发给大模型?
- 扣子工作流在这条链路里解决了什么?
- RAG 背后的检索增强生成思路是什么?
- WebSocket 流式输出为什么会让前端状态变复杂?
这篇文章就从“调用扣子工作流”这个真实入口出发,整理我对知识库问答的理解。
为什么不直接问大模型
假设用户问:
L2 阶段的孩子适合练哪些跑酷动作?
如果直接把这个问题发给通用大模型,它可能会给出一套看起来合理的训练建议。但这个回答不一定符合具体机构自己的课程体系。比如:
- L1、L2、L3 每个等级分别对应哪些动作。
- 某个动作是否已经纳入当前课程。
- 不同年龄段是否有安全限制。
- 教练希望使用怎样的教学话术。
- 某些训练动作是否需要特定器械或保护措施。
这些信息不是模型“聪不聪明”的问题,而是模型有没有拿到业务资料的问题。
知识库问答要解决的就是:让模型回答时尽量基于已有资料,而不是凭通用知识猜。
整体调用链路
这个问答模块可以拆成三层:
| 层级 | 主要职责 |
|---|---|
| 小程序前端 | 输入问题、展示回答、维护会话状态、处理 loading、失败、重试和流式渲染 |
| 业务服务层 | 登录校验、会话管理、工作流调用、WebSocket 事件转发、错误封装 |
| 扣子工作流 | 知识库检索、Prompt 编排、模型调用、条件分支和兜底回复 |
用户发送问题后的链路大概是:
1 | 用户输入问题 |
这里比较关键的是:前端并不直接调用扣子平台,也不直接管理知识库。前端关心的是用户问题、会话、工作流选择和实时展示;扣子工作流负责 AI 编排;业务服务负责把两者连接起来。
扣子工作流在这里解决了什么
扣子工作流更像是一个可视化的 AI 编排工具。它不是只调用一次模型,而是可以把一次问答拆成多个步骤:
1 | 接收用户问题 |
这对业务项目很有价值。因为很多时候我们并不想一开始就自己维护向量数据库、Embedding 服务和复杂的 AI 编排逻辑。先用工作流平台把知识库问答跑通,可以更快验证这个功能是否真的有用。
但使用工作流不代表不需要理解原理。回答不准时,仍然要知道从哪些环节排查:知识库内容是否完整、检索是否命中、Prompt 是否约束清楚、输出格式是否稳定、前端是否正确展示。
扣子工作流可以怎么搭建
一个知识库问答工作流可以按下面的思路搭建。
1. 准备知识库
以少儿跑酷教学场景为例,知识库内容可以包括:
- 会员等级说明。
- 不同等级对应的训练动作。
- 动作教学说明。
- 训练安全注意事项。
- 常见问题与标准回答。
资料质量会直接影响回答效果。如果知识库内容本身不完整,后面再怎么调 Prompt,也很难生成准确回答。
2. 设计工作流输入
工作流至少要接收用户问题:
1 | { |
如果业务上下文更复杂,也可以传入更多信息:
1 | { |
这些上下文能帮助工作流理解用户问题,尤其是“这个”“那个”这类指代不清的表达。
3. 添加知识库检索节点
知识库检索节点的作用是:根据用户问题,从已有资料中找出最相关的片段。
比如用户问“L2 阶段适合练哪些动作?”,检索节点应该尽量命中:
1 | L2 阶段包含猫爬、基础翻越、平衡训练…… |
这里通常要关注:
- 检索哪个知识库。
- 返回多少个相关片段。
- 相似度低于多少认为未命中。
- 是否返回来源信息,方便后续排查。
4. 增加条件判断
检索并不总是成功。工作流里最好加条件判断:
1 | 如果命中有效资料 |
这样可以避免知识库没有资料时,模型仍然强行编一个答案。
5. 编写模型节点 Prompt
模型节点的 Prompt 要明确约束回答范围。一个简化版本可以是:
1 | 你是少儿跑酷教学助手。 |
这里最重要的是“根据给定资料回答”和“资料中没有答案就说明未找到”。知识库问答宁愿保守一点,也不要生成看似专业但没有依据的内容。
6. 设计输出格式
为了让业务服务和前端更容易处理,工作流最好输出稳定结构,而不是完全自由文本。
例如:
1 | { |
这样服务层可以根据结果决定:是正常流式输出答案,还是返回推荐问题卡片。
RAG:工作流背后的知识库问答原理
扣子工作流帮我们屏蔽了很多底层细节,但知识库问答背后常见的思路仍然是 RAG,也就是 Retrieval-Augmented Generation,可以理解成“检索增强生成”。
一个简化版 RAG 流程是:
1 | 资料入库阶段: |
它和普通聊天最大的区别是:模型不只看到用户问题,还会看到从知识库里找出来的上下文。
1. chunk:知识库的最小检索单元
知识库通常不会把整篇文档一次性丢给模型,而是先切成多个片段。这个片段通常叫 chunk。
比如动作资料可以这样切:
| chunk_id | title | content | metadata |
|---|---|---|---|
| c001 | L2 猫爬训练 | L2 阶段可以练习猫爬,重点是手脚协调和身体稳定 | level=L2, type=action |
| c002 | L2 基础翻越 | 基础翻越需要注意支撑点、重心转移和落地缓冲 | level=L2, type=action |
| c003 | 训练安全规则 | 训练前需要热身,高风险动作必须由教练保护 | type=safety |
chunk 切得太大,会带入很多无关内容;切得太小,又可能丢失上下文。所以比较理想的方式是按业务语义切分,比如按等级、动作、注意事项来拆。
2. Embedding:把文本变成可以比较的向量
Embedding 可以粗略理解为“把文本变成一组能表达语义的数字”。这样系统就不只能做关键词匹配,还能找到语义相近的内容。
例如:
1 | “L2 阶段适合练哪些动作” |
和:
1 | “L2 猫爬训练、基础翻越、平衡训练” |
表面文字不完全一样,但语义相关。Embedding 的作用就是让系统能找到这种语义相关的资料。
3. TopK:不是只找一个答案
检索时通常不会只取一个片段,而是取 TopK 个相关片段。比如 Top 5。
原因是用户问题可能同时涉及多个方面:
- 动作列表。
- 训练注意事项。
- 适合年龄。
- 安全保护。
多个片段一起提供给模型,回答会更完整。但 TopK 也不能太大,否则上下文里会混入噪声。
4. 相似度阈值:控制“没找到”的边界
知识库问答需要有“我不知道”的能力。如果检索结果相似度很低,就应该认为没有可靠资料,而不是强行回答。
可以把逻辑理解成:
1 | if (retrievedChunks.length === 0 || maxScore < threshold) { |
这也是工作流里需要条件判断节点的原因。
5. Prompt 拼接:让模型基于资料回答
检索到资料后,需要把资料和用户问题一起组织成 Prompt。
1 | 你是少儿跑酷教学助手。 |
这样模型回答时就有了依据。
为什么用 WebSocket 做流式输出
AI 回答可能需要几秒甚至更久。如果等完整答案生成后再返回,用户会感觉页面卡住。流式输出的体验会更自然:模型生成一点,前端展示一点。
在这个项目里,问答结果通过 WebSocket 推回前端。可以把事件理解成:
1 | 发送问题 |
一个简化的流式事件可以设计成:
1 | type StreamChunk = { |
前端收到每个 chunk 后,把 content 追加到当前 AI 消息上。收到 done 后,再把消息状态从 streaming 改成 success。
前端流式渲染的几个难点
1. 本地先插入占位消息
用户点击发送后,不能等服务端返回才显示消息。更好的体验是:
1 | 插入用户消息 |
这样用户马上能看到自己的问题,AI 回答也有固定位置承接后续内容。
2. 新会话需要临时 ID
如果用户在一个新会话里第一次提问,前端发送消息时,服务端真实会话可能还没有创建出来。这个时候前端可以先用一个临时会话 ID 存放消息。
等服务端返回真实会话 ID 后,再把临时会话里的消息迁移过去。
这个设计解决了一个常见体验问题:用户刚发出第一条消息时,页面不能空等服务端创建会话。
3. chunk 需要幂等处理
实时通信里最容易出问题的是重复消息。比如网络断开后重连,服务端可能会补发一部分内容。如果前端直接追加,就会出现重复文字。
可以用 requestId + chunkIndex 做幂等判断:
1 | const processedChunkIndex = new Map<string, number>(); |
这个细节和 AI 模型本身关系不大,但会直接影响用户看到的结果是否稳定。
4. done 事件要负责收尾
流式输出不是把文字拼完就结束。收到完成事件后,还要做收尾:
- 把消息状态改成成功。
- 清理当前请求的运行状态。
- 清理 chunk 去重记录。
- 如果有图片或推荐选项,更新到消息里。
- 如果有未完成的超时计时器,也要清掉。
否则下一次发送消息时,可能会被旧状态影响。
断线恢复:不只是重新连上
WebSocket 断开后,重新连接只是第一步。更重要的是:断线期间服务端有没有继续生成内容?前端有没有错过消息?
一种常见做法是记录当前会话最后处理到的 stream 信息。重连时告诉服务端:
1 | 我在这个 session 里最后处理到 lastStreamId |
服务端根据这个信息返回错过的消息,前端再把这些消息重新走一遍原来的事件处理流程。
这样做的好处是:补发消息可以复用已有的流式处理逻辑,而不是单独写一套恢复逻辑。
多端在线:为什么会有只读模式
这个项目里还涉及同一个账号多端在线的问题。如果两个设备同时进入同一个会话,并且都能发送问题,就可能出现上下文冲突。
一种处理方式是给会话里的设备分角色:
chatter:可以发送问题。viewer:只能查看,不能发送。
当服务端判断当前设备只能浏览时,前端就要禁用输入框或发送按钮,并提示用户当前是只读模式。
这类设计看起来和 AI 没关系,但对真实问答产品很重要。因为 AI 对话天然依赖上下文,多端同时写入很容易让会话状态变乱。
回答不准时怎么排查
知识库问答效果不好时,不一定是模型的问题。可以按链路拆开看。
1. 工作流是否选对
如果业务里有多个工作流,前端发送问题时需要带上正确的工作流 ID。选错工作流,后面的知识库和 Prompt 都可能不对。
2. 用户问题是否清晰
如果用户只问“这个怎么练”,系统可能不知道“这个”指什么。可以在前端引导用户补充信息,或者由工作流先做问题改写。
3. 知识库是否有内容
如果知识库里根本没有 L2 阶段的动作说明,模型就没有可靠依据。这个时候应该补知识,而不是只调 Prompt。
4. 检索是否命中
知识库里有内容,不代表一定能检索出来。标题、关键词、同义词、切分方式、metadata 都会影响命中效果。
5. WebSocket 推送是否完整
工作流返回了正确内容,不代表前端一定能看到正确内容。还要检查是否出现了漏推、重复推、乱序推,以及前端是否正确处理 chunk。
6. 前端状态是否清理干净
如果请求运行态、chunk 去重记录、临时会话映射没有清理干净,下一次请求可能会被旧状态影响。
这次学习后的理解
这次梳理之后,我对“调用 AI 工作流”有了更具体的理解。
它不是简单地把问题发给一个接口,而是一条完整链路:前端负责交互和状态,业务服务负责鉴权、会话、协议转换和错误处理,扣子工作流负责知识库检索和模型生成,WebSocket 负责把生成过程实时推给用户。
对于知识库问答来说,真正影响效果的不只有模型能力,还包括知识库质量、chunk 切分、检索命中率、Prompt 约束、未命中兜底、流式展示、错误恢复和状态清理。