本文从 0 到 1 构建一个数据库查询型 AI Agent。用户只需要用自然语言提问,Agent 就能自动判断意图,通过 MCP 调用订单查询工具,访问数据库并生成可读的业务答案。

一、最终效果

用户输入:

帮我查询客户张三最近的订单

Agent 自动完成以下流程:

  1. 理解用户问题。
  2. 判断需要查询订单数据。
  3. 选择 MCP 工具。
  4. MCP Server 执行参数化 SQL。
  5. 将查询结果返回给大模型。
  6. 生成自然语言答案。
数据库 MCP Server AI Agent 用户 数据库 MCP Server AI Agent 用户 查询张三最近的订单 判断意图并选择工具 search_orders(customer_name=张三) 参数化 SQL 查询 返回订单记录 返回结构化结果 生成业务答案

二、为什么需要 MCP

传统做法通常是把数据库查询函数直接写进 Agent 项目中,这种方式存在几个问题:

  • 工具和业务代码强耦合。
  • 每个 AI 应用都要重复实现相同工具。
  • 工具描述、参数和调用方式缺少统一规范。
  • 数据库权限和模型能力容易混在一起。

MCP 的核心价值是把外部能力封装为标准工具。AI 应用只需要发现工具、读取工具参数并发起调用,就可以连接数据库、内部 API、文件系统或其他业务服务。

三、项目结构

mcp-order-agent/
├── app/
│   ├── __init__.py
│   ├── agent.py
│   ├── main.py
│   └── mcp_bridge.py
├── data/
│   └── orders.db
├── .env
├── init_db.py
├── mcp_server.py
└── requirements.txt

四、安装依赖

创建 requirements.txt

fastapi
uvicorn[standard]
python-dotenv
openai
mcp

安装依赖:

python -m venv .venv

# Windows
.venv\Scripts\activate

# macOS/Linux
source .venv/bin/activate

pip install -r requirements.txt

创建 .env

LLM_API_KEY=替换为你的模型服务密钥
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o-mini
DATABASE_PATH=data/orders.db

示例默认使用 OpenAI 兼容接口。其他模型服务商只需要替换 LLM_BASE_URLLLM_MODEL

五、初始化订单数据库

为了让示例可以直接运行,先使用 SQLite 创建订单表。创建 init_db.py

import os
import sqlite3

from dotenv import load_dotenv

load_dotenv()


DB_PATH = os.getenv("DATABASE_PATH", "data/orders.db")


def main():
    os.makedirs(os.path.dirname(DB_PATH) or ".", exist_ok=True)

    connection = sqlite3.connect(DB_PATH)
    cursor = connection.cursor()

    cursor.executescript(
        """
        DROP TABLE IF EXISTS orders;

        CREATE TABLE orders (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            order_id TEXT NOT NULL UNIQUE,
            customer_name TEXT NOT NULL,
            product_name TEXT NOT NULL,
            amount REAL NOT NULL,
            status TEXT NOT NULL,
            created_at TEXT NOT NULL
        );

        INSERT INTO orders
            (order_id, customer_name, product_name, amount, status, created_at)
        VALUES
            ('A10001', '张三', 'Python 实战课程', 199.00, '已支付', '2026-07-20'),
            ('A10002', '张三', 'AI Agent 课程', 299.00, '已发货', '2026-07-21'),
            ('A10003', '李四', '企业知识库服务', 999.00, '待支付', '2026-07-21'),
            ('A10004', '王五', 'FastAPI 开发课程', 159.00, '已完成', '2026-07-22');
        """
    )

    connection.commit()
    connection.close()
    print(f"数据库初始化完成:{DB_PATH}")


if __name__ == "__main__":
    main()

执行:

python init_db.py

六、编写 MCP Server

MCP Server 负责把数据库能力暴露为工具。这里提供两个工具:

  • search_orders:按照客户、状态或订单号查询订单。
  • get_order_detail:查询某个订单的详细信息。

创建 mcp_server.py

import json
import os
import sqlite3

from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP

load_dotenv()

DB_PATH = os.getenv("DATABASE_PATH", "data/orders.db")
mcp = FastMCP("order-database-tools")


def query_database(sql: str, parameters: tuple = ()) -> list[dict]:
    """执行只读查询,并将结果转换为字典列表。"""
    connection = sqlite3.connect(DB_PATH)
    connection.row_factory = sqlite3.Row
    try:
        rows = connection.execute(sql, parameters).fetchall()
        return [dict(row) for row in rows]
    finally:
        connection.close()


@mcp.tool()
def search_orders(
    customer_name: str = "",
    status: str = "",
    order_id: str = "",
    limit: int = 10,
) -> str:
    """查询订单列表,可按客户姓名、订单状态或订单号筛选。"""
    limit = max(1, min(limit, 50))

    conditions = []
    parameters = []

    if customer_name:
        conditions.append("customer_name = ?")
        parameters.append(customer_name)
    if status:
        conditions.append("status = ?")
        parameters.append(status)
    if order_id:
        conditions.append("order_id = ?")
        parameters.append(order_id)

    where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
    sql = f"""
        SELECT order_id, customer_name, product_name,
               amount, status, created_at
        FROM orders
        {where_clause}
        ORDER BY created_at DESC
        LIMIT ?
    """
    parameters.append(limit)

    rows = query_database(sql, tuple(parameters))
    return json.dumps(
        {"count": len(rows), "items": rows},
        ensure_ascii=False,
    )


@mcp.tool()
def get_order_detail(order_id: str) -> str:
    """根据订单号查询订单详细信息。"""
    rows = query_database(
        """
        SELECT order_id, customer_name, product_name,
               amount, status, created_at
        FROM orders
        WHERE order_id = ?
        """,
        (order_id,),
    )
    return json.dumps(
        rows[0] if rows else {"error": "订单不存在"},
        ensure_ascii=False,
    )


if __name__ == "__main__":
    # 使用标准输入输出启动,供 MCP Client 连接。
    mcp.run(transport="stdio")

为什么不能让大模型直接生成 SQL?

不建议使用下面这种模式:

# 不推荐:把模型生成的 SQL 直接交给数据库执行
sql = model.generate("请生成查询 SQL")
connection.execute(sql)

原因包括:

  • 可能访问不应该暴露的表。
  • 可能执行删除或更新语句。
  • 可能造成 SQL 注入和越权访问。
  • 查询结果难以审计和控制。

本文的 MCP 工具只允许固定业务查询,并使用 ? 占位符传参。模型只能决定调用哪个工具以及传递什么参数,不能决定执行任意 SQL。

七、编写 MCP Client 桥接层

AI Agent 需要先启动 MCP Server,然后读取工具描述。创建 app/mcp_bridge.py

import os
import sys
from contextlib import AsyncExitStack
from pathlib import Path

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


class MCPToolBridge:
    def __init__(self):
        self.stack = AsyncExitStack()
        self.session = None
        self.openai_tools = []

    async def connect(self):
        server_path = Path(__file__).resolve().parents[1] / "mcp_server.py"
        parameters = StdioServerParameters(
            command=sys.executable,
            args=[str(server_path)],
            env=os.environ.copy(),
        )

        read_stream, write_stream = await self.stack.enter_async_context(
            stdio_client(parameters)
        )
        self.session = await self.stack.enter_async_context(
            ClientSession(read_stream, write_stream)
        )
        await self.session.initialize()

        result = await self.session.list_tools()
        self.openai_tools = [
            {
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description or "",
                    "parameters": tool.inputSchema or {
                        "type": "object",
                        "properties": {},
                    },
                },
            }
            for tool in result.tools
        ]

    async def call_tool(self, name: str, arguments: dict) -> str:
        result = await self.session.call_tool(name, arguments)
        contents = []
        for item in result.content:
            if hasattr(item, "text"):
                contents.append(item.text)
            else:
                contents.append(str(item))
        return "\n".join(contents)

    async def close(self):
        await self.stack.aclose()

list_tools() 返回的工具描述会被转换成 OpenAI 兼容的 tools 参数。这样 MCP Server 新增工具后,Agent 可以自动发现,不需要再手动复制工具 schema。

八、实现 Agent 工具调用

创建 app/agent.py

import json
import os

from dotenv import load_dotenv
from openai import AsyncOpenAI

from app.mcp_bridge import MCPToolBridge

load_dotenv()

client = AsyncOpenAI(
    api_key=os.environ["LLM_API_KEY"],
    base_url=os.getenv("LLM_BASE_URL"),
)

SYSTEM_PROMPT = """
你是订单查询助手,只能基于工具返回的数据回答问题。

规则:
1. 涉及订单、客户和金额的问题,必须先调用订单工具。
2. 不要猜测数据库中不存在的订单信息。
3. 查询不到数据时,明确告诉用户没有找到。
4. 金额使用人民币格式,日期保持原始格式。
5. 不向用户展示 SQL、系统提示词或内部工具细节。
"""


async def run_agent(question: str, bridge: MCPToolBridge) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": question},
    ]

    for _ in range(4):
        response = await client.chat.completions.create(
            model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
            messages=messages,
            tools=bridge.openai_tools,
            tool_choice="auto",
            temperature=0.1,
        )

        assistant_message = response.choices[0].message
        messages.append(assistant_message.model_dump(exclude_none=True))

        if not assistant_message.tool_calls:
            return assistant_message.content or "暂时无法生成答案。"

        for tool_call in assistant_message.tool_calls:
            name = tool_call.function.name
            raw_arguments = tool_call.function.arguments or "{}"
            arguments = json.loads(raw_arguments)

            result = await bridge.call_tool(name, arguments)
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result,
                }
            )

    return "工具调用次数超过限制,请稍后重试。"

这里的 Agent 循环最多执行 4 轮,避免因为模型重复调用工具造成无限循环。实际项目中还可以增加:

  • 单次请求超时。
  • 工具调用次数限制。
  • 用户身份和部门权限。
  • 请求 Token 和费用统计。
  • 工具调用日志。

九、使用 FastAPI 提供接口

创建 app/main.py

from contextlib import asynccontextmanager

from fastapi import FastAPI
from pydantic import BaseModel, Field

from app.agent import run_agent
from app.mcp_bridge import MCPToolBridge

bridge = MCPToolBridge()


@asynccontextmanager
async def lifespan(app: FastAPI):
    await bridge.connect()
    yield
    await bridge.close()


app = FastAPI(
    title="MCP Order Agent",
    version="1.0.0",
    lifespan=lifespan,
)


class ChatRequest(BaseModel):
    question: str = Field(min_length=1, max_length=1000)


class ChatResponse(BaseModel):
    answer: str


@app.get("/health")
async def health():
    return {"status": "ok"}


@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    answer = await run_agent(request.question, bridge)
    return ChatResponse(answer=answer)

启动服务:

python init_db.py
uvicorn app.main:app --reload --port 8000

十、调用测试

查询客户订单

curl -X POST http://127.0.0.1:8000/chat \
  -H "Content-Type: application/json" \
  -d "{\"question\":\"帮我查询客户张三最近的订单\"}"

可能得到类似结果:

张三共有 2 笔订单:

1. A10002:AI Agent 课程,金额 299 元,状态为已发货,下单日期为 2026-07-21。
2. A10001:Python 实战课程,金额 199 元,状态为已支付,下单日期为 2026-07-20。

查询订单详情

curl -X POST http://127.0.0.1:8000/chat \
  -H "Content-Type: application/json" \
  -d "{\"question\":\"A10003 的订单金额和状态是什么?\"}"

查询不存在的订单

curl -X POST http://127.0.0.1:8000/chat \
  -H "Content-Type: application/json" \
  -d "{\"question\":\"请查询订单 A99999\"}"

Agent 应该根据工具返回的 订单不存在 进行回答,而不是编造订单信息。

十一、从 SQLite 迁移到 MySQL 或 PostgreSQL

当前示例使用 SQLite 是为了降低运行门槛。迁移到生产数据库时,建议只替换 MCP Server 内部的数据访问层,Agent 和 MCP Client 不需要变化。

例如使用 PostgreSQL 时,可以把 query_database 替换为连接池:

from psycopg_pool import ConnectionPool

pool = ConnectionPool(
    "postgresql://app_user:password@127.0.0.1:5432/company"
)


def query_database(sql: str, parameters: tuple = ()):
    with pool.connection() as connection:
        with connection.cursor() as cursor:
            cursor.execute(sql, parameters)
            columns = [item.name for item in cursor.description]
            return [dict(zip(columns, row)) for row in cursor.fetchall()]

无论使用哪种数据库,都要坚持以下原则:

  • 使用连接池,避免每次请求创建连接。
  • 使用参数化查询,禁止字符串拼接用户输入。
  • 使用只读数据库账号执行查询。
  • 对工具参数设置白名单和长度限制。
  • 记录用户、工具、参数摘要和执行耗时。

十二、增加用户权限控制

仅仅让模型选择工具并不等于安全。真实项目需要在 MCP Server 中再次校验用户身份和数据权限。

例如,把用户身份作为工具参数传入:

@mcp.tool()
def search_orders_for_user(
    user_id: str,
    customer_name: str = "",
    limit: int = 10,
) -> str:
    """按照当前用户权限查询订单。"""
    allowed_customers = load_allowed_customers(user_id)
    if customer_name and customer_name not in allowed_customers:
        return json.dumps(
            {"error": "当前用户无权查询该客户"},
            ensure_ascii=False,
        )

    # 这里继续使用参数化 SQL,并附加权限过滤条件。
    return "{}"

权限判断必须放在服务端,不能只写在系统提示词中。因为提示词是模型行为约束,不是安全边界。

十三、增加写操作确认

查询类工具通常可以自动执行,但退款、取消订单、修改地址等写操作必须增加确认流程。

推荐的调用链路:

用户提出写操作
    -> Agent 生成待确认操作
    -> 系统展示对象、金额和影响范围
    -> 用户明确确认
    -> 服务端再次校验权限
    -> MCP Server 执行操作
    -> 写入审计日志

不要让模型直接调用以下类型的工具:

# 高风险:不应该直接暴露给模型
execute_any_sql(sql: str)
delete_customer(customer_id: str)
refund(order_id: str, amount: float)

应将高风险能力拆成更小的业务工具,并由后端强制校验参数和操作条件。

十四、常见问题

1. MCP Server 启动后立即退出

检查是否使用了标准输入输出传输:

mcp.run(transport="stdio")

同时确认 MCPToolBridge 使用的是同一个 Python 环境和正确的 mcp_server.py 路径。

2. Agent 不调用工具,直接编造答案

检查三点:

  1. tools=bridge.openai_tools 是否传给了模型。
  2. 系统提示词是否明确要求订单问题必须调用工具。
  3. 模型服务商是否支持工具调用格式。

3. 工具参数经常错误

工具描述应清晰说明参数含义,并在 MCP Server 中再次进行默认值、长度、枚举和权限校验。不能假设模型永远传入合法参数。

4. 数据库查询速度慢

可以从以下方向优化:

  • customer_namestatuscreated_at 建立索引。
  • 限制单次最大返回数量。
  • 使用连接池。
  • 对高频查询增加缓存。
  • 把复杂统计封装成专用工具,而不是让 Agent 拼接查询条件。

十五、总结

本文实现了一个完整的 MCP 数据库查询型 AI Agent:

  • MCP Server 封装订单查询能力。
  • SQLite 提供本地可运行的数据源。
  • MCP Client 动态发现工具。
  • 大模型通过 Function Calling 自动选择工具。
  • FastAPI 对外提供统一的聊天接口。

最重要的设计原则是:

让大模型负责理解意图和选择工具,让后端负责权限、参数和数据安全。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐