免费与数据库聊天:使用 Python 构建私有 AI 代理
Source: Dev.to

你一定熟悉这种情景。你正沉浸在思考状态中,解决一个复杂的后端问题,这时产品经理的 Slack 通知弹了出来:
“嗨,你能快速列出上周在德国注册的所有用户吗?我 10 分钟后有个会议需要用到。”
你会写 SQL,甚至可以在睡梦中写出查询。但你不应该必须这么做。
每一分钟用来编写临时报告查询的时间,都是从实际产品功能开发中偷走的时间。
解决方案不是再构建另一个复杂的管理后台。
解决方案是给你的团队一个能说他们语言的工具。
在本教程中,我们将构建一个 “与数据库聊天” 应用。它允许非技术用户用普通英文提问,而 AI 在后台生成并执行安全的只读 SQL。
最棒的是? 它可以 100 % 本地运行。
- 无需 API 密钥: $0.00 成本。
- 无数据隐私风险: 你的生产模式永远不会离开你的机器。
- 无需 “胶水代码” 噩梦: 我们将使用全新的 模型上下文协议(Model Context Protocol,MCP) 来保持架构简洁。
阅读完本指南后,你将拥有一个可部署的 Docker 容器,能够把 “嗨,你能把这些数据拉出来吗?” 变成 “当然,直接问机器人就行。”
让我们把时间夺回来。

蓝图
为了让本教程易于消化,我将专注于 AI 集成的 核心逻辑。我不会让你看到 50 行的 Python 导入语句。
你可以在仓库中获取完整代码(包括 Docker 设置和种子数据生成器)。克隆它,运行 docker-compose up,并跟随教程。
架构:“AI 的 USB‑C”
在我们写下任何代码之前,需要先弄清楚 为什么 要使用 模型上下文协议(Model Context Protocol,MCP)。
在 LLM 早期,想把模型连接到数据库往往需要编写凌乱的“胶水代码”。你必须手动把表定义注入提示词,使用正则解析出 SQL,并自行处理各种错误边缘情况。这样既脆弱又难以维护。
MCP 通过标准化连接来解决这些问题。可以把它想象成 AI 的 USB‑C 接口:
- 资源(上下文): AI 会询问 “我有什么数据?” 你的服务器返回数据库模式(schema)。
- 工具(操作): AI 决定 “我需要执行查询”。你的服务器安全地执行相应函数。
我们正在把 大脑(LLM)与 双手(数据库执行)解耦。
我们将构建的技术栈
| 组件 | 技术 |
|---|---|
| 大脑 | 在 Ollama 上运行的 Mistral 7B(因其可靠的 JSON 输出而被选中) |
| 界面 | 用户聊天的 Streamlit 应用 |
| 服务器 | 使用 FastMCP 的 Python 脚本,负责持有 SQLite 连接 |

步骤 1:环境(Docker 是你的朋友)
我们将使用 Docker 来避免“在我的机器上可以工作”的症候群。容器中包含我们的 UI 和数据库,但仍然可以与运行在本机上的 Ollama 实例通信。
创建一个 docker-compose.yml 文件。这就是你的 一键 设置:
services:
mcp-reporter:
build: .
container_name: pocket_analyst
ports:
- "8501:8501"
volumes:
- ./data:/app/data
environment:
# Crucial: lets the container talk to Ollama on your laptop
- OLLAMA_HOST=http://host.docker.internal:11434
extra_hosts:
- "host.docker.internal:host-gateway"
第2步:服务器(为 AI “赋予手”)
我们使用 FastMCP 库来创建服务器。此脚本完成两件事:
- 暴露数据库模式(Context)。
- 提供查询数据库的函数(Tool)。
注意:为简洁起见省略了模板代码。完整实现请参见仓库中的 src/server.py。
第3步:AI 大脑(通过 Ollama 的 Mistral 7B)
在本地运行 Ollama 并拉取 Mistral 7B 模型:
ollama serve # starts the Ollama server (default port 11434)
ollama pull mistral # downloads the model
在 FastMCP 服务器中,我们配置一个客户端与该端点通信,并指示模型:
- 通过
get_schema资源请求模式(schema)。 - 生成仅包含 JSON 的响应,其中包含 SQL 查询。
- 使用生成的 SQL 调用
query_database。
模型的响应随后会被转发回 Streamlit UI。
第4步:前端(Streamlit 聊天)
一个最小的 Streamlit 应用(app.py)处理聊天 UI:
import streamlit as st
import httpx
st.title("🗂️ Chat with your Database")
if "messages" not in st.session_state:
st.session_state.messages = []
def send_message(user_msg: str):
st.session_state.messages.append({"role": "user", "content": user_msg})
with httpx.Client(base_url="http://localhost:8000") as client:
resp = client.post("/chat", json={"message": user_msg})
assistant_msg = resp.json()["reply"]
st.session_state.messages.append({"role": "assistant", "content": assistant_msg})
for msg in st.session_state.messages:
st.chat_message(msg["role"]).write(msg["content"])
if prompt := st.chat_input("Ask a question about the data…"):
send_message(prompt)
运行 UI:
streamlit run app.py
访问 http://localhost:8501 开始聊天。
第5步:综合全部内容
# 1️⃣ Build and start the containers
docker-compose up --build
# 2️⃣ In another terminal, start Ollama (if not already running)
ollama serve
# 3️⃣ Open the Streamlit UI in your browser
# → http://localhost:8501
现在你应该可以用自然语言提问,例如:
- “上周有多少用户在德国注册?”
- “展示过去一个月每个国家的交易总额。”
AI 将会:
- 检索模式。
- 生成安全的只读 SQL。
- 对 SQLite 数据库执行查询。
- 将结果返回到聊天中。
回顾
- Zero API keys – 所有内容本地运行。
- Zero data leakage – 您的模式永不离开您的机器。
- Clean architecture – MCP 清晰地分离了上下文、工具和 LLM 大脑。
- Dockerised – 一条命令即可启动整个堆栈。
试一试,微调提示词,并将此模式适配到您自己的数据库(Postgres、MySQL 等)。祝您玩得开心!
第 3 步:大脑(为何选择 Mistral)
现在我们需要 Host —— 管理对话的 Streamlit 应用。
我们使用 Ollama 来运行模型。测试得出的关键洞见是:使用 Mistral 7B,而不是 Llama 3。
虽然 Llama 3 很强大,但较小的模型往往在严格的 JSON 格式上会出现问题。你要求
{ "sql": "…" }
而 Llama 可能会回复 “Sure! Here is your JSON: { … }”。这段额外的对话文字会导致解析器出错。
Mistral 7B 则异常守规。它会保持沉默,只输出我们需要的 JSON,以触发我们的工具。
src/app.py(摘录)
# ... imports and Streamlit page config omitted ...
# The System Prompt is where we "program" the behavior
# We explicitly tell the model about the schema and the output format
SYSTEM_PROMPT = """
You are a Data Analyst.
1. Output ONLY a JSON object with the tool call.
2. The format must be: { "tool": "query_database", "sql": "SELECT..." }
3. Always convert cents to main currency units in the final answer.
"""
# ... inside the main chat loop ...
if prompt := st.chat_input("Ask a question..."):
# 1. Send the user's request to Ollama
# Note: We use format='json' to force structured output
response = client.chat(
model="mistral",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
format="json"
)
# 2. Parse the tool call
# (Full error handling and JSON parsing logic is in the repo)
tool_call = json.loads(response['message']['content'])
if tool_call['tool'] == 'query_database':
# 3. Execute the SQL via our MCP Server
result = call_mcp_tool(tool_call['sql'])
# 4. Feed the data back to Mistral to summarize
final_answer = client.chat(
model="mistral",
messages=[
# … previous messages …
{"role": "user", "content": f"Data: {result}"}
]
)
st.write(final_answer['message']['content'])

第4步:与您的数据对话
在运行 docker-compose up 后,容器会自动执行 seed_data.py 脚本(已包含在仓库中),该脚本会向 SQLite 数据库填充 2,000 条真实的 “Mock Stripe” 交易。
测试 1 – “复杂过滤”
Prompt
我们昨天从德国收到的信用卡支付失败次数是多少?
Generated SQL
SELECT count(*)
FROM transactions
WHERE country_code = 'DE'
AND status = 'failed'
AND payment_method = 'card'
AND created_at > date('now', '-1 day');
测试 2 – “临时转换”
Prompt
计算上个月每个国家的欧元支付总额,并以 CSV 格式输出。
Result (CSV)
"Country Code","Total EUR Payments"
"US",2873932
"DE",1093878
"GB",533471
"BR",492770
"FR",454523
"JP",348649
传统仪表盘的致命缺陷在于它们只能回答你预先预测用户会提出的问题。该架构解决了你从未预见到的问题,用语言的无限灵活性取代了一套有限的硬编码按钮。
让我们继续成长。
订阅以自信地提升你的工程师职业生涯。
