AI App Stability Hinges on Data Boundaries, Not Model Calls
The Stability of AI Applications Lies in the Data Boundaries Before and After Model Calls
Abstract: This article puts three practices into the same picture to understand: how web content enters RAG, how model tokens flow into a Vue page, and how meeting transcripts are organized into minutes by a fixed process. The focus is not on memorizing APIs, but on identifying the data boundaries at each layer: chunks, SSE lines, JSON, and action items. The code in the text is for static analysis, execution has not been verified.
Calling a large model interface once is not difficult; what is difficult is making inputs retrievable, outputs continuously displayable, and repetitive tasks stably reusable.
I view this type of AI application as three data processing pipelines:
Knowledge: Webpage → Document → chunk → Retrieval → Model Answer
Output: Byte Stream → SSE Line → JSON → delta → Vue Page
Process: Meeting Text → Facts → Topics → Action Items → Meeting Minutes
The underlying commonality is: Raw data must first be delineated with correct boundaries before it can be reliably processed.
The Key to RAG is Not the Model, but the Chunk
The web loader first fetches page content from a URL, then converts it into a Document. After that, the text splitter divides the long text based on length and separators.
const documents = await cheerioLoader.load()
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 400,
chunkOverlap: 100,
separators: ["。", "!", "?"]
})
const chunks = await splitter.splitDocuments(documents)
These three configurations solve different problems:
chunkSize: Limits the text granularity for a single retrieval;separators: Prioritizes splitting at natural Chinese sentence boundaries;chunkOverlap: Allows adjacent fragments to share context, reducing the probability of semantic truncation.
Only after this come embedding, the vector store, and the retriever:
Document[] → chunks → vectorStore → retriever → context → LLM
A very practical debugging habit is: First confirm the document is loaded, then check the chunking, and finally check the vector retrieval.
If the console already shows an empty array [] and 0 chunks, the problem usually lies in web scraping, dynamic page rendering, or CSS selectors, not in the vector store.
In Streaming Returns, Network Chunks Are Not JSON Boundaries
The response.body received by the browser is a ReadableStream, and each reader.read() returns a small segment of binary data.
const reader = response.body?.getReader()
const decoder = new TextDecoder()
const { value, done } = await reader?.read()
const text = decoder.decode(value)
The value here is typically a Uint8Array. Uint8Array can be roughly understood as a "byte array"; HTTP transmits bytes at the lower level, and TextDecoder interprets the bytes back into text.
Streaming model interfaces usually send data in SSE format:
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: [DONE]
What the frontend needs to do is: remove the data: prefix, parse the JSON, extract the incremental text, and update the reactive state.
const data = JSON.parse(incoming)
const delta = data.choices[0].delta.content
content.value += delta
The page refreshes progressively not because the model "directly manipulates the DOM," but because the change in content.value triggers Vue's reactive rendering.
What buffer Actually Solves
The most critical boundary misalignment is:
One reader.read() ≠ One SSE message ≠ One complete JSON
The server sends a complete JSON, but the network might split it in half; it might also stuff multiple messages into one delivery. This is commonly known as half-packets and sticky packets.
buffer is used to save the incomplete data from the last read, to be concatenated after the next read:
const text = buffer + decoder.decode(value, { stream: true })
const lines = text.split("\n")
buffer = lines.pop() ?? ""
The last segment does not end with a newline, indicating it might be incomplete, so it cannot be parsed directly and must be held for the next round. This is clearer than "caching after JSON.parse() fails": it directly handles fragments according to the line boundaries of SSE.
Writing Repetitive Work as a Skill, Don't Let the Model Guess
The meeting minutes Skill embodies another type of "boundary control." It explicitly specifies:
- Which requests trigger this Skill;
- Which content belongs to verifiable facts;
- How to remove filler words and repetitions from the transcript;
- How to distinguish decisions, items to be confirmed, and differing viewpoints;
- Action items missing an owner or time must be left blank, not guessed.
The final output uses a fixed structure: meeting basic information, meeting objectives, meeting content, and action items.
Input: Meeting Transcript
↓
Fact Boundaries and Denoising
↓
Summarize Discussion by Topic
↓
Extract Owner, Time, Deliverables
↓
Fixed Template Meeting Minutes
This is the value of a good Skill: turning the "must not guess" and "must not miss fields" of a high-frequency task into a process, rather than relying on the model's improvisation every time.
Two Engineering Details Easy to Stumble On
1. fromDocuments Cannot Be Directly Used with new
The material once showed the following call and reported is not a constructor:
new MemoryVectorStore.fromDocuments(...)
The error already indicates that fromDocuments is not a constructor. It is more like a factory method and should usually be called directly and awaited:
const vectorStore = await MemoryVectorStore.fromDocuments(
chunks,
embeddings
)
The specific writing should still be based on the official API of the currently installed version.
2. Do Not Put Real Model Keys in the Vue Frontend
If the API Key is passed to the frontend via VITE_ prefixed environment variables, it will appear in the browser-side build artifacts. Users can view request headers and potentially obtain the key.
The secure link should be:
Vue Browser → Your Own Backend → Large Model Service Provider
The frontend only sends questions to its own backend; the backend saves the API Key and forwards the streaming results. The 402 Payment Required appearing in the material indicates the service provider received the request, but the account balance or payment status is unavailable; this is not a problem with SSE parsing itself.
A General Troubleshooting Framework
When an AI project shows "no data, no content, errors," I locate the issue by these four layers:
- Input Layer: Was the webpage, request body, or transcript actually obtained?
- Boundary Layer: Were selectors, chunks, SSE newlines, and JSON correctly identified?
- Transformation Layer: Do decoding,
JSON.parse(), embedding, and API calling methods match? - External Layer: Are keys, balance, model names, permissions, and dependency versions usable?
Look at upstream data first, then downstream effects; look at HTTP status codes first, then parsing code. This order avoids a lot of time wasted on "troubleshooting at the wrong layer."
Conclusion
RAG, streaming output, and Agent Skills seem to belong to different directions, but they are all solving the same engineering problem: turning irregular raw input into results with clear boundaries that are processable and verifiable.
When you can articulate why a Document needs to be chunked, why streaming data needs a buffer, and why meeting minutes cannot guess fields, you are no longer just calling an AI API, but building a more reliable AI application.