LangChain.js 和 Next.js LLM 后端应用于协助博客撰写和总结领域是一个非常实用的方向!这涉及到理解和处理文本内容,并生成新的、有结构的信息。
根据您之前提供的代码和需求,我们可以在此基础上进行更具针对性的功能规划和技术实现。
博客撰写和总结助手的核心功能设想
基于您的需求,我们可以将功能分为以下几大类:
- 文章内容理解与分析:
- 摘要生成: 快速理解文章核心内容。
- 关键词提取: 识别文章主题和关键概念。
- 实体识别: 识别文章中提及的人物、地点、组织等。
- 情感分析: (可选) 判断文章的整体情绪倾向。
- 内容创作辅助:
- 标题/副标题生成: 根据文章内容提供吸引人的标题建议。
- 段落扩展/改写: 针对某个主题或草稿段落进行内容扩充或重新表达。
- 引言/结论撰写: 辅助生成文章的开头和结尾。
- 润色和校对: 检查语法、拼写,并提供表达优化建议。
- 内容大纲生成: 根据主题或初步想法生成文章结构。
- 多模态支持:
- 图片描述生成: 如果有图片,可以生成图片的文字描述或配文。
- 图片摘要/解读: (可选) 对图片内容进行简要说明。
- 用户交互与管理:
- 历史记录与管理: 保存用户生成的内容和对话历史。
- 自定义提示词: 允许用户创建和保存自己的提示词模板。
技术实现方案调整与优化
我们将重点关注如何将 LangChain.js 的强大能力与 Next.js 的后端特性结合,以实现上述功能。
1. 后端架构调整 (pages/api/
和 lib/
)
由于功能的多样性,建议为每个主要功能创建独立的 API 路由,或者在现有 API 中通过 action
或 type
参数进行区分。
推荐的 API 路由结构:
pages/
├── api/
│ ├── blog/
│ │ ├── summarize.ts # 摘要生成
│ │ ├── keywords.ts # 关键词提取
│ │ ├── outline.ts # 大纲生成
│ │ ├── expand-rewrite.ts # 段落扩展/改写
│ │ ├── title-suggest.ts # 标题建议
│ │ └── proofread.ts # 润色校对
│ └── chat.ts # 通用聊天辅助 (如果保留)
├── lib/
│ ├── langchain/
│ │ ├── models.ts # LLM 模型实例化 (getChatLLM, getEmbeddings)
│ │ ├── chains.ts # 各种 LangChain 链的封装 (摘要链, 关键词链等)
│ │ ├── tools.ts # 如果需要 Agent 工具
│ │ └── memory.ts # 内存管理 (如果通用聊天需要记忆)
│ └── utils.ts # 通用工具函数 (如错误处理帮助函数)
lib/langchain/models.ts
(基础模型配置):
// lib/langchain/models.ts
import { ChatOpenAI } from "@langchain/openai";
import { OpenAIEmbeddings } from "@langchain/openai";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";// 根据需要选择 OpenAI 或 Google Gemini
const USE_OPENAI = process.env.LLM_PROVIDER === 'openai';export const getChatLLM = () => {if (USE_OPENAI) {if (!process.env.OPENAI_API_KEY) {throw new Error("OPENAI_API_KEY is not set in .env.local");}return new ChatOpenAI({openAIApiKey: process.env.OPENAI_API_KEY,temperature: 0.7,modelName: "gpt-3.5-turbo", // 或 "gpt-4", "gpt-4o"});} else {if (!process.env.GOOGLE_API_KEY) {throw new Error("GOOGLE_API_KEY is not set in .env.local");}return new ChatGoogleGenerativeAI({apiKey: process.env.GOOGLE_API_KEY,modelName: "gemini-pro",temperature: 0.7,});}
};export const getEmbeddings = () => {if (USE_OPENAI) {if (!process.env.OPENAI_API_KEY) {throw new Error("OPENAI_API_KEY is not set in .env.local");}return new OpenAIEmbeddings({openAIApiKey: process.env.OPENAI_API_KEY,modelName: "text-embedding-ada-002",});} else {if (!process.env.GOOGLE_API_KEY) {throw new Error("GOOGLE_API_KEY is not set in .env.local");}return new GoogleGenerativeAIEmbeddings({apiKey: process.env.GOOGLE_API_KEY,modelName: "embedding-001",});}
};
.env.local
增加配置:
LLM_PROVIDER=openai # 或 google
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
GOOGLE_API_KEY=YOUR_GOOGLE_API_KEY
LLM_API_SECRET_KEY=your_super_secret_api_key_12345
2. 实现具体的 LangChain.js 功能链
以下是几个核心功能的示例实现,都放在 lib/langchain/chains.ts
中。
lib/langchain/chains.ts
(示例):
// lib/langchain/chains.ts
import { ChatOpenAI } from "@langchain/openai"; // 或 ChatGoogleGenerativeAI
import { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser, JsonOutputParser } from "@langchain/core/output_parsers";
import { getChatLLM, getEmbeddings } from "./models"; // 从 models 导入
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { Document } from "@langchain/core/documents";
import { RunnableSequence } from "@langchain/core/runnables";// --- 1. 摘要生成 ---
export async function generateSummary(text: string): Promise<string> {const chatModel = getChatLLM();const prompt = ChatPromptTemplate.fromMessages([SystemMessagePromptTemplate.fromTemplate("你是一个专业的编辑助理,擅长提炼文章要点。"),HumanMessagePromptTemplate.fromTemplate("请为以下文章生成一份简洁的摘要,字数控制在100字以内,并确保包含核心观点和主要发现。\n\n文章内容:\n{text}"),]);const outputParser = new StringOutputParser();const chain = prompt.pipe(chatModel).pipe(outputParser);return chain.invoke({ text });
}// --- 2. 关键词提取 ---
// 假设我们需要一个包含关键词数组的 JSON 输出
interface KeywordsOutput {keywords: string[];
}export async function extractKeywords(text: string): Promise<string[]> {const chatModel = getChatLLM();const prompt = ChatPromptTemplate.fromMessages([SystemMessagePromptTemplate.fromTemplate(`你是一个专业的SEO分析师。请从提供的文章中提取5-10个最相关的关键词,以英文逗号分隔的字符串形式返回。`),HumanMessagePromptTemplate.fromTemplate("文章内容:\n{text}\n\n提取的关键词:"),]);const outputParser = new StringOutputParser(); // 我们可以让LLM直接返回逗号分隔的字符串const chain = prompt.pipe(chatModel).pipe(outputParser);const result = await chain.invoke({ text });return result.split(',').map(keyword => keyword.trim()).filter(Boolean); // 分割并清理
}// --- 3. 标题建议 ---
export async function suggestTitles(content: string): Promise<string[]> {const chatModel = getChatLLM();const prompt = ChatPromptTemplate.fromMessages([SystemMessagePromptTemplate.fromTemplate("你是一个创意内容策划师,擅长为博客文章生成吸引人的标题。"),HumanMessagePromptTemplate.fromTemplate("请根据以下文章内容,提供3-5个不同的、有吸引力的博客文章标题建议。每个标题一行。不包含其他任何额外内容。\n\n文章内容:\n{content}"),]);const outputParser = new StringOutputParser();const chain = prompt.pipe(chatModel).pipe(outputParser);const result = await chain.invoke({ content });return result.split('\n').map(s => s.trim()).filter(Boolean);
}// --- 4. 段落扩展/改写 (RAG 辅助) ---
// 这里的 RAG 并非从外部知识库检索,而是辅助理解用户意图和原始段落。
// 如果需要真正的外部知识 RAG,则需要集成向量数据库。
export async function expandOrRewriteParagraph(originalParagraph: string,instruction: string // 例如 "扩展细节", "用更正式的语言改写", "简化"
): Promise<string> {const chatModel = getChatLLM();const prompt = ChatPromptTemplate.fromMessages([SystemMessagePromptTemplate.fromTemplate(`你是一个专业的文章编辑。你的任务是根据用户的指令对提供的段落进行扩展或改写。请严格按照指令操作,如果指令是扩展,则在原有基础上增加更多细节;如果指令是改写,则保持原意但使用不同的表达方式。指令: ${instruction}`),HumanMessagePromptTemplate.fromTemplate("原始段落:\n{paragraph}\n\n{instruction}后的段落:"),]);const outputParser = new StringOutputParser();const chain = prompt.pipe(chatModel).pipe(outputParser);return chain.invoke({ paragraph: originalParagraph, instruction });
}// --- 5. 内容大纲生成 (基于主题或初步想法) ---
export async function generateOutline(topic: string, requirements?: string): Promise<string> {const chatModel = getChatLLM();const prompt = ChatPromptTemplate.fromMessages([SystemMessagePromptTemplate.fromTemplate(`你是一个结构化思考的AI助手,擅长为博客文章生成清晰的大纲。`),HumanMessagePromptTemplate.fromTemplate(`请为主题“${topic}”生成一份详细的博客文章大纲。
${requirements ? `请注意以下要求:${requirements}` : ''}大纲结构示例:
# 主标题
## 一级标题
### 二级标题
- 要点1
- 要点2
...
`),]);const outputParser = new StringOutputParser();const chain = prompt.pipe(chatModel).pipe(outputParser);return chain.invoke({ topic });
}// --- 6. 润色和校对 ---
export async function proofreadAndRefine(text: string): Promise<string> {const chatModel = getChatLLM();const prompt = ChatPromptTemplate.fromMessages([SystemMessagePromptTemplate.fromTemplate(`你是一个专业的文章校对和润色专家。请检查以下文本的语法、拼写、标点错误,并提供表达更流畅、更专业的修改建议。
请直接给出修改后的完整文本,并用markdown加粗显示所有改动的地方。
`),HumanMessagePromptTemplate.fromTemplate("待润色文本:\n{text}"),]);const outputParser = new StringOutputParser();const chain = prompt.pipe(chatModel).pipe(outputParser);return chain.invoke({ text });
}// --- 7. 多模态支持 (图片描述) ---
// 针对 Google Gemini Pro Vision 模型
// 注意:ChatGoogleGenerativeAI 的 invoke 接受 BaseMessage[] 作为输入
// 图像数据需要是 Base64 编码
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";export async function generateImageDescription(base64Image: string, promptText: string): Promise<string> {// 确保使用支持多模态的 LLM,例如 "gemini-pro-vision"const chatModel = new ChatGoogleGenerativeAI({apiKey: process.env.GOOGLE_API_KEY,modelName: "gemini-pro-vision",temperature: 0.3,});const message = new HumanMessage({content: [{type: "text",text: promptText, // 用户对图片的问题或描述要求},{type: "image_url",image_url: `data:image/jpeg;base64,${base64Image}`, // 或 image/png 等},],});const res = await chatModel.invoke([message]);return String(res.content);
}// --- RAG (基于文档检索) 基础结构 ---
// 如果您需要真正的 RAG 来回答关于您博客数据的问题
let blogVectorStore: MemoryVectorStore | null = null; // 实际应为外部数据库export async function initializeBlogVectorStore(docs: Document[]) {if (blogVectorStore) return blogVectorStore;const embeddings = getEmbeddings();const splitter = new RecursiveCharacterTextSplitter({chunkSize: 1000,chunkOverlap: 200,});// 假设 `docs` 是从文件加载的博客文章 Document[]const splitDocs = await splitter.splitDocuments(docs);blogVectorStore = await MemoryVectorStore.fromDocuments(splitDocs, embeddings);console.log("Blog vector store initialized.");return blogVectorStore;
}export async function queryBlogContentRAG(question: string): Promise<string> {if (!blogVectorStore) {throw new Error("Blog vector store not initialized. Call initializeBlogVectorStore first.");}const chatModel = getChatLLM();const retriever = blogVectorStore.asRetriever();const prompt = ChatPromptTemplate.fromMessages([SystemMessagePromptTemplate.fromTemplate(`你是一个博客内容专家,擅长从提供的博客文章片段中找到答案。请根据以下上下文信息回答用户的问题。如果信息不足,请礼貌地告知。上下文: {context}`),HumanMessagePromptTemplate.fromTemplate("{question}"),]);const chain = RunnableSequence.from([{context: retriever,question: (input: string) => input,},prompt,chatModel,new StringOutputParser(),]);return chain.invoke(question);
}
3. 后端 API 路由 (pages/api/blog/*.ts
)
为每个功能创建独立的 API 路由,以提供清晰的接口。
pages/api/blog/summarize.ts
示例:
// pages/api/blog/summarize.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { generateSummary } from '../../../lib/langchain/chains';
import { authenticateRequest } from '../../../lib/utils/auth'; // 导入认证函数interface RequestBody {content: string;
}interface ResponseData {summary?: string;error?: string;
}export default async function handler(req: NextApiRequest,res: NextApiResponse<ResponseData>
) {const timestamp = new Date().toISOString();// 认证检查const authError = authenticateRequest(req);if (authError) {console.warn(`[${timestamp}] Unauthorized access to /api/blog/summarize: ${authError}`);return res.status(401).json({ error: authError });}if (req.method !== 'POST') {res.setHeader('Allow', ['POST']);console.warn(`[${timestamp}] Method Not Allowed: ${req.method} for ${req.url}`);return res.status(405).json({ error: 'Method Not Allowed' });}const { content }: RequestBody = req.body;if (!content || typeof content !== 'string' || content.trim().length === 0) {console.error(`[${timestamp}] Bad Request: Missing or invalid 'content'. IP: ${req.socket.remoteAddress}`);return res.status(400).json({ error: 'Missing or invalid blog content for summarization.' });}try {console.log(`[${timestamp}] Generating summary for content length: ${content.length}`);const summary = await generateSummary(content);console.log(`[${timestamp}] Summary generated successfully.`);res.status(200).json({ summary });} catch (error: any) {console.error(`[${timestamp}] Error generating summary:`, error.message, error.stack);res.status(500).json({ error: error.message || 'Failed to generate summary.' });}
}
其他功能 API 路由 (pages/api/blog/*.ts
) 结构类似,只需替换核心的 LangChain 链调用。
pages/api/blog/image-description.ts
(多模态处理):
// pages/api/blog/image-description.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { generateImageDescription } from '../../../lib/langchain/chains';
import { authenticateRequest } from '../../../lib/utils/auth';interface RequestBody {base64Image: string; // 图像的 Base64 编码字符串promptText?: string; // 辅助性提示,如“描述这张图片中的主要内容”
}interface ResponseData {description?: string;error?: string;
}export default async function handler(req: NextApiRequest,res: NextApiResponse<ResponseData>
) {const timestamp = new Date().toISOString();const authError = authenticateRequest(req);if (authError) {console.warn(`[${timestamp}] Unauthorized access to /api/blog/image-description: ${authError}`);return res.status(401).json({ error: authError });}if (req.method !== 'POST') {res.setHeader('Allow', ['POST']);console.warn(`[${timestamp}] Method Not Allowed: ${req.method} for ${req.url}`);return res.status(405).json({ error: 'Method Not Allowed' });}const { base64Image, promptText = "请描述这张图片的内容。" }: RequestBody = req.body;if (!base64Image || typeof base64Image !== 'string' || !base64Image.startsWith('data:image')) {console.error(`[${timestamp}] Bad Request: Missing or invalid 'base64Image'. IP: ${req.socket.remoteAddress}`);return res.status(400).json({ error: 'Missing or invalid base64 image data.' });}try {// 提取纯 Base64 数据部分const base64Data = base64Image.split(',')[1];if (!base64Data) {return res.status(400).json({ error: 'Invalid Base64 image format.' });}console.log(`[${timestamp}] Generating image description for image data length: ${base64Data.length}`);const description = await generateImageDescription(base64Data, promptText);console.log(`[${timestamp}] Image description generated successfully.`);res.status(200).json({ description });} catch (error: any) {console.error(`[${timestamp}] Error generating image description:`, error.message, error.stack);res.status(500).json({ error: error.message || 'Failed to generate image description.' });}
}
4. 辅助函数 (lib/utils/
)
lib/utils/auth.ts
(认证帮助函数):
// lib/utils/auth.ts
import { NextApiRequest } from 'next';export function authenticateRequest(req: NextApiRequest): string | null {const providedApiKey = req.headers['x-api-key'];const expectedApiKey = process.env.LLM_API_SECRET_KEY;if (!expectedApiKey) {// 这是开发环境的警报,生产环境不应发生console.warn("LLM_API_SECRET_KEY is not set in environment variables. API will be unprotected.");return null; // 或者在生产环境抛出错误}if (!providedApiKey || providedApiKey !== expectedApiKey) {return 'Unauthorized: Invalid or missing API Key';}return null; // 认证成功
}
5. 前端集成 (UI/UX)
前端(例如您的博客编辑页面)需要调用这些新的 API 路由,并将结果展示给用户。
前端示例 (简化版,展示如何调用新 API):
假设您有一个博客编辑器页面,其中包含一个文本区域和一些按钮。
// pages/blog/edit.tsx (示例片段)
import React, { useState } from 'react';
import { Button, Input, message, Upload, Spin } from 'antd'; // 假设使用 Ant Design// ... (导入其他必要的组件和类型) ...export default function BlogEditor() {const [blogContent, setBlogContent] = useState('');const [summary, setSummary] = useState('');const [keywords, setKeywords] = useState<string[]>([]);const [titles, setTitles] = useState<string[]>([]);const [imageDescription, setImageDescription] = useState('');const [loading, setLoading] = useState(false);const callApi = async (endpoint: string, data: any) => {setLoading(true);try {const response = await fetch(`/api/blog/${endpoint}`, {method: 'POST',headers: {'Content-Type': 'application/json','X-API-KEY': process.env.NEXT_PUBLIC_LLM_API_SECRET_KEY || '', // 确保在 next.config.js 中暴露},body: JSON.stringify(data),});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.error || `Failed to call ${endpoint} API.`);}return await response.json();} catch (err: any) {message.error(`操作失败: ${err.message}`);return null;} finally {setLoading(false);}};const handleSummarize = async () => {const result = await callApi('summarize', { content: blogContent });if (result) {setSummary(result.summary);message.success('摘要生成成功!');}};const handleExtractKeywords = async () => {const result = await callApi('keywords', { content: blogContent });if (result) {setKeywords(result.keywords);message.success('关键词提取成功!');}};const handleSuggestTitles = async () => {const result = await callApi('title-suggest', { content: blogContent });if (result) {setTitles(result.titles);message.success('标题建议生成成功!');}};const handleImageUpload = async (file: File) => {if (!file.type.startsWith('image/')) {message.error('只能上传图片文件!');return false;}const reader = new FileReader();reader.readAsDataURL(file);reader.onload = async () => {const base64Image = reader.result as string; // 'data:image/jpeg;base64,...'const result = await callApi('image-description', { base64Image });if (result) {setImageDescription(result.description);message.success('图片描述生成成功!');}};reader.onerror = (error) => {message.error('文件读取失败。');console.error(error);};return false; // 阻止 Upload 组件自动上传};return (<div style={{ padding: '20px' }}><h1>博客内容助手</h1><Input.TextArearows={10}placeholder="在此输入您的博客文章内容..."value={blogContent}onChange={(e) => setBlogContent(e.target.value)}style={{ marginBottom: '20px' }}/><div style={{ marginBottom: '20px' }}><Button onClick={handleSummarize} loading={loading}>生成摘要</Button><Button onClick={handleExtractKeywords} loading={loading} style={{ marginLeft: '10px' }}>提取关键词</Button><Button onClick={handleSuggestTitles} loading={loading} style={{ marginLeft: '10px' }}>建议标题</Button></div>{summary && (<Card title="文章摘要" style={{ marginBottom: '20px' }}><p>{summary}</p></Card>)}{keywords.length > 0 && (<Card title="提取关键词" style={{ marginBottom: '20px' }}>{keywords.map(kw => <Tag key={kw}>{kw}</Tag>)}</Card>)}{titles.length > 0 && (<Card title="建议标题" style={{ marginBottom: '20px' }}><ul>{titles.map((title, index) => <li key={index}>{title}</li>)}</ul></Card>)}{/* 图片描述功能 */}<Card title="图片描述生成" style={{ marginBottom: '20px' }}><Uploadaccept="image/*"beforeUpload={handleImageUpload}showUploadList={false}disabled={loading}><Button loading={loading}>上传图片并生成描述</Button></Upload>{imageDescription && <p style={{ marginTop: '10px' }}>{imageDescription}</p>}</Card>{/* 其他功能按钮和展示区... */}</div>);
}
next.config.js
配置环境变量以暴露给前端:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {reactStrictMode: true,env: {// 暴露给前端的环境变量需要以 NEXT_PUBLIC_ 开头NEXT_PUBLIC_LLM_API_SECRET_KEY: process.env.LLM_API_SECRET_KEY,// 如果您需要在前端根据 LLM_PROVIDER 选择不同的 UI 提示,也可以暴露// NEXT_PUBLIC_LLM_PROVIDER: process.env.LLM_PROVIDER,},
};module.exports = nextConfig;
6. 部署考虑
- Serverless Function 限制: 注意 Vercel/Netlify 等平台的 Serverless Function 的内存和超时限制。复杂的 Agent 或 RAG 操作可能需要更长的执行时间或更多的内存。如果遇到超时,可能需要优化 LLM 调用逻辑,或考虑使用专用的服务器部署。
- 成本: LLM 调用是按 Token 计费的。确保前端有适当的输入限制和提示,避免用户无意中生成过长的内容导致高额费用。
- 安全性: 再次强调 API Key 的安全管理。生产环境中,客户端不应直接拥有
LLM_API_SECRET_KEY
,而应通过用户认证系统(如 NextAuth.js)来授权前端请求后端 API。
总结
通过上述规划和代码示例,您可以构建一个功能丰富的博客撰写和总结助手。核心思路是:
- 模块化: 将 LangChain 链和 API 路由解耦,方便管理和扩展。
- 通用化: 封装 LLM 模型实例化,方便切换 LLM 提供商。
- 用户体验: 考虑加载状态、错误提示、流式传输(如果需要)以提升用户体验。
- 安全性: 实现基本的 API 密钥验证,并在生产环境中升级为更强大的认证方案。