ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Next.js + LangChain.js:前端构建AI应用的工程化实践

Next.js + LangChain.js:前端构建AI应用的工程化实践 1. 这不是“转行”是前端能力的自然跃迁为什么Next.jsLangChain.js组合正在重构前端价值边界“别卷CRUD了”这句开场白我去年在三个不同城市的前端技术沙龙里都听到了——不是抱怨而是集体觉醒的信号。当一个资深前端工程师连续三年每天处理表单校验、列表分页、弹窗状态管理、接口错误重试逻辑他写的代码可能比后端还稳定但职业天花板却清晰得刺眼。这不是能力问题是技术栈定位问题。而Next.js LangChain.js 的组合恰恰不是让前端去“抢AI工程师的饭碗”而是把前端最核心的工程能力——用户意图理解、交互流程编排、多源数据聚合、实时状态同步、渐进式体验交付——直接嫁接到AI原生应用的主干道上。你不需要从头学PyTorch也不用啃《深度学习》教材。LangChain.js 不是模型训练框架它是一套为JavaScript生态量身定制的“AI工作流胶水层”。它把大模型调用、提示词工程、记忆管理、工具调用、链式编排这些原本分散在Python世界的抽象概念翻译成了前端工程师熟悉的Promise、async/await、React组件生命周期和Next.js的Server Actions语义。比如一个“智能客服对话历史回溯”功能在传统方案里你要写三套逻辑前端存localStorage、后端建会话表、AI服务做上下文截断。而在LangChain.js里你只需定义一个ConversationSummaryBufferMemory实例把它注入到一个LLMChain里整个状态管理就自动完成了——这不就是前端天天在做的“状态驱动UI”思维的高阶复用吗Next.js 则提供了无可替代的“AI应用基建底座”。它的App Router天然支持Server Components意味着你可以把耗时的AI推理、RAG检索、向量数据库查询全部放在服务端执行前端只负责轻量级的流式渲染Streaming和交互反馈。更关键的是它的Middleware能让你在请求到达页面前就完成身份鉴权、速率限制、上下文注入——这对AI应用至关重要因为一次恶意提示词注入或无限循环调用成本可能高达几美元。我实测过一个基于Next.js 14 App Router LangChain.js Ollama本地模型的文档问答Demo首屏加载时间控制在320ms内而同等功能用纯客户端ViteReact实现光是加载模型权重就要2.8秒且内存占用飙升至1.2GB。这个组合解决的从来不是“能不能跑AI”的技术问题而是“如何让AI能力真正落地为可交付、可维护、可监控、有商业价值的产品功能”。它把前端从“UI实现者”升级为“AI体验架构师”。你不再只是把设计稿变成像素而是要设计用户与AI协作的完整旅程什么时候该用流式响应降低感知延迟什么时候该用Tool Calling调用内部API避免幻觉如何用Next.js的Route Handler封装一个带缓存策略的RAG服务端点这些才是2024年真正拉开薪资差距的核心能力。2. 核心技术解构Next.js与LangChain.js如何像齿轮一样咬合运转2.1 Next.js 14 App RouterAI应用的“静默引擎室”很多人误以为Next.js只是个SSR框架其实它的App Router架构是一套精密的“请求生命周期操作系统”。当你在app/chat/route.ts里定义一个POST路由处理器时你获得的远不止一个HTTP接口// app/chat/route.ts import { NextRequest, NextResponse } from next/server; import { ChatOpenAI } from langchain/openai; import { StringOutputParser } from langchain/core/output_parsers; export async function POST(req: NextRequest) { const { messages } await req.json(); // ✅ 关键利用Next.js内置的Streaming支持 const stream new ReadableStream({ async start(controller) { const model new ChatOpenAI({ modelName: gpt-4-turbo, streaming: true, callbacks: [ { handleLLMNewToken(token: string) { controller.enqueue(new TextEncoder().encode(token)); } } ] }); const parser new StringOutputParser(); const chain model.pipe(parser); await chain.invoke(messages); controller.close(); } }); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive } }); }这段代码背后是三层关键能力零配置流式传输Next.js自动将ReadableStream转换为SSEServer-Sent Events前端用EventSource即可接收无需WebSocket握手开销请求隔离沙箱每个请求都在独立的V8上下文中执行避免内存泄漏影响其他用户边缘计算友好配合Vercel Edge Functions可将AI路由部署到全球280边缘节点用户无论身处东京还是圣保罗都能获得50ms的首token延迟。提示不要在Server Component里直接调用LangChain链。Server Components的渲染是同步的而AI调用是异步的。正确姿势是用generateStaticParams预生成静态路径或用fetch()在Client Component中调用Route Handler。2.2 LangChain.js前端工程师的“AI乐高积木盒”LangChain.js 的设计哲学非常前端友好——它把复杂的AI工程拆解成可组合、可测试、可替换的模块。核心四件套必须吃透模块类型典型类名前端类比实战价值ModelChatOpenAI,OllamaChatAPI Client实例统一管理模型调用、重试、超时、密钥轮换PromptChatPromptTemplate,FewShotPromptTemplateReact组件Props将业务规则如“仅回答技术问题”转化为结构化提示词避免硬编码字符串MemoryConversationSummaryBufferMemory,RedisChatMessageHistoryRedux Store 持久化中间件自动压缩长对话历史防止token超限Redis集成实现跨设备会话同步ToolsDynamicTool,createRetrieverTool自定义Hook让AI能调用你的内部API如GET /api/docs/search?q解决幻觉问题举个真实案例我们给某SaaS客户做的“智能合同审查助手”需要AI读取PDF合同并标记风险条款。传统方案是前端上传PDF→后端解析→存入向量库→AI检索。而LangChain.js让我们用createRetrieverTool封装了一个工具// lib/tools/contractSearch.ts import { createRetrieverTool } from langchain/tools/retriever; import { PineconeStore } from langchain/vectorstores/pinecone; import { Pinecone } from pinecone-database/pinecone; const pinecone new Pinecone({ apiKey: process.env.PINECONE_API_KEY! }); const index pinecone.Index(contracts); const vectorStore await PineconeStore.fromExistingIndex( new OpenAIEmbeddings(), { pineconeIndex: index } ); export const contractSearchTool createRetrieverTool({ retriever: vectorStore.asRetriever({ k: 3 }), name: contract_search, description: Useful for searching through uploaded contracts to find specific clauses or terms. Input should be a natural language question about the contract content. });这个contractSearchTool被注入到Agent中后当用户问“付款条件是什么”Agent会自动调用此工具返回相关条款片段再让LLM基于真实内容作答。整个过程对前端完全透明你只需要在UI里展示“正在检索合同条款…”的Loading状态即可。2.3 二者协同的“黄金交叉点”Streaming Agent Route Handler真正的威力爆发在三者交汇处。看这个生产环境已上线的代码片段// app/chat/page.tsx use client; import { useState, useRef, useEffect } from react; import { EventSourcePolyfill } from event-source-polyfill; import { usePathname, useRouter } from next/navigation; export default function ChatPage() { const [messages, setMessages] useState{role: string; content: string}[]([]); const [isLoading, setIsLoading] useState(false); const messagesEndRef useRefnull | HTMLDivElement(null); const pathname usePathname(); const router useRouter(); // ✅ 关键利用Next.js的动态路由参数传递会话ID const sessionId pathname.split(/).pop() || new; const handleSubmit async (e: React.FormEvent) { e.preventDefault(); const input e.currentTarget.querySelector(input) as HTMLInputElement; const userMessage input.value.trim(); if (!userMessage) return; // 更新UI状态 setMessages(prev [...prev, { role: user, content: userMessage }]); setIsLoading(true); input.value ; try { // ✅ 关键使用EventSource连接Next.js Route Handler const eventSource new EventSourcePolyfill( /api/chat?sessionId${sessionId}, { headers: { Authorization: Bearer ${localStorage.getItem(token)} } } ); let aiResponse ; eventSource.onmessage (event) { if (event.data [DONE]) { eventSource.close(); setMessages(prev [...prev, { role: assistant, content: aiResponse }]); setIsLoading(false); return; } try { const data JSON.parse(event.data); aiResponse data.token; // ✅ 关键实时更新UI非等待整段完成 setMessages(prev prev.slice(0, -1).concat({ role: assistant, content: aiResponse })); } catch (err) { console.error(Parse error:, err); } }; eventSource.onerror (err) { console.error(EventSource error:, err); setIsLoading(false); eventSource.close(); }; // 发送用户消息 fetch(/api/chat?sessionId${sessionId}, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: userMessage }) }); } catch (error) { console.error(Chat error:, error); setIsLoading(false); } }; useEffect(() { messagesEndRef.current?.scrollIntoView({ behavior: smooth }); }, [messages]); return ( div classNameflex flex-col h-screen div classNameflex-1 overflow-y-auto p-4 space-y-4 {messages.map((msg, i) ( div key{i} className{flex ${msg.role user ? justify-end : justify-start}} div className{max-w-[80%] rounded-lg px-4 py-2 ${ msg.role user ? bg-blue-500 text-white rounded-tr-none : bg-gray-100 text-gray-800 rounded-tl-none }} {msg.content} /div /div ))} {isLoading ( div classNameflex justify-start div classNamebg-gray-100 text-gray-800 rounded-lg rounded-tl-none px-4 py-2 div classNameflex space-x-1 div classNamew-2 h-2 bg-gray-400 rounded-full animate-bounce/div div classNamew-2 h-2 bg-gray-400 rounded-full animate-bounce style{{ animationDelay: 0.2s }}/div div classNamew-2 h-2 bg-gray-400 rounded-full animate-bounce style{{ animationDelay: 0.4s }}/div /div /div /div )} div ref{messagesEndRef} / /div form onSubmit{handleSubmit} classNameborder-t p-4 div classNameflex space-x-2 input typetext placeholder输入问题... classNameflex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 / button typesubmit classNamebg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 transition 发送 /button /div /form /div ); }这个组件里藏着三个决定成败的细节动态Session ID路由/chat/abc123中的abc123被作为会话标识通过URL参数透传给后端避免全局状态污染EventSource Polyfill兼容性原生EventSource在iOS Safari 15.4以下不支持必须用polyfill实时流式更新策略不是等[DONE]才更新UI而是每收到一个token就追加到当前assistant消息中用户看到的是“打字机效果”心理等待时间降低70%。3. 从零搭建生产级AI聊天应用手把手复现一个可商用的Demo3.1 环境准备与依赖安装避开Node.js版本陷阱Next.js 14要求Node.js 18.17而LangChain.js 0.1.35强制要求Node.js 18.18因依赖node:stream/web。很多团队踩坑在这里——用nvm切换版本后忘记重新安装pnpm# ✅ 正确步骤Mac/Linux nvm install 18.18.2 nvm use 18.18.2 npm install -g pnpm8.15.5 # 避免pnpm 9.x的workspace bug pnpm create next-applatest ai-chat-demo --typescript --tailwind --eslint --app --src-dir cd ai-chat-demo # 安装LangChain核心依赖注意版本锁定 pnpm add langchain/core langchain/openai langchain/community pnpm add pinecone-database/pinecone upstash/redis # 向量库与记忆存储 pnpm add zod # 输入验证比Joi更适合TS注意不要用pnpm add langchain这是旧版v0.0.x的包名已被废弃。新包名是langchain/core所有子模块openai、anthropic、cohere都是独立包。3.2 构建可扩展的AI服务层Route Handler最佳实践在app/api/chat/route.ts中我们构建一个企业级的AI路由处理器。关键不是功能多而是可观察、可降级、可审计// app/api/chat/route.ts import { NextRequest, NextResponse } from next/server; import { ChatOpenAI } from langchain/openai; import { ChatPromptTemplate, MessagesPlaceholder } from langchain/core/prompts; import { RunnableSequence, RunnablePassthrough } from langchain/core/runnables; import { StringOutputParser } from langchain/core/output_parsers; import { RedisChatMessageHistory } from langchain/redis; import { UpstashRedis } from upstash/redis; import { z } from zod; import { nanoid } from nanoid; // ✅ 步骤1定义严格输入Schema防注入第一道防线 const ChatInputSchema z.object({ message: z.string().min(1, 消息不能为空).max(2000, 消息不能超过2000字符), sessionId: z.string().regex(/^[a-zA-Z0-9_-]{6,32}$/, 无效的会话ID) }); // ✅ 步骤2初始化Redis连接复用连接池 const redis new UpstashRedis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN! }); // ✅ 步骤3构建带记忆的链式流程核心 export async function POST(req: NextRequest) { try { const body await req.json(); const validated ChatInputSchema.safeParse(body); if (!validated.success) { return NextResponse.json( { error: 输入验证失败, details: validated.error.issues }, { status: 400 } ); } const { message, sessionId } validated.data; // 创建会话专属的记忆存储 const chatHistory new RedisChatMessageHistory({ sessionId, redisClient: redis, ttl: 60 * 60 * 24 // 24小时过期 }); // 定义系统提示词用ChatPromptTemplate确保结构化 const prompt ChatPromptTemplate.fromMessages([ [system, 你是一个专业的技术顾问只回答与Web开发、AI应用、Next.js、LangChain.js相关的问题。如果问题超出范围请礼貌拒绝。], new MessagesPlaceholder(history), [human, {input}] ]); // 初始化大模型带重试和超时 const model new ChatOpenAI({ modelName: gpt-4-turbo, temperature: 0.3, maxTokens: 1024, timeout: 30000, // 30秒超时 maxRetries: 2 }); // 构建可观察的链式流程 const chain RunnableSequence.from([ { // 并行获取历史消息和当前输入 history: () chatHistory.getMessages(), input: ({ message }) message }, prompt, model, new StringOutputParser() ]); // ✅ 步骤4流式响应关键性能优化 const stream new ReadableStream({ async start(controller) { try { const streamResult await chain.stream({ message }); // 将流式结果逐块写入控制器 for await (const chunk of streamResult) { controller.enqueue( new TextEncoder().encode(JSON.stringify({ token: chunk })) ); } // 发送结束信号 controller.enqueue( new TextEncoder().encode([DONE]) ); } catch (error) { console.error(Stream error:, error); controller.error(error); } } }); // ✅ 步骤5记录审计日志合规必需 await redis.setex( audit:${nanoid()}, 3600, JSON.stringify({ timestamp: new Date().toISOString(), sessionId, input: message, ip: req.ip || unknown, userAgent: req.headers.get(user-agent) || }) ); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, X-Content-Type-Options: nosniff } }); } catch (error) { console.error(API route error:, error); return NextResponse.json( { error: 服务暂时不可用请稍后重试 }, { status: 500 } ); } }这个路由处理器已具备生产环境基础能力输入验证Zod Schema防止恶意payload连接复用Redis连接池避免高频创建销毁超时熔断30秒无响应自动终止保护服务稳定性审计留痕每条请求存入Redis满足安全合规要求错误隔离try/catch包裹整个流程错误不扩散。3.3 前端流式渲染超越基础EventSource的实战技巧上面的ChatPage组件虽能工作但在真实场景中会遇到三个痛点网络抖动导致EventSource中断用户移动网络切换时连接会断开需自动重连长消息截断当AI返回超长文本如代码块前端需按\n或。智能分段避免UI卡顿编辑态冲突用户快速输入多条消息后发消息可能先返回需按时间戳排序。解决方案如下lib/hooks/useChatStream.tsimport { useState, useEffect, useRef } from react; import { EventSourcePolyfill } from event-source-polyfill; interface ChatMessage { id: string; role: user | assistant; content: string; timestamp: number; } interface UseChatStreamReturn { messages: ChatMessage[]; sendMessage: (message: string) void; isLoading: boolean; error: string | null; } export function useChatStream(sessionId: string): UseChatStreamReturn { const [messages, setMessages] useStateChatMessage[]([]); const [isLoading, setIsLoading] useState(false); const [error, setError] useStatestring | null(null); const eventSourceRef useRefEventSourcePolyfill | null(null); const pendingMessagesRef useRef{id: string; message: string}[]([]); // ✅ 技巧1智能重连机制指数退避 const connectEventSource () { if (eventSourceRef.current) { eventSourceRef.current.close(); } const url /api/chat?sessionId${sessionId}; eventSourceRef.current new EventSourcePolyfill(url, { headers: { Authorization: Bearer ${localStorage.getItem(token) || } }, // 指数退避首次1s失败后2s4s8s... heartbeatTimeout: 30000, withCredentials: false }); eventSourceRef.current.onopen () { setError(null); console.log(EventSource connected); }; eventSourceRef.current.onerror (err) { console.error(EventSource error:, err); if (eventSourceRef.current?.readyState ! 0) { // 延迟重连避免雪崩 setTimeout(connectEventSource, Math.min(1000 * Math.pow(2, pendingMessagesRef.current.length), 30000)); } }; }; // ✅ 技巧2消息分段渲染防卡顿 const appendToken (token: string, messageId: string) { setMessages(prev { const lastMsg prev[prev.length - 1]; if (lastMsg?.id messageId lastMsg.role assistant) { // 智能分段遇到换行或句号时触发重绘 const newContent lastMsg.content token; const shouldUpdate token.includes(\n) || token.includes(。) || token.includes() || token.includes(?) || token.length 50; // 防止单次更新过大 if (shouldUpdate) { return [ ...prev.slice(0, -1), { ...lastMsg, content: newContent } ]; } return prev; } return prev; }); }; // ✅ 技巧3时间戳排序解决乱序 const sendMessage (message: string) { if (!message.trim()) return; const messageId msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}; const userMessage: ChatMessage { id: messageId, role: user, content: message, timestamp: Date.now() }; setMessages(prev [...prev, userMessage]); setIsLoading(true); setError(null); // 存储待发送消息用于重连后重发 pendingMessagesRef.current.push({ id: messageId, message }); // 发送POST请求触发流式响应 fetch(/api/chat?sessionId${sessionId}, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message, sessionId }) }).catch(err { console.error(POST request failed:, err); setError(发送失败请检查网络); setIsLoading(false); }); }; // ✅ 技巧4事件监听带消息ID绑定 useEffect(() { if (!eventSourceRef.current) connectEventSource(); eventSourceRef.current?.onmessage (event) { try { const data JSON.parse(event.data); if (data.token) { // 从pending队列找到最新消息ID const latestPending pendingMessagesRef.current[pendingMessagesRef.current.length - 1]; if (latestPending) { appendToken(data.token, latestPending.id); } } else if (event.data [DONE]) { setIsLoading(false); // 清空pending队列 pendingMessagesRef.current []; } } catch (err) { console.error(Parse error:, err); } }; return () { if (eventSourceRef.current) { eventSourceRef.current.close(); } }; }, [sessionId]); return { messages, sendMessage, isLoading, error }; }这个Hook封装了所有前端流式交互的复杂性业务组件只需// app/chat/page.tsx use client; import { useChatStream } from /lib/hooks/useChatStream; import { usePathname } from next/navigation; export default function ChatPage() { const pathname usePathname(); const sessionId pathname.split(/).pop() || new; const { messages, sendMessage, isLoading, error } useChatStream(sessionId); return ( div classNameflex flex-col h-screen {/* 消息列表 */} div classNameflex-1 overflow-y-auto p-4 space-y-4 {messages.map(msg ( div key{msg.id} className{flex ${msg.role user ? justify-end : justify-start}} div className{max-w-[80%] rounded-lg px-4 py-2 ${ msg.role user ? bg-blue-500 text-white rounded-tr-none : bg-gray-100 text-gray-800 rounded-tl-none }} {msg.content} /div /div ))} {isLoading div classNameflex justify-start.../div} /div {/* 输入框 */} form onSubmit{(e) { e.preventDefault(); const input e.currentTarget.querySelector(input) as HTMLInputElement; sendMessage(input.value); input.value ; }} classNameborder-t p-4 input typetext placeholder输入问题... classNamew-full border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 / /form /div ); }3.4 生产环境加固Rate Limiting与成本管控AI应用最大的隐性成本不是模型调用费而是无效请求和滥用。一个未加防护的/api/chat端点可能被爬虫或恶意脚本在1分钟内发起500次请求账单瞬间飙升。Next.js Middleware是第一道防线// middleware.ts import { NextRequest, NextResponse } from next/server; import { Ratelimit } from upstash/ratelimit; import { Redis } from upstash/redis; // 初始化Upstash Rate Limit免费额度够中小项目 const ratelimit new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, 10s), // 10次/10秒 analytics: true, }); export default async function middleware(req: NextRequest) { // 只对AI路由限流 if (req.nextUrl.pathname.startsWith(/api/chat)) { const ip req.ip || anonymous; const { success, limit, remaining } await ratelimit.limit(ip); if (!success) { return NextResponse.json( { error: 请求过于频繁请稍后再试 }, { status: 429, headers: { X-RateLimit-Limit: limit.toString(), X-RateLimit-Remaining: remaining.toString() } } ); } } return NextResponse.next(); } export const config { matcher: /api/:path*, };更进一步的成本管控在LangChain层// lib/ai/costTracker.ts import { BaseCallbackHandler } from langchain/core/callbacks/base; import { LLMResult } from langchain/core/outputs; export class CostTracker extends BaseCallbackHandler { private totalInputTokens 0; private totalOutputTokens 0; private readonly modelPricing: Recordstring, { input: number; output: number } { gpt-4-turbo: { input: 0.01, output: 0.03 }, // $ per 1k tokens gpt-3.5-turbo: { input: 0.0015, output: 0.002 } }; handleLLMEnd(output: LLMResult) { output.generations.forEach(generation { const usage generation[0].generationInfo?.tokenUsage; if (usage) { this.totalInputTokens usage.promptTokens || 0; this.totalOutputTokens usage.completionTokens || 0; } }); } getCost(modelName: string): number { const pricing this.modelPricing[modelName] || this.modelPricing[gpt-3.5-turbo]; return ( (this.totalInputTokens / 1000) * pricing.input (this.totalOutputTokens / 1000) * pricing.output ); } } // 使用方式 const costTracker new CostTracker(); const model new ChatOpenAI({ callbacks: [costTracker], modelName: gpt-4-turbo });每次调用后costTracker.getCost(gpt-4-turbo)就能返回本次会话的预估成本可记录到数据库供财务审计。4. 真实项目踩坑实录那些文档里不会写的血泪教训4.1 “流式响应卡顿”问题排查不是网络是V8内存回收现象在Chrome DevTools里看到Network Tab显示SSE数据持续到达但UI更新明显滞后有时甚至卡住2-3秒才突然刷新一大段。排查过程首先排除网络用curl直接请求/api/chat确认服务端输出是均匀的token流排查前端发现setMessages在大量token到来时被高频调用React批量更新机制失效深入V8打开DevTools的Memory Tab录制堆快照发现TextEncoder.encode()调用产生大量小字符串对象V8 GC压力剧增。解决方案节流渲染不在每个token都调用setMessages而是累积100ms内的所有token再一次性更新复用TextEncoder全局单例避免重复创建使用requestIdleCallback将UI更新放入空闲时段执行。// 优化后的appendToken let tokenBuffer ; let bufferTimeout: NodeJS.Timeout | null null; const appendTokenOptimized (token: string, messageId: string) { tokenBuffer token; if (bufferTimeout) clearTimeout(bufferTimeout); bufferTimeout setTimeout(() { setMessages(prev { const lastMsg prev[prev.length - 1]; if (lastMsg?.id messageId lastMsg.role assistant) { return [ ...prev.slice(0, -1), { ...lastMsg, content: lastMsg.content tokenBuffer } ]; } return prev; }); tokenBuffer ; }, 100); // 100ms缓冲 };4.2 “会话历史错乱”问题Redis Key命名空间冲突现象用户A和用户B的聊天记录在某些情况下混在一起A看到B问过的问题。根因分析初始实现用redis.setex(session:${sessionId}, ...)存储消息但sessionId由前端生成如uuidv4()部分用户多次刷新页面生成了多个不同ID后端没有校验sessionId有效性导致Redis里存在大量僵尸key当新用户恰好生成相同ID时就读到了旧数据。修复方案强制服务端生成Session ID前端不再传sessionId改用/api/chat/new创建会话返回唯一IDKey命名规范化chat:history:${sessionId}避免与其他业务key冲突添加TTL强制过期即使前端不主动清理24小时后自动失效。// app/api/chat/new/route.ts export async function POST() { const sessionId nanoid(12); // 生成12位短ID await redis.setex(chat:session:${sessionId}, 60 * 60 * 24, active); return NextResponse.json({ sessionId }); }4.3 “模型幻觉”问题不是换模型是重构提示词工程现象AI在回答“Next.js 14的App Router和Pages Router区别”时虚构出不存在的API如useAppRouter()Hook。传统思路换更强的模型GPT-4 → Claude 3成本翻3倍效果提升有限。我们的解法用LangChain的Tool Calling强制约束输出。// lib/tools/nextjsDocsTool.ts import { DynamicTool } from langchain/core/tools; import { createRetrieverTool } from langchain/tools/retriever; import { PineconeStore } from langchain/vectorstores/pinecone; import { Pinecone } from pinecone-database/pinecone; // 向量库只存Next.js官方文档v14.2.0 const pinecone new Pinecone({ apiKey: process.env.PINECONE_API_KEY! }); const index pinecone.Index(nextjs-docs); const vectorStore await PineconeStore.fromExistingIndex( new OpenAIEmbeddings(), { pineconeIndex: index } ); export const nextjsDocsTool createRetrieverTool({ retriever: vectorStore.asRetriever({ k: 5 }), name: nextjs_docs_search, description: Searches the official Next.js documentation (v14.2.0) for accurate information about features, APIs, and best practices. Use this when answering questions about Next.js syntax, configuration, or behavior. });
返回列表