AI 应用的稳定性,藏在模型调用前后的数据边界里

AI 应用的稳定性,藏在模型调用前后的数据边界里

摘要:本文把三个实践放到同一张图里理解:网页内容如何进入 RAG、模型 Token 如何流进 Vue 页面、会议文字稿如何被固定流程整理成纪要。重点不是背 API,而是识别每层的数据边界:chunk、SSE 行、JSON 与行动项。文中代码为静态分析,运行未验证

调用一次大模型接口并不难,难的是让输入可检索、输出能持续展示、重复任务可稳定复用。

我把这类 AI 应用看成三条数据处理链路:

text 复制代码
知识:网页 → Document → chunk → 检索 → 模型回答
输出:字节流 → SSE 行 → JSON → delta → Vue 页面
流程:会议文本 → 事实 → 议题 → 行动项 → 会议纪要

它们底层的共性是:原始数据必须先被划出正确边界,才能被可靠处理。

RAG 的关键不是模型,而是 chunk

网页加载器会先从 URL 获取页面内容,再转换为 Document。之后文本分割器根据长度和分隔符将长文拆分。

js 复制代码
const documents = await cheerioLoader.load()

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 400,
  chunkOverlap: 100,
  separators: ["。", "!", "?"]
})

const chunks = await splitter.splitDocuments(documents)

这三个配置分别在解决不同问题:

  • chunkSize:限制单次检索的文本粒度;
  • separators:优先在中文自然语句边界处切分;
  • chunkOverlap:让相邻片段共享上下文,降低语义被截断的概率。

后续才是 embedding、向量库和 retriever:

text 复制代码
Document[] → chunks → vectorStore → retriever → context → LLM

一个很实用的调试习惯是:先确认加载到文档,再检查切块,最后才检查向量检索。

如果控制台已经出现空数组 []0 个 chunk,那问题通常在网页抓取、页面动态渲染或 CSS 选择器,而不在向量库。

流式返回中,网络块不是 JSON 边界

浏览器拿到的 response.bodyReadableStream,每次 reader.read() 返回的是一小段二进制数据。

js 复制代码
const reader = response.body?.getReader()
const decoder = new TextDecoder()

const { value, done } = await reader?.read()
const text = decoder.decode(value)

这里的 value 通常是 Uint8ArrayUint8Array 可以先粗略理解为"字节数组";HTTP 在底层传字节,TextDecoder 再把字节解释成文本。

流式模型接口通常以 SSE 形式发数据:

text 复制代码
data: {"choices":[{"delta":{"content":"你"}}]}

data: {"choices":[{"delta":{"content":"好"}}]}

data: [DONE]

前端要做的是:去掉 data: 前缀、解析 JSON、取出增量文本,并更新响应式状态。

js 复制代码
const data = JSON.parse(incoming)
const delta = data.choices[0].delta.content
content.value += delta

页面会逐步刷新,不是因为模型"直接操作了 DOM",而是 content.value 变化触发了 Vue 的响应式渲染。

buffer 解决的到底是什么

最重要的边界错位是:

text 复制代码
一次 reader.read() ≠ 一条 SSE 消息 ≠ 一个完整 JSON

服务端发送一条完整 JSON,但网络可能将它切成两半;也可能一次塞进多条消息。这就是常说的半包和粘包。

buffer 用来保存上一次未完成的数据,等下一次读取后再拼接:

js 复制代码
const text = buffer + decoder.decode(value, { stream: true })
const lines = text.split("\n")
buffer = lines.pop() ?? ""

最后一段没有换行结束,说明它可能还不完整,因此不能直接解析,需要留到下一轮。这比"JSON.parse() 失败后再缓存"更清晰:它直接按照 SSE 的行边界处理残片。

把重复工作写成 Skill,不让模型猜

会议纪要 Skill 体现的是另一种"边界控制"。它明确规定:

  • 哪些需求会触发该 Skill;
  • 哪些内容属于可验证事实;
  • 如何从文字稿中删除口头禅和重复内容;
  • 如何区分决定、待确认事项和不同观点;
  • 行动项缺少负责人或时间时必须留空,不能补猜。

最终输出使用固定结构:会议基本信息、会议目标、会议内容和行动项。

text 复制代码
输入:会议文字稿
  ↓
事实边界与去噪
  ↓
按主题归纳讨论
  ↓
抽取负责人、时间、交付物
  ↓
固定模板会议纪要

这就是一个好 Skill 的价值:把高频任务中的"不能乱猜"和"不能漏字段"写成流程,而不是每次都依赖模型临场发挥。

两个容易踩坑的工程细节

1. fromDocuments 不能直接配合 new

材料中曾出现下面的调用并报出 is not a constructor

js 复制代码
new MemoryVectorStore.fromDocuments(...)

报错已经说明 fromDocuments 不是构造函数。它更像一个工厂方法,通常应直接调用并等待结果:

js 复制代码
const vectorStore = await MemoryVectorStore.fromDocuments(
  chunks,
  embeddings
)

具体写法仍需以当前安装版本的官方 API 为准。

2. 不要把真实模型密钥放到 Vue 前端

如果 API Key 通过 VITE_ 前缀环境变量传给前端,它会出现在浏览器端构建产物中。用户能够查看请求头,也就可能拿到密钥。

安全的链路应该是:

text 复制代码
Vue 浏览器 → 自己的后端 → 大模型服务商

前端只向自家后端发送问题;后端保存 API Key 并转发流式结果。材料中出现的 402 Payment Required 表明服务商收到了请求,但账户额度或支付状态不可用,这不是 SSE 解析本身的问题。

一个通用排错框架

当 AI 项目出现"没数据、没内容、报错"时,我会按这四层定位:

  1. 输入层:网页、请求体或文字稿是否真的拿到了?
  2. 边界层:选择器、chunk、SSE 换行、JSON 是否被正确识别?
  3. 转换层 :解码、JSON.parse()、embedding、API 调用方式是否匹配?
  4. 外部层:密钥、余额、模型名、权限和依赖版本是否可用?

先看上游数据,再看下游效果;先看 HTTP 状态码,再看解析代码。这个顺序能避免很多"在错误层排查"的时间浪费。

结尾

RAG、流式输出和 Agent Skill 看起来分属不同方向,但它们都在解决同一个工程问题:把不规则的原始输入,变成边界清晰、可处理、可验证的结果。

当你能说清 Document 为什么要切块、为什么流式数据需要 buffer、为什么会议纪要不能补猜字段时,就已经不只是在调用 AI API,而是在搭建一个更可靠的 AI 应用。

相关推荐
吃饱了得干活1 小时前
LangChain 提示词模板:从拼接字符串到组合流水线
langchain·llm
夜影风2 小时前
智能体开发的“脚手架“:主流框架(LangChain、AutoGen等)选型指南
人工智能·langchain·ai agent
海上彼尚14 小时前
Nodejs也能写Agent - 16.LangGraph篇 - 条件分支与循环
前端·后端·langchain·node.js
Darling噜啦啦18 小时前
从 URL 到 Chunk:网页爬取与语义切割实战,构建 RAG 知识库的第一步
langchain
zhr_math_king20 小时前
LangGraph-快速入门-第一个例子
python·langchain
BraveWang21 小时前
【LangChain 1.x】07、生产环境模型管理|能力检测、限流、监控与容错
langchain
小折耳猫_1 天前
智能体评估与测试体系
langchain·智能体·langgraph
lzjava20241 天前
LangChain Prompt提示词工程
前端·langchain·prompt
拾年2751 天前
RAG 灵魂三问:你的文档是怎么"喂"给大模型的?
langchain·node.js·llm