chatbot项目01讲解

3 阅读32分钟

核心文件总览

共 4 个核心文件,分工明确:配置模型 + 工具类 (llms.py)后端 API 服务 (main.py)前端交互界面 (webUI.py)用户提示词模板 (prompt_template_user.txt) ,实现「前端输入→后端接口请求→模型推理→结果返回展示」的完整聊天流程。

各文件作用

1. llms.py - 模型初始化工具类(核心依赖)

核心作用:封装不同大模型的配置,提供统一的模型初始化接口,是整个项目的模型入口

  • 定义 4 类模型配置(openai/oneapi/qwen/ollama),包含base_url/api_key/model等关键信息;
  • 自定义异常LLMInitializationError,专门处理模型初始化失败;
  • 核心函数initialize_llm():根据传入的模型类型,创建并返回ChatOpenAI实例,做了配置校验、异常捕获;
  • 封装函数get_llm():调用初始化函数,失败时自动重试默认模型(openai),简化上层调用;
  • 支持本地开源模型(ollama/deepseek-r1)、云端模型(openai / 千问)、代理模型(oneapi)。

2. main.py - 后端 API 服务(核心枢纽)

核心作用:基于 FastAPI 搭建后端服务,暴露标准的/v1/chat/completions接口,连接模型 (llms.py)前端 (webUI.py) ,处理会话上下文、流式 / 非流式响应、请求分发。

  • 初始化配置:加载 LangSmith 跟踪(调试用)、定义服务端口 8012、读取系统 / 用户提示词模板;

  • 数据模型:用 Pydantic 定义请求 / 响应格式(和 OpenAI API 兼容),保证参数规范;

  • 会话管理:基于 LangGraph 搭建状态图,用MemorySaver实现上下文记忆,通过userId+conversationId区分不同会话;

  • 核心逻辑:

    1. 应用启动时(lifespan),通过llms.py初始化模型、创建 LangGraph 图、生成可视化图;
    2. 接收前端请求,解析用户问题,拼接系统 + 用户提示词;
    3. 支持流式响应(逐字返回)和非流式响应(一次性返回),对模型结果做格式化(分段、代码块处理);
    4. 暴露 POST 接口,供前端调用,统一返回 JSON / 流数据。
  • 辅助功能:format_response()美化模型输出,提升可读性;save_graph_visualization()生成状态图 PNG,方便调试。

3. webUI.py - 前端可视化界面(用户交互层)

核心作用:基于 Gradio 快速搭建可视化聊天界面,无需前端开发,用户可直接输入问题,展示模型回复,是用户操作入口

  • 配置:指定后端 API 地址(localhost:8012)、默认开启流式输出

  • 核心函数send_message()

    1. 接收用户输入和聊天历史,封装成后端要求的请求参数;
    2. main.py/v1/chat/completions接口发送 POST 请求;
    3. 流式接收后端返回的结果,实时更新聊天界面;
    4. 对模型返回的/标记做格式化(转为「思考过程 / 最终回复」),优化展示;
    5. 异常处理:请求失败、JSON 解析错误时,给用户友好提示;
  • 界面布局:简单的聊天窗口 + 输入框 + 发送按钮,支持回车 / 点击发送,发送后清空输入框。

4. prompt_template_user.txt - 用户提示词模板(简单配置)

核心作用:定义用户问题的固定模板,供main.py读取和拼接,方便后续统一修改用户提示词格式,解耦提示词和业务代码

  • 内容仅为用户问:{query}{query}是占位符,会被实际的用户问题替换;
  • 配合系统提示词模板(prompt_template_system.txt,代码中读取但未提供文件),共同组成发给模型的完整提示词。

4 个文件协同工作流程(完整调用链)

启动阶段(先启动后端,再启动前端)

  1. 运行main.py

    • 触发lifespan启动逻辑,调用llms.pyget_llm()初始化指定模型(默认 openai);
    • 创建 LangGraph 状态图(带上下文记忆),生成可视化图;
    • 启动 FastAPI 服务,监听0.0.0.0:8012,等待前端请求。
  2. 运行webUI.py

    • 启动 Gradio 前端,监听127.0.0.1:7860,生成可视化聊天界面;
    • 前端默认指向后端 API 地址localhost:8012,建立连接。

聊天交互阶段(用户操作→结果展示)

plaintext

用户在webUI输入问题→点击发送
    ↓
webUI.py封装请求参数(messages/stream/userId等)→向后端8012端口发送POST请求
    ↓
main.py接收请求→解析用户问题→拼接系统提示词+prompt_template_user.txt的模板(替换{query})
    ↓
main.py调用LangGraph的stream/astream方法→底层调用llms.py初始化的模型实例进行推理
    ↓
模型返回推理结果→main.py对结果格式化(分段/代码块)→按流式/非流式返回给webUI.py
    ↓
webUI.py接收后端结果→实时更新聊天界面(流式逐字展示)→对`/`标记二次格式化,展示「思考过程+最终回复」
    ↓
用户在前端看到完整回复,可继续输入下一个问题(main.py通过userId+conversationId保留上下文)

核心协同关键点

  1. 接口兼容:后端main.py实现了和 OpenAI 一致的/v1/chat/completions接口,前端webUI.py按该规范请求,解耦前后端;
  2. 模型统一入口main.py不直接写模型配置,而是通过llms.pyget_llm()获取模型,后续换模型只需改llm_type参数,无需修改业务代码;
  3. 流式响应打通:从前端stream_flag=True→后端request.stream→LangGraphastream→前端逐行接收,全链路支持流式输出;
  4. 上下文记忆main.py通过 LangGraph+MemorySaver,结合userId+conversationId实现会话记忆,前端只需传递历史,无需处理上下文逻辑。

关键亮点(地铁速记)

  1. 前后端分离,后端提供标准 API,前端快速可视化;
  2. 多模型兼容,换模型仅需修改llm_type
  3. 支持流式 / 非流式响应,上下文记忆,异常全链路捕获;
  4. 提示词解耦,格式修改无需改代码;
  5. 符合 OpenAI API 规范,后续可对接其他兼容该规范的前端 / 工具。

llm

代码整体概述

这段 Python 代码主要实现了对不同类型大语言模型(LLM)的初始化和获取功能,基于langchain_openai库的ChatOpenAI类封装,支持 OpenAI、OneAPI、通义千问(Qwen)、Ollama 四种模型类型,包含日志记录、异常处理、默认配置回退等特性。

逐行 / 逐段详细讲解

1. 导入模块与基础配置

python

运行

import os
from typing import Optional
import logging
from langchain_openai import ChatOpenAI
  • import os:导入 Python 内置的os模块,用于访问操作系统相关功能(如环境变量、系统路径)。
  • from typing import Optional:从typing模块导入Optional类型注解,用于标识变量 / 返回值可以是指定类型或None
  • import logging:导入 Python 内置的logging模块,用于记录程序运行时的日志(如信息、错误)。
  • from langchain_openai import ChatOpenAI:从langchain_openai库中导入ChatOpenAI类,该类是 LangChain 框架对 OpenAI 风格 API 的大模型客户端封装,可用于调用兼容 OpenAI 接口的 LLM。

python

运行

# 设置日志模版
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
  • logging.basicConfig(...):配置日志的基础格式和级别:

    • level=logging.INFO:设置日志默认级别为 INFO,即只输出 INFO 及更高级别(WARNING、ERROR、CRITICAL)的日志。
    • format=...:定义日志输出格式,包含%(asctime)s(日志记录时间)、%(name)s(日志器名称)、%(levelname)s(日志级别)、%(message)s(日志内容)。
  • logger = logging.getLogger(__name__):创建当前模块的日志器实例,__name__是 Python 内置变量,代表当前模块的名称,后续通过该实例记录日志。

2. 模型配置字典

python

运行

# 模型配置字典
MODEL_CONFIGS = {
    "openai": {
        "base_url": "https://api.chatanywhere.tech",
        "api_key": os.getenv("OPENAI_API_KEY"),
        "model": "gpt-5-mini"
    },
    "oneapi": {
        "base_url": "http://139.224.72.218:3000/v1",
        "api_key": "sk-ROhn6RNxulVXhlkZ0713F29093Ea49AcAcA29b96125aF1Ff",
        "model": "qwen-max"
    },
    "qwen": {
        "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "api_key": "sk-5cee351038c943648971907366eabafe",
        "model": "qwen-max"
    },
    "ollama": {
        "base_url": "http://localhost:11434/v1",
        "api_key": "ollama",
        "model": "deepseek-r1:14b"
    }
}
  • 定义字典MODEL_CONFIGS,键是模型类型(openai/oneapi/qwen/ollama),值是对应模型的配置字典,包含三个核心参数:

    • base_url:模型 API 的基础请求地址(不同平台的接口地址不同,如 Ollama 是本地地址,通义千问是阿里云兼容 OpenAI 的地址)。

    • api_key:调用模型的身份验证密钥:

      • openaiapi_key通过os.getenv("OPENAI_API_KEY")从系统环境变量中获取,避免硬编码敏感信息。
      • oneapi/qwen/ollamaapi_key直接硬编码(实际开发中不推荐,此处为示例)。
    • model:指定具体的模型版本(如gpt-5-miniqwen-maxdeepseek-r1:14b)。

3. 默认配置常量

python

运行

# 默认配置
DEFAULT_LLM_TYPE = "openai"
DEFAULT_TEMPERATURE = 0.7
  • DEFAULT_LLM_TYPE = "openai":定义默认的 LLM 类型为openai,当指定模型类型无效 / 初始化失败时,回退使用该默认值。
  • DEFAULT_TEMPERATURE = 0.7:定义模型生成文本的温度参数默认值为 0.7(温度越高,生成结果越随机;越低越精准)。

4. 自定义异常类

python

运行

class LLMInitializationError(Exception):
    """自定义异常类用于LLM初始化错误"""
    pass
  • 定义自定义异常类LLMInitializationError,继承自 Python 内置的Exception类,专门用于标识 LLM 初始化过程中出现的错误,使异常类型更具语义化。
  • 类内的文档字符串说明该异常的用途,pass表示类体无额外逻辑(仅作为异常标识)。

5. 核心函数:初始化 LLM 实例

python

运行

def initialize_llm(llm_type: str = DEFAULT_LLM_TYPE) -> Optional[ChatOpenAI]:
    """
    初始化LLM实例

    Args:
        llm_type (str): LLM类型,可选值为 'openai', 'oneapi', 'qwen', 'ollama'

    Returns:
        ChatOpenAI: 初始化后的LLM实例

    Raises:
        LLMInitializationError: 当LLM初始化失败时抛出
    """
  • 定义函数initialize_llm,功能是根据指定的llm_type初始化对应的 LLM 实例:

    • 函数参数llm_type: str = DEFAULT_LLM_TYPE:参数类型注解为字符串,默认值是DEFAULT_LLM_TYPE(即openai)。
    • 返回值注解-> Optional[ChatOpenAI]:表示返回值可以是ChatOpenAI实例或None(实际代码中成功则返回实例,失败则抛异常,None仅为类型注解的兼容)。
    • 文档字符串(docstring):说明函数功能、参数、返回值、抛出的异常类型,提升代码可读性。

python

运行

    try:
        # 检查llm_type是否有效
        if llm_type not in MODEL_CONFIGS:
            raise ValueError(f"不支持的LLM类型: {llm_type}. 可用的类型: {list(MODEL_CONFIGS.keys())}")
  • 进入try代码块,捕获初始化过程中的异常:

    • 首先检查传入的llm_type是否在MODEL_CONFIGS的键中(即是否是支持的模型类型)。
    • 如果不在,主动抛出ValueError,提示不支持的类型和可用类型列表。

python

运行

        config = MODEL_CONFIGS[llm_type]

        # 特殊处理ollama类型
        if llm_type == "ollama":
            os.environ["OPENAI_API_KEY"] = "NA"
  • MODEL_CONFIGS中取出对应llm_type的配置字典,赋值给config
  • ollama类型做特殊处理:设置系统环境变量OPENAI_API_KEY"NA"(因为 Ollama 本地部署的模型不需要真实的 OpenAI API Key,该操作是为了兼容ChatOpenAI类的参数要求)。

python

运行

        # 创建LLM实例
        llm = ChatOpenAI(
            base_url=config["base_url"],
            api_key=config["api_key"],
            model=config["model"],
            temperature=DEFAULT_TEMPERATURE,
            timeout=30,  # 添加超时配置(秒)
            max_retries=2  # 添加重试次数
        )
  • 创建ChatOpenAI类的实例llm,传入以下参数:

    • base_url:从配置字典中取base_url,指定模型 API 地址。
    • api_key:从配置字典中取api_key,用于身份验证。
    • model:从配置字典中取model,指定具体模型版本。
    • temperature:使用默认值DEFAULT_TEMPERATURE(0.7)。
    • timeout=30:设置请求超时时间为 30 秒,避免请求长时间阻塞。
    • max_retries=2:设置请求失败后的最大重试次数为 2 次,提升容错性。

python

运行

        logger.info(f"成功初始化 {llm_type} LLM")
        return llm
  • 记录 INFO 级别的日志,提示成功初始化指定类型的 LLM。
  • 返回初始化好的llm实例。

python

运行

    except ValueError as ve:
        logger.error(f"LLM配置错误: {str(ve)}")
        raise LLMInitializationError(f"LLM配置错误: {str(ve)}")
    except Exception as e:
        logger.error(f"初始化LLM失败: {str(e)}")
        raise LLMInitializationError(f"初始化LLM失败: {str(e)}")
  • 捕获ValueError(如无效模型类型):

    • 记录 ERROR 级别的日志,说明配置错误原因。
    • 抛出自定义异常LLMInitializationError,并携带错误信息。
  • 捕获其他所有异常(如网络错误、API 密钥错误等):

    • 记录 ERROR 级别的日志,说明初始化失败原因。
    • 抛出自定义异常LLMInitializationError,并携带错误信息。

6. 封装函数:获取 LLM 实例(带默认回退)

python

运行

def get_llm(llm_type: str = DEFAULT_LLM_TYPE) -> ChatOpenAI:
    """
    获取LLM实例的封装函数,提供默认值和错误处理

    Args:
        llm_type (str): LLM类型

    Returns:
        ChatOpenAI: LLM实例
    """
  • 定义封装函数get_llm,功能是获取 LLM 实例,相比initialize_llm增加了 “默认配置回退” 的逻辑:

    • 参数和返回值注解与initialize_llm基本一致,返回值明确为ChatOpenAI(无Optional,因为失败会回退默认配置)。
    • 文档字符串说明函数是封装层,提供默认值和错误处理。

python

运行

    try:
        return initialize_llm(llm_type)
    except LLMInitializationError as e:
        logger.warning(f"使用默认配置重试: {str(e)}")
        if llm_type != DEFAULT_LLM_TYPE:
            return initialize_llm(DEFAULT_LLM_TYPE)
        raise  # 如果默认配置也失败,则抛出异常
  • 尝试调用initialize_llm获取指定类型的 LLM 实例,若成功则直接返回。

  • 若捕获到LLMInitializationError(初始化失败):

    • 记录 WARNING 级别的日志,提示使用默认配置重试。
    • 检查当前失败的llm_type是否不是默认类型(DEFAULT_LLM_TYPE):如果是,则调用initialize_llm使用默认类型重试,并返回结果。
    • 如果默认类型也初始化失败(llm_type已是默认值),则执行raise重新抛出异常,终止流程。

7. 示例使用代码

python

运行

# 示例使用
"""赋值为"__main__":当你直接运行这个 Python 文件时(比如在 VS Code 里点▶️、终端执行python xxx.py)

赋值为文件名:当这个 Python 文件被其他 Python 文件导入作为模块使用时"""
if __name__ == "__main__":
    try:
        # 测试不同类型的LLM初始化
        llm_openai = get_llm("openai")
        llm_qwen = get_llm("qwen")

        # 测试无效类型
        llm_invalid = get_llm("invalid_type")
    except LLMInitializationError as e:
        logger.error(f"程序终止: {str(e)}")
  • 注释说明__name__的作用:Python 中,当文件被直接运行时,__name__等于"__main__";当文件被导入为模块时,__name__等于文件名。

  • if __name__ == "__main__"::仅当文件被直接运行时,执行以下测试代码。

  • 进入try块,测试 LLM 初始化:

    • llm_openai = get_llm("openai"):尝试获取openai类型的 LLM 实例。
    • llm_qwen = get_llm("qwen"):尝试获取qwen类型的 LLM 实例。
    • llm_invalid = get_llm("invalid_type"):尝试获取无效类型的 LLM 实例(会触发错误,进而回退到默认类型)。
  • 捕获LLMInitializationError:若所有重试(包括默认类型)都失败,记录 ERROR 级别的日志,提示程序终止及错误原因。

代码核心逻辑总结

  1. 配置层面:定义多类模型的 API 地址、密钥、版本,以及默认参数。
  2. 初始化层面:封装initialize_llm函数,校验模型类型、处理特殊模型(Ollama)、创建ChatOpenAI实例,捕获异常并抛出自定义异常。
  3. 容错层面:封装get_llm函数,在指定模型初始化失败时,自动回退到默认模型重试。
  4. 测试层面:通过if __name__ == "__main__"编写测试代码,验证不同模型类型的初始化效果。

整体代码结构清晰,兼顾了配置管理、异常处理、日志记录和容错机制,是典型的 “配置 - 初始化 - 封装 - 测试” 的工具类代码风格。

main

代码整体概述

这是一个基于 FastAPI 搭建的大模型聊天接口服务,整合了 LangGraph(状态图)、LangChain 等库实现带上下文记忆的对话功能,支持流式 / 非流式响应,还包含日志、状态图可视化、响应格式化等辅助能力,最终对外提供标准的 /v1/chat/completions 接口(对齐 OpenAI 接口格式)。

逐段详细讲解

1. 导入依赖模块

python

运行

import os
import re
import uuid
import time
import json
import logging
from contextlib import asynccontextmanager
from pydantic import BaseModel, Field
from typing import List, Optional
from langchain_core.prompts import PromptTemplate
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from llms import get_llm
from langgraph.checkpoint.memory import MemorySaver
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
import uvicorn
  • os:用于读取 / 设置环境变量;
  • re:正则表达式,用于后续响应文本的格式化处理;
  • uuid:生成唯一 ID,用于接口响应的 id 字段;
  • time:获取时间戳,用于接口响应的 created 字段;
  • json:处理 JSON 数据序列化 / 反序列化,尤其流式响应中需手动构造 JSON 字符串;
  • logging:配置日志,记录服务运行的关键信息(如请求、错误、初始化状态);
  • asynccontextmanager:创建异步上下文管理器,用于管理 FastAPI 应用的生命周期;
  • pydantic 相关(BaseModel, Field):定义数据模型,做请求 / 响应的参数校验和结构化;
  • typing 相关(List, Optional, Annotated):类型注解,约束变量 / 函数的类型,提升代码可读性和健壮性;
  • TypedDict:定义带类型的字典(状态图的状态结构);
  • langgraph 相关(StateGraph, START, END, add_messages):构建对话状态图,实现上下文记忆和流程控制;
  • PromptTemplate:LangChain 提供的提示词模板类,用于标准化提示词格式;
  • get_llm:自定义的获取大模型实例的函数(来自 llms 模块,代码中未展示实现);
  • MemorySaver:LangGraph 的内存检查点,用于存储会话上下文(内存级别的会话记忆);
  • FastAPI/HTTPException:搭建 Web 服务、抛出 HTTP 异常;
  • JSONResponse/StreamingResponse:FastAPI 的响应类,分别返回 JSON 格式响应、流式响应;
  • uvicorn:运行 FastAPI 应用的 ASGI 服务器。

2. 环境变量与日志配置

python

运行

# 设置LangSmith环境变量 进行应用跟踪,实时了解应用中的每一步发生了什么
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "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"

# 设置日志模版
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
  • 配置 LangSmith 环境变量:LANGCHAIN_TRACING_V2 开启 LangChain 的跟踪功能,LANGCHAIN_API_KEY 是访问 LangSmith 的密钥,用于监控大模型调用、状态图执行等流程;
  • 配置日志:设置日志级别为 INFO(会输出 INFO 及以上级别日志),定义日志格式(包含时间、日志器名称、级别、消息),并创建当前模块的日志器 logger

3. 提示词模板与全局配置

python

运行

# prompt模版设置相关 根据自己的实际业务进行调整
PROMPT_TEMPLATE_TXT_SYS = "prompt_template_system.txt"
with open(PROMPT_TEMPLATE_TXT_SYS, "r", encoding="utf-8") as f: sys_prompt = f.read()
PROMPT_TEMPLATE_TXT_USER = "prompt_template_user.txt"
with open(PROMPT_TEMPLATE_TXT_USER, "r", encoding="utf-8") as f: user_prompt = f.read()

# openai:调用gpt模型,oneapi:调用oneapi方案支持的模型,ollama:调用本地开源大模型,qwen:调用阿里通义千问大模型
llm_type = "openai"

# API服务设置相关
PORT = 8012

# 申明全局变量 全局调用
graph = None
  • 读取提示词模板:从 prompt_template_system.txt(系统提示词)、prompt_template_user.txt(用户提示词)文件中读取文本内容,分别赋值给 sys_promptuser_prompt,后续用于构造大模型的输入提示;
  • 定义大模型类型:llm_type 指定要调用的大模型类型(如 openai/gpt、oneapi、ollama、通义千问);
  • 定义服务端口:PORT = 8012 表示服务将运行在 8012 端口;
  • 声明全局变量 graph:用于存储后续构建的 LangGraph 状态图对象,全局复用。

4. Pydantic 模型定义(请求 / 响应结构)

python

运行

# 定义消息类,用于封装API接口返回数据
# 定义Message类,规定之后使用messages时,每个消息对象都必须包含role和content两个字段
class Message(BaseModel):
    role: str
    content: str

# 定义ChatCompletionRequest类
class ChatCompletionRequest(BaseModel):
    messages: List[Message]
    """List:表示这个字段的类型是列表
[Message]:是对列表的强约束—— 列表里不能随便塞值,每个元素都必须是定义的Message类的对象"""
    stream: Optional[bool] = False
    userId: Optional[str] = None
    conversationId: Optional[str] = None

# 定义ChatCompletionResponseChoice类
class ChatCompletionResponseChoice(BaseModel):
    index: int
    message: Message
    finish_reason: Optional[str] = None

# 定义ChatCompletionResponse类
class ChatCompletionResponse(BaseModel):
    id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}") #每次创建对象,都会生成一个全新的、不重复的 ID
    object: str = "chat.completion"
    created: int = Field(default_factory=lambda: int(time.time())) #每次创建对象,都会获取当前的系统时间,生成全新的时间戳
    choices: List[ChatCompletionResponseChoice]
    system_fingerprint: Optional[str] = None
  • Message 类:标准化消息结构,role(角色,如 user/assistant/system)和 content(消息内容)是必选字段;

  • ChatCompletionRequest 类:定义接口的请求参数结构:

    • messages:消息列表,每个元素都是 Message 实例,存储对话上下文;
    • stream:可选布尔值,标识是否需要流式响应(默认 False);
    • userId/conversationId:可选字符串,分别标识用户 ID、会话 ID,用于区分不同用户 / 不同会话的上下文;
  • ChatCompletionResponseChoice 类:定义响应中 choices 数组的元素结构:

    • index:选项索引(此处固定为 0,因为只有一个回复);
    • messageMessage 实例,存储助手的回复内容;
    • finish_reason:可选字符串,标识回复结束原因(如 stop);
  • ChatCompletionResponse 类:定义接口的响应结构(对齐 OpenAI 接口格式):

    • id:响应唯一 ID,通过 uuid.uuid4().hex 生成,default_factory 表示创建实例时自动生成;
    • object:固定值 chat.completion,标识响应类型;
    • created:响应创建时间戳,创建实例时自动获取当前时间;
    • choices:回复列表,元素为 ChatCompletionResponseChoice 实例;
    • system_fingerprint:可选字段,标识系统指纹(此处未使用)。

5. 定义 LangGraph 状态结构

python

运行

# 定义chatbot的状态
class State(TypedDict):
    messages: Annotated[list, add_messages] #实现会话上下文的连续(比如用户问 “你好”,再问 “你是谁”,模型能知道上下文);
  • State 类:继承 TypedDict,定义 LangGraph 状态图的状态结构;
  • messages 字段:类型为 list,并通过 add_messages 注解实现「消息追加」—— 每次执行状态图时,新消息会追加到 messages 列表中,从而保留对话上下文。

6. 创建 LangGraph 状态图

python

运行

# 创建和配置chatbot的状态图
def create_graph(llm) -> StateGraph:  #创建一个最简单的状态图(只有一个 chatbot 节点),把 LLM 模型封装成带内存记忆的聊天机器人,返回可执行的状态图对象
    try:
        # 构建graph
        graph_builder = StateGraph(State) #State类保存整个会话的所有关键数据;

        # 定义chatbot的node
        def chatbot(state: State) -> dict:
            # 处理当前状态并返回 LLM 响应
            return {"messages": [llm.invoke(state["messages"])]}

        # 配置graph
        graph_builder.add_node("chatbot", chatbot)
        graph_builder.add_edge(START, "chatbot")
        graph_builder.add_edge("chatbot", END)

        # 这里使用内存存储 也可以持久化到数据库
        memory = MemorySaver()

        # 编译生成graph并返回
        return graph_builder.compile(checkpointer=memory)

    except Exception as e:
        raise RuntimeError(f"Failed to create graph: {str(e)}")
  • 函数作用:接收大模型实例 llm,构建并返回一个带内存记忆的 LangGraph 状态图;

  • 步骤拆解:

    1. 初始化状态图构建器:graph_builder = StateGraph(State),绑定状态结构 State

    2. 定义 chatbot 节点函数:接收当前状态 state,调用 llm.invoke(state["messages"]) 让大模型处理上下文消息,返回包含新消息的字典(新消息会追加到状态的 messages 中);

    3. 配置状态图:

      • add_node("chatbot", chatbot):添加名为 chatbot 的节点,关联上述节点函数;
      • add_edge(START, "chatbot"):添加「起始节点 → chatbot 节点」的边,即流程从 START 开始,先执行 chatbot 节点;
      • add_edge("chatbot", END):添加「chatbot 节点 → 结束节点」的边,即 chatbot 节点执行完后流程结束;
    4. 配置内存存储:MemorySaver() 是 LangGraph 的内存检查点,用于存储不同会话的状态(上下文),也可替换为数据库持久化方案;

    5. 编译状态图:graph_builder.compile(checkpointer=memory) 生成可执行的状态图对象,返回该对象;

    6. 异常处理:捕获构建过程中的异常,包装为 RuntimeError 抛出。

7. 状态图可视化保存

python

运行

# 将构建的graph可视化保存为 PNG 文件
def save_graph_visualization(graph: StateGraph, filename: str = "graph.png") -> None:
    try:
        with open(filename, "wb") as f:
            f.write(graph.get_graph().draw_mermaid_png())
        logger.info(f"Graph visualization saved as {filename}")
    except IOError as e:
        logger.info(f"Warning: Failed to save graph visualization: {str(e)}")
  • 函数作用:将 LangGraph 状态图转换为 Mermaid 格式的 PNG 图片并保存;

  • 步骤:

    1. graph.get_graph().draw_mermaid_png():获取状态图的图形结构,生成 PNG 格式的二进制数据;
    2. 将二进制数据写入指定文件(默认 graph.png);
    3. 捕获 IO 异常(如文件写入失败),记录警告日志。

8. 响应格式化函数

python

运行

# 格式化响应,对输入的文本进行段落分隔、添加适当的换行符,以及在代码块中增加标记,以便生成更具可读性的输出
def format_response(response):
    # 使用正则表达式 \n{2, }将输入的response按照两个或更多的连续换行符进行分割。这样可以将文本分割成多个段落,每个段落由连续的非空行组成
    paragraphs = re.split(r'\n{2,}', response)
    # 空列表,用于存储格式化后的段落
    formatted_paragraphs = []
    # 遍历每个段落进行处理
    for para in paragraphs:
        # 检查段落中是否包含代码块标记
        if '```' in para:
            # 将段落按照```分割成多个部分,代码块和普通文本交替出现
            parts = para.split('```')
            for i, part in enumerate(parts):
                # 检查当前部分的索引是否为奇数,奇数部分代表代码块
                if i % 2 == 1:  # 这是代码块
                    # 将代码块部分用换行符和```包围,并去除多余的空白字符
                    parts[i] = f"\n```\n{part.strip()}\n```\n"
            # 将分割后的部分重新组合成一个字符串
            para = ''.join(parts)
        else:
            # 否则,将句子中的句点后面的空格替换为换行符,以便句子之间有明确的分隔
            para = para.replace('. ', '.\n')
        # 将格式化后的段落添加到formatted_paragraphs列表
        # strip()方法用于移除字符串开头和结尾的空白字符(包括空格、制表符 \t、换行符 \n等)
        formatted_paragraphs.append(para.strip())
    # 将所有格式化后的段落用两个换行符连接起来,以形成一个具有清晰段落分隔的文本
    return '\n\n'.join(formatted_paragraphs)
  • 函数作用:优化大模型回复的格式,提升可读性(处理段落、代码块、换行);

  • 步骤:

    1. 分割段落:用正则 \n{2,} 把响应文本按「两个及以上换行」分割成段落;

    2. 遍历处理每个段落:

      • 若段落包含代码块标记 `:按 ` 分割,奇数索引部分(代码块内容)包裹标准的代码块标记(加换行、去首尾空白),再重新拼接;
      • 若无代码块:将句点 + 空格(. )替换为句点 + 换行(.\n),让句子分行;
    3. 清理并拼接:每个段落去首尾空白后,用两个换行连接所有段落,返回格式化后的文本。

9. FastAPI 应用生命周期管理

python

运行

# 定义了一个异步函数lifespan,它接收一个FastAPI应用实例app作为参数。这个函数将管理应用的生命周期,包括启动和关闭时的操作
# 函数在应用启动时执行一些初始化操作,如加载上下文数据、以及初始化问题生成器
# 函数在应用关闭时执行一些清理操作
# @asynccontextmanager 装饰器用于创建一个异步上下文管理器,它允许你在 yield 之前和之后执行特定的代码块,分别表示启动和关闭时的操作
@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动时执行
    # 申明引用全局变量,在函数中被初始化,并在整个应用中使用
    global graph

    try:
        logger.info("正在初始化模型、定义Graph...")
        #(1)初始化LLM
        llm = get_llm(llm_type)
        #(2)定义Graph
        graph = create_graph(llm)
        #(3)将Graph可视化图保存
        save_graph_visualization(graph)
        logger.info("初始化完成")
    except Exception as e:
        logger.error(f"初始化过程中出错: {str(e)}")
        # raise 关键字重新抛出异常,以确保程序不会在错误状态下继续运行
        raise

    # yield 关键字将控制权交还给FastAPI框架,使应用开始运行
    # 分隔了启动和关闭的逻辑。在yield 之前的代码在应用启动时运行,yield 之后的代码在应用关闭时运行
    yield
    # 关闭时执行
    logger.info("正在关闭...")

# lifespan参数用于在应用程序生命周期的开始和结束时执行一些初始化或清理工作,lifespan的核心价值就是 「全局只执行一次」
app = FastAPI(lifespan=lifespan)
  • @asynccontextmanager:装饰器将 lifespan 转为异步上下文管理器,管理 FastAPI 应用的「启动 - 运行 - 关闭」全生命周期;

  • 启动阶段:

    1. 声明使用全局变量 graph
    2. 调用 get_llm(llm_type) 获取大模型实例;
    3. 调用 create_graph(llm) 构建状态图,赋值给全局 graph
    4. 调用 save_graph_visualization(graph) 保存状态图可视化图片;
    5. 捕获初始化异常,记录错误日志并重新抛出(避免应用在错误状态启动);
  • yield:交出控制权,FastAPI 应用开始接收请求;

  • 关闭阶段:yield 后的代码在应用关闭时执行,仅记录「正在关闭」日志;

  • 初始化应用:app = FastAPI(lifespan=lifespan) 绑定生命周期函数,确保初始化逻辑「全局只执行一次」。

10. 核心接口:/v1/chat/completions

python

运行

# 封装POST请求接口,与大模型进行问答
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
    # 判断初始化是否完成
    if not graph:
        logger.error("服务未初始化")
        raise HTTPException(status_code=500, detail="服务未初始化")

    try:
        logger.info(f"收到聊天完成请求: {request}")

        query_prompt = request.messages[-1].content
        logger.info(f"用户问题是: {query_prompt}")

        config = {"configurable": {"thread_id": request.userId+"@@"+request.conversationId}}
        logger.info(f"用户当前会话信息: {config}")

        prompt_template_system = PromptTemplate.from_template(sys_prompt)
        prompt_template_user = PromptTemplate.from_template(user_prompt)
        prompt = [
            {"role": "system", "content": prompt_template_system.template}, {"role": "user", "content": prompt_template_user.format(query=query_prompt)}
        ]

        # 处理流式响应
        if request.stream:
            async def generate_stream():
                chunk_id = f"chatcmpl-{uuid.uuid4().hex}"
                async for message_chunk, metadata in graph.astream({"messages": prompt}, config, stream_mode="messages"):
                    chunk = message_chunk.content
                    logger.info(f"chunk: {chunk}")
                    # 在处理过程中产生每个块
                    yield f"data: {json.dumps({'id': chunk_id,'object': 'chat.completion.chunk','created': int(time.time()),'choices': [{'index': 0,'delta': {'content': chunk},'finish_reason': None}]})}\n\n"
                # 流结束的最后一块
                yield f"data: {json.dumps({'id': chunk_id,'object': 'chat.completion.chunk','created': int(time.time()),'choices': [{'index': 0,'delta': {},'finish_reason': 'stop'}]})}\n\n"
            # 返回fastapi.responses中StreamingResponse对象
            return StreamingResponse(generate_stream(), media_type="text/event-stream")

        # 处理非流式响应处理
        else:
            try:
                events = graph.stream({"messages": prompt}, config)
                for event in events:
                    for value in event.values():
                        result = value["messages"][-1].content
            except Exception as e:
                logger.info(f"Error processing response: {str(e)}")

            formatted_response = str(format_response(result))
            logger.info(f"格式化的搜索结果: {formatted_response}")

            response = ChatCompletionResponse(
                choices=[
                    ChatCompletionResponseChoice(
                        index=0,
                        message=Message(role="assistant", content=formatted_response),
                        finish_reason="stop"
                    )
                ]
            )
            logger.info(f"发送响应内容: \n{response}")
            # 返回fastapi.responses中JSONResponse对象
            # model_dump()方法通常用于将Pydantic模型实例的内容转换为一个标准的Python字典,以便进行序列化
            return JSONResponse(content=response.model_dump())

    except Exception as e:
        logger.error(f"处理聊天完成时出错:\n\n {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))
  • 接口作用:对外提供 POST 类型的聊天接口,对齐 OpenAI 的 /v1/chat/completions 格式,支持流式 / 非流式响应;

  • 前置检查:判断全局 graph 是否初始化,未初始化则抛出 500 异常;

  • 核心逻辑(try 块内):

    1. 日志记录:打印收到的请求、用户的问题(取 messages 最后一条的 content);

    2. 构建会话配置:config 中的 thread_iduserId@@conversationId 拼接而成,用于 LangGraph 区分不同用户 / 会话的上下文;

    3. 构造提示词:

      • PromptTemplate.from_template 分别加载系统 / 用户提示词模板;
      • 拼接为 prompt 列表(system 角色 + user 角色),其中用户提示词通过 format(query=query_prompt) 填充用户问题;
    4. 流式响应处理(request.stream=True):

      • 定义异步生成器 generate_stream()
      • 调用 graph.astream(...) 异步流式执行状态图,stream_mode="messages" 表示按消息块返回;
      • 遍历每个消息块,构造 SSE(Server-Sent Events)格式的响应(data: {JSON字符串}\n\n),逐块 yield;
      • 流结束时,返回最后一个块(delta 为空,finish_reason="stop");
      • 返回 StreamingResponse,媒体类型为 text/event-stream(SSE 格式);
    5. 非流式响应处理(request.stream=False):

      • 调用 graph.stream(...) 执行状态图,遍历返回的事件,提取大模型的回复内容(value["messages"][-1].content);
      • 调用 format_response(result) 格式化回复内容;
      • 构造 ChatCompletionResponse 实例(按 Pydantic 模型结构化);
      • 调用 model_dump() 将 Pydantic 模型转为字典,通过 JSONResponse 返回;
  • 异常处理:捕获接口处理过程中的所有异常,记录错误日志并抛出 500 异常。

11. 启动服务

python

运行

if __name__ == "__main__":
    logger.info(f"在端口 {PORT} 上启动服务器")
    # uvicorn是一个用于运行ASGI应用的轻量级、超快速的ASGI服务器实现
    # 用于部署基于FastAPI框架的异步PythonWeb应用程序
    uvicorn.run(app, host="0.0.0.0", port=PORT)
  • 当脚本直接运行时(__name__ == "__main__"):

    1. 记录启动日志(端口号);
    2. 调用 uvicorn.run 启动 FastAPI 应用,host="0.0.0.0" 表示监听所有网络接口,port=PORT 绑定配置的 8012 端口。

代码核心流程总结

  1. 服务启动:通过 lifespan 初始化大模型、构建 LangGraph 状态图(带内存记忆)、保存状态图可视化;

  2. 接收请求:客户端调用 /v1/chat/completions 接口,传入对话上下文、用户 / 会话 ID、流式标识;

  3. 处理请求:

    • 构造带上下文的提示词;
    • 流式:异步流式执行状态图,逐块返回 SSE 格式响应;
    • 非流式:执行状态图,格式化回复后返回 JSON 响应;
  4. 上下文管理:通过 thread_id(userId+conversationId)和 MemorySaver 实现不同会话的上下文隔离与记忆。

webui

代码整体概述

这段代码是基于 Gradio 框架搭建的一个聊天界面,通过调用本地后端接口(http://localhost:8012/v1/chat/completions)实现和大模型的交互,支持流式输出(默认)和非流式输出两种模式,还会对模型返回的内容做特定格式化处理,并加入了日志记录便于调试。

逐行 / 逐段详细讲解

1. 导入依赖库

python

运行

import gradio as gr
import requests
import json
import logging
import re
  • import gradio as gr:导入 Gradio 库并简写为 grGradio 是用于快速构建机器学习 / Web 演示界面的 Python 库,这里用来做聊天前端。
  • import requests:导入 requests 库,用于发送 HTTP 请求,调用后端的聊天接口。
  • import json:导入 json 库,用于处理 JSON 格式的数据(后端接口请求 / 响应都是 JSON 格式)。
  • import logging:导入 logging 库,用于记录程序运行中的日志(比如请求失败、数据解析错误等),方便调试和排查问题。
  • import re:导入 re 库(正则表达式库),用于后续对模型返回的文本做替换格式化。

2. 作者注释

python

运行

# Author:@南哥AGI研习社 (B站 or YouTube 搜索“南哥AGI研习社”)
  • 这是单行注释,标注代码的作者信息,以及作者的 B 站 / YouTube 账号,仅用于说明,不影响程序运行。

3. 配置日志

python

运行

# 设置日志模版
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
  • logging.basicConfig(...):配置日志的基础格式和级别。

    • level=logging.INFO:日志级别设为 INFO,意味着会记录 INFO 及以上级别(WARN、ERROR、CRITICAL)的日志,DEBUG 级别的不会记录。
    • format=...:定义日志输出的格式,包含:%(asctime)s(日志记录时间)、%(name)s(日志器名称)、%(levelname)s(日志级别)、%(message)s(日志内容)。
  • logger = logging.getLogger(__name__):创建一个日志器实例,__name__ 代表当前模块的名称,后续通过这个 logger 来输出日志(比如 logger.info()logger.error())。

4. 定义后端接口参数

python

运行

# 后端服务接口地址
url = "http://localhost:8012/v1/chat/completions"
headers = {"Content-Type": "application/json"}
  • url:指定后端聊天接口的地址,代码中是本地 8012 端口的 /v1/chat/completions 路径(兼容 OpenAI 的接口格式)。
  • headers:定义 HTTP 请求的头部信息,Content-Type: application/json 表示请求体是 JSON 格式,后端能正确解析。

5. 定义流式输出开关

python

运行

# 默认流式输出 True or False
stream_flag = True
  • 定义布尔变量 stream_flag,默认值为 True,用来控制后续调用接口时是用流式输出(逐字 / 逐段返回)还是非流式输出(一次性返回全部内容)。

6. 核心函数:send_message(处理消息发送和响应)

python

运行

def send_message(user_message, history):
  • 定义函数 send_message,接收两个参数:

    • user_message:用户在前端输入的消息文本。
    • history:Gradio Chatbot 组件的历史对话记录(格式为列表,每个元素是 [角色, 内容],比如 [["user", "你好"], ["assistant", "您好!"]])。
6.1 封装接口请求参数

python

运行

    # 封装请求的参数
    data = {
        "messages": [{"role": "user", "content": user_message}],
        "stream": stream_flag,
        "userId": "123",
        "conversationId": "123"
    }
  • 构造传给后端接口的 JSON 数据(字典格式):

    • messages:列表,包含用户的消息对象,role: "user" 表示角色是用户,content 是用户输入的文本。
    • stream:是否流式输出,值为 stream_flag(默认 True)。
    • userId/conversationId:自定义的用户 ID 和对话 ID,后端可用来区分不同用户 / 对话(这里硬编码为 123,仅做示例)。
6.2 初始化回复状态(等待提示)

python

运行

    # 等待LLM产生token前的等待状态
    history = history + [["user", user_message], ["assistant", "正在生成回复..."]]
    yield history
  • 更新历史对话记录:把用户当前输入的消息(["user", user_message])和助手的 “正在生成回复...” 提示(["assistant", "正在生成回复..."])添加到历史记录末尾。
  • yield history:通过生成器(yield)实时返回更新后的历史记录,让前端 Chatbot 立刻显示 “正在生成回复...”,提升用户体验(而非等待后端响应后才更新)。
6.3 定义格式化函数:format_response

python

运行

    # 对deepseek-r1模型进行格式化处理
    def format_response(full_text):
        # 精确替换  和 <|FunctionCallEnd|>