传统全栈的工程前提是确定性:输入确定,逻辑确定,输出确定,故障点集中在数据库、网络和代码缺陷上,排查路径清晰。AI 全栈的底层假设被推翻了——同一个输入,模型两次调用可能给出不同结果;推理延迟随 Token 长度和负载剧烈波动;幻觉、格式非法、工具调用循环成为新的故障类别。这意味着,把一个大模型 API 塞进原有的前端/后端结构里,得到的不是“AI 全栈”,而是一个把不确定性偷偷放进了确定性管道的脆弱系统。真正的 AI 全栈,是从前端流式渲染、后端流式代理、AI 编排层到网关治理层,每一层都围绕“不确定性”重新设计契约。
传统前端拿到的是已经完成的 JSON,渲染是一个同步动作。AI 前端拿到的是一个尚未完成的、逐 token 到达的流。Vercel AI SDK 6 的 useChat 把这个流封装成一个有限状态机:submitted | streaming | ready | error,同时把消息模型从“一个 content: string”翻转为 parts: MessagePart[]——一条消息内部可能同时包含文本、工具调用、推理过程和附件。这个变化的影响远超 API 换名:你的渲染单元从“一条消息”变成了“一条消息里的一个 part”,旧的 message.content 拼接写法在 v6 下会直接失效。
// app/chat/ChatPanel.tsx
'use client';
import { useChat } from 'ai/react';
export function ChatPanel() {
const { messages, sendMessage, status, stop, error } = useChat({
api: '/api/chat',
maxSteps: 5, // Agent 多步工具调用的上限
onError: (e) => reportToSentry(e),
});
return (
<div>
{messages.map(m => (
<div key={m.id}>
{m.parts.map((p, i) => {
if (p.type === 'text') return <Markdown key={i} text={p.text} />;
if (p.type === 'tool-call') return <ToolCall key={i} part={p} />;
if (p.type === 'reasoning') return <ThinkCollapsible key={i} text={p.text} />;
return null; // 未来新增 part 类型不会让页面崩溃
})}
</div>
))}
<Composer onSend={sendMessage} disabled={status === 'streaming'} />
{status === 'streaming' && <button onClick={stop}>停止</button>}
{error && <ErrorBanner onRetry={() => location.reload()} />}
</div>
);
}default return null 不是偷懒,而是为 SDK 未来新增 audio、image-generation 等 part 类型留出的演进空间。前端不再是“等结果”的被动渲染层,而是一个对流式中间状态做分支编排的运行时。
AI 全栈的后端最容易犯的错误,是把它写成同步调用:等模型生成完毕,再一次性返回。这在前端表现为“白屏十几秒然后突然出现全部文字”,用户体验等同于没有流式。正确的后端职责是流式代理 + 协议适配。如果 AI 编排层用 FastAPI + LangGraph,SSE 是最直接的承载方式:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from langgraph.graph import StateGraph
app = FastAPI()
async def event_stream(graph, inputs: dict, config: dict):
async for chunk in graph.astream(inputs, config, stream_mode="messages"):
token = chunk[0].content
if token:
yield f"data: {json.dumps({'type': 'text', 'text': token})}\n\n"
yield "data: [DONE]\n\n"
@app.post("/api/chat")
async def chat(req: ChatRequest):
graph = build_graph() # LangGraph StateGraph
return StreamingResponse(
event_stream(graph, {"messages": req.messages}, {"configurable": {"thread_id": req.thread_id}}),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)X-Accel-Buffering: no 是上线时最容易被漏掉的一行——Nginx 默认会缓冲 SSE 流,导致前端看到的仍然是“批量到达”而不是逐 token 到达。thread_id 交给 LangGraph 的 checkpointer 做会话持久化,多轮对话的状态不在前端内存里,而在编排层可恢复。FastAPI 本身不生成 token,它只负责把 AI 层的流无损、不缓冲地递到 HTTP 边界上。
前端和后端把流跑通,只是 Demo 的门槛。生产环境真正拉开差距的是网关层:模型路由与故障切换、Token 感知的限流、语义缓存、成本可观测。这一层在传统全栈里不存在,因为传统 API 不按 Token 计费、没有“相似请求复用回答”的可能。以下是一个同时做语义缓存和双桶限流的最小网关骨架:
class AIGateway:
def __init__(self, similarity_threshold=0.92, max_rpm=60, max_tpm=100_000):
self.cache = SemanticCache(threshold=similarity_threshold, ttl=1800)
self.limiter = DualTokenBucket(max_rpm=max_rpm, max_tpm=max_tpm)
async def dispatch(self, org_id: str, query: str, est_tokens: int) -> dict:
if (cached := await self.cache.lookup(query)) is not None:
return {"source": "cache", "answer": cached}
allowed = await self.limiter.acquire(org_id, est_tokens)
if not allowed:
return {"source": "rejected", "reason": "rate_limited"}
result = await call_llm(query) # 真正的模型调用
await self.cache.store(query, result["answer"])
await record_usage(org_id, result["usage"])
return {"source": "llm", **result}语义缓存的关键在阈值:0.92 意味着只有语义上几乎等价的查询才命中,否则“帮我改一下这段代码”和“帮我重写这段代码”会被错误地当成同一个请求。限流必须是双桶——只限请求数不限制 Token,一个超长 prompt 就能绕过 RPM 把 TPM 打爆;只限 Token 不限制请求数,高频短请求的调度开销同样会拖垮服务。这两项加上模型故障时的 stale 降级,才是 AI 网关区别于普通 API Gateway 的实质。
AI 全栈的“全”,不在于前端用了 Next.js、后端用了 FastAPI、中间挂了 LangChain——这些是选型清单,不是架构。真正的全栈贯通,是前端能消费流式的中间状态,后端能无损转发流,AI 层能在不确定的输出上建立可恢复的编排,网关层能在成本和稳定性失控之前把请求拦下来。每一层都在处理同一个东西:传统全栈可以推到系统边界之外的不确定性,在 AI 全栈里必须被每一层显式接住。飞书学习:xcnysqigiht4.feishu.cn/wiki/Sfy0wBIwmiaN3vkN3NTcdqarnIB?from=from_copylink
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。