「固化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接口的场景
- 高频查询:每天被大量调用的基础查询
- 参数固定:查询条件、返回字段都是固定的
- 安全敏感:涉及核心数据、权限管控的查询
- 性能要求高:要求毫秒级返回的查询
- 逻辑复杂:多表关联、复杂计算、业务规则多的查询
❌ 适合走Text-to-SQL的场景
- 临时查询:运营、运维临时拍脑袋的问题
- 多维度筛选:条件组合多变,无法枚举
- 探索分析:数据统计、趋势分析、排名类问题
- 不常用查询:一个月用不了几次的查询
公交维修场景示例(可直接套用)
| 固化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基础上扩展,包含:
- 3个示例固化业务Rest接口(你可以换成自己的真实接口)
- 把业务接口封装成Agent工具
- 保留Text-to-SQL能力
- Agent自动判断调用哪种方式
- 统一的自然语言问答接口
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个地方:
- 把
get_fleet_vehicles等示例函数,换成你真实的业务查询逻辑 - 对应修改
@tool封装函数里的参数和描述 - 有新接口就写新的
@tool函数,加到BUSINESS_TOOLS列表里
七、运行测试
启动服务后,访问[http://localhost:8000/docs](http://localhost:8000/docs),测试以下问题:
- 走固化接口 :
查一下一公司119车队的车辆清单→ Agent会调用query_fleet_vehicles工具 - 走固化接口 :
11901号车的维修记录→ Agent会调用query_vehicle_repair_history工具 - 走SQL灵活查询 :
近30天哪个车队的工单最多→ Agent发现没有对应工具,自动调用sql_database_query
开启verbose=True的话,控制台可以清楚看到Agent的决策过程:选了哪个工具、传了什么参数、返回了什么结果。