(十)分层架构的多文件工程

工程化模块化拆分方案

下面将单文件 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}

四、扩展指引(后续加功能按这个来)

  1. 加新业务查询 :在 services/query_service.py 写逻辑 → agents/tools.py 封装成 Tool → Agent 自动就能调用
  2. 加新写操作 :在 services/repair_service.py 写逻辑 → agents/tools.py 封装成 Tool → 在 prompts.py 补充使用规则
  3. 加新接口 :在 api/ 下新建 router 文件 → main.py 里注册路由
  4. 改Prompt :只改 agents/prompts.py,不用动其他代码
  5. 改配置 :只改 .env 或 config/settings.py

五、拆分优势

  • 职责清晰:每个文件只做一件事,找问题改代码不用翻上千行
  • 便于协作:前端、后端、Prompt 工程师可以各改各的文件
  • 可测试性:业务服务可以单独写单元测试,不依赖 Agent 和接口
  • 易于扩展:加功能只加对应文件,不会让主文件越来越臃肿
  • 安全可控 :写操作集中在 services/repair_service.py,审计和权限控制方便
相关推荐
小小张说故事41 分钟前
Python 装饰器到底是个啥?从 @ 符号到手写一个,5 个例子讲透
后端·python
Dawson Zhu1 小时前
《Agentic Design Patterns》第 10 章导读:模型上下文协议(MCP)
人工智能·语言模型·架构·aigc·agi
乘风gg1 小时前
Spec Kit vs OpenSpec vs Superpowers:8 个 Skill 的落地实录与工程方法论
前端·ai编程·claude
haerapi1 小时前
KingbaseES 全文检索实战之前:先建立中文搜索的可解释基线
开发语言·python·全文检索
半甜柠檬1 小时前
WebCodex实战:把ChatGPT网页端接进本地项目
人工智能·ai·chatgpt·开源软件·ai编程
专业程序开发源1 小时前
SSM笔记本在线销售系统32649-计算机课程设计、毕业设计
java·spring boot·后端·python·django·php·课程设计
四六的六1 小时前
GPT-6 会自己画界面了:从写页面到定规则,前端在 Intelligent UI 时代的新活法
人工智能·个人开发·ai编程·ai大模型·组件库·ai产品·ui界面
脉动数据行情11 小时前
Python asyncio 异步实现比特币 BTC 实时行情监听 高并发版
开发语言·python·区块链
ClickHouseDB1 小时前
ClickHouse Terraform Provider 正式支持 ClickStack 资源管理
网络·数据库·python