(七)「固化 Rest 接口 + Text-to-SQL 灵活查询」双模式 Agent 架构教程

「固化Rest接口 + Text-to-SQL灵活查询」双模式Agent架构教程

这个方案非常适合企业级场景:高频、固定、安全敏感的查询做成固化Rest接口,保证性能、稳定和可控;临时、灵活、探索性的查询走Text-to-SQL,保证灵活性。Agent 自动判断该用哪种方式,前端只需要发自然语言,完全感知不到底层差异。

下面从架构设计、划分标准、工具封装、完整代码、优化技巧五个维度完整讲解,全程复用你之前的FastAPI+LangChain技术栈,可直接落地。


一、整体架构设计

核心逻辑

用户只需要一个自然语言问答入口,Agent 内部自动决策:

复制代码
用户自然语言提问
    ↓
Agent 理解需求
    ├─ 有现成的固化接口能满足 → 调用你写好的Rest接口 → 返回结构化结果 → 整理成自然语言回答
    └─ 没有对应接口/需求太灵活 → 自己生成SQL → 查询MySQL → 返回结果 → 整理成自然语言回答

核心优势

维度 固化Rest接口 Text-to-SQL灵活查询
性能 极高,预定义SQL,可加缓存 中等,实时生成执行
准确性 100%可控,逻辑固定 较高,依赖模型能力
安全性 极强,参数化查询,无注入风险 较好,依赖只读账号+校验
灵活性 低,改需求要改接口 极高,随便问什么
开发成本 每个接口都要开发 一次搭建,无限查询

最佳实践原则

80%的高频常用查询固化成接口,20%的临时灵活查询走Text-to-SQL,兼顾效率、成本和灵活性。


二、第一步:划分哪些查询做固化接口

划分标准

✅ 适合固化成Rest接口的场景
  1. 高频查询:每天被大量调用的基础查询
  2. 参数固定:查询条件、返回字段都是固定的
  3. 安全敏感:涉及核心数据、权限管控的查询
  4. 性能要求高:要求毫秒级返回的查询
  5. 逻辑复杂:多表关联、复杂计算、业务规则多的查询
❌ 适合走Text-to-SQL的场景
  1. 临时查询:运营、运维临时拍脑袋的问题
  2. 多维度筛选:条件组合多变,无法枚举
  3. 探索分析:数据统计、趋势分析、排名类问题
  4. 不常用查询:一个月用不了几次的查询

公交维修场景示例(可直接套用)

固化Rest接口(你提前写好) 走Text-to-SQL(灵活生成)
查询指定车队的车辆清单 一公司上个月电机故障最多的3辆车
查询指定车辆的维修记录 近30天哪个车队故障率最高
查询车队月度工单统计 2026年购置的车辆维修率是多少
查询故障类型TOP10排名 车牌尾号为5的车一共有多少维修工单
查询待修工单列表 一公司和二公司的维修工时对比

三、第二步:把你的Rest接口封装成Agent可用的工具

LangChain 里的 Tool(工具) 就是 Agent 可以调用的"外挂能力"。你只需要把写好的Rest接口,按照LangChain的规范包装成Tool,Agent就能看懂并自动调用。

两种封装方式(根据你的部署选择)

方式A:本地函数封装(推荐,接口和Agent在同一个服务)

如果你的业务接口和Agent服务写在同一个FastAPI项目里,直接把业务函数封装成Tool,不用发HTTP请求,性能最好。

方式B:HTTP远程调用封装(接口是独立服务)

如果你的业务接口是单独的服务(比如Java写的后端接口),就用requests发HTTP调用,封装成Tool。


四、完整可运行代码示例

下面的代码在你之前的main.py基础上扩展,包含:

  1. 3个示例固化业务Rest接口(你可以换成自己的真实接口)
  2. 把业务接口封装成Agent工具
  3. 保留Text-to-SQL能力
  4. Agent自动判断调用哪种方式
  5. 统一的自然语言问答接口
python 复制代码
from dotenv import load_dotenv
import os
from contextlib import asynccontextmanager
from typing import List, Dict

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from langchain_community.utilities import SQLDatabase
from langchain_community.agent_toolkits import create_sql_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate

load_dotenv()

# 全局变量
sql_agent = None
bus_tools = []
agent_executor = None

# ===================== 1. 模拟你写好的业务Rest接口(替换成你的真实接口) =====================
# 这里用本地函数模拟,真实场景就是你的业务查询逻辑
# 实际生产中,这些就是你提前写好的、经过测试的、高性能的数据库查询接口

def get_fleet_vehicles(company_name: str, fleet_name: str) -> List[Dict]:
    """
    查询指定分公司、指定车队的车辆清单(固化接口示例1)
    真实场景这里就是你写的SQL查询,或者调用你的业务服务
    """
    # 模拟返回,真实场景替换成你的数据库查询
    return [
        {"bus_no": "11901", "plate_no": "沪A12345", "model": "宇通ZK6125", "status": "运营中"},
        {"bus_no": "11902", "plate_no": "沪A12346", "model": "宇通ZK6125", "status": "维修中"},
        {"bus_no": "11903", "plate_no": "沪A12347", "model": "申龙SLK6119", "status": "运营中"},
    ]

def get_vehicle_repair_history(bus_no: str) -> List[Dict]:
    """
    查询指定车辆的历史维修记录(固化接口示例2)
    """
    return [
        {"order_id": "R202609001", "fault_type": "电机故障", "repair_time": "2026-09-01", "content": "更换轴承"},
        {"order_id": "R202608015", "fault_type": "电路故障", "repair_time": "2026-08-15", "content": "更换线束"},
    ]

def get_fleet_monthly_stats(fleet_name: str, month: str) -> Dict:
    """
    查询指定车队某月的维修统计(固化接口示例3)
    month格式:YYYY-MM,如2026-09
    """
    return {
        "fleet_name": fleet_name,
        "month": month,
        "total_orders": 28,
        "completed_orders": 25,
        "fault_top": ["电机系统", "底盘系统", "电气系统"],
        "avg_repair_hours": 4.2
    }

# ===================== 2. 把业务接口封装成Agent工具 =====================
# 用@tool装饰器,把函数变成Agent可识别的工具
# 注意:函数的文档字符串非常重要!Agent就是根据文档描述来判断什么时候调用这个工具

@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 f"车辆清单:{str(result)}"

@tool
def query_vehicle_repair_history(bus_no: str) -> str:
    """
    查询指定车辆的历史维修记录。
    参数:
    - bus_no: 车辆自编号,比如"11901"
    当用户查询某辆车的维修记录、维修历史、修过什么时,优先使用此工具。
    """
    result = get_vehicle_repair_history(bus_no)
    return f"维修记录:{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 f"月度统计:{str(result)}"

# 把所有工具收集起来
BUSINESS_TOOLS = [query_fleet_vehicles, query_vehicle_repair_history, query_fleet_monthly_stats]

# ===================== 3. 服务启动初始化 =====================
@asynccontextmanager
async def lifespan(app: FastAPI):
    global sql_agent, agent_executor

    print("正在初始化数据库连接...")
    db = SQLDatabase.from_uri(
        os.getenv("MYSQL_URI"),
        include_tables=["bus_vehicle", "bus_fleet", "repair_order"],
        sample_rows_in_table_info=2,
        view_support=False
    )

    print("正在初始化大模型...")
    llm = ChatOpenAI(
        model=os.getenv("MODEL_NAME"),
        api_key=os.getenv("OPENAI_API_KEY"),
        base_url=os.getenv("OPENAI_BASE_URL"),
        temperature=0
    )

    # --- 3.1 创建SQL查询工具 ---
    print("正在创建SQL查询Agent...")
    sql_agent = create_sql_agent(
        llm=llm,
        db=db,
        agent_type="openai-tools",
        verbose=False,
        max_iterations=10,
        handle_parsing_errors=True
    )

    # 把SQL Agent也包装成一个工具
    @tool
    def sql_database_query(query: str) -> str:
        """
        通过自然语言查询公交维修数据库,支持灵活的筛选、统计、关联查询。
        当没有现成的业务工具可以满足用户需求时,使用此工具查询数据库。
        参数:
        - query: 用户的自然语言查询问题
        """
        result = sql_agent.invoke({"input": query})
        return result["output"]

    # --- 3.2 合并所有工具:业务接口工具 + SQL查询工具 ---
    all_tools = BUSINESS_TOOLS + [sql_database_query]

    # --- 3.3 创建总Agent,负责调度所有工具 ---
    print("正在创建总调度Agent...")
    
    system_prompt = """
    你是公交机务数据查询助手,你可以调用多个工具来回答用户问题。

    【工具使用规则】
    1. 优先使用业务工具(query_fleet_vehicles、query_vehicle_repair_history、query_fleet_monthly_stats)
    2. 只有当业务工具无法满足用户需求时,才使用sql_database_query工具
    3. 不要重复调用相同的工具
    4. 如果参数不明确,直接询问用户补充信息,不要猜测

    【回答要求】
    1. 用自然语言回答,先给结论,再列数据
    2. 数据清晰,格式易读
    3. 不要暴露你调用了什么工具,也不要提到SQL、接口等技术术语
    """

    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,  # 调试时开True,看调用过程
        max_iterations=8,
        handle_parsing_errors=True
    )

    print("服务启动完成!")
    yield
    print("服务已关闭")

# ===================== 4. FastAPI应用 =====================
app = FastAPI(
    title="公交机务智能查询系统",
    description="固化接口+灵活SQL双模式智能查询",
    version="2.0.0",
    lifespan=lifespan
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ===================== 5. 对外接口 =====================
class QueryRequest(BaseModel):
    question: str

class QueryResponse(BaseModel):
    code: int = 200
    message: str = "success"
    data: str = ""

@app.get("/health", summary="健康检查")
async def health_check():
    return {"code": 200, "message": "服务运行正常"}

@app.post("/api/query", summary="自然语言智能查询", response_model=QueryResponse)
async def query_data(request: QueryRequest):
    """
    统一智能查询入口,自动选择最优查询方式
    """
    if not agent_executor:
        raise HTTPException(status_code=500, detail="服务未初始化完成")

    question = request.question.strip()
    if not question:
        raise HTTPException(status_code=400, detail="问题不能为空")

    try:
        result = agent_executor.invoke({"input": question})
        return QueryResponse(
            code=200,
            message="success",
            data=result["output"]
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"查询失败:{str(e)}")

五、核心优化技巧

1. 让Agent优先调用固化接口

在系统提示词里明确优先级:业务工具优先,SQL兜底。这样能保证大部分高频请求都走稳定的固化接口,只有真正灵活的问题才走SQL。

2. 提升工具判断准确率

  • 每个Tool的文档字符串(docstring)一定要写清楚:功能、参数、适用场景
  • 工具命名要语义化,比如query_fleet_vehicles比tool1好太多
  • 可以在系统提示词里加Few-shot示例,告诉它什么问题用什么工具

3. 错误兜底机制

如果调用业务接口失败(比如参数错误、接口异常),可以让Agent自动 fallback 到SQL查询工具,保证可用性。

4. 权限控制

  • 固化接口里可以做细粒度权限控制(比如A车队的人只能查A车队的数据)
  • SQL查询可以加行级权限过滤,在Prompt里强制加上数据权限条件

5. 性能优化

  • 固化接口可以加缓存(Redis),高频查询直接返回缓存
  • SQL查询限制超时,防止慢查询拖垮服务

六、怎么接入你自己的真实接口

只需要改3个地方:

  1. 把get_fleet_vehicles等示例函数,换成你真实的业务查询逻辑
  2. 对应修改@tool封装函数里的参数和描述
  3. 有新接口就写新的@tool函数,加到BUSINESS_TOOLS列表里

七、运行测试

启动服务后,访问[http://localhost:8000/docs](http://localhost:8000/docs),测试以下问题:

  1. 走固化接口 :查一下一公司119车队的车辆清单 → Agent会调用query_fleet_vehicles工具
  2. 走固化接口 :11901号车的维修记录 → Agent会调用query_vehicle_repair_history工具
  3. 走SQL灵活查询 :近30天哪个车队的工单最多 → Agent发现没有对应工具,自动调用sql_database_query

开启verbose=True的话,控制台可以清楚看到Agent的决策过程:选了哪个工具、传了什么参数、返回了什么结果。

相关推荐
YEGE学AI算法1 小时前
KWS语音唤醒系统完整链路:音频前处理、Fbank、阈值与冷却逻辑
python·音视频·语音唤醒·kws·fbank
zhanghaha13141 小时前
AI Agent_13 模型里的「多少 B」是什么 详细讲解
ai
挖掘狂人1 小时前
AI工具选型误区:别再迷信海外模型,国产工具已完成场景反超
大数据·人工智能·ai编程
kida_yuan1 小时前
不想花钱写了一个 Flask 知识库(续)
python
云樱梦海1 小时前
5 分钟上手 IndexTTS 2.5 便携包:不用装 Python、不挑显卡、本地离线跑语音克隆
开发语言·python·tts·indextts2.5
Java小白笔记1 小时前
Java 函数式接口1:从无参任务到自定义多参数查询
java·windows·python
xcLeigh1 小时前
本体驱动的AI大模型:方法与实践
人工智能·ai·大模型·agent·提示词·语义建模
网络毒刘1 小时前
Rules 冲突排查:多条规则互相打架时如何用优先级、范围与示例消歧
agent·ai编程·cursor·rules
洞窝技术1 小时前
Jev从入门到实战-读懂System One模型并跑通智能if语句
ai编程