当前位置:首页>python>AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用

AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用

  • 2026-02-09 03:23:46
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
# 1. 引言
在大语言模型(LLM)快速发展的今天,几乎所有产品都在借助大模型进行重塑与升级。在过去一段时间,各类旨在提升效率的 AI Agent 如雨后春笋般涌现,尤其是 Coding Agent 的兴起,在一定程度上对前端开发者的职业前景带来了冲击与挑战。一些走在行业前沿的公司甚至开始提倡“前后端再度融合”,这意味着未来开发者可能需要向具备 AI 能力的全栈工程师转型。因此,掌握 AI 全栈相关的知识与技能变得非常重要。
本文将带你通过实战,从零开始搭建一个基于Python (FastAPI) 、LangChainVue3的全栈 LLM 聊天应用程序。另外我们将使用 DeepSeek 作为底层模型进行学习。
技术栈前瞻
  1. 后端: Python 3, FastAPI (Web 框架), LangChain (LLM 编排), Uvicorn (ASGI 服务器)
  2. 前端: Vue 3, TypeScript, Vite (构建工具)
  3. 模型: DeepSeek API (兼容 OpenAI 格式)
我是 Cobyte,欢迎添加 v:icobyte,学习 AI 全栈。
# 2. 为什么选择 Python ?
在 AI 领域,Python 无疑是首选的开发语言。因此,如果想通过学习 AI 全栈技术来获得一份理想工作,掌握 Python 几乎是必经之路。这就像在国内想从事后端开发,Java 绝对是不二之选。对于前端背景的同学而言,虽然也可以通过 Node.js 入门 AI 开发,但就整体就业前景和发展空间来看,跟 Node.js 相比 Python 的优势也是断层领先。同时,Python 作为一门入门门槛较低的语言,学习起来相对轻松,所以大家无需过于担心学习难度问题。
最后本人提倡在实战中学习 Python,并且完全可以借助 AI 进行辅导学习。
# 2. Python 环境配置
我们这里只提供 Windows 环境的讲解,其他的环境自行 AI,Python 的环境搭建还是十分简单的。
1.  访问官网下载安装包
https://www.python.org/downloads
选择对应的平台版本:
2.  安装时勾选 "Add Python to PATH"
3.  验证安装
打开终端命令工具输入以下命令行:
python --versionpip --version
出现如下信息则说明安装成功了。
最后编辑器我们可以选择 VS Code,只需在拓展中安装以下插件即可。
我们前面说到了我们是使用 DeepSeek 作为底层模型进行学习,所以我们需要去 DeepSeek 的 API 开放平台申请一个大模型的 API key。申请地址如下:https://platform.deepseek.com/api_keys。当然我们需要充一点钱,就充几块也够我们学习用了。
# 3. Python 快速入门
## 3.1 Hello World
我们创建一个 `simple-llm-app` 的项目目录,然后在根目录创建一个 `.env` 文件,用于存放项目的环境变量配置,跟前端的项目一样。我们这里设置上面申请到的 DeepSeek 开放平台的 API key。
DEEPSEEK_API_KEY=sk-xxxxxx
然后我们可以通过 python-dotenv 库读从 `.env` 文件中读取它,我们创建一个 test.py 的文件,里面的代码如下:
import osfrom dotenv import load_dotenv# 加载环境变量 (读取 .env 中的 DEEPSEEK_API_KEY)load_dotenv()# 打印print(os.getenv("DEEPSEEK_API_KEY"))
其中 `dotenv` 库需要安装 python-dotenv 依赖,安装方法也跟安装 npm 包类似,命令如下:
pip install python-dotenv
接着执行 test.py 文件,执行命令跟 Node.js 类似:
python test.py
我们就可以在命令终端看到 `.env` 文件中的  DeepSeek API key 了。这算是成功输出了 Python 的 Hello world。
## 3.2 Python 语法入门
接着我们继续了解 Python 的相关语法。在 Python 中,使用 `from ... import ...`,在 ES6 JavaScript 中,我们使用 `import ... from ...`。
所以上述代码的 `import os` -> 类似于 Node.js 中的 `import * as os from 'os'`, `os` 是一个内置库。
`from dotenv import load_dotenv` 则类似于从 npm 包中导入一个类,比如: `import load_dotenv from 'dotenv'` 。
Python:没有显式的变量声明关键字,直接通过赋值创建变量。
# Python - 直接赋值,无需关键字name = "张三"AGE = 25 # 常量(约定)没有内置的常量类型,但通常用全大写变量名表示常量,实际上可以修改is_student = True
JavaScript:使用 `var`、`let` 或 `const` 声明变量。
// JavaScript - 必须使用关键字let name = "张三";const age = 25;  // 常量 使用 `const` 声明常量,不可重新赋值。var isStudent = true;  // 旧方式
注释对比
Python注释:
   单行注释:以 # 开头
# 这是一个Python单行注释name = "张三"  # 这是行尾注释
   多行注释:可以使用三个单引号 ''' 或三个双引号 """ 包裹
'''这是一个Python多行注释可以跨越多行实际上这是字符串,但常用作注释'''"""双引号三引号也可以这在Python中通常用作文档字符串(docstring)"""
JavaScript 注释:
   单行注释:以 `//` 开头
// 这是一个JavaScript单行注释let name = "张三";  // 这是行尾注释
*   多行注释:以 `/*` 开头,以 `*/` 结尾
/* 这是一个JavaScript多行注释 可以跨越多行 这是真正的注释语法*//** * 用户类,表示系统中的一个用户 * @class */classUser{}
好了我们不贪杯,实战中遇到不同的 Python 语法,我们再针对学习或者借助 AI 通过与 JavaScript 语法进行横向对比,对于有一定编程基础的我们,肯定非常容易理解的。相信通过上述 Python 语法的学习,聪明的你再回头看上述示例的 Python 代码,肯定可以看懂了。
我们这里只是简单介绍上面代码中涉及到的 Python 语法,本人推荐在实战中进行学习。更多 JavaScript 视觉学习 Python:https://langshift.dev/zh-cn/docs/js2py
## 3.3 FastAPI 框架快速入门
### 3.3.1 FastAPI 是什么
FastAPI 是一个现代、高性能(与 NodeJS 和 Go 相当)的 Web 框架,用于构建 API,基于 Python 3.6+ 并使用了标准的 Python 类型提示。但它本身不提供完整的 Web 服务器功能,而是通过 ASGI(Asynchronous Server Gateway Interface)与服务器进行通信。
Uvicorn 是一个高性能的 ASGI 服务器,它支持异步编程,能够运行 FastAPI 这样的异步 Web 应用。所以 FastAPI 需要配合 Uvicorn 使用,这样才能够充分发挥 FastAPI 的异步特性,提供极高的性能。同时,Uvicorn 在开发和部署时都非常方便。
简单来说
  •  FastAPI 负责:路由、验证、序列化、依赖注入等应用逻辑
  •  Uvicorn 负责:HTTP 协议解析、并发处理、连接管理等服务器功能
两者结合形成了现代 Python Web 开发的黄金组合,既能享受 Python 的便捷,又能获得接近 Go、Node.js 的性能。
### 3.3.2 基本示例
我们创建一个 server.py 文件,输入以下示例代码:
from fastapi import FastAPIapp = FastAPI()@app.get("/")def read_root():    return {"message""我是 Cobyte,欢迎添加 v:icobyte,学习 AI 全栈。"}# 程序的入口点if __name__ == "__main__":    import uvicorn    uvicorn.run(app, host="127.1.1.1", port=9527)
上述代码引用了两个依赖 fastapi 和 uvicorn,我们通过 pip 进行安装一下:
pip install fastapi uvicorn
然后我们在终端启动服务:python server.py,运行结果如下:
接着我们在浏览器打开 http://127.1.1.1:9527 显示如下:
### 3.3.3 路径参数和查询参数
示例:
@app.get("/items/{id}")def read_item(    idint    limit: int = 10,         # 默认值    q: Optional[str] = None# 可选参数    short: bool = False,     # 默认值    tags: List[str] = []     # 列表参数):    item = {"id"id"limit": limit, "tags": tags}    if q:        item.update({"q": q})    if not short:        item.update({"desc""长说明"})    return item
重启服务,在浏览器输入:http://127.1.1.1:9527/items/1?q=cobyte ,结果如下:
总结
  • 路径参数:在路径中声明的参数,如 `id`。
  • 查询参数:作为函数参数,但不是路径参数,将自动解释为查询参数。
### 3.3.4 FastAPI 中的模型定义
在 FastAPI 中,我们经常需要处理来自客户端的请求数据,例如 POST 请求的 JSON 体。为了确保数据的正确性,我们需要验证数据是否符合预期的格式和类型。使用 Pydantic 模型可以让我们以一种声明式的方式定义数据的结构,并自动进行验证。
Pydantic 是一个 Python 库,用于数据验证和设置管理,主要基于 Python 类型提示(type hints)。它可以在运行时提供类型检查,并且当数据无效时提供详细的错误信息。
Pydantic 的核心功能是定义数据的结构(模型),并自动验证传入的数据是否符合这个结构。它非常适用于以下场景:
  1. 验证用户输入(例如 API 请求的数据)
  2. 配置管理
  3. 数据序列化和反序列化(例如将 JSON 数据转换为 Python 对象)
Pydantic 模型使用 Python 的类来定义,类的属性使用类型注解来指定类型,并且可以设置默认值。
请求体(Request Body)和响应模型(Response Model)的示例如:
from pydantic import BaseModel, validator, Fieldfrom typing import OptionalListimport re# 请求体(Request Body)class UserRequest(BaseModel):    username: str = Field(..., min_length=3, max_length=50)    password: str    email: str    @validator('username')    def username_alphanumeric(cls, v):        if not re.match('^[a-zA-Z0-9_]+$', v):            raise ValueError('只能包含字母、数字和下划线')        return v    @validator('email')    def email_valid(cls, v):        if '@' not in v:            raise ValueError('无效的邮箱地址')        return v.lower()  # 转换为小写    @validator('password')    def password_strong(cls, v):        if len(v) < 6:            raise ValueError('密码至少6位')        return v# 响应模型(Response Model)class UserResponse(BaseModel):    username: str    email: str@app.post("/user/", response_model=UserResponse)async def create_user(user: UserRequest):    # 密码会被过滤,不会出现在响应中    return user
FastAPI 会自动从 Pydantic 模型生成 API 文档,我们在 server.py 文件中添加了上述示例之后,重启服务,访问 http://127.1.1.1:9527/docs 可以看到:
并且我们还可以在文档地址中进行测试,这里就不展开讲了。
### 3.3.5 异步和中间件
示例:
from fastapi import Request@app.middleware("http")async def add_process_time_header(request: Request, call_next):    response = await call_next(request)    response.headers["X-Process-Time"] = str(process_time)    return response
我们可以看到 Python 的这个异步语法跟 JavaScript 的 async/await 是一样的语法。
### 3.3.6 CORS 配置
通过设置 CORS 配置允许前端跨域访问。
from fastapi.middleware.cors import CORSMiddleware# CORS 配置:允许前端跨域访问app.add_middleware(    CORSMiddleware,    allow_origins=["*"],  # 在生产环境中建议设置为具体的前端域名    allow_credentials=True,    allow_methods=["*"],  # 允许的方法    allow_headers=["*"],  # 允许的头部)
到此本文所用到的 FastAPI 知识就基本介绍完毕了,后续再在实战中进行学习,先上了 AI 全栈的车再说。
# 4. LLM 和 OpenAI 接口快速入门
## 4.1 入门示例代码
让我们从安装依赖开始,借助 DeepSeek 大模型一起探索 OpenAI 接口规范。
pip install openai
接着我们在 test.py 中添加如下代码:
import osfrom dotenv import load_dotenv# 加载环境变量 (读取 .env 中的 DEEPSEEK_API_KEY)load_dotenv()# 加载 OpenAI 库,从这里也可以看到 Python 的库加载顺序跟 JavaScript ES6 import 是不一样,反而有点像 requriefrom openai import OpenAI# 初始化客户端client = OpenAI(    api_key=os.getenv("DEEPSEEK_API_KEY"), # 身份验证凭证,确保你有权访问 API    base_url="https://api.deepseek.com" # 将请求重定向到 DeepSeek 的服务器(而非 OpenAI))# 构建聊天请求response = client.chat.completions.create(  model="deepseek-chat"# 指定模型版本  temperature=0.5,  messages=[   # 对话消息数组      {"role""user""content""你是谁?"}  ])# 打印结果print(response.choices[0].message.content.strip())
终端输出结果如下:
可以看到我们成功调用了 DeepSeek 大模型。
在 `openai` 中,返回的 `response` 对象是一个 Pydantic 模型,如果我们想详细查看 response 返回的结果,可以使用它自带的 `.model_dump_json()` 方法。
# 使用 model_dump_json 并指定缩进print(response.model_dump_json(indent=2))
可以看到通过上述方式打印大模型响应的信息如下:
## 4.2 choices 字段详解
我们从上面打印的结果可以了知道,大模型返回的文本信息是存储在 `choices` 字段中的,所以我们来了解一下它。
在调用 chat.completions.create 时,如果设置了 n 参数(n>1),那么模型会生成多个输出,此时 `choices` 字段就会包含多个元素。每个 choice 代表一个可能的响应,我们可以通过遍历 choices 来获取所有响应。
另外,即使 n=1(默认值),choices 也是一个列表,只不过只包含一个元素。所以我们上述例子中才通过 `response.choices[0]` 来获取大模型的返回结果。
## 4.3 流式响应
因为大模型本质上是一个预测生成器,简单来说就是你输入一句话,大模型就预测下一个字。因此我们希望在模型生成文本的同时就显示给用户,提高交互的实时性。这就是**流式响应**。代码设置如下:
# 构建聊天请求response = client.chat.completions.create(  model="deepseek-chat"# 指定模型版本  temperature=0.5,  messages=[   # 对话消息数组      {"role""user""content""你是谁?"}  ],+  stream=True# 启用流式传输)+# response是一个生成器,在Python中,生成器是一种迭代器,每次迭代返回一个值。这里,每次迭代返回一个chunk(部分响应)。+for chunk in response:                           # 1. 遍历响应流+    if chunk.choices[0].delta.content:           # 2. 检查是否有内容+        print(chunk.choices[0].delta.content,    # 3. 打印内容+              end="",                            # 4. 不换行+              flush=True)                        # 5. 立即刷新缓冲区
输出结果如下:
## 4.4 temperature 参数
我个人觉得那么多大模型参数中 temperature 参数还是比较重要的,值得我们了解一下。模型在生成每一个词时,都会计算一个所有可能的下一个词的概率分布(例如,“苹果”概率0.3,“香蕉”概率0.5,“水果”概率0.2)。temperature 的值会影响这个概率分布的形状,从而改变模型最终根据这个分布进行“抽样”选择的结果。
一个简单的比喻:选餐厅吃饭
  • Temperature = 0.0永远只去评分最高、去过无数次的那一家最保险的餐厅。结果最稳定,但永远没有新体验。
  • Temperature = 1.0大多数时候去那家最好的,但偶尔也会根据评价试试附近其他不错的餐厅。平衡了可靠性和新鲜感。
  • Temperature = 1.5经常尝试新餐厅,甚至包括一些评价奇特或小众的地方。体验非常丰富,但有时会“踩雷”。
总结与建议
1.  追求确定性时调低 (接近0) :当你需要精确、可靠、可复现的结果时,如生成代码、数学推导、事实问答、指令严格遵循。
2.  追求创造性和多样性时调高 (>1.0) :当你需要创意、多样化表达、故事生成、诗歌时。
3.  通用场景用中间值 (0.8-1.2):大多数对话、摘要、分析等任务,这个范围能提供既连贯又有一定灵活性的输出。
## 4.5 消息角色
在 OpenAI API 中,`messages` 数组中的每条消息都有一个 `role` 字段,它定义了消息的来源和用途。消息角色主要有三种:system、user、assistant。此外,在后续的更新中,还引入了 tool 和 function 等角色,但最基础的是前三种。
1. system (系统)
   作用: 设置助手的背景、行为、指令等。
   特点:
      通常作为第一条消息,用于设定对话的上下文和规则。
      不是必须的,但可以显著影响助手的行为。
    示例:
{"role": "system", "content": "你是一个专业的翻译助手,只能将中文翻译成英文,其他问题一律不回答。"}
2. user (用户)
   作用: 用户输入的问题或指令
   特点:
       代表对话中的人类用户
       每个请求必须至少包含一条 user 消息
       通常是最后一条消息(除了流式响应)
   示例:
messages = [        {"role""system""content""你是一个有帮助的助手"},        {"role""user""content""什么是机器学习?"}    ]
3. assistant (助手)
   作用: 代表助手之前的回复。
   特点:
       在多轮对话中保存历史回复
       帮助模型保持对话连贯性
       在单轮对话中不需要此角色
   示例:
messages = [        {"role""system""content""你是一个数学老师"},        {"role""user""content""2+2等于多少?"},        {"role""assistant""content""2+2等于4"},        {"role""user""content""那3+3呢?"}  # 模型知道这是新问题    ]
通过合理组合这些角色,你可以构建从简单问答到复杂多轮对话的各种应用场景。记住:清晰的角色定义和恰当的消息组织是获得高质量回复的基础。我们这里先介绍前三种核心角色。
# 5. LangChain 入门
## 5.1 怎么理解 LangChain 框架
从前端的视角来理解,LangChain 就好比是 Vue 或 React 这类框架。在前端开发中,如果没有 Vue 或 React,我们就需要直接编写大量操作浏览器 API 的底层代码;而有了这类框架,它们封装了常见的交互逻辑和状态管理,让开发变得更高效、更结构化。类似地,LangChain 实际上是一套封装了大型语言模型常见操作模式的方案,它帮助开发者更便捷地调用、组合与管理大模型的能力,而无需每次都从头编写复杂的模型交互代码。
## 5.2 LangChain 调用 LLM 示例
接着我们在项目根目录下创建一个 `llm-app.py` 文件,输入以下内容:
import osfrom langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplatefrom langchain_core.output_parsers import StrOutputParserfrom dotenv import load_dotenv# 1. 加载环境变量 (读取 .env 中的 DEEPSEEK_API_KEY)load_dotenv()# 2. 创建组件# 相对于上面的使用 OpenAI 的接口,现在经过 LangChain 封装后确实简洁了很多llm = ChatOpenAI(    model="deepseek-chat"    temperature=0.7,    api_key=os.getenv("DEEPSEEK_API_KEY"),    base_url="https://api.deepseek.com/v1")# 创建了一个人类角色的提示模板,`from_template` 方法允许我们通过一个字符串模板来定义提示,默认是人类角色。prompt = ChatPromptTemplate.from_template("{question}")# 创建解析器parser = StrOutputParser()# 将AI响应转换为字符串,通过前面的知识我们知道大模型返回的数据一般包含很多数据,# 很多时候我们只需要其中的文本内容。`StrOutputParser` 就是用来提取这个文本内容的# 3. 组合链 (LCEL 语法) Python LangChain 常见的链式调用chain = prompt | llm | parser# 等价于:输入 → 模板填充 → AI处理 → 结果解析# 4. 执行result = chain.invoke({"question""你是谁?"})# 内部执行:填充"你是谁?" → 调用API → 解析响应 → 返回字符串# 5. 打印结果print(result)
然后在终端安装对应的依赖(这个步骤跟前端也很像,所以学习 Python 是很简单的):
pip install langchain_openai langchain_core dotenv
接着在终端执行
# 跟前端的 node llm-app.js 等价python llm-app.py
终端输出结果如下:
可以看到我们成功执行了一个 Python + LangChain 的应用程序。
## 5.2 消息模板系统
我们上面的注释讲解了 `prompt = ChatPromptTemplate.from_template("{question}")` 这句代码默认创建了一个人类角色的提示模板,也就是 `{"role": "user", "content": "用户输入的内容"}`。
LangChain 作为一个强大的 LLM 应用开发框架, 为了让开发者能够精确控制对话的流程和结构,提供了灵活且强大的消息模板系统。LangChain 的消息模板系统基于角色(role)的概念,将对话分解为不同类型的信息单元。目前的类型如下:
角色
用途
对应 OpenAI 角色
SystemMessagePromptTemplate
system
系统指令、设定
system
HumanMessagePromptTemplate
human
用户输入
user
AssistantMessagePromptTemplate
assistant
AI 回复
assistant
AIMessagePromptTemplate
ai
AI 回复(别名)
assistant
ToolMessagePromptTemplate
tool
工具调用结果
tool
FunctionMessagePromptTemplate
function
函数调用结果
function
ChatPromptTemplate 则是消息系统的核心容器,负责协调各种消息类型:
from langchain_core.prompts import (    SystemMessagePromptTemplate,    HumanMessagePromptTemplate,    AssistantMessagePromptTemplate)system = SystemMessagePromptTemplate.from_template(...)human = HumanMessagePromptTemplate.from_template(...)assistant = AssistantMessagePromptTemplate.from_template(...)prompt = ChatPromptTemplate.from_messages([system, human, assistant])
所以上述入门实例代码可以进行以下修改:
-from langchain_core.prompts import ChatPromptTemplate+from langchain_core.prompts import ChatPromptTemplate,HumanMessagePromptTemplate# 省略...-# 创建了一个人类角色的提示模板,`from_template` 方法允许我们通过一个字符串模板来定义提示,默认是人类角色。-prompt = ChatPromptTemplate.from_template("{question}")+human = HumanMessagePromptTemplate.from_template("{question}")+prompt = ChatPromptTemplate.from_messages([human])# 省略...
然后重新在终端执行 `python llm-app.py` 依然正常输出。
同时通过 LangChain 消息模型来理解大模型的调用过程也变得十分的清晰,所以整个流程是:
输入 → prompt → llm → parser → 输出         ↓    {"question""你是谁?"}         ↓    prompt 处理:创建消息 "你是谁?"         ↓    llm 处理:调用 LLM 处理,返回 AIMessage 对象         ↓    parser 处理:提取文本内容         ↓    最终结果字符串
在 LangChain 中还有一个最基础的模板类 PromptTemplate 用于构建字符串提示。下面我们也来了解一下它的基本用法。
from langchain_core.prompts import PromptTemplate# 方式1:使用 from_template 类方法(最常用)prompt = PromptTemplate.from_template("请解释什么是{concept}。")# 方式2:直接实例化prompt = PromptTemplate(    input_variables=["concept"],     template="请解释什么是{concept}。")
综上所述我们通过理解和掌握 LangChain 这些核心概念,才能高效地构建可靠、可维护的 LLM 应用。此外,LangChain 的消息模板系统仍在不断发展当中,我们需要不断地持续关注。
## 5.3 LangChain 链式调用(管道操作符)
在 LangChain 中所谓的链式调用是通过管道操作符 `|` 来实现的,也就是通过 `|` 实现将一个函数的输出作为下一个函数的输入。
例如上述的示例代码中的:
# LangChain 中的管道操作chain = prompt | llm | output_parser
  • 等价于手动执行链的每一步:
# 第一步:prompt 处理messages = prompt.invoke({"question""你是谁?"})# messages = [HumanMessage(content="你是谁?")]# 第二步:llm 处理response = llm.invoke(messages)# response = AIMessage(content="我是DeepSeek...")# 第三步:parser 处理result = parser.invoke(response)# result = "我是DeepSeek..."
在标准 Python 语法中,`|` 是**按位或操作符**,用于:
  • 整数的按位运算:`5 | 3 = 7`
  • 集合的并集运算:`{1, 2} | {2, 3} = {1, 2, 3}`
  • 从 Python 3.10 开始,用于类型联合:`int | str`
但 LangChain 通过 重载(overload)  `|` 操作符,赋予了它新的含义:
  • `|` 在 LangChain 中是一种**语法糖**,让链式操作更直观
  • 它不是 Python 的新语法,而是通过操作符重载实现的框架特定功能
  • 这种设计让 LangChain 的代码更加简洁和易读
# 6. LLM 聊天应用后端
## 6.1 后端架构设计
我们遵循单一职责原则(SRP)进行分层架构设计,将系统划分为API层、业务层和数据层,旨在实现高内聚、低耦合,提升代码的可维护性、可测试性和可扩展性。
API层  专注于处理 HTTP 协议相关的逻辑,包括路由定义、请求验证、响应序列化和跨域处理等。它作为系统的入口点,负责与客户端进行通信,并将业务逻辑委托给下层。这种设计使得我们可以独立地调整 API 暴露方式(如支持 WebSocket)而不影响核心业务逻辑。
业务层  封装 LLM 的核心应用逻辑,例如与 AI 模型的交互、对话历史管理和流式生成等。这一层独立于 Web 框架,使得业务逻辑可以复用于其他场景(如命令行界面或批处理任务)。同时,业务层的单一职责确保了我们能够针对 LLM 交互进行优化和测试,而无需关心 HTTP 细节。
数据层  通过 Pydantic 定义系统的数据模型,包括请求、响应结构和内部数据传输对象。通过集中管理数据模型,我们确保了数据格式的一致性,并便于进行数据验证和类型提示。这种分离使得数据结构的变更更加可控,同时也为生成 API 文档提供了便利。
## 6.1 实现业务层和数据层
实现业务层其实就是封装 LLM 的核心应用逻辑。通过将复杂的 LLM 调用逻辑、提示工程和流式处理封装在独立的类中,这样 API 层只需关注请求与响应,而无需了解 LangChain 或特定 API 的细节。这使得底层技术栈的迭代或更换(例如从 LangChain 切换到其他操作大模型的框架或更改 LangChain 最新的 API)变得轻而易举,只需修改封装类内部实现,而对外接口保持不变,实现了有效隔离。
创建 `./backend/llm_app.py` 文件,内容如下:
import osfrom typing import Generatorfrom dotenv import load_dotenvfrom langchain_openai import ChatOpenAIfrom langchain_core.prompts import PromptTemplatefrom langchain_core.output_parsers import StrOutputParser# 加载环境变量load_dotenv()class LLMApp:    def __init__(self, model_name="deepseek-chat", temperature=0.7):        """        初始化 LLM 应用程序        """        # 检查 DeepSeek API 密钥        if not os.getenv("DEEPSEEK_API_KEY"):            raise ValueError("请在 .env 文件中设置 DEEPSEEK_API_KEY 环境变量")        # 初始化配置        self.model_name = model_name        self.temperature = temperature        self.api_key = os.getenv("DEEPSEEK_API_KEY")        self.base_url = "https://api.deepseek.com/v1"        # 初始化非流式 LLM (用于普通任务)        self.llm = self._create_llm(streaming=False)        # 初始化流式 LLM (用于流式对话)        self.streaming_llm = self._create_llm(streaming=True)        # 输出解析器        self.output_parser = StrOutputParser()        # 初始化对话链        self._setup_chains()    def _create_llm(self, streaming: bool = False):        """创建 LLM 实例"""        return ChatOpenAI(            model_name=self.model_name,            temperature=self.temperature,            api_key=self.api_key,            base_url=self.base_url,            streaming=streaming        )    def _setup_chains(self):        """设置处理链"""        # 带上下文的对话 Prompt        conversation_prompt = PromptTemplate(            input_variables=["chat_history""user_input"],            template="""你是一个有用的 AI 助手。请根据对话历史回答用户的问题。            对话历史:            {chat_history}            用户:{user_input}            助手:"""        )        # 注意:这里我们只定义 prompt,具体执行时再组合        self.conversation_prompt = conversation_prompt    def format_history(self, history_list) -> str:        """格式化聊天历史"""        if not history_list:            return "无历史对话"        formatted = []        for msg in history_list:            # 兼容 Pydantic model 或 dict            if isinstance(msg, dict):                role = msg.get('role''unknown')                content = msg.get('content''')            else:                role = getattr(msg, 'role''unknown')                content = getattr(msg, 'content''')            formatted.append(f"{role}{content}")        return "\n".join(formatted[-10:])  # 只保留最近 10 条    def stream_chat(self, user_input: str, chat_history: list) -> Generator[strNoneNone]:        """流式对话生成器"""        try:            history_text = self.format_history(chat_history)            # 构建链:Prompt | StreamingLLM | OutputParser            chain = self.conversation_prompt | self.streaming_llm | self.output_parser            # 执行流式生成            for chunk in chain.stream({                "chat_history": history_text,                "user_input": user_input            }):                yield chunk        except Exception as e:            yield f"Error: {str(e)}"
接下来我们对上述封装的 LLM 类的功能进行测试,测试前先在 ./backend/.env 文件中添加 DeepSeek 开放平台的 API key。
DEEPSEEK_API_KEY=sk-xxxxxx
接着创建 `./backend/test.py` 文件写上以下测试代码。
from llm_app import LLMApp# 测试llmApp = LLMApp()# 模拟聊天历史chat_history = [    {"role""user""content""你好"},    {"role""assistant""content""你好!有什么可以帮助你的吗?"},]# 模拟用户输入user_input = "请介绍一下人工智能"# 收集流式响应response_chunks = []for chunk in llmApp.stream_chat(user_input, chat_history):    response_chunks.append(chunk)    # 模拟实时显示    print(chunk, end="", flush=True)# 合并响应full_response = "".join(response_chunks)print(f"\n完整响应: {full_response}")
测试结果如下:
接着我们通过 Pydantic 来定义数据的结构(模型)
创建 ./backend/models.py 文件,内容如下:
from pydantic import BaseModelfrom typing import ListOptionalclass ChatMessage(BaseModel):    """单条聊天消息"""    role: str  # "user" 或 "assistant"    content: strclass ChatRequest(BaseModel):    """聊天请求模型"""    message: str    chat_history: Optional[List[ChatMessage]] = []
修改 `./backend/test.py` 文件,内容如下:
import jsonimport asynciofrom llm_app import LLMAppfrom models import ChatRequest, ChatMessage# 测试llmApp = LLMApp()# 模拟聊天历史chat_history = [    {"role""user""content""你好"},    {"role""assistant""content""你好!有什么可以帮助你的吗?"},]# 模拟用户输入user_input = "请介绍一下人工智能"# 模拟 SSE 的流式聊天响应async def chat_stream(request: ChatRequest):    # 1. 发送开始事件    yield f"data: {json.dumps({'type''start'})}\n\n"    await asyncio.sleep(0.01# 让出控制权,以便运行其他任务。    full_response = ""    # 2. 生成并发送 token    for token in llmApp.stream_chat(request.message, request.chat_history):        full_response += token        yield f"data: {json.dumps({'type''token''content': token})}\n\n"        await asyncio.sleep(0.01)    # 3. 发送结束事件    yield f"data: {json.dumps({'type''end''full_response': full_response})}\n\n"# 异步测试函数async def test_chat_stream():    # 使用 Pydantic 模型实现数据序列化和反序列化(即将JSON数据转换为Python对象)    request = ChatRequest(message=user_input, chat_history=chat_history)    async for chunk in chat_stream(request):        print(chunk)# 在异步编程中,我们使用asyncio.run()来运行一个异步函数(coroutine)作为程序的入口点。asyncio.run(test_chat_stream())
打印结果如下:
在上述的测试代码中的 chat\_stream 函数实现一个基于 Server-Sent Events (SSE) 的流式聊天响应的异步生成器,它接收一个 `ChatRequest` 对象,然后逐步生成事件流。事件流格式遵循 SSE 规范,每个事件以 "data: " 开头,后跟 JSON 字符串,并以两个换行符结束。
1.  首先,发送一个开始事件,通知客户端开始生成响应。
2.  然后,通过调用 `llmApp.stream_chat` 方法,逐个获取 token,并将每个 token 作为一个事件发送。
3.  在发送每个 token 事件后,使用 `await asyncio.sleep(0.01)` 来让出控制权,这样其他任务可以运行,避免阻塞。
4.  同时,将每个 token 累加到 `full_response` 中,以便在最后发送整个响应。
5.  最后,发送一个结束事件,并包含完整的响应内容。
这样设计的好处:
  • 流式传输:可以逐步将响应发送给客户端,客户端可以实时看到生成的 token,提升用户体验(如打字机效果)。
  • 异步:使用异步生成器,可以在等待模型生成下一个 token 时让出控制权,提高并发性能。
  • 事件驱动:通过定义不同类型的事件(开始、token、结束),客户端可以方便地根据事件类型进行处理。
## 6.2 实现 API 层
上面测试代码中实现的 chat\_stream 函数,其实就是我们接下来要实现的 流式对话接口,即接收用户的消息和聊天历史,通过流式方式返回 LLM 的响应。同时我们再实现一个健康检查接口,提供服务器的健康状态,包括 LLM 应用是否初始化成功、模型名称等,便于监控。
根据上面所学的知识,我们实现一个基于 FastAPI 的 LLM 聊天 API 服务。
我们创建 `./backend/server.py` 文件,内容如下:
import jsonimport asynciofrom datetime import datetimefrom fastapi import FastAPI, HTTPExceptionfrom fastapi.middleware.cors import CORSMiddlewarefrom fastapi.responses import StreamingResponsefrom llm_app import LLMAppfrom models import ChatRequest, HealthResponseapp = FastAPI(title="Cobyte LLM Chat API")# CORS 配置:允许前端跨域访问app.add_middleware(    CORSMiddleware,    allow_origins=["*"],  # 在生产环境中建议设置为具体的前端域名    allow_credentials=True,    allow_methods=["*"],    allow_headers=["*"],)# 全局 LLM 应用实例llm_app = None@app.on_event("startup")async def startup_event():    """应用启动时初始化 LLM"""    global llm_app    try:        print("正在初始化 LLM 应用...")        llm_app = LLMApp()        print("✅ LLM 应用初始化成功")    except Exception as e:        print(f"❌ LLM 应用初始化失败: {e}")@app.get("/api/health")async def health_check():    """健康检查接口"""    return HealthResponse(        status="healthy" if llm_app else "unhealthy",        model="deepseek-chat",        api_configured=llm_app is not None,        timestamp=datetime.now().isoformat()    )@app.post("/api/chat/stream")async def chat_stream(request: ChatRequest):    """流式对话接口"""    if not llm_app:        raise HTTPException(status_code=500, detail="LLM 服务未就绪")    async def generate():        try:            # 1. 发送开始事件            yield f"data: {json.dumps({'type''start'})}\n\n"            await asyncio.sleep(0.01# 让出控制权            full_response = ""            # 2. 生成并发送 token            # 注意:llm_app.stream_chat 是同步生成器,但在 FastAPI 中可以正常工作            # 如果需要完全异步,需要使用 AsyncChatOpenAI,这里为了简单保持同步调用            for token in llm_app.stream_chat(request.message, request.chat_history):                full_response += token                yield f"data: {json.dumps({'type''token''content': token})}\n\n"                await asyncio.sleep(0.01)            # 3. 发送结束事件            yield f"data: {json.dumps({'type''end''full_response': full_response})}\n\n"        except Exception as e:            error_msg = str(e)            print(f"生成错误: {error_msg}")            yield f"data: {json.dumps({'type''error''message': error_msg})}\n\n"    return StreamingResponse(generate(), media_type="text/event-stream")if __name__ == "__main__":    import uvicorn    uvicorn.run(app, host="0.0.0.0", port=8000)
至此我们基于 FastAPI 实现了 API 层。核心功能就是提供了两个 API:
1.  流式对话接口 `/api/chat/stream`
       支持 Server-Sent Events (SSE) 流式响应
       接收用户消息,实时返回 AI 生成的回复
       支持对话历史管理
2.  健康检查接口 `/api/health`
       检查服务状态
       返回 API 配置信息
## 6.3 依赖管理
为了更好地管理我们的依赖,我们可以创建一个 `./backend/requirements.txt` 文件,将使用到的依赖都设置到这个文件中:
fastapi>=0.109.0uvicorn>=0.27.0python-dotenv>=1.0.0langchain>=1.2.9langchain-openai>=0.0.5pydantic>=2.5.0
这样我们就可以进行以下方式进行安装依赖了。
# 安装依赖pip install -r requirements.txt
# 7. 前端聊天界面
先创建一个 Vue3 + TS 的前端项目,我们在根目录下执行以下命令:
npm create vite@latest frontend --template vue-ts
接下来我们主要实现以下核心功能:
1.  对话界面
       消息列表展示(用户消息 + AI 回复)
       输入框 + 发送按钮
       流式显示 AI 回复(逐字显示效果)
       加载状态提示
2.  交互功能
       发送消息(Enter 键/点击按钮)
       清空对话历史
       滚动到最新消息
./frontend/src/types/chat.ts 文件如下:
export interface Message {  idstring  role'user' | 'assistant'  contentstring  timestampnumber  streaming?: boolean  // 是否正在流式生成}export interface ChatRequest {  messagestring  chat_historyArray<{    rolestring    contentstring  }>}export interface SSEEvent {  type'start' | 'token' | 'end' | 'error'  content?: string  full_response?: string  message?: string}
./frontend/src/api/chat.ts 文件内容如下:
import type { ChatRequestSSEEvent } from '../types/chat'const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'export class ChatAPI {  /**   * 流式对话接口   */  static streamChat(    payloadChatRequest,    onToken(tokenstring) => void,    onComplete(fullResponsestring) => void,    onError(errorstring) => void  ): () => void {    // 使用 fetch API 配合 ReadableStream 来处理 POST 请求的流式响应    // 因为标准的 EventSource 不支持 POST 请求    const controller = new AbortController()    const fetchStream = async () => {      try {        const response = await fetch(`${API_BASE_URL}/api/chat/stream`, {          method'POST',          headers: {            'Content-Type''application/json',          },          bodyJSON.stringify(payload),          signal: controller.signal,        })        if (!response.ok) {          throw new Error(`HTTP error! status: ${response.status}`)        }        const reader = response.body?.getReader()        const decoder = new TextDecoder()        if (!reader) throw new Error('Response body is null')        let buffer = ''        while (true) {          const { done, value } = await reader.read()          if (done) break          const chunk = decoder.decode(value, { streamtrue })          buffer += chunk          // 处理 buffer 中的每一行          const lines = buffer.split('\n\n')          buffer = lines.pop() || '' // 保留最后一个可能不完整的块          for (const line of lines) {            if (line.startsWith('data: ')) {              const jsonStr = line.slice(6)              try {                const dataSSEEvent = JSON.parse(jsonStr)                switch (data.type) {                  case 'start':                    break                  case 'token':                    if (data.contentonToken(data.content)                    break                  case 'end':                    if (data.full_responseonComplete(data.full_response)                    return // 正常结束                  case 'error':                    onError(data.message || 'Unknown error')                    return                }              } catch (e) {                console.error('JSON parse error:', e)              }            }          }        }      } catch (errorany) {        if (error.name === 'AbortError'return        onError(error.message)      }    }    fetchStream()    // 返回取消函数    return () => controller.abort()  }  /**   * 健康检查   */  static async healthCheck() {    try {      const response = await fetch(`${API_BASE_URL}/api/health`)      return await response.json()    } catch (error) {      console.error('Health check failed', error)      return { status'error' }    }  }}
./frontend/src/composables/useChat.ts 文件内容如下:
import { ref, nextTick } from 'vue'import type { Message } from '../types/chat'import { ChatAPI } from '../api/chat'export function useChat() {  const messages = ref<Message[]>([])  const isLoading = ref(false)  const currentStreamingMessage = ref<Message | null>(null)  // 用于取消当前的请求  let cancelStream: (() => void) | null = null  /**   * 滚动到底部   */  const scrollToBottom = () => {    nextTick(() => {      const container = document.querySelector('.message-list')      if (container) {        container.scrollTo({          top: container.scrollHeight,          behavior'smooth'        })      }    })  }  /**   * 发送消息   */  const sendMessage = async (contentstring) => {    if (!content.trim() || isLoading.valuereturn    // 1. 添加用户消息    const userMessageMessage = {      idDate.now().toString(),      role'user',      content: content.trim(),      timestampDate.now()    }    messages.value.push(userMessage)    // 准备发送给后端的历史记录(去掉刚加的这一条,因为后端只要之前的)    // 或者你可以根据设计决定是否包含当前条,通常 API 设计是:新消息 + 历史    // 我们的后端设计是:message + chat_history    const historyPayload = messages.value.slice(0, -1).map(m => ({      role: m.role,      content: m.content    }))    // 2. 创建 AI 消息占位符    const aiMessageMessage = {      id: (Date.now() + 1).toString(),      role'assistant',      content'',      timestampDate.now(),      streamingtrue    }    messages.value.push(aiMessage)    currentStreamingMessage.value = aiMessage    isLoading.value = true    scrollToBottom()    // 3. 调用流式 API    cancelStream = ChatAPI.streamChat(      {        message: content.trim(),        chat_history: historyPayload      },      // onToken      (token) => {        if (currentStreamingMessage.value) {          currentStreamingMessage.value.content += token          scrollToBottom()        }      },      // onComplete      (fullResponse) => {        if (currentStreamingMessage.value) {          // 确保内容完整          if (currentStreamingMessage.value.content !== fullResponse && fullResponse) {             currentStreamingMessage.value.content = fullResponse          }          currentStreamingMessage.value.streaming = false        }        currentStreamingMessage.value = null        isLoading.value = false        cancelStream = null        scrollToBottom()      },      // onError      (error) => {        if (currentStreamingMessage.value) {          currentStreamingMessage.value.content += `\n[错误: ${error}]`          currentStreamingMessage.value.streaming = false        }        currentStreamingMessage.value = null        isLoading.value = false        cancelStream = null        scrollToBottom()      }    )  }  /**   * 清空历史   */  const clearHistory = () => {    if (cancelStream) {      cancelStream()      cancelStream = null    }    messages.value = []    isLoading.value = false    currentStreamingMessage.value = null  }  return {    messages,    isLoading,    sendMessage,    clearHistory  }}
./frontend/src/App.vue 文件内容如下:
<template>  <divclass="app-container">    <headerclass="chat-header">      <divclass="header-content">        <h1>🤖 DeepSeek 对话助手</h1>        <divclass="status-badge":class="{ online: isServerOnline }">          {{ isServerOnline ? '在线' : '离线' }}        </div>      </div>      <button @click="clearHistory"class="clear-btn"title="清空对话">        🗑️      </button>    </header>    <mainclass="message-list">      <divv-if="messages.length === 0"class="empty-state">        <p>👋 你好!我是基于 DeepSeek 的 AI 助手。</p>        <p>请在下方输入问题开始对话。</p>      </div>      <div        v-for="msg in messages"         :key="msg.id"         class="message-wrapper"        :class="msg.role"      >        <divclass="avatar">          {{ msg.role === 'user' ? '👤' : '🤖' }}        </div>        <divclass="message-content">          <divclass="bubble">            {{ msg.content }}            <spanv-if="msg.streaming"class="cursor">|</span>          </div>        </div>      </div>    </main>    <footerclass="input-area">      <divclass="input-container">        <textarea          v-model="inputContent"          placeholder="输入消息... (Enter 发送, Shift+Enter 换行)"          @keydown.enter.exact.prevent="handleSend"          :disabled="isLoading"          rows="1"          ref="textareaRef"        ></textarea>        <button          @click="handleSend"           :disabled="isLoading || !inputContent.trim()"          class="send-btn"        >          {{ isLoading ? '...' : '发送' }}        </button>      </div>    </footer>  </div></template><scriptsetuplang="ts">import { ref, onMounted, watch } from 'vue'import { useChat } from './composables/useChat'import { ChatAPI } from './api/chat'const { messages, isLoading, sendMessage, clearHistory } = useChat()const inputContent = ref('')const textareaRef = ref<HTMLTextAreaElement | null>(null)const isServerOnline = ref(false)// 检查服务器状态onMounted(async () => {  const health = await ChatAPI.healthCheck()  isServerOnline.value = health.status === 'healthy'})// 自动调整输入框高度watch(inputContent, () => {  if (textareaRef.value) {    textareaRef.value.style.height = 'auto'    textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px'  }})const handleSend = () => {  if (inputContent.value.trim() && !isLoading.value) {    sendMessage(inputContent.value)    inputContent.value = ''    // 重置高度    if (textareaRef.value) {      textareaRef.value.style.height = 'auto'    }  }}</script><style>:root {  --primary-color#4a90e2;  --bg-color#f5f7fa;  --chat-bg#ffffff;  --user-msg-bg#e3f2fd;  --bot-msg-bg#f5f5f5;  --border-color#e0e0e0;}* {  box-sizing: border-box;  margin0;  padding0;}body {  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans''Helvetica Neue', sans-serif;  background-colorvar(--bg-color);  height100vh;  overflow: hidden;}.app-container {  max-width800px;  margin0 auto;  height100%;  display: flex;  flex-direction: column;  background-colorvar(--chat-bg);  box-shadow0 0 20px rgba(0,0,0,0.05);}/* Header */.chat-header {  padding1rem;  border-bottom1px solid var(--border-color);  display: flex;  justify-content: space-between;  align-items: center;  background: white;  z-index10;}.header-content h1 {  font-size1.2rem;  color#333;}.status-badge {  font-size0.8rem;  padding2px 6px;  border-radius4px;  background#ff5252;  color: white;  display: inline-block;  margin-left8px;}.status-badge.online {  background#4caf50;}.clear-btn {  background: none;  border: none;  cursor: pointer;  font-size1.2rem;  padding5px;  border-radius50%;  transition: background 0.2s;}.clear-btn:hover {  background#f0f0f0;}/* Message List */.message-list {  flex1;  overflow-y: auto;  padding20px;  display: flex;  flex-direction: column;  gap20px;}.empty-state {  text-align: center;  margin-top50px;  color#888;}.message-wrapper {  display: flex;  gap12px;  max-width85%;}.message-wrapper.user {  align-self: flex-end;  flex-direction: row-reverse;}.avatar {  width36px;  height36px;  border-radius50%;  background#eee;  display: flex;  align-items: center;  justify-content: center;  font-size1.2rem;  flex-shrink0;}.bubble {  padding12px 16px;  border-radius12px;  line-height1.5;  white-space: pre-wrap;  word-break: break-word;}.message-wrapper.user .bubble {  backgroundvar(--user-msg-bg);  color#0d47a1;  border-radius12px 2px 12px 12px;}.message-wrapper.assistant .bubble {  backgroundvar(--bot-msg-bg);  color#333;  border-radius2px 12px 12px 12px;}.cursor {  display: inline-block;  width2px;  height1em;  background#333;  animation: blink 1s infinite;  vertical-align: middle;}@keyframes blink {  0%100% { opacity1; }  50% { opacity0; }}/* Input Area */.input-area {  padding20px;  border-top1px solid var(--border-color);  background: white;}.input-container {  display: flex;  gap10px;  align-items: flex-end;  background#f8f9fa;  padding10px;  border-radius12px;  border1px solid var(--border-color);}textarea {  flex1;  border: none;  background: transparent;  resize: none;  max-height150px;  padding8px;  font-size1rem;  font-family: inherit;  outline: none;}.send-btn {  backgroundvar(--primary-color);  color: white;  border: none;  padding8px 20px;  border-radius8px;  cursor: pointer;  font-weight600;  transition: opacity 0.2s;}.send-btn:disabled {  opacity0.5;  cursor: not-allowed;}</style>
./frontend/src/style.css 文件内容如下:
:root {  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;  line-height1.5;  font-weight400;  color-scheme: light dark;  colorrgba(2552552550.87);  background-color#242424;  font-synthesis: none;  text-rendering: optimizeLegibility;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;}body {  margin0;  display: flex;  place-items: center;  min-width320px;  min-height100vh;}#app {  width100%;  height100vh;}.container {  max-width600px;  margin0 auto;  padding2rem;  background-color#1a1a1a;  border-radius12px;  box-shadow0 4px 20px rgba(0000.3);}.header {  text-align: center;  margin-bottom2rem;}.header h1 {  font-size2rem;  color#ffffff;  margin0;}.header p {  font-size1rem;  color#bbbbbb;  margin0;}.form-group {  margin-bottom1.5rem;}.form-group label {  display: block;  margin-bottom0.5rem;  color#ffffff;  font-size0.9rem;}.form-group input {  width100%;  padding0.75rem;  border1px solid #444;  border-radius6px;  background-color#2a2a2a;  color#ffffff;  font-size1rem;}.form-group textarea {  width100%;  padding0.75rem;  border1px solid #444;  border-radius6px;  background-color#2a2a2a;  color#ffffff;  font-size1rem;  resize: vertical;}.form-group button {  width100%;  padding0.75rem;  border: none;  border-radius6px;  background-color#4caf50;  color#ffffff;  font-size1rem;  cursor: pointer;  transition: background-color 0.3s ease;}.form-group button:hover {  background-color#45a049;}.error-message {  color#ff4d4d;  font-size0.8rem;  margin-top0.5rem;  display: none;}.success-message {  color#4caf50;  font-size0.8rem;  margin-top0.5rem;  display: none;}@media (max-width600px) {  .container {    padding1rem;  }  .form-group input,  .form-group textarea {    font-size0.9rem;  }  .form-group button {    font-size0.9rem;  }}
前端比较简单,前端部分的实现就不进行详细讲解了。
# 8. 运行与验证
## 8.1 启动后端
打开一个终端窗口:
cd backend# 1. 安装依赖pip install -r requirements.txt# 2. 设置 API Key (重要!)# 编辑 .env 文件,填入你的 DeepSeek API Key# DEEPSEEK_API_KEY=sk-... # 3. 启动服务器python server.py# 服务将运行在 http://0.0.0.0:8000
## 8.2 启动前端
打开一个新的终端窗口:
cd frontend# 1. 安装依赖npm install# 2. 启动开发服务器npm run dev
访问前端地址,你就可以看到一个简洁的聊天界面。
当你输入问题并点击发送时,请求会经过: `前端` -> `FastAPI` -> `LangChain` -> `DeepSeek API` -> `返回结果`。
# 9. 总结
通过本文,我们完成了一个最小可行性产品(MVP)。从零开始搭建一个基于 Python (FastAPI) 、LangChain 和 Vue3 的全栈 LLM 聊天应用程序。
这个项目虽然简单,但它包含了一个 AI 应用的完整骨架。你可以在此基础上扩展更多功能,例如添加对话历史记忆 (Memory)  或 RAG (知识库检索) 。
接下来我将继续输出更多 AI 全栈的相关知识,欢迎大家关注本栏目。我是 Cobyte,欢迎添加 v:icobyte,学习 AI 全栈。

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-02-09 03:45:55 HTTP/2.0 GET : https://f.mffb.com.cn/a/474439.html
  2. 运行时间 : 0.140629s [ 吞吐率:7.11req/s ] 内存消耗:4,819.90kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=f0adf07747bcd5eb18dffee93c82fa40
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000379s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000552s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000276s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000289s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000532s ]
  6. SELECT * FROM `set` [ RunTime:0.000213s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000501s ]
  8. SELECT * FROM `article` WHERE `id` = 474439 LIMIT 1 [ RunTime:0.009772s ]
  9. UPDATE `article` SET `lasttime` = 1770579955 WHERE `id` = 474439 [ RunTime:0.010999s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.005785s ]
  11. SELECT * FROM `article` WHERE `id` < 474439 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000445s ]
  12. SELECT * FROM `article` WHERE `id` > 474439 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.020425s ]
  13. SELECT * FROM `article` WHERE `id` < 474439 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.015770s ]
  14. SELECT * FROM `article` WHERE `id` < 474439 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.002712s ]
  15. SELECT * FROM `article` WHERE `id` < 474439 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.004350s ]
0.142242s