从 RAG 切块到流式输出:我把 AI 应用拆成了三条数据链路
从 RAG 切块到流式输出:我把 AI 应用拆成了三条数据链路
摘要:做 AI 应用时,网页正文、模型 Token 和会议文字稿看起来是三种不同数据,但它们都要经历“获取 → 结构化 → 输出”的过程。本文结合一个 RAG 脚本、一个 Vue 流式页面和一个会议纪要 Skill,梳理数据如何进入系统、在哪里被切分,以及常见错误如何定位。文中代码为静态分析,运行未验证。
很多初学者做 AI 项目时,会把重点放在“调用模型”。但真实链路里,模型调用只是最后一步:RAG 先要把知识变成可检索片段,流式页面要把网络字节变成逐步显示的文字,Skill 则要把开放式需求收束成稳定流程。
这篇文章的目标是建立一个统一视角:先搞清数据边界,再谈 AI 能力。
1. 三个看似不同的问题
材料中有三类任务:
- 从网页加载文章,切分后做检索增强问答;
- 在 Vue 页面中接收大模型的流式返回;
- 将会议文字稿整理为结构化纪要。
它们共享同一个骨架:
原始输入
↓
读取或加载
↓
按业务边界切分、解析、归类
↓
生成用户可消费的结果
区别只在于“边界”不同:RAG 的边界是文本块,流式响应的边界是 SSE 消息行,会议纪要的边界是事实、议题和行动项。
2. RAG:先把网页变成能检索的知识块
RAG 脚本中,CheerioWebBaseLoader 负责加载网页,并通过 CSS 选择器筛选正文范围;RecursiveCharacterTextSplitter 负责把长文本切成多个 chunk。
const cheerioLoader = new CheerioWebBaseLoader(url, {
selector: ".main-area p"
})
const documents = await cheerioLoader.load()
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400,
chunkOverlap: 100,
separators: ["。", "!", "?"]
})
const splitDocuments = await textSplitter.splitDocuments(documents)
这里有两个关键参数。
chunkSize:检索的最小知识单位
chunkSize: 400 表示单个文本块最大约 400 个字符。整篇文章直接向量化,检索会过于粗糙;拆成小块后,问题更容易命中某个具体段落。
chunkOverlap:避免切断上下文
chunkOverlap: 100 让相邻 chunk 共享部分文本。例如解释“Token 如何流到浏览器”的句子被切在中间时,后一块还能保留前文,降低语义断裂的风险。
完整链路可以记成:
网页 URL
→ Loader
→ Document[]
→ TextSplitter
→ chunks
→ Embeddings
→ VectorStore
→ Retriever
→ LLM
一个排错顺序:先看 documents,再看向量库
静态材料中,加载结果曾打印为 [],随后切分结果为 0 个 chunk。这时问题首先不在检索,而在更前面的“网页是否抓到、选择器是否匹配”。
实践中先打印:
console.log(documents.length)
console.log(documents[0]?.pageContent.slice(0, 200))
确认数据存在后,再进入切分、向量化和检索。这个习惯适用于所有数据管道:上游为空,下游再正确也没有意义。
3. 流式输出:一次读取不等于一条完整 JSON
Vue 页面使用 fetch 请求模型接口。当开启流式返回时,响应体是 ReadableStream:浏览器不断从网络拿到二进制块,而不是等完整答案生成后一次拿到。
const reader = response.body?.getReader()
const decoder = new TextDecoder()
const { value, done } = await reader?.read()
const text = decoder.decode(value)
材料中的笔记提到 Uint8Array。它是一个存放 0~255 无符号整数的数组;每个元素是一个字节。HTTP 传输在底层就是字节,TextDecoder 的任务是把这些字节按字符编码解释为文本。
SSE 的外层格式
流式接口通常按类似格式返回:
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: [DONE]
解析步骤是:
- 从流中读取
Uint8Array; - 解码为字符串;
- 找出
data:开头的行; - 去掉前缀;
- 用
JSON.parse()把 JSON 字符串变成对象; - 读取
choices[0].delta.content并追加到content.value。
const data = JSON.parse(incoming)
const delta = data.choices[0].delta.content
content.value += delta
Vue 的 ref 是响应式数据。content.value 改变后,模板中绑定 content 的位置会更新,于是用户看到文字逐步出现。
4. buffer:处理半包与粘包
最容易误解的一点是:reader.read() 的返回边界由网络决定,不由 JSON 或 SSE 决定。
可能发生两种情况:
半包:一次只收到半条 JSON
粘包:一次收到多条 SSE 消息
例如,服务端原本要发送:
data: {"choices":[{"delta":{"content":"你好"}}]}
但浏览器第一次可能只收到:
data: {"choices":[{"delta":{"content":"你
此时立刻 JSON.parse() 会失败。buffer 用来保存这段残缺内容,下一次读取后再拼接:
const chunkValue = buffer + decoder.decode(value)
更稳健的思路是先按换行拆分,再把最后一段未结束的文本放回 buffer:
const text = buffer + decoder.decode(value, { stream: true })
const lines = text.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.startsWith("data: ")) continue
// 解析完整行
}
一句话总结:网络 chunk 是传输单位,SSE 行是协议单位,JSON 是数据单位;三者边界不保证重合。
5. Skill:让 Agent 面对重复任务时不“自由发挥”
会议纪要 Skill 给出了另一个很实用的思路:把重复任务写成明确规范。
该 Skill 定义了:
- 触发条件:用户提到会议纪要、会议记录、行动项等;
- 事实边界:不补全原文没有的信息;
- 处理流程:通读、去噪、按主题归纳、提取行动项;
- 输出约束:固定 Markdown 模板,负责人或截止时间缺失时留空;
- 自检项:区分已决定、待确认和不同观点。
它不是为了让模型“写得更花”,而是为了让模型少漏、少猜、可复核。
可以把一个好 Skill 理解成:
高频任务
+ 明确输入
+ 固定步骤
+ 输出模板
+ 自检规则
= 可复用的 Agent 专项能力
6. 两个真实且容易忽视的问题
问题一:把工厂方法当构造函数
RAG 脚本中出现过:
new MemoryVectorStore.fromDocuments(...)
而错误信息是 fromDocuments is not a constructor。这类报错的含义很明确:fromDocuments 是方法,不是可用 new 创建实例的构造函数。应以当前安装依赖版本的 API 为准,通常是直接调用并等待结果。
const vectorStore = await MemoryVectorStore.fromDocuments(
splitDocuments,
embeddings
)
问题二:不能把真实 API Key 放进 Vite 前端
流式页面从 import.meta.env.VITE_DEEPSEEK_API_KEY 读取密钥,并直接发往第三方接口。以 VITE_ 开头的变量会进入浏览器端构建产物,因此用户可以在浏览器中获取它。
正确边界应是:
浏览器 → 自己的后端 → 大模型服务商
浏览器只传问题和流式请求参数;真正的 API Key 只保存在后端环境变量中。材料里出现的 402 Payment Required 说明请求已到达服务商,但账户额度、余额或套餐不可用;这与流式解析逻辑是两个层次的问题。
7. 可迁移的方法论
从这三份材料中可以提炼出一个面试和项目都能用的答题框架:
- 先问数据从哪里来:网页、HTTP 响应、文字稿还是数据库?
- 再问数据边界是什么:文档块、消息行、JSON、议题还是行动项?
- 再问结构化规则是什么:选择器、分隔符、协议前缀、模板字段?
- 最后问错误在哪一层:账户额度、网络传输、解析、依赖 API,还是业务数据为空?
AI 应用并不神秘。它的稳定性,往往取决于你是否认真处理了模型前后的数据:输入是否干净、边界是否正确、输出是否可验证。
结语
RAG 切块解决“知识如何被精准找到”,流式解析解决“模型结果如何实时到达页面”,Skill 解决“重复工作如何被稳定交给 Agent”。它们共同强调的不是某个框架 API,而是一种工程意识:在每一层都明确输入、边界、转换和输出。
更多推荐


所有评论(0)