前言
LangChain 框架最初以 Python 版本为主,随后推出 JavaScript 版本,这也是官方持续维护的两个主要版本。
LangChain 旨在简化基于大型语言模型(LLM )的应用程序开发,可以说是开发 LLM 应用的首选开源框架,其的核心目标是通过提供模块化的工具和抽象层,帮助开发者轻松集成 LLM 与外部数据源、工具和上下文管理机制,从而构建智能、交互式的应用程序。
本文以Next.js 全栈框架为主并结合 LangChain.js 和 DeepSeek 构建一个简易聊天机器人,其功能主要包含流式输出 、动态提示模板 、记忆功能 、支持 Markdown 渲染 、代码高亮,后续再逐步探索更高级的功能。
初始化
在命令行终端输入 npx create-next-app@latest
初始化一个 Next.js 项目。
按照提示一步一步操作即可,其脚手架会帮我们自动安装所需要的依赖。
安装依赖
安装 LangChain.js 所需要的依赖包
安装 LangChain.js 和 LangChain.js 提供的对 DeepSeek 的官方集成包。
在终端命令行输入如下命令:pnpm install langchain @langchain/deepseek
。
安装 UI 组件库和图标库
安装 shadcn/ui UI 组件库。
在终端命令行输入如下命令:pnpm dlx shadcn@latest init
。
安装 Lucide 图标库。
在终端命令行输入如下命令:pnpm install lucide
。
项目配置
配置 API_KEY
在项目根目录新建 .env.local
,输入从 DeepSeek 开放平台获取到的 API_KEY
ini
DEEPSEEK_API_KEY=sk-xxxxxxxxx
配置样式
在终端命令行输入 pnpm add sonner tailwindcss-animate @tailwindcss/typography
在项目根目录新建 tailwind.config.ts
文件并输入如下内容:
ts
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}' // 根据你的项目结构调整
],
theme: {
extend: {}
},
plugins: [
require('tailwindcss-animate'), // 添加此行
require('@tailwindcss/typography')
]
}
构建业务逻辑
新建 /src/lib/chatbot.ts
文件,在该文件下输入如下内容:
ts
// lib/chatbot.ts
import { ChatDeepSeek } from '@langchain/deepseek'
import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages'
import { BufferMemory } from 'langchain/memory'
// 初始化 DeepSeek 模型(全局单例,避免重复实例化)
const model = new ChatDeepSeek({
apiKey: process.env.DEEPSEEK_API_KEY,
model: 'deepseek-chat',
temperature: 0.7, // 控制生成文本的随机性,值越高越有创意
maxTokens: 500, // 限制每次响应的最大 token 数量
streaming: true // 启用流式输出
})
// 定义系统提示模板,提供机器人角色和行为
const defaultSystemPrompt = `
你是一个友好的助手,名字叫"小智"。请用简洁、自然的中文回答用户问题。
如果用户问到你的身份,告诉他们你是 星途 AI 人工智能助手。
始终保持礼貌,避免生成冗长的回答。
返回的内容以 Markdown 格式返回结果(如列表、标题、代码块)。
如果回答涉及代码,请使用 \`\`\` 包裹代码并指定语言(如 \`\`\`javascript)。
`
// 初始化内存,用于保存对话历史
const memory = new BufferMemory({
returnMessages: true, // 返回完整消息对象,而非纯文本
memoryKey: 'history' // 内存中的键名
})
// 流式输出函数
export async function getChatResponseStream(userInput: string, customSystemPrompt?: string): Promise<ReadableStream> {
try {
// 构造包含历史和当前输入的消息
const { history } = await memory.loadMemoryVariables({})
console.log('Memory variables:', history)
// 支持动态提示
const systemPrompt = customSystemPrompt || defaultSystemPrompt
const messages = [
new SystemMessage(systemPrompt), // 系统提示
...(history as (HumanMessage | AIMessage)[]), // 历史消息
new HumanMessage(userInput) // 当前用户输入
]
// 获取流式响应
const stream = await model.stream(messages)
// 用于保存完整回复
let fullResponse = ''
return new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
// console.log('Stream chunk:', chunk)
const text = chunk.content as string
fullResponse += text
controller.enqueue(new TextEncoder().encode(text))
}
// 保存到内存
await memory.saveContext({ input: userInput }, { output: fullResponse })
controller.close()
} catch (error) {
console.error('Stream error:', error)
controller.enqueue(new TextEncoder().encode('抱歉,流式处理出错!'))
controller.close()
}
},
cancel() {
console.log('Stream cancelled')
}
})
} catch (error) {
console.error('Chat stream error:', error)
return new ReadableStream({
start(controller) {
const errorMessage = '抱歉,我遇到了一些问题,请稍后再试!'
controller.enqueue(new TextEncoder().encode(errorMessage))
controller.close()
}
})
}
}
// 可选:清空对话历史
export function clearChatHistory() {
memory.clear()
}
通过上述代码分析可知:
在业务逻辑层面,我们能够使用 model.stream
并结合 new ReadableStream
返回流式响应结果。
构建 API 路由
新建 /src/app/api/chat/route.ts
文件,在该文件下输入如下内容:
ts
// app/api/chat/route.ts
import { getChatResponseStream } from '@/lib/chatbot'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const { message, systemPrompt }: { message: string; systemPrompt?: string } = await request.json()
if (!message) {
return new Response(JSON.stringify({ error: 'No message provided' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
try {
const stream = await getChatResponseStream(message, systemPrompt)
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8', // 返回纯文本流
'Transfer-Encoding': 'chunked' // 支持分块传输
}
})
} catch (error) {
return new Response(JSON.stringify({ error: (error as Error).message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
}
新建 /src/app/api/chat/clear/route.ts
文件,在该文件下输入如下内容:
ts
// app/api/chat/clear/route.ts
import { clearChatHistory } from '@/lib/chatbot'
import { NextResponse } from 'next/server'
export async function POST() {
try {
clearChatHistory()
return NextResponse.json({ message: 'Chat history cleared' }, { status: 200 })
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 500 })
}
}
构建前端页面
安装必要的 UI 组件:
在命令行终端输入:pnpm dlx shadcn@latest add button input scroll-area select avatar
。
安装必要的 Markdown 渲染组件:
在命令行终端输入:react-markdown remark-gfm rehype-highlight highlight.js
。
为配合 Mrakdown 渲染需要在 src/app/globals.css
文件顶部替换为如下内容:
css
@tailwind utilities;
@import 'tailwindcss/preflight';
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'highlight.js/styles/github.css'; /* GitHub 风格 */
最后,在项目中新建 /src/app/chat/page.tsx
文件,在该文件下输入如下内容:
tsx
// app/chat/page.tsx
'use client'
import { useState, useEffect, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { toast } from 'sonner'
import { Send, User, Bot, Trash2, Loader2 } from 'lucide-react'
import ReactMarkdown from 'react-markdown' // 引入 Markdown 渲染
import remarkGfm from 'remark-gfm' // 支持 GFM
import rehypeHighlight from 'rehype-highlight'
// 定义消息类型
interface Message {
text: string
sender: 'user' | 'bot'
}
export default function Home() {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState<string>('')
const [isLoading, setIsLoading] = useState<boolean>(false)
const [mode, setMode] = useState<string>('friendly') // 动态模式
const scrollAreaRef = useRef<HTMLDivElement>(null)
// 自动滚动到底部
useEffect(() => {
if (scrollAreaRef.current) {
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight
}
}, [messages])
const getSystemPrompt = () => {
switch (mode) {
case 'formal':
return '你是一个正式的助手,请用专业、礼貌的中文回答问题。'
case 'funny':
return '你是一个幽默的助手,请用风趣的中文回答问题,尽量让人开心。'
default:
return undefined // 使用默认提示
}
}
const handleSend = async () => {
if (!input.trim()) return
const newMessage: Message = {
text: input,
sender: 'user'
}
setMessages((prev) => [...prev, newMessage])
setInput('')
setIsLoading(true)
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: input, systemPrompt: getSystemPrompt() })
})
if (!res.ok) {
throw new Error('API request failed')
}
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let botMessage = ''
setMessages((prev) => [...prev, { text: '', sender: 'bot' }])
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
console.log('Received chunk:', chunk)
botMessage += chunk
setIsLoading(false)
setMessages((prev) => {
const updated = [...prev]
updated[updated.length - 1] = {
...updated[updated.length - 1],
text: botMessage
}
return updated
})
}
} catch (error) {
console.error('Error fetching chat response:', error)
toast.error('无法获取回复,请稍后再试。', {
description: '发生了网络错误或 API 问题。'
})
setMessages((prev) => [
...prev,
{
text: '抱歉,出了点问题!',
sender: 'bot'
}
])
} finally {
setIsLoading(false)
}
}
const handleClear = async () => {
setMessages([])
try {
const res = await fetch('/api/chat/clear', {
method: 'POST'
})
if (!res.ok) throw new Error('Failed to clear chat history')
toast.success('聊天记录已清除', {
description: '所有消息已成功删除。'
})
} catch (error) {
console.error('Error clearing chat history:', error)
toast.error('清除聊天记录失败', {
description: '请稍后再试。'
})
}
}
return (
<div className="max-w-[1000px] mx-auto p-4 h-screen flex flex-col">
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold">简易聊天机器人</h1>
<div className="flex gap-2">
<Select value={mode} onValueChange={setMode}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="选择模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="friendly">友好模式</SelectItem>
<SelectItem value="formal">正式模式</SelectItem>
<SelectItem value="funny">幽默模式</SelectItem>
</SelectContent>
</Select>
<Button variant="ghost" size="icon" onClick={handleClear}>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
<ScrollArea className="flex-1 border rounded-md p-4 mb-4" ref={scrollAreaRef}>
{messages.length === 0 && !isLoading && <p className="text-center text-muted-foreground">开始聊天吧!</p>}
{messages.map((msg, index) => (
<div key={index} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'} mb-4`}>
{msg.sender === 'bot' && (
<Avatar className="mr-2">
<AvatarFallback>
<Bot className="w-6 h-6 flex-shrink-0" />
</AvatarFallback>
</Avatar>
)}
<div className={`flex flex-wrap items-start gap-2 max-w-[70%] p-3 rounded-lg ${msg.sender === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
<div className="prose prose-sm max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
{msg.text}
</ReactMarkdown>
</div>
</div>
{msg.sender === 'user' && (
<Avatar className="ml-2">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>
<User className="w-5 h-5 flex-shrink-0" />
</AvatarFallback>
</Avatar>
)}
</div>
))}
{isLoading && messages.length > 0 && (
<div className="flex justify-start mb-4">
<Avatar className="mr-2">
<AvatarFallback>
<Bot className="w-6 h-6 flex-shrink-0" />
</AvatarFallback>
</Avatar>
<div className="flex items-start gap-2 max-w-[70%] p-3 rounded-lg bg-muted">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-sm text-muted-foreground">思考中...</span>
</div>
</div>
</div>
)}
</ScrollArea>
<div className="flex gap-2">
<Input value={input} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)} onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => e.key === 'Enter' && !isLoading && handleSend()} placeholder="输入消息..." className="flex-1" disabled={isLoading} />
<Button onClick={handleSend} disabled={isLoading} className="w-[50px]">
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
</Button>
</div>
</div>
)
}
效果演示
演示示例

项目源码
本文总结
通过上述代码分析,我们可以发现我们已经实现一个基本的聊天对话界面,但是优化的空间还有很多,如调用外部工具、搜索、文件上传与分析等,我们可以充分利用 LangChain 所提供的能力实现强大的功能,为最终实现 AI Agent 做铺垫。