流量套餐客服大模型系统 逐文件逐行超详细解析
本次解析将按文件维度,从代码逐行解读 + 参数含义 + 逻辑设计 + 技术原理 + 业务落地五个层面,把每个文件的核心代码、隐藏逻辑、设计初衷讲透,同时关联各文件间的调用关系,让你彻底理解这套带长 / 短期记忆、流式 / 非流式响应、多模型适配的大模型客服系统实现细节。
所有文件的核心调用链路:webUI.py/apiTest.py → 调用 main.py 的 8012 接口 → main.py 调用 llms.py 初始化模型 → 基于 demoWithMemory.py 的记忆逻辑处理对话 → 加载 prompt_template_*.txt 业务规则 → 完成响应并返回
一、llms.py - 全系统模型入口(底层封装,核心解耦)
核心定位:统一封装所有大模型 / 嵌入模型的初始化逻辑,为上层main.py/demoWithMemory.py提供标准化的模型实例,实现模型与业务逻辑完全解耦,切换模型仅需修改llm_type参数。
1. 导入依赖 & 日志配置
python
运行
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from typing import Optional
import logging
# 设置日志模版
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
os:用于设置环境变量(适配 Ollama 本地模型);ChatOpenAI:LangChain 封装的 OpenAI 风格聊天模型类,兼容所有实现 OpenAI 接口的大模型(本地 Ollama、OneAPI、通义千问等);OpenAIEmbeddings:向量嵌入模型类,将文本转为向量,为长期记忆的向量检索提供能力;typing.Optional:类型注解,标识参数可为空,提升代码可读性和规范性;logging:Python 内置日志库,配置时间 - 日志名 - 级别 - 信息的格式,方便调试和问题排查。
2. 模型核心配置字典
python
运行
MODEL_CONFIGS = {
"openai": {
"base_url": "https://yunwu.ai/v1", # 云雾AI的OpenAI兼容接口
"api_key": "sk-3QztQa9rwwKc9MjkgjtnK0GjTdtnVjDkEswHcX0uEm5uQB1E", # 平台API密钥
"chat_model": "gpt-4o-mini", # 聊天模型名
"embedding_model": "text-embedding-3-small" # 嵌入模型名
},
"oneapi": {
"base_url": "http://139.224.72.218:3000/v1", # 自建OneAPI服务地址
"api_key": "sk-EDjbeeCYkD1OnI9E48018a018d2d4f44958798A261137591",
"chat_model": "qwen-max", # 通义千问大模型
"embedding_model": "text-embedding-v1"
},
"qwen": {
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", # 阿里通义千问官方兼容接口
"api_key": "sk-80a72f794bc4488d85798d590e96db43",
"chat_model": "qwen-max",
"embedding_model": "text-embedding-v1"
},
"ollama": {
"base_url": "http://localhost:11434/v1", # 本地Ollama的OpenAI兼容接口
"api_key": "ollama", # Ollama无需真实密钥,任意值即可
"chat_model": "deepseek-r1:14b", # 本地部署的deepseek-r1 14B模型
"embedding_model": "nomic-embed-text:latest" # 本地嵌入模型
}
}
设计初衷:将所有模型的配置集中管理,避免硬编码在业务逻辑中,后续新增 / 修改模型仅需在该字典中添加配置,无需改动其他代码。关键特性:所有模型均实现OpenAI 接口规范,因此可通过同一个ChatOpenAI类初始化,这是多模型适配的核心基础。
3. 全局默认配置
python
运行
DEFAULT_LLM_TYPE = "openai" # 默认使用openai类型模型(云雾AI)
DEFAULT_TEMPERATURE = 0.7 # 模型温度系数:0=确定性回答,1=创造性回答,0.7兼顾稳定和灵活
temperature:大模型核心参数,控制输出的随机性,客服场景设为 0.7,既保证回答符合套餐规则,又有一定口语化灵活性。
4. 自定义异常类
python
运行
class LLMInitializationError(Exception):
"""自定义异常类用于LLM初始化错误"""
pass
设计初衷:为模型初始化错误创建专属异常,与其他通用异常(如 IOError、JSONDecodeError)区分,方便上层代码精准捕获并处理模型初始化问题,提升错误排查效率。
5. 核心初始化函数:initialize_llm
python
运行
def initialize_llm(llm_type: str = DEFAULT_LLM_TYPE) -> tuple[ChatOpenAI, OpenAIEmbeddings]:
"""
初始化LLM实例
Args:
llm_type (str): LLM类型,可选值为 'openai', 'oneapi', 'qwen', 'ollama'
Returns:
tuple: (ChatOpenAI实例, OpenAIEmbeddings实例)
Raises:
LLMInitializationError: 当LLM初始化失败时抛出
"""
try:
# 1. 校验模型类型是否合法
if llm_type not in MODEL_CONFIGS:
raise ValueError(f"不支持的LLM类型: {llm_type}. 可用的类型: {list(MODEL_CONFIGS.keys())}")
config = MODEL_CONFIGS[llm_type] # 获取对应模型配置
# 2. 特殊处理Ollama本地模型:Ollama无需真实OpenAI API密钥,强制设置为NA
if llm_type == "ollama":
os.environ["OPENAI_API_KEY"] = "NA"
# 3. 初始化聊天模型
llm = ChatOpenAI(
base_url=config["base_url"], # 模型接口地址
api_key=config["api_key"], # 接口密钥
model=config["chat_model"], # 模型名
temperature=DEFAULT_TEMPERATURE, # 温度系数
timeout=30, # 超时时间:30秒,避免模型响应过慢导致服务阻塞
max_retries=2 # 重试次数:失败后重试2次,提升服务鲁棒性
)
# 4. 初始化嵌入模型(为长期记忆的向量检索服务)
embedding = OpenAIEmbeddings(
base_url=config["base_url"],
api_key=config["api_key"],
model=config["embedding_model"],
deployment=config["embedding_model"] # 部署名,与模型名一致即可
)
logger.info(f"成功初始化 {llm_type} LLM")
return llm, embedding # 返回聊天模型+嵌入模型元组
# 捕获配置错误(如非法llm_type)
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)}")
核心作用:接收模型类型,返回可直接调用的聊天模型 + 嵌入模型实例,所有异常均封装为LLMInitializationError,上层统一捕获。关键参数:timeout和max_retries是生产级服务的必备配置,避免因模型接口问题导致整个服务挂死。
6. 封装函数:get_llm
python
运行
def get_llm(llm_type: str = DEFAULT_LLM_TYPE) -> ChatOpenAI:
"""
获取LLM实例的封装函数,提供默认值和错误重试逻辑
Args:
llm_type (str): LLM类型
Returns:
tuple: (ChatOpenAI实例, OpenAIEmbeddings实例)
"""
try:
return initialize_llm(llm_type) # 尝试初始化指定模型
except LLMInitializationError as e:
logger.warning(f"使用默认配置重试: {str(e)}")
# 若指定模型初始化失败,且不是默认模型,则重试默认的openai模型
if llm_type != DEFAULT_LLM_TYPE:
return initialize_llm(DEFAULT_LLM_TYPE)
raise # 若默认模型也失败,直接抛出异常,终止程序
设计初衷:为模型初始化添加失败重试机制,提升服务的容错性。例如:若指定ollama模型但本地未启动,自动重试openai模型,保证服务能正常运行。
7. 示例测试代码
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)}")
作用:单独运行llms.py时,可测试模型初始化逻辑是否正常,快速排查模型配置 / 网络问题。
二、demoWithMemory.py - 记忆逻辑核心 Demo(基础验证)
核心定位:实现 LangGraph 的长 / 短期记忆核心逻辑,是main.py业务化记忆逻辑的基础原型,专门用于验证记忆功能的有效性(用户隔离、会话隔离、记忆存储 / 检索),无需启动后端服务,直接运行即可测试。
1. 导入依赖
python
运行
import os
import uuid # 生成唯一标识,为长期记忆的每条记录分配唯一ID
from langgraph.store.base import BaseStore # LangGraph存储基类,用于长期记忆
from langchain_core.runnables import RunnableConfig # LangGraph配置类,传递user_id/thread_id
from langgraph.graph import StateGraph, START, END, MessagesState # LangGraph核心组件
from llms import get_llm # 导入自定义的模型初始化函数
import sys # 用于程序异常退出
from langgraph.checkpoint.memory import MemorySaver # 短期记忆存储:保存对话上下文
from langgraph.store.memory import InMemoryStore # 长期记忆存储:基于内存的向量存储
关键组件说明:
表格
| 组件 | 核心作用 |
|---|---|
StateGraph | LangGraph 状态图,定义业务执行流程(START→节点→END),是对话逻辑的载体 |
MessagesState | LangGraph 内置状态类,核心存储messages列表(对话消息),状态图的核心数据 |
MemorySaver | 短期记忆组件,基于thread_id隔离会话,保存对话的上下文状态 |
InMemoryStore | 长期记忆组件,基于向量检索,保存用户专属信息,基于user_id隔离用户 |
RunnableConfig | 配置类,传递configurable参数(user_id/thread_id),实现用户 / 会话隔离 |
2. 全局配置
python
运行
llm_type = "openai" # 测试使用的模型类型
3. 核心函数:create_graph(构建 LangGraph 状态图)
python
运行
def create_graph(llm_type: str) -> StateGraph:
try:
# 1. 初始化模型:调用llms.py的get_llm,获取聊天模型+嵌入模型
llm, embedding = get_llm(llm_type)
# 2. 初始化长期记忆存储:InMemoryStore(内存向量存储)
in_memory_store = InMemoryStore(
index={
"embed": embedding, # 嵌入模型:将文本转为向量
"dims": 1536, # 向量维度:text-embedding-3-small的维度为1536,需与嵌入模型匹配
}
)
# 3. 初始化状态图:以MessagesState为状态载体
graph_builder = StateGraph(MessagesState)
# 4. 定义短期记忆过滤函数:控制上下文长度,避免模型输入过长
def filter_messages(messages: list):
if len(messages) <= 3:
return messages
return messages[-3:] # 只保留最后3条消息,作为短期上下文(核心:限制上下文长度)
# 5. 定义状态图核心节点:chatbot(处理所有对话逻辑,含长/短期记忆)
def chatbot(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
# ---------------------- 长期记忆逻辑:存储/检索/使用 ----------------------
# 5.1 定义长期记忆的命名空间:(固定标识, user_id),基于user_id隔离不同用户的记忆
namespace = ("memories", config["configurable"]["user_id"])
# 5.2 检索长期记忆:将用户最新问题作为查询,从当前用户的命名空间中检索相关记忆
memories = store.search(namespace, query=str(state["messages"][-1].content))
# 5.3 拼接检索到的记忆:将多条记忆拼接为字符串,用于拼接到系统提示词
info = "\n".join([d.value["data"] for d in memories])
# 5.4 构建带记忆的系统提示词:将用户记忆传入,让模型“记住”用户专属信息
system_msg = f"You are a helpful assistant talking to the user. User info: {info}"
# 5.5 存储新的长期记忆:当用户消息包含“记住”关键词时,将指定信息存入向量库
last_message = state["messages"][-1] # 获取用户最新消息
if "记住" in last_message.content.lower(): # 触发条件:消息含“记住”
memory = "我的频道是南哥AGI研习社。" # 要存储的记忆内容(Demo固定值)
# 存储记忆:(命名空间, 唯一ID, 记忆数据),uuid保证每条记忆ID唯一
store.put(namespace, str(uuid.uuid4()), {"data": memory})
# ---------------------- 短期记忆逻辑:过滤上下文 ----------------------
messages = filter_messages(state["messages"]) # 只保留最后3条消息
# ---------------------- 调用大模型生成回复 ----------------------
response = llm.invoke(
[{"role": "system", "content": system_msg}] + messages # 系统提示词+过滤后的上下文
)
return {"messages": [response]} # 将模型回复存入状态,更新MessagesState
# 6. 配置状态图:添加节点+定义边
graph_builder.add_node("chatbot", chatbot) # 添加chatbot节点
graph_builder.add_edge(START, "chatbot") # 起始点→chatbot节点
graph_builder.add_edge("chatbot", END) # chatbot节点→结束点
# 7. 初始化短期记忆组件:MemorySaver
memory = MemorySaver()
# 8. 编译状态图:整合长/短期记忆,生成可调用的graph实例
# checkpointer=memory:短期记忆由MemorySaver管理
# store=in_memory_store:长期记忆由InMemoryStore管理
return graph_builder.compile(checkpointer=memory, store=in_memory_store)
except Exception as e:
raise RuntimeError(f"Failed to create graph: {str(e)}")
长记忆核心逻辑(3 步) :
- 检索:用户提问→将问题向量化→从当前用户的向量库中检索相关记忆;
- 使用:将检索到的记忆拼接到系统提示词→传入大模型;
- 存储:用户消息含 “记住”→将指定内容向量化→存入当前用户的向量库,分配唯一 UUID。短记忆核心逻辑:仅保留最后 3 条对话,平衡上下文连续性和模型推理效率,避免上下文过长导致模型卡顿 / 收费过高。
4. 辅助函数:save_graph_visualization(状态图可视化)
python
运行
def save_graph_visualization(graph: StateGraph, filename: str = "graph.png") -> None:
try:
with open(filename, "wb") as f:
# 将状态图转为Mermaid格式的PNG图片,直观查看流程
f.write(graph.get_graph().draw_mermaid_png())
print(f"Graph visualization saved as {filename}")
except IOError as e:
print(f"Warning: Failed to save graph visualization: {str(e)}")
设计初衷:LangGraph 状态图的流程可通过可视化直观展示,方便开发阶段调试,快速定位节点 / 边的配置问题。
5. 辅助函数:stream_response(处理流式响应)
python
运行
def stream_response(graph: StateGraph, user_input: str, config) -> None:
try:
# 调用graph.stream,传入用户输入和配置,获取流式响应
events = graph.stream({"messages": [{"role": "user", "content": user_input}]}, config)
# 遍历流式响应,打印模型回复
for event in events:
for value in event.values():
print("Assistant:", value["messages"][-1].content)
except Exception as e:
print(f"Error processing response: {str(e)}")
6. 主函数:main(核心测试逻辑)
python
运行
def main():
try:
# 1. 构建状态图+可视化
graph = create_graph(llm_type)
save_graph_visualization(graph)
except RuntimeError as e:
print(f"Error: {str(e)}")
sys.exit(1) # 状态图构建失败,程序退出
# 测试1:存储记忆 → user_id=1,thread_id=1
config = {"configurable": {"thread_id": "1", "user_id": "1"}}
input_message = {"role": "user", "content": "记住:我的频道是南哥AGI研习社"}
# graph.stream:非异步流式调用,stream_mode="values":只返回状态值
for chunk in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print() # 格式化打印模型回复
# 测试2:检索记忆 → 同一用户(user_id=1),不同会话(thread_id=2),验证跨会话记忆
config = {"configurable": {"thread_id": "2", "user_id": "1"}}
input_message = {"role": "user", "content": "我的频道是什么?"}
for chunk in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
# 测试3:用户隔离 → 不同用户(user_id=2),提问相同问题,验证无法检索到其他用户的记忆
config = {"configurable": {"thread_id": "3", "user_id": "2"}}
input_message = {"role": "user", "content": "我的频道是什么?"}
for chunk in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
# 测试4:交互式聊天 → user_id=4,thread_id=4,支持手动输入问题测试
print("Chatbot ready! Type 'quit', 'exit', or 'q' to end the conversation.")
config = {"configurable": {"thread_id": "4", "user_id": "4"}}
while True:
try:
user_input = input("User: ").strip()
# 退出条件
if user_input.lower() in {"quit", "exit", "q"}:
print("Goodbye!")
break
# 空输入处理
if not user_input:
print("Please enter something to chat about!")
continue
# 处理用户输入并返回回复
stream_response(graph, user_input, config)
# 捕获用户手动中断(Ctrl+C)
except KeyboardInterrupt:
print("\nInterrupted by user. Goodbye!")
break
# 捕获其他异常,触发兜底逻辑
except Exception as e:
print(f"Unexpected error: {str(e)}")
fallback_input = "What do you know about LangGraph?"
print(f"User (fallback): {fallback_input}")
stream_response(graph, fallback_input)
break
if __name__ == "__main__":
main()
4 组测试的设计目的:
- 测试 1:验证记忆存储功能,含 “记住” 关键词时能正常存入向量库;
- 测试 2:验证跨会话记忆检索,同一用户不同会话能获取到之前存储的记忆;
- 测试 3:验证用户隔离,不同用户无法获取到其他用户的记忆(核心:多用户场景的必备特性);
- 测试 4:提供交互式测试,方便手动输入任意问题验证记忆逻辑。
三、main.py - 后端核心服务(FastAPI + 业务化改造,生产级入口)
核心定位:将demoWithMemory.py的记忆逻辑业务化改造(适配流量套餐客服场景),基于 FastAPI 实现标准 OpenAI 格式的/v1/chat/completions接口,提供流式 / 非流式响应,是前端webUI.py和测试脚本apiTest.py的核心调用目标(端口 8012)。与 demoWithMemory.py 的核心区别:
- 适配流量套餐客服业务,修改系统提示词和记忆存储内容;
- 基于 FastAPI 实现异步 Web 接口,支持高并发;
- 实现标准化的请求 / 响应模型(兼容 OpenAI);
- 加载本地提示词文件(
prompt_template_*.txt),实现业务规则可配置; - 增加全局异常处理和日志精细化记录,适配生产级服务。
1. 导入依赖(在 demo 基础上新增 FastAPI 相关组件)
python
运行
import os
import re
import uuid
import time
import json
import logging
from contextlib import asynccontextmanager # FastAPI生命周期管理
from pydantic import BaseModel, Field # 数据校验与模型定义
from typing import List, Optional
from langchain_core.prompts import PromptTemplate # 提示词模板
from langgraph.store.base import BaseStore
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END, MessagesState
from llms import get_llm
from langgraph.checkpoint.memory import MemorySaver
from langgraph.store.memory import InMemoryStore
from fastapi import FastAPI, HTTPException # FastAPI核心组件
from fastapi.responses import JSONResponse, StreamingResponse # FastAPI响应类
import uvicorn # 运行FastAPI的ASGI服务器
新增关键组件说明:
表格
| 组件 | 核心作用 |
|---|---|
asynccontextmanager | 管理 FastAPI 生命周期,实现启动初始化和关闭清理 |
pydantic.BaseModel | 定义请求 / 响应的数据模型,实现自动数据校验和结构化输出 |
FastAPI | 高性能异步 Web 框架,构建 RESTful API,支持异步接口和高并发 |
HTTPException | FastAPI 异常类,返回标准化的 HTTP 错误响应(如 500/400) |
JSONResponse/StreamingResponse | 分别返回 JSON 格式响应和流式响应(SSE) |
uvicorn | ASGI 服务器,用于运行 FastAPI 异步应用,替代 Python 内置的 wsgiref |
PromptTemplate | LangChain 提示词模板类,加载本地 txt 文件中的提示词,实现业务规则可配置 |
2. 全局配置 & 环境变量
python
运行
# 设置LangSmith环境变量:开启LangChain应用跟踪,实时查看模型调用/状态图执行流程
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "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"
# 日志配置(与llms.py一致)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 提示词文件路径:业务规则写在txt文件中,无需改代码即可调整
PROMPT_TEMPLATE_TXT_SYS = "prompt_template_system.txt" # 系统提示词(套餐规则+回复风格)
PROMPT_TEMPLATE_TXT_USER = "prompt_template_user.txt" # 用户提示词模板(接收用户问题)
# 模型类型配置
llm_type = "openai"
# API服务端口
PORT = 8012
# 全局变量:存储编译后的LangGraph状态图,供所有接口调用,避免重复初始化
graph = None
LangSmith 作用:LangChain 官方的应用跟踪平台,可实时查看模型调用记录、提示词内容、状态图执行步骤、记忆检索结果,是开发和调试 LangChain/LangGraph 应用的必备工具。
3. Pydantic 数据模型(兼容 OpenAI 接口规范)
严格遵循OpenAI 的 Chat Completions 接口规范定义请求 / 响应模型,保证接口的通用性(可直接对接所有兼容 OpenAI 的前端 / 工具)。
python
运行
# 单条消息模型:对应OpenAI的Message对象
class Message(BaseModel):
role: str # 角色:user/assistant/system
content: str # 消息内容
# 接口请求模型:对应OpenAI的ChatCompletionRequest
class ChatCompletionRequest(BaseModel):
messages: List[Message] # 消息列表,至少包含1条用户消息
stream: Optional[bool] = False # 是否流式响应,默认非流式
userId: Optional[str] = None # 用户ID,用于长期记忆隔离
conversationId: Optional[str] = None # 会话ID,用于短期记忆隔离
# 响应中的选择项模型:对应OpenAI的ChatCompletionResponseChoice
class ChatCompletionResponseChoice(BaseModel):
index: int # 选择项索引,固定为0(单模型)
message: Message # 模型回复的消息
finish_reason: Optional[str] = None # 结束原因:stop=正常结束
# 接口响应模型:对应OpenAI的ChatCompletionResponse
class ChatCompletionResponse(BaseModel):
# 响应ID:自动生成,格式为chatcmpl-+UUID十六进制
id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
object: str = "chat.completion" # 固定值,标识响应类型
created: int = Field(default_factory=lambda: int(time.time())) # 响应时间戳(秒)
choices: List[ChatCompletionResponseChoice] # 选择项列表
system_fingerprint: Optional[str] = None # 系统指纹,暂不使用
Field(default_factory=...) :Pydantic 特性,每次创建实例时自动生成值,避免所有响应使用相同的 ID / 时间戳。
4. 核心函数:create_graph(业务化改造的状态图构建)
在demoWithMemory.py的基础上,针对流量套餐客服场景做了 3 处核心修改,其余逻辑一致:
python
运行
def create_graph(llm, in_memory_store) -> StateGraph:
try:
graph_builder = StateGraph(MessagesState)
# 短期记忆过滤函数(与demo一致)
def filter_messages(messages: list):
if len(messages) <= 3:
return messages
return messages[-3:]
# 核心chatbot节点(业务化改造核心)
def chatbot(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
# 长期记忆命名空间(与demo一致)
namespace = ("memories", config["configurable"]["user_id"])
memories = store.search(namespace, query=str(state["messages"][-1].content))
info = "\n".join([d.value["data"] for d in memories])
# 改造1:系统提示词适配流量套餐客服场景,将记忆用于“指定客服名字”
system_msg = f"你是一个推荐手机流量套餐的客服代表,你的名字由用户指定。用户指定你的名称为: {info}"
# 改造2:记忆存储内容适配业务,用户说“记住”时,存储“你的名字是南哥”(客服名字)
last_message = state["messages"][-1]
if "记住" in last_message.content.lower():
memory = "你的名字是南哥。" # 业务化的记忆内容,替代demo的频道信息
store.put(namespace, str(uuid.uuid4()), {"data": memory})
# 短期记忆(与demo一致)
messages = filter_messages(state["messages"])
# 调用模型(与demo一致)
response = llm.invoke(
[{"role": "system", "content": system_msg}] + messages
)
return {"messages": [response]}
# 状态图配置(与demo一致)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
memory = MemorySaver()
# 编译状态图(与demo一致)
return graph_builder.compile(checkpointer=memory, store=in_memory_store)
except Exception as e:
raise RuntimeError(f"Failed to create graph: {str(e)}")
业务化改造的核心目的:将通用的记忆 Demo,改造为专用于流量套餐推荐的客服系统,记忆功能仅用于存储 / 检索用户指定的客服名字,贴合实际业务场景。
5. 辅助函数:save_graph_visualization(与 demo 一致,略)
6. 辅助函数:format_response(响应格式化,提升可读性)
核心作用:对大模型的原始输出进行格式化处理,解决模型输出排版混乱、代码块无标记的问题,让前端展示的内容更易读。
python
运行
def format_response(response):
# 1. 按两个及以上换行符分割段落,解决段落不分隔的问题
paragraphs = re.split(r'\n{2,}', response)
formatted_paragraphs = []
# 2. 遍历每个段落进行处理
for para in paragraphs:
# 2.1 处理代码块:为代码块添加标准的```包裹,并格式化换行
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) # 重新拼接
# 2.2 处理普通文本:句点后换行,解决句子连在一起的问题
else:
para = para.replace('. ', '.\n')
# 2.3 去除段落首尾空白字符,添加到格式化列表
formatted_paragraphs.append(para.strip())
# 3. 用两个换行符连接所有格式化后的段落,返回最终结果
return '\n\n'.join(formatted_paragraphs)
处理效果:
-
原始输出:`经济套餐 50 元 10G。畅游套餐 180 元 100G。```python print (123) ````
-
格式化后:
plaintext
经济套餐50元10G。 畅游套餐180元100G。print(123)
生成代码
7. FastAPI 生命周期管理:lifespan
核心作用:实现 FastAPI 的启动初始化和关闭清理,启动时完成模型初始化、内存存储初始化、状态图构建、可视化,避免每次接口调用都重复初始化,提升性能。
python
运行
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时执行的初始化逻辑(核心:初始化全局graph变量)
global graph
try:
logger.info("正在初始化模型、定义Graph...")
# 1. 初始化模型:调用llms.py的get_llm
llm, embedding = get_llm(llm_type)
# 2. 初始化长期记忆存储:InMemoryStore
in_memory_store = InMemoryStore(
index={
"embed": embedding,
"dims": 1536,
}
)
# 3. 构建业务化的状态图
graph = create_graph(llm, in_memory_store)
# 4. 状态图可视化
save_graph_visualization(graph)
logger.info("初始化完成")
except Exception as e:
logger.error(f"初始化过程中出错: {str(e)}")
raise # 初始化失败,终止服务启动
yield # 交出控制权,FastAPI开始运行,处理接口请求
# 关闭时执行的清理逻辑(本系统无特殊清理需求,仅打印日志)
logger.info("正在关闭...")
@asynccontextmanager:异步上下文管理器,yield之前是启动逻辑,之后是关闭逻辑,是 FastAPI 推荐的生命周期管理方式(替代旧的on_startup/on_shutdown)。
8. 初始化 FastAPI 应用
python
运行
# 将lifespan传入FastAPI,启动时自动执行初始化
app = FastAPI(lifespan=lifespan)
9. 核心接口:/v1/chat/completions(POST)
全局唯一的业务接口,处理所有聊天请求,支持流式 / 非流式响应,是前端 / 测试脚本的核心调用目标,逐行解析核心逻辑:
python
运行
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
# 1. 校验服务是否初始化完成:全局graph为None则返回500错误
if not graph:
logger.error("服务未初始化")
raise HTTPException(status_code=500, detail="服务未初始化")
try:
logger.info(f"收到聊天完成请求: {request}")
# 2. 获取用户最新问题:取messages列表的最后一条(用户消息)
query_prompt = request.messages[-1].content
logger.info(f"用户问题是: {query_prompt}")
# 3. 构建LangGraph配置:实现用户/会话隔离
# thread_id:userId@@conversationId,基于用户+会话隔离短期记忆
# user_id:基于用户隔离长期记忆
config = {"configurable": {"thread_id": request.userId+"@@"+request.conversationId, "user_id": request.userId}}
logger.info(f"用户当前会话信息: {config}")
# 4. 加载本地提示词文件,构建最终的prompt
# 4.1 加载系统提示词(套餐规则+回复风格)
prompt_template_system = PromptTemplate.from_file(PROMPT_TEMPLATE_TXT_SYS)
# 4.2 加载用户提示词模板(接收用户问题)
prompt_template_user = PromptTemplate.from_file(PROMPT_TEMPLATE_USER)
# 4.3 拼接最终prompt:系统提示词 + 格式化后的用户问题
prompt = [
{"role": "system", "content": prompt_template_system.template},
{"role": "user", "content": prompt_template_user.template.format(query=query_prompt)}
]
# ---------------------- 流式响应处理(stream=True) ----------------------
if request.stream:
# 定义异步生成器:逐块返回流式响应(SSE格式)
async def generate_stream():
# 生成流式响应的唯一ID
chunk_id = f"chatcmpl-{uuid.uuid4().hex}"
# 调用graph.astream:异步流式调用,stream_mode="messages":只返回消息块
async for message_chunk, metadata in graph.astream({"messages": prompt}, config, stream_mode="messages"):
chunk = message_chunk.content # 获取单块内容
logger.info(f"chunk: {chunk}")
# 按SSE(Server-Sent Events)格式返回:data: + JSON字符串 + \n\n
yield f"data: {json.dumps({
'id': chunk_id,
'object': 'chat.completion.chunk', # 固定值,标识流式块
'created': int(time.time()),
'choices': [{
'index': 0,
'delta': {'content': chunk}, # delta:增量内容
'finish_reason': None
}]
})}\n\n"
# 流结束:返回最后一个块,delta为空,finish_reason=stop,告知前端流结束
yield f"data: {json.dumps({
'id': chunk_id,
'object': 'chat.completion.chunk',
'created': int(time.time()),
'choices': [{
'index': 0,
'delta': {}, # 空delta标识流结束
'finish_reason': 'stop'
}]
})}\n\n"
# 返回StreamingResponse:媒体类型为text/event-stream(SSE标准)
return StreamingResponse(generate_stream(), media_type="text/event-stream")
# ---------------------- 非流式响应处理(stream=False,默认) ----------------------
else:
try:
# 调用graph.stream:同步调用,获取完整响应
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}")
# 构建标准化的响应对象(基于Pydantic模型)
response = ChatCompletionResponse(
choices=[
ChatCompletionResponseChoice(
index=0,
message=Message(role="assistant", content=formatted_response),
finish_reason="stop"
)
]
)
logger.info(f"发送响应内容: \n{response}")
# 返回JSON格式响应:将Pydantic模型转为字典(model_dump())
return JSONResponse(content=response.model_dump())
# 捕获所有业务异常,返回500错误并记录日志
except Exception as e:
logger.error(f"处理聊天完成时出错:\n\n {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
流式响应核心原理:采用SSE(Server-Sent Events) 协议,前端通过一次 HTTP 请求建立长连接,后端逐块返回数据,格式为data: {JSON}\n\n,前端监听message事件即可实时渲染,比 WebSocket 更轻量,适合大模型的流式输出场景。非流式响应核心:一次性返回完整的 JSON 数据,适合对实时性要求不高的场景,处理逻辑更简单。
10. 启动服务
python
运行
if __name__ == "__main__":
logger.info(f"在端口 {PORT} 上启动服务器")
# 启动uvicorn服务器:host=0.0.0.0(允许外部访问),port=8012
uvicorn.run(app, host="0.0.0.0", port=PORT)
host=0.0.0.0:表示服务器监听所有网络接口,不仅限于本地 127.0.0.1,外部设备(如手机、另一台电脑)可通过 IP 访问该接口。
四、prompt_template_system.txt & prompt_template_user.txt - 业务规则配置(无代码,纯配置)
核心定位:将流量套餐的业务规则和模型的回复风格从代码中剥离,写在纯文本文件中,实现业务规则可配置,后续新增 / 修改套餐、调整回复风格,无需改动任何代码,仅需修改 txt 文件,极大提升运维效率。
1. prompt_template_system.txt(系统提示词,核心业务规则)
txt
你可以帮助用户选择最合适的流量套餐产品。可以选择的套餐包括:
经济套餐,月费50元,10G流量;
畅游套餐,月费180元,100G流量;
无限套餐,月费300元,1000G流量;
校园套餐,月费150元,200G流量,仅限在校生。
记住:回复时要求你使用口语,亲切一些。不用说“抱歉”。直接给出回答。NO COMMENTS. NO ACKNOWLEDGEMENTS.
核心规则拆解:
- 业务规则:明确 4 类套餐的月费 + 流量 + 限制条件(校园套餐仅限在校生);
- 回复风格:口语、亲切,避免生硬;
- 禁忌规则:不说 “抱歉”、直接回答、无多余评论、无致谢,贴合客服高效回复的场景。
2. prompt_template_user.txt(用户提示词模板,接收问题)
txt
用户问:
{query}
核心作用:为用户问题提供标准化的格式,通过{query}占位符接收用户的实际问题,与PromptTemplate.from_file配合使用,实现用户问题的结构化传入。
五、webUI.py - Gradio 可视化前端(快速使用,无需前端代码)
核心定位:基于 Gradio 快速构建轻量级可视化聊天前端,无需编写 HTML/CSS/JS 代码,一行命令启动,直接调用main.py的 8012 接口,适合开发测试和快速演示,非生产级前端(生产级可替换为 Vue/React)。
1. 导入依赖
python
运行
import gradio as gr # 快速构建可视化前端的核心库
import requests # 同步HTTP请求库,调用后端8012接口
import json
import logging
import re # 正则表达式,用于格式化模型输出
2. 全局配置
python
运行
# 日志配置(与其他文件一致)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 后端接口地址(main.py的8012接口)
url = "http://localhost:8012/v1/chat/completions"
# 请求头:固定为application/json,标识请求体为JSON格式
headers = {"Content-Type": "application/json"}
# 默认流式输出:False=非流式,可手动改为True开启流式
stream_flag = False
3. 核心函数:send_message(处理用户输入 + 调用后端 + 更新前端)
Gradio 的核心事件处理函数,用户点击 “发送” 或按回车时触发,生成器函数(yield) 用于实时更新聊天界面,逐行解析: 面,逐行解析:
python
运行
def send_message(user_message, history):
# 1. 封装后端接口的请求参数(与main.py的ChatCompletionRequest模型一致)
data = {
"messages": [{"role": "user", "content": user_message}], # 用户消息
"stream": stream_flag, # 是否流式
"userId": "123", # 固定用户ID,测试用
"conversationId": "123" # 固定会话ID,测试用
}
# 2. 初始化聊天状态:添加用户消息+“正在生成回复...”的加载状态,实时更新前端
history = history + [["user", user_message], ["assistant", "正在生成回复..."]]
yield history # 向Gradio返回更新后的聊天记录,前端实时渲染
# 3. 定义模型输出格式化函数:适配deepseek-r1模型的输出格式(含思考过程标记)
def format_response(full_text):
# 精确替换模型的思考过程标记: formatted_text = full_text
formatted_text = re.sub(r' formatted_text = re.sub(r'', '**思考过程**:\n', formatted_text)
formatted_text = re.sub(r'', '\n\n**最终回复**:\n', formatted_text)
return formatted_text.strip() # 去除首尾空白
# ---------------------- 流式输出处理(stream_flag=True) ----------------------
if stream_flag:
assistant_response = "" # 存储拼接后的模型回复
try:
# 发送流式POST请求:stream=True
with requests.post(url, headers=headers, data=json.dumps(data), stream=True) as response:
# 逐行遍历流式响应
for line in response.iter_lines():
if line:
# 解码并去除SSE的data: 前缀
json_str = line.decode('utf-8').strip("data: ")
if not json_str:
logger.info(f"收到空字符串,跳过...")
continue
# 校验JSON格式是否合法
if json_str.startswith('{') and json_str.endswith('}'):
try:
response_data = json.loads(json_str)
# 获取流式增量内容
if 'delta' in response_data['choices'][0]:
content = response_data['choices'][0]['delta'].get('content', '')
# 格式化增量内容
formatted_content = format_response(content)
logger.info(f"接收数据:{formatted_content}")
# 拼接增量内容
assistant_response += formatted_content
# 更新聊天记录:替换加载状态为拼接后的回复
updated_history = history[:-1] + [["assistant", assistant_response]]
yield updated_history # 实时返回给前端
# 流结束:检测到finish_reason=stop,退出循环
if response_data.get('choices', [{}])[0].get('finish_reason') == "stop":
logger.info(f"接收JSON数据结束")
break
# JSON解析错误处理
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}")
yield history[:-1] + [["assistant", "解析响应时出错,请稍后再试。"]]
break
else:
logger.info(f"无效JSON格式: {json_str}")
else:
logger.info(f"收到空行")
# 循环正常结束但未检测到stop,说明流不完整
else:
logger.info("流式响应结束但未明确结束")
yield history[:-1] + [["assistant", "未收到完整响应。"]]
# 请求异常处理(如网络错误、后端服务未启动)
except requests.RequestException as e:
logger.error(f"请求失败: {e}")
yield history[:-1] + [["assistant", "请求失败,请稍后再试。"]]
# ---------------------- 非流式输出处理(stream_flag=False,默认) ----------------------
else:
# 发送非流式POST请求
response = requests.post(url, headers=headers, data=json.dumps(data))
# 解析JSON响应
response_json = response.json()
# 获取模型完整回复
assistant_content = response_json['choices'][0]['message']['content']
# 格式化回复
formatted_content = format_response(assistant_content)
logger.info(f"非流式输出,格式化后的内容是: {formatted_content}")
# 更新聊天记录:替换加载状态为格式化后的回复
updated_history = history[:-1] + [["assistant", formatted_content]]
yield updated_history
核心设计:
- 加载状态:用户发送消息后,立即显示 “正在生成回复...”,提升用户体验,避免前端无响应;
- 生成器函数:通过
yield实时返回更新后的聊天记录,Gradio 会自动渲染,实现实时更新; - 格式化函数:专门适配 deepseek-r1 模型的输出格式(含思考过程标记),将标记替换为更易读的格式;
- 异常全覆盖:处理 JSON 解析错误、网络错误、流不完整等异常,保证前端不会崩溃,返回友好的错误提示。
4. 构建 Gradio 前端界面
python
运行
# 初始化Gradio的Blocks布局(比默认布局更灵活)
with gr.Blocks() as demo:
# 聊天记录组件:Chatbot,标签为“聊天对话”
chatbot = gr.Chatbot(label="聊天对话")
# 行布局:包含输入框和发送按钮
with gr.Row():
# 列布局:占比8,输入框
with gr.Column(scale=8):
message = gr.Textbox(label="请输入消息", placeholder="在此输入您的消息")
# 列布局:占比2,发送按钮
with gr.Column(scale=2):
send = gr.Button("发送")
# 绑定事件:发送按钮点击 → 调用send_message函数
send.click(send_message, [message, chatbot], chatbot)
# 绑定事件:输入框按回车 → 调用send_message函数
message.submit(send_message, [message, chatbot], chatbot)
# 绑定事件:发送按钮点击 → 清空输入框
send.click(lambda: "", None, message)
# 绑定事件:输入框按回车 → 清空输入框
message.submit(lambda: "", None, message)
界面设计逻辑:
- 采用Blocks 布局:Gradio 的高级布局方式,支持行 / 列嵌套,比默认的
gr.Interface更灵活; - 行 + 列布局:输入框占 8 份,发送按钮占 2 份,符合视觉习惯;
- 事件绑定:支持点击发送和回车发送,发送后自动清空输入框,提升操作体验。
5. 启动前端
python
运行
if __name__ == "__main__":
# 启动Gradio服务:监听127.0.0.1,端口7860
demo.launch(server_name="127.0.0.1", server_port=7860)
- 启动后访问
http://127.0.0.1:7860即可打开聊天界面,直接使用。
六、apiTest.py - 后端接口测试脚本(轻量级调试)
核心定位:无需启动 Gradio 前端,直接通过命令行测试main.py的 8012 接口,适合开发阶段的快速调试,排查接口调用、流式 / 非流式响应、记忆功能等问题,逻辑与webUI.py高度相似,更轻量化。
1. 核心逻辑(与 webUI.py 一致)
- 配置后端接口地址、请求头、流式开关;
- 定义测试输入文本(可直接修改,测试不同问题);
- 封装请求参数(固定 userId=123456,conversationId=123456);
- 分别处理流式 / 非流式响应,打印日志和响应内容;
- 捕获异常,打印错误信息。
2. 关键区别
- 无前端界面,所有结果通过日志打印到命令行;
- 无聊天记录管理,仅测试单次接口调用;
- 无输出格式化,直接打印模型原始输出;
- 测试文本可直接注释 / 取消注释,快速切换测试用例。
3. 测试用例设计
脚本中预设了 6 个测试用例,覆盖核心业务场景:
记住你的名字是南哥。→ 测试长期记忆存储;200元以下,流量大的套餐有啥?→ 测试套餐推荐核心业务;你叫什么名字?→ 测试长期记忆检索;就刚刚提到的这个套餐,是多少钱?→ 测试短期记忆(上下文理解) ;有没有豪华套餐?→ 测试未知套餐处理;- 重复上述用例 → 测试记忆的持久性。
七、各文件核心关联与完整运行流程
1. 文件核心关联
表格
| 文件名 | 依赖文件 | 核心输出 / 提供能力 |
|---|---|---|
| llms.py | 无 | 标准化的聊天模型 + 嵌入模型实例 |
| demoWithMemory.py | llms.py | 记忆逻辑 Demo,验证长 / 短期记忆有效性 |
| prompt_template_*.txt | 无 | 业务规则配置,提供提示词模板 |
| main.py | llms.py、prompt_*.txt | FastAPI 后端服务,8012 接口,业务化记忆 |
| webUI.py | main.py | Gradio 可视化前端,调用 8012 接口 |
| apiTest.py | main.py | 命令行测试脚本,调试 8012 接口 |
2. 完整运行流程(生产 / 演示场景)
-
启动后端:运行
main.py→ 初始化模型 → 构建状态图 → 启动 FastAPI 服务(8012 端口); -
启动前端:运行
webUI.py→ 启动 Gradio 服务(7860 端口); -
用户交互:打开
http://127.0.0.1:7860→ 输入问题(如 “200 元以下流量大的套餐有啥?”); -
前端调用后端:webUI.py 封装请求 → 调用
http://localhost:8012/v1/chat/completions; -
后端处理:
- 加载提示词模板,拼接系统 + 用户提示词;
- 检索用户长期记忆(如有),拼接到系统提示词;
- 过滤短期记忆(保留最后 3 条);
- 调用大模型生成回复;
- 格式化回复,按流式 / 非流式返回;
-
前端渲染:接收后端响应 → 实时更新聊天界面 → 展示模型回复。
3. 调试流程(开发阶段)
- 运行
llms.py→ 测试模型初始化是否正常; - 运行
demoWithMemory.py→ 测试长 / 短期记忆逻辑是否正常; - 运行
main.py→ 启动后端服务; - 运行
apiTest.py→ 命令行测试后端接口是否正常; - 运行
webUI.py→ 可视化测试整体流程。
八、系统核心设计亮点与生产级优化建议
1. 核心设计亮点
- 极致解耦:模型、业务逻辑、前端、测试完全分离,单一职责,便于维护和扩展;
- 接口标准化:完全兼容 OpenAI 的 Chat Completions 接口,可直接对接所有兼容 OpenAI 的前端 / 工具;
- 记忆分层:长期记忆(用户专属信息)+ 短期记忆(对话上下文),贴合人类交流习惯,支持用户 / 会话隔离;
- 可配置化:模型配置、业务规则、服务端口均通过配置 / 文件定义,无需改代码即可调整;
- 异常全覆盖:关键步骤均做异常捕获和处理,返回友好的错误提示,提升服务鲁棒性;
- 可视化:LangGraph 状态图可视化、Gradio 前端可视化,降低开发和调试成本;
- 异步高并发:FastAPI + 异步接口,支持高并发请求,适合生产级部署。
2. 生产级优化建议
(1)记忆持久化
- 长期记忆:将
InMemoryStore替换为Pinecone/Chroma/Milvus/FAISS等向量数据库,服务重启后记忆不丢失; - 短期记忆:将
MemorySaver替换为Redis/PostgreSQL等持久化存储,支持会话记录回溯; - 对话记录:将用户对话记录存入MySQL/MongoDB,支持聊天记录查询、导出、分析。
(2)服务增强
- 接口鉴权:添加 API Key/Token/JWT 鉴权,防止接口被恶意调用;
- 请求限流:添加接口限流(如 Redis + 令牌桶),防止高并发压垮服务;
- 日志持久化:将日志写入ELK/ Loki,支持日志检索、分析、告警;
- 配置中心:将模型配置、服务端口、提示词路径等放入Nacos/Apollo配置中心,无需重启服务即可修改配置;
- 容器化部署:将服务打包为Docker 镜像,配合 K8s 实现容器化部署、扩缩容、故障自愈。
(3)功能扩展
- 多轮上下文优化:将短期记忆的消息过滤逻辑改为基于 token 数的过滤(而非固定 3 条),更灵活;
- 套餐动态配置:将套餐信息从提示词移到数据库 / 配置中心,支持动态增删改查套餐;
- 用户管理:添加用户登录、注册、权限管理,支持多用户独立使用;
- 会话管理:前端添加会话列表、会话重命名、会话删除,支持多会话切换;
- 前端美化:将 Gradio 前端替换为Vue/React/Uniapp开发的专业前端,提升用户体验。
(4)性能优化
- 模型缓存:对高频问题的回复进行缓存(如 Redis),避免重复调用模型,提升响应速度;
- 嵌入模型优化:使用本地嵌入模型(如 BGE)替代云端模型,降低调用成本,提升检索速度;
- 异步优化:将所有同步操作(如文件读取、数据库操作)改为异步,提升服务并发能力;
- 模型量化:若使用本地模型,对模型进行4/8bit 量化,降低显存占用,提升推理速度。
这套代码是大模型落地实际业务(客服) 的优秀原型,整合了当前主流的大模型开发技术(LangGraph/LangChain、FastAPI、向量检索),结构清晰、解耦性好,经过上述优化后,可直接用于生产级部署。