构建自然语言数据库接口
创建一个系统,让用户使用通俗英语提问,由模型通过函数调用生成 SQL,应用安全地执行查询,再由模型讲解结果。
构建自然语言数据库接口 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Natural Language to SQL: The Vision
Imagine asking your database 'Which customers spent more than $1,000 last month?' and getting an answer — without writing a single SQL query. A natural language database interface uses function calling to let the LLM generate SQL, your application executes it safely, and the model narrates the results in plain English. This pattern democratizes data access for non-technical users.
System Architecture Overview
The NL-to-SQL pipeline has four components working together:
- Schema context: The LLM receives your database schema so it knows what tables and columns exist.
- SQL generation: The model generates a SQL query as a function call argument.
- Safe execution: Your app validates and runs the query, then returns results.
- Result narration: The model receives query results and explains them in natural language.
Defining the Query Database Tool
Define a query_database function that accepts a SQL SELECT statement. The schema description in the function definition teaches the model which tables and columns are available, so it generates accurate queries without guessing.
query_db_tool = {
'type': 'function',
'function': {
'name': 'query_database',
'description': '''Execute a read-only SQL query on the company database.
Use this to answer questions about customers, orders, and products.
Only SELECT statements are allowed. Never use DROP, DELETE, UPDATE, or INSERT.
Available tables:
- customers (id, name, email, created_at, country)
- orders (id, customer_id, total_amount, status, created_at)
- order_items (id, order_id, product_id, quantity, unit_price)
- products (id, name, category, price, stock_quantity)
''',
'parameters': {
'type': 'object',
'properties': {
'sql': {
'type': 'string',
'description': 'A valid PostgreSQL SELECT statement.'
},
'explanation': {
'type': 'string',
'description': 'One-sentence explanation of what this query does.'
}
},
'required': ['sql', 'explanation']
}
}
}Safe SQL Execution
Never execute raw SQL from the model without validation. Implement a safety layer that: only allows SELECT statements, rejects dangerous keywords, limits result rows to prevent memory issues, and runs in a read-only database transaction. Defense in depth is critical when executing LLM-generated code.
import re
import psycopg2
DANGEROUS_KEYWORDS = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE', 'ALTER', 'CREATE', 'EXEC']
def execute_safe_query(sql: str, max_rows: int = 100) -> list:
'''Execute a read-only SQL query with safety guards.'''
sql_upper = sql.upper().strip()
# Only allow SELECT
if not sql_upper.startswith('SELECT'):
raise ValueError('Only SELECT statements are allowed.')
# Block dangerous keywords
for keyword in DANGEROUS_KEYWORDS:
if re.search(r'\b' + keyword + r'\b', sql_upper):
raise ValueError(f'Forbidden keyword: {keyword}')
conn = psycopg2.connect('postgresql://readonly_user:pass@localhost/appdb')
with conn:
with conn.cursor() as cur:
# Enforce read-only transaction
cur.execute('SET TRANSACTION READ ONLY')
cur.execute(sql)
columns = [desc[0] for desc in cur.description]
rows = cur.fetchmany(max_rows)
return [dict(zip(columns, row)) for row in rows]Injecting Schema Context Into the System Prompt
The model generates better SQL when it can see the full database schema. Build a system prompt that includes table definitions, column names and types, and example values for categorical columns. This lets the model know whether to use country = 'US' or country_code = 'US' without guessing.
SYSTEM_PROMPT = '''You are a data analyst assistant with access to the company database.
When users ask data questions, use the query_database tool to look up the answer.
Always explain your query in plain English before executing it.
Database schema:
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
country VARCHAR(2) -- ISO 2-letter code: 'US', 'UK', 'DE', etc.
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
total_amount NUMERIC(10,2),
status VARCHAR -- 'pending', 'shipped', 'delivered', 'cancelled'
created_at TIMESTAMPTZ DEFAULT NOW()
);
Only use columns that exist in the schema above.
'''Formatting Query Results for the Model
Raw database results (lists of dicts) need to be formatted as readable text before sending them back to the model. Convert the result set to a compact representation — a table or JSON summary — that the model can reference when narrating the answer. Avoid sending thousands of rows; summarize large result sets.
import json
def format_results(rows: list, max_display: int = 20) -> str:
if not rows:
return 'The query returned no results.'
total = len(rows)
display = rows[:max_display]
# Format as a simple table
if display:
columns = list(display[0].keys())
lines = [' | '.join(columns)]
lines.append('-' * len(lines[0]))
for row in display:
lines.append(' | '.join(str(row[col]) for col in columns))
result = '\n'.join(lines)
if total > max_display:
result += f'\n... ({total - max_display} more rows not shown)'
return resultFull Pipeline Implementation
Putting it all together: the function that processes a user question, calls the model to generate SQL, executes the query safely, and feeds results back for narration. The model receives both the original question and the query results, then produces a plain-English answer.
from openai import OpenAI
import json
client = OpenAI()
def answer_data_question(user_question: str) -> str:
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_question}
]
# First call: get SQL from model
resp = client.chat.completions.create(
model='gpt-4o', messages=messages, tools=[query_db_tool]
)
assistant_msg = resp.choices[0].message
messages.append(assistant_msg)
if resp.choices[0].finish_reason == 'tool_calls':
tc = assistant_msg.tool_calls[0]
args = json.loads(tc.function.arguments)
print(f'Executing: {args["explanation"]}')
print(f'SQL: {args["sql"]}')
try:
rows = execute_safe_query(args['sql'])
result_text = format_results(rows)
except ValueError as e:
result_text = f'Query blocked: {str(e)}'
messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result_text})
# Second call: narrate results
final = client.chat.completions.create(model='gpt-4o', messages=messages)
return final.choices[0].message.content
return assistant_msg.contentHandling Multi-Step Data Questions
Complex questions may require multiple queries. 'Who are our top 5 customers by revenue, and what are their most recent orders?' needs two queries: one to find top customers, then one to get their orders. Allow the model to issue multiple sequential tool calls by running the dispatch loop multiple times until finish_reason='stop'.
def answer_complex_question(user_question: str) -> str:
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_question}
]
for _ in range(5): # Max 5 query rounds
resp = client.chat.completions.create(
model='gpt-4o', messages=messages, tools=[query_db_tool]
)
msg = resp.choices[0].message
messages.append(msg)
if resp.choices[0].finish_reason == 'stop':
return msg.content # Model is done
# Process tool call and loop
tc = msg.tool_calls[0]
args = json.loads(tc.function.arguments)
try:
rows = execute_safe_query(args['sql'])
result = format_results(rows)
except Exception as e:
result = f'Error: {str(e)}'
messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result})
return 'Could not complete the analysis within the step limit.'Preventing SQL Injection Risks
Even with the SELECT-only guard, a crafty model (or adversarial user) could try to exfiltrate data via subqueries or comment-based tricks. Additional protections include: using a read-only database user that has SELECT permission only, running in a separate connection pool, and validating that table names in the query match your schema whitelist.
ALLOWED_TABLES = {'customers', 'orders', 'order_items', 'products'}
def validate_tables_in_sql(sql: str) -> bool:
'''Check that only whitelisted tables are referenced in the query.'''
import sqlparse
parsed = sqlparse.parse(sql)[0]
table_names = set()
from_seen = False
for token in parsed.flatten():
if token.ttype is sqlparse.tokens.Keyword and token.value.upper() in ('FROM', 'JOIN'):
from_seen = True
elif from_seen and token.ttype is sqlparse.tokens.Name:
table_names.add(token.value.lower())
from_seen = False
unknown = table_names - ALLOWED_TABLES
if unknown:
raise ValueError(f'References unknown tables: {unknown}')
return TrueCaching Common Queries
Many business questions are asked repeatedly with the same answer: 'How many customers do we have?' 'What was last month's revenue?' Cache these results in Redis with a short TTL. Check the cache before executing the query — this reduces database load and speeds up responses for common analytical questions.
import redis
import hashlib
import json
r = redis.Redis.from_url('redis://localhost:6379')
def cached_query(sql: str, ttl_seconds: int = 300) -> list:
cache_key = 'nl_query:' + hashlib.sha256(sql.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached)
rows = execute_safe_query(sql)
r.setex(cache_key, ttl_seconds, json.dumps(rows, default=str))
return rowsExplaining Queries to Users
Build trust by showing users the SQL query that was generated alongside the natural language answer. When users can see 'I ran this query: SELECT COUNT(*) FROM customers WHERE country = ?UK?' they can verify the answer is correct and learn SQL patterns. The explanation field in our tool schema is perfect for this.
Quick Check
Test your understanding of building a natural language database interface.
Lesson Recap
In this lesson you learned: the query_database tool schema injects schema context so the model generates accurate SQL, safety validation must block non-SELECT statements and dangerous keywords before execution, and a loop of model calls enables multi-step data analysis requiring sequential queries. Next up we explore the Model Context Protocol (MCP), the open standard for connecting AI to external tools.
常见问题解答
「构建自然语言数据库接口」课时是免费的吗?
是的 — 「构建自然语言数据库接口」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。
「构建自然语言数据库接口」这节课中我会学到什么?
创建一个系统,让用户使用通俗英语提问,由模型通过函数调用生成 SQL,应用安全地执行查询,再由模型讲解结果。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 AI Engineering Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「构建自然语言数据库接口」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 AI Engineering Academy 课中编写并运行代码吗?
能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 为 API 定义函数模式
- 在应用中处理工具调用
- 并行调用函数
- 构建自然语言数据库接口