工程化模块化拆分方案
下面将单文件 main.py 拆分为分层架构的多文件工程,遵循「单一职责、分层解耦」原则,后续加功能、改逻辑只需要找对应文件,维护成本大幅降低。
一、最终项目目录结构
bus_repair_ai/ # 项目根目录
├── .env # 环境变量配置
├── main.py # 项目启动入口(仅做启动和装配)
├── config/ # 配置层
│ └── settings.py # 全局配置、常量、环境加载
├── database/ # 数据库层
│ └── db.py # 数据库连接(只读/写权限分离)
├── services/ # 业务逻辑层
│ ├── query_service.py # 只读业务查询逻辑
│ └── repair_service.py # 维修记录写入逻辑
├── agents/ # Agent 核心层
│ ├── prompts.py # 所有 Prompt 模板集中管理
│ ├── tools.py # LangChain Tool 工具封装
│ └── agent_builder.py # Agent 构建与初始化
├── api/ # 接口层
│ ├── __init__.py
│ ├── chat_router.py # 智能对话接口
│ ├── file_router.py # Excel 下载接口
│ └── health_router.py # 健康检查接口
└── utils/ # 通用工具层
├── session.py # 多轮会话管理
└── excel_exporter.py # Excel 生成与文件管理
二、各文件完整代码
1. 配置层 config/settings.py
集中管理所有配置、常量、环境变量,整个项目统一从这里取配置。
python
import os
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# 数据库配置
MYSQL_URI = os.getenv("MYSQL_URI", "")
WRITE_MYSQL_URI = os.getenv("WRITE_MYSQL_URI", MYSQL_URI)
# 大模型配置
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "[https://api.openai.com/v1](https://api.openai.com/v1)")
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
# Excel 临时文件配置
TEMP_EXCEL_DIR = "temp_excel_files"
EXCEL_EXPIRE_HOURS = 1
# 会话配置
MAX_HISTORY_ROUNDS = 20
# 允许的表
QUERY_INCLUDE_TABLES = ["bus_vehicle", "bus_fleet", "repair_order"]
2. 数据库层 database/db.py
数据库连接单独封装,读写权限分离,统一管理。
python
from langchain_community.utilities import SQLDatabase
from config.settings import MYSQL_URI, WRITE_MYSQL_URI, QUERY_INCLUDE_TABLES
# 只读数据库实例(用于查询、Text-to-SQL)
_read_db = None
# 写权限数据库实例(仅用于新增记录)
_write_db = None
def get_read_db() -> SQLDatabase:
"""获取只读数据库连接(单例)"""
global _read_db
if not _read_db:
_read_db = SQLDatabase.from_uri(
MYSQL_URI,
include_tables=QUERY_INCLUDE_TABLES,
sample_rows_in_table_info=2,
view_support=False
)
return _read_db
def get_write_db() -> SQLDatabase:
"""获取写权限数据库连接(单例,最小权限)"""
global _write_db
if not _write_db:
_write_db = SQLDatabase.from_uri(
WRITE_MYSQL_URI,
include_tables=["repair_order"],
view_support=False
)
return _write_db
3. 业务逻辑层 services/query_service.py
所有只读业务查询逻辑都放这里,和 Agent 解耦。
python
from typing import List, Dict
def get_fleet_vehicles(company_name: str, fleet_name: str) -> List[Dict]:
"""查询指定分公司、指定车队的车辆清单"""
# 真实场景替换为数据库查询
return [
{"车辆自编号": "11901", "车牌号": "沪A12345", "车型": "宇通ZK6125", "状态": "运营中"},
{"车辆自编号": "11902", "车牌号": "沪A12346", "车型": "宇通ZK6125", "状态": "维修中"},
{"车辆自编号": "11903", "车牌号": "沪A12347", "车型": "申龙SLK6119", "状态": "运营中"},
]
def get_vehicle_repair_history(bus_no: str) -> List[Dict]:
"""查询指定车辆的历史维修记录"""
return [
{"工单号": "R202609001", "故障类型": "电机故障", "维修时间": "2026-09-01", "维修内容": "更换轴承"},
{"工单号": "R202608015", "故障类型": "电路故障", "维修时间": "2026-08-15", "维修内容": "更换线束"},
]
def get_fleet_monthly_stats(fleet_name: str, month: str) -> Dict:
"""查询指定车队某月的维修统计"""
return {
"车队名称": fleet_name,
"统计月份": month,
"总工单数量": 28,
"已完成工单": 25,
"故障类型TOP3": "电机系统、底盘系统、电气系统",
"平均维修工时(小时)": 4.2
}
4. 业务逻辑层 services/repair_service.py
所有写操作业务逻辑集中管理,安全校验也在这里做。
python
from datetime import datetime
from database.db import get_write_db
def add_repair_record(
bus_no: str,
fault_type: str,
repair_content: str,
repair_time: str = None,
handler: str = None,
remark: str = None
) -> str:
"""
新增一条车辆维修记录
返回:成功/失败信息
"""
try:
db = get_write_db()
# 默认当前时间
if not repair_time:
repair_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 参数化插入,防注入
sql = """
INSERT INTO repair_order (vehicle_id, fault_type, repair_content, order_time, handler, remark, status)
SELECT vehicle_id, :fault_type, :repair_content, :order_time, :handler, :remark, 2
FROM bus_vehicle
WHERE bus_no = :bus_no
"""
params = {
"bus_no": bus_no,
"fault_type": fault_type,
"repair_content": repair_content,
"order_time": repair_time,
"handler": handler,
"remark": remark
}
db.run(sql, parameters=params)
return f"✅ 维修记录添加成功!\n车辆:{bus_no}\n故障类型:{fault_type}\n维修时间:{repair_time}"
except Exception as e:
return f"❌ 添加失败:{str(e)}\n请检查车辆编号是否正确,或联系管理员。"
5. 通用工具层 utils/session.py
会话历史管理独立封装。
python
from config.settings import MAX_HISTORY_ROUNDS
# 会话存储器
_session_store = {}
def get_session_history(session_id: str) -> list:
"""获取会话历史,不存在则新建"""
if session_id not in _session_store:
_session_store[session_id] = []
return _session_store[session_id]
def append_history(session_id: str, user_question: str, ai_answer: str):
"""追加一轮对话,自动截断旧记录"""
history = get_session_history(session_id)
history.append(f"用户:{user_question}")
history.append(f"助手:{ai_answer}")
# 保留最近 N 轮
if len(history) > MAX_HISTORY_ROUNDS * 2:
_session_store[session_id] = history[-MAX_HISTORY_ROUNDS * 2:]
def clear_session(session_id: str):
"""清空指定会话"""
if session_id in _session_store:
del _session_store[session_id]
6. 通用工具层 utils/excel_exporter.py
Excel 导出、文件清理独立封装。
python
import os
import uuid
from datetime import datetime, timedelta
from config.settings import TEMP_EXCEL_DIR, EXCEL_EXPIRE_HOURS
# 确保目录存在
os.makedirs(TEMP_EXCEL_DIR, exist_ok=True)
def clean_expired_files():
"""清理过期的临时Excel文件"""
try:
cutoff = datetime.now() - timedelta(hours=EXCEL_EXPIRE_HOURS)
for filename in os.listdir(TEMP_EXCEL_DIR):
file_path = os.path.join(TEMP_EXCEL_DIR, filename)
if os.path.isfile(file_path):
mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
if mtime < cutoff:
os.remove(file_path)
except:
pass
def generate_excel(data_list: list, file_title: str = "数据导出") -> tuple:
"""
生成Excel文件
返回:(file_id, file_name, download_url)
"""
import openpyxl
clean_expired_files()
file_id = str(uuid.uuid4())
file_name = f"{file_title}_{datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"
file_path = os.path.join(TEMP_EXCEL_DIR, f"{file_id}.xlsx")
# 创建工作簿
wb = openpyxl.Workbook()
ws = wb.active
ws.title = file_title[:31] if len(file_title) <= 31 else "数据"
# 写入表头
headers = list(data_list[0].keys())
ws.append(headers)
# 写入数据
for row in data_list:
ws.append([str(row.get(h, "")) for h in headers])
# 自动调整列宽
for col in ws.columns:
max_len = 0
col_letter = col[0].column_letter
for cell in col:
try:
if len(str(cell.value)) > max_len:
max_len = len(str(cell.value))
except:
pass
ws.column_dimensions[col_letter].width = min(max(max_len + 2, 8), 50)
wb.save(file_path)
download_url = f"[http://localhost:8000/api/download/{file_id}?filename={file_name}](http://localhost:8000/api/download/{file_id}?filename={file_name})"
return file_id, file_name, download_url
def get_excel_path(file_id: str) -> str:
"""获取Excel文件本地路径"""
return os.path.join(TEMP_EXCEL_DIR, f"{file_id}.xlsx")
7. Agent 层 agents/prompts.py
所有 Prompt 集中管理,修改文案不用碰业务代码。
python
# 总调度 Agent 系统提示词
SYSTEM_PROMPT = """
你是公交机务智能助手,支持查询数据、导出Excel、新增维修记录。
【核心规则】
1. 查询类优先用业务工具,满足不了再用 sql_database_query
2. 写操作只能用 add_repair_record 工具,绝对不能用SQL生成写操作
3. 新增维修记录必须严格按流程来,禁止跳过确认直接提交
【新增维修记录流程】
用户说要加维修记录、登记维修、新增工单时,按以下步骤执行:
1. 先检查对话历史里已经有哪些字段信息
2. 每次只问一个缺失的必填字段,一问一答,不要一次问多个
3. 必填字段:车辆自编号(bus_no)、故障类型(fault_type)、维修内容(repair_content)
可选字段:维修时间、处理人、备注
4. 故障类型只能是:电机系统、底盘系统、电气系统、车身系统,不对就提示用户选择
5. 所有必填字段收集完后,向用户复述一遍全部信息,确认无误后再调用 add_repair_record
6. 如果用户说"确认"、"没问题"、"提交",再调用工具写入
7. 写入成功后告知用户结果,失败就说明原因
【导出Excel规则】
先查询数据,再用 export_to_excel 导出,返回下载链接
【回答要求】
1. 自然口语化,不要太生硬
2. 不要暴露工具、SQL、接口等技术术语
3. 不确定就问用户,不要猜测
"""
8. Agent 层 agents/tools.py
所有 LangChain Tool 统一封装,只依赖业务服务层。
python
import ast
from langchain_core.tools import tool
from services.query_service import get_fleet_vehicles, get_vehicle_repair_history, get_fleet_monthly_stats
from services.repair_service import add_repair_record
from database.db import get_read_db
from langchain_community.agent_toolkits import create_sql_agent
from langchain_openai import ChatOpenAI
from config.settings import MODEL_NAME, OPENAI_API_KEY, OPENAI_BASE_URL
from utils.excel_exporter import generate_excel
# ========== 业务查询工具 ==========
@tool
def query_fleet_vehicles(company_name: str, fleet_name: str) -> str:
"""
查询指定分公司和车队的车辆清单,返回车辆数据列表。
参数:
- company_name: 分公司名称,比如"第一运营分公司"、"一公司"
- fleet_name: 车队名称,比如"119车队"
当用户查询某个车队的车辆清单、车辆列表时,优先使用此工具。
"""
if company_name in ["一公司", "第一分公司"]:
company_name = "第一运营分公司"
result = get_fleet_vehicles(company_name, fleet_name)
return str(result)
@tool
def query_vehicle_repair_history(bus_no: str) -> str:
"""
查询指定车辆的历史维修记录,返回维修记录列表。
参数:
- bus_no: 车辆自编号,比如"11901"
当用户查询某辆车的维修记录、维修历史、修过什么时,优先使用此工具。
"""
result = get_vehicle_repair_history(bus_no)
return str(result)
@tool
def query_fleet_monthly_stats(fleet_name: str, month: str) -> str:
"""
查询指定车队某个月的维修统计数据。
参数:
- fleet_name: 车队名称,比如"119车队"
- month: 月份,格式为YYYY-MM,比如"2026-09"
当用户查询车队月度统计、维修数据、工单数量时,优先使用此工具。
"""
result = get_fleet_monthly_stats(fleet_name, month)
return str(result)
# ========== SQL 灵活查询工具 ==========
@tool
def sql_database_query(query: str) -> str:
"""
通过自然语言灵活查询公交维修数据库,支持筛选、统计、关联查询。
当没有现成的业务工具可以满足需求时,使用此工具。
参数:
- query: 用户的自然语言查询问题
注意:只能查询,不能修改、删除、新增数据。
"""
llm = ChatOpenAI(
model=MODEL_NAME,
api_key=OPENAI_API_KEY,
base_url=OPENAI_BASE_URL,
temperature=0
)
db = get_read_db()
agent = create_sql_agent(
llm=llm,
db=db,
agent_type="openai-tools",
verbose=False,
max_iterations=10,
handle_parsing_errors=True
)
result = agent.invoke({"input": query})
return result["output"]
# ========== Excel 导出工具 ==========
@tool
def export_to_excel(data: str, file_title: str = "数据导出") -> str:
"""
将查询到的数据导出为Excel文件,返回下载链接。
参数:
- data: 要导出的数据,必须是Python列表字典格式的字符串
- file_title: 导出文件的名称
当用户要求导出数据、下载Excel、生成报表时,使用此工具。
使用步骤:先调用查询工具获取数据,再调用此工具导出。
"""
try:
data_list = ast.literal_eval(data.strip())
except Exception as e:
return f"数据解析失败,无法导出:{str(e)}"
if not isinstance(data_list, list) or len(data_list) == 0:
return "没有有效数据可导出"
if not isinstance(data_list[0], dict):
return "数据格式错误,必须是字典列表格式"
try:
_, _, download_url = generate_excel(data_list, file_title)
return f"Excel文件已生成,点击下载:{download_url}\n文件有效期1小时,请及时保存。"
except Exception as e:
return f"生成Excel失败:{str(e)}"
# ========== 新增维修记录工具 ==========
@tool
def add_repair_record_tool(
bus_no: str,
fault_type: str,
repair_content: str,
repair_time: str = None,
handler: str = None,
remark: str = None
) -> str:
"""
新增一条车辆维修记录到数据库。
参数:
- bus_no: 车辆自编号,必填,比如"11901"
- fault_type: 故障类型,必填,可选值:电机系统、底盘系统、电气系统、车身系统
- repair_content: 维修内容描述,必填
- repair_time: 维修时间,可选,格式YYYY-MM-DD HH:MM:SS,不传默认当前时间
- handler: 处理人,可选
- remark: 备注,可选
【使用规则】
1. 必须所有必填参数都收集完整才能调用
2. 调用前必须向用户复述信息并确认
3. 故障类型必须是可选值范围内的
"""
return add_repair_record(
bus_no=bus_no,
fault_type=fault_type,
repair_content=repair_content,
repair_time=repair_time,
handler=handler,
remark=remark
)
# 所有工具列表
ALL_TOOLS = [
query_fleet_vehicles,
query_vehicle_repair_history,
query_fleet_monthly_stats,
sql_database_query,
export_to_excel,
add_repair_record_tool
]
9. Agent 层 agents/agent_builder.py
Agent 构建器,统一初始化,对外暴露实例。
python
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate
from config.settings import MODEL_NAME, OPENAI_API_KEY, OPENAI_BASE_URL
from agents.prompts import SYSTEM_PROMPT
from agents.tools import ALL_TOOLS
# Agent 单例
_agent_executor = None
def build_agent() -> AgentExecutor:
"""构建并返回 Agent 执行器(单例)"""
global _agent_executor
if _agent_executor:
return _agent_executor
llm = ChatOpenAI(
model=MODEL_NAME,
api_key=OPENAI_API_KEY,
base_url=OPENAI_BASE_URL,
temperature=0.3
)
prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM_PROMPT),
("user", "{input}"),
("agent_scratchpad", "{agent_scratchpad}"),
])
agent = create_openai_tools_agent(llm, ALL_TOOLS, prompt)
_agent_executor = AgentExecutor(
agent=agent,
tools=ALL_TOOLS,
verbose=True,
max_iterations=15,
handle_parsing_errors=True
)
return _agent_executor
10. 接口层 api/health_router.py
python
from fastapi import APIRouter
router = APIRouter(tags=["系统"])
@router.get("/health", summary="健康检查")
async def health_check():
return {"code": 200, "message": "服务运行正常"}
11. 接口层 api/chat_router.py
python
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from agents.agent_builder import build_agent
from utils.session import get_session_history, append_history
router = APIRouter(prefix="/api", tags=["智能对话"])
class ChatRequest(BaseModel):
session_id: str
question: str
class ChatResponse(BaseModel):
code: int = 200
message: str = "success"
data: str = ""
@router.post("/chat", summary="智能对话接口(支持多轮)", response_model=ChatResponse)
async def chat(request: ChatRequest):
agent = build_agent()
if not agent:
raise HTTPException(status_code=500, detail="Agent未初始化")
session_id = request.session_id.strip()
question = request.question.strip()
if not session_id:
raise HTTPException(status_code=400, detail="session_id不能为空")
if not question:
raise HTTPException(status_code=400, detail="问题不能为空")
try:
# 拼接历史对话
history = get_session_history(session_id)
history_text = "\n".join(history) if history else ""
full_input = f"""
【对话历史】
{history_text}
【当前用户说】
{question}
"""
# 调用 Agent
result = agent.invoke({"input": full_input})
answer = result["output"]
# 保存历史
append_history(session_id, question, answer)
return ChatResponse(data=answer)
except Exception as e:
raise HTTPException(status_code=500, detail=f"处理失败:{str(e)}")
12. 接口层 api/file_router.py
python
import os
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from utils.excel_exporter import get_excel_path
router = APIRouter(prefix="/api", tags=["文件下载"])
@router.get("/download/{file_id}", summary="下载Excel文件")
async def download_excel(file_id: str, filename: str = "数据导出.xlsx"):
file_path = get_excel_path(file_id)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="文件不存在或已过期")
return FileResponse(
path=file_path,
filename=filename,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
13. 主入口 main.py
精简到只做启动、装配、生命周期管理。
python
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.health_router import router as health_router
from api.chat_router import router as chat_router
from api.file_router import router as file_router
from database.db import get_read_db
from agents.agent_builder import build_agent
@asynccontextmanager
async def lifespan(app: FastAPI):
"""服务启动时预热初始化"""
print("正在初始化数据库连接...")
get_read_db()
print("正在构建智能Agent...")
build_agent()
print("✅ 服务启动完成!")
yield
print("服务已关闭")
# 创建应用
app = FastAPI(
title="公交机务智能查询系统",
description="支持查询、导出、新增维修记录的智能对话系统",
version="2.2.0",
lifespan=lifespan
)
# 跨域配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(health_router)
app.include_router(chat_router)
app.include_router(file_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
三、运行方式
1. 启动命令
和之前完全一致,功能 100% 兼容:
bash
python main.py
# 或者
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
2. 接口地址不变
- 文档页:
[http://localhost:8000/docs](http://localhost:8000/docs) - 对话接口:
POST /api/chat - 下载接口:
GET /api/download/{file_id}
四、扩展指引(后续加功能按这个来)
- 加新业务查询 :在
services/query_service.py写逻辑 →agents/tools.py封装成 Tool → Agent 自动就能调用 - 加新写操作 :在
services/repair_service.py写逻辑 →agents/tools.py封装成 Tool → 在prompts.py补充使用规则 - 加新接口 :在
api/下新建 router 文件 →main.py里注册路由 - 改Prompt :只改
agents/prompts.py,不用动其他代码 - 改配置 :只改
.env或config/settings.py
五、拆分优势
- 职责清晰:每个文件只做一件事,找问题改代码不用翻上千行
- 便于协作:前端、后端、Prompt 工程师可以各改各的文件
- 可测试性:业务服务可以单独写单元测试,不依赖 Agent 和接口
- 易于扩展:加功能只加对应文件,不会让主文件越来越臃肿
- 安全可控 :写操作集中在
services/repair_service.py,审计和权限控制方便