概述
在本教程中,您将学习如何使用 LangChain 代理 构建一个能够回答有关 SQL 数据库问题的代理。 在宏观层面上,代理将:构建 SQL 数据库的问答系统需要执行模型生成的 SQL 查询。这样做存在固有的风险。请确保您的数据库连接权限始终尽可能地针对代理的需求进行窄范围的限制。这将减轻但不能消除构建模型驱动系统的风险。
概念
我们将涵盖以下概念:- 用于从 SQL 数据库读取的 工具
- LangChain 代理
- 人机交互 (Human-in-the-loop) 流程
设置
安装
Copy
npm i langchain @langchain/core typeorm sqlite3 zod
LangSmith
设置 LangSmith 以检查您的链或代理内部正在发生的事情。然后设置以下环境变量:Copy
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."
1. 选择一个 LLM
选择一个支持 工具调用 的模型:- OpenAI
- Anthropic
- Azure
- Google Gemini
- Bedrock Converse
👉 Read the OpenAI chat model integration docs
Copy
npm install @langchain/openai
Copy
import { initChatModel } from "langchain";
process.env.OPENAI_API_KEY = "your-api-key";
const model = await initChatModel("gpt-5.2");
👉 Read the Anthropic chat model integration docs
Copy
npm install @langchain/anthropic
Copy
import { initChatModel } from "langchain";
process.env.ANTHROPIC_API_KEY = "your-api-key";
const model = await initChatModel("claude-sonnet-4-6");
👉 Read the Azure chat model integration docs
Copy
npm install @langchain/azure
Copy
import { initChatModel } from "langchain";
process.env.AZURE_OPENAI_API_KEY = "your-api-key";
process.env.AZURE_OPENAI_ENDPOINT = "your-endpoint";
process.env.OPENAI_API_VERSION = "your-api-version";
const model = await initChatModel("azure_openai:gpt-5.2");
👉 Read the Google GenAI chat model integration docs
Copy
npm install @langchain/google-genai
Copy
import { initChatModel } from "langchain";
process.env.GOOGLE_API_KEY = "your-api-key";
const model = await initChatModel("google-genai:gemini-2.5-flash-lite");
👉 Read the AWS Bedrock chat model integration docs
Copy
npm install @langchain/aws
Copy
import { initChatModel } from "langchain";
// Follow the steps here to configure your credentials:
// https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started.html
const model = await initChatModel("bedrock:gpt-5.2");
2. 配置数据库
您将在本教程中创建一个 SQLite 数据库。SQLite 是一个轻量级数据库,易于设置和使用。我们将加载chinook 数据库,这是一个代表数字媒体商店的示例数据库。
为了方便起见,我们已将数据库 (Chinook.db) 托管在公共 GCS 存储桶上。
Copy
import fs from "node:fs/promises";
import path from "node:path";
const url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db";
const localPath = path.resolve("Chinook.db");
async function resolveDbPath() {
if (await fs.exists(localPath)) {
return localPath;
}
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Failed to download DB. Status code: ${resp.status}`);
const buf = Buffer.from(await resp.arrayBuffer());
await fs.writeFile(localPath, buf);
return localPath;
}
3. 添加数据库交互工具
使用langchain/sql_db 中可用的 SqlDatabase 包装器与数据库进行交互。该包装器提供了一个简单的接口来执行 SQL 查询和获取结果:
Copy
import { SqlDatabase } from "@langchain/classic/sql_db";
import { DataSource } from "typeorm";
let db: SqlDatabase | undefined;
async function getDb() {
if (!db) {
const dbPath = await resolveDbFile();
const datasource = new DataSource({ type: "sqlite", database: dbPath });
db = await SqlDatabase.fromDataSourceParams({ appDataSource: datasource });
}
return db;
}
async function getSchema() {
const db = await getDb();
return await db.getTableInfo();
}
6. 实现人机交互审查
在执行代理的 SQL 查询之前检查它们是否有任何意外操作或低效之处是明智的。 LangChain 代理支持内置的 人机交互中间件,以便为代理工具调用添加监督。让我们配置代理在调用sql_db_query 工具时暂停以供人工审查:
Copy
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model,
tools,
system_prompt=system_prompt,
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={"sql_db_query": True},
description_prefix="Tool execution pending approval",
),
],
checkpointer=InMemorySaver(),
)
sql_db_query 工具之前暂停以供审查:
Copy
question = "Which genre on average has the longest tracks?"
config = {"configurable": {"thread_id": "1"}}
for step in agent.stream(
{"messages": [{"role": "user", "content": question}]},
config,
stream_mode="values",
):
if "__interrupt__" in step:
print("INTERRUPTED:")
interrupt = step["__interrupt__"][0]
for request in interrupt.value["action_requests"]:
print(request["description"])
elif "messages" in step:
step["messages"][-1].pretty_print()
else:
pass
Copy
...
INTERRUPTED:
Tool execution pending approval
Tool: sql_db_query
Args: {'query': 'SELECT g.Name AS Genre, AVG(t.Milliseconds) AS AvgTrackLength FROM Track t JOIN Genre g ON t.GenreId = g.GenreId GROUP BY g.Name ORDER BY AvgTrackLength DESC LIMIT 1;'}
Copy
from langgraph.types import Command
for step in agent.stream(
Command(resume={"decisions": [{"type": "approve"}]}),
config,
stream_mode="values",
):
if "messages" in step:
step["messages"][-1].pretty_print()
elif "__interrupt__" in step:
print("INTERRUPTED:")
interrupt = step["__interrupt__"][0]
for request in interrupt.value["action_requests"]:
print(request["description"])
else:
pass
Copy
================================== Ai Message ==================================
Tool Calls:
sql_db_query (call_7oz86Epg7lYRqi9rQHbZPS1U)
Call ID: call_7oz86Epg7lYRqi9rQHbZPS1U
Args:
query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgDuration FROM Track JOIN Genre ON Track.GenreId = Genre.GenreId GROUP BY Genre.Name ORDER BY AvgDuration DESC LIMIT 5;
================================= Tool Message =================================
Name: sql_db_query
[('Sci Fi & Fantasy', 2911783.0384615385), ('Science Fiction', 2625549.076923077), ('Drama', 2575283.78125), ('TV Shows', 2145041.0215053763), ('Comedy', 1585263.705882353)]
================================== Ai Message ==================================
The genre with the longest average track length is "Sci Fi & Fantasy" with an average duration of about 2,911,783 milliseconds, followed by "Science Fiction" and "Drama."
4. 执行 SQL 查询
在运行命令之前,请在_safe_sql 中检查 LLM 生成的命令:
Copy
const DENY_RE = /\b(INSERT|UPDATE|DELETE|ALTER|DROP|CREATE|REPLACE|TRUNCATE)\b/i;
const HAS_LIMIT_TAIL_RE = /\blimit\b\s+\d+(\s*,\s*\d+)?\s*;?\s*$/i;
function sanitizeSqlQuery(q) {
let query = String(q ?? "").trim();
// block multiple statements (allow one optional trailing ;)
const semis = [...query].filter((c) => c === ";").length;
if (semis > 1 || (query.endsWith(";") && query.slice(0, -1).includes(";"))) {
throw new Error("multiple statements are not allowed.")
}
query = query.replace(/;+\s*$/g, "").trim();
// read-only gate
if (!query.toLowerCase().startsWith("select")) {
throw new Error("Only SELECT statements are allowed")
}
if (DENY_RE.test(query)) {
throw new Error("DML/DDL detected. Only read-only queries are permitted.")
}
// append LIMIT only if not already present
if (!HAS_LIMIT_TAIL_RE.test(query)) {
query += " LIMIT 5";
}
return query;
}
SQLDatabase 中的 run 和 execute_sql 工具来执行命令:
Copy
import { tool } from "langchain"
import * as z from "zod";
const executeSql = tool(
async ({ query }) => {
const q = sanitizeSqlQuery(query);
try {
const result = await db.run(q);
return typeof result === "string" ? result : JSON.stringify(result, null, 2);
} catch (e) {
throw new Error(e?.message ?? String(e))
}
},
{
name: "execute_sql",
description: "Execute a READ-ONLY SQLite SELECT query and return results.",
schema: z.object({
query: z.string().describe("SQLite SELECT query to execute (read-only)."),
}),
}
);
5. 使用 createAgent
使用 createAgent 用最少的代码构建一个 ReAct 代理。该代理将解释请求并生成 SQL 命令。工具将检查命令的安全性,然后尝试执行该命令。如果命令有误,错误消息将返回给模型。然后,模型可以检查原始请求和新的错误消息,并生成新的命令。这可以持续到 LLM 生成成功命令或达到结束计数。这种向模型提供反馈(在本例中为错误消息)的模式非常强大。
使用描述性系统提示初始化代理以自定义其行为:
Copy
import { SystemMessage } from "langchain";
const getSystemPrompt = async () => new SystemMessage(`You are a careful SQLite analyst.
Authoritative schema (do not invent columns/tables):
${await getSchema()}
Rules:
- Think step-by-step.
- When you need data, call the tool \`execute_sql\` with ONE SELECT query.
- Read-only only; no INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE.
- Limit to 5 rows unless user explicitly asks otherwise.
- If the tool returns 'Error:', revise the SQL and try again.
- Limit the number of attempts to 5.
- If you are not successful after 5 attempts, return a note to the user.
- Prefer explicit column lists; avoid SELECT *.
`);
Copy
import { createAgent } from "langchain";
const agent = createAgent({
model: "gpt-5",
tools: [executeSql],
systemPrompt: getSystemPrompt,
});
6. 运行代理
在示例查询上运行代理并观察其行为:Copy
const question = "Which genre, on average, has the longest tracks?";
const stream = await agent.stream(
{ messages: [{ role: "user", content: question }] },
{ streamMode: "values" }
);
for await (const step of stream) {
const message = step.messages.at(-1);
console.log(`${message.role}: ${JSON.stringify(message.content, null, 2)}`);
}
Copy
human: Which genre, on average, has the longest tracks?
ai:
tool: [{"Genre":"Sci Fi & Fantasy","AvgMilliseconds":2911783.0384615385}]
ai: Sci Fi & Fantasy — average track length ≈ 48.5 minutes (about 2,911,783 ms).
您可以在 LangSmith 跟踪 中检查上述运行的所有方面,包括采取的步骤、调用的工具、LLM 看到的提示等等。
(可选) 使用 Studio
Studio 提供了一个“客户端”循环以及内存,因此您可以将其作为聊天界面运行并查询数据库。您可以问类似“告诉我数据库的架构”或“向我展示前 5 位客户的发票”之类的问题。您将看到生成的 SQL 命令和结果输出。有关如何开始的详细信息如下。在 Studio 中运行您的代理
在 Studio 中运行您的代理
除了前面提到的包之外,您还需要:在您将运行的目录中,您需要一个包含以下内容的
Copy
npm i -g langgraph-cli@latest
langgraph.json 文件:Copy
{
"dependencies": ["."],
"graphs": {
"agent": "./sqlAgent.ts:agent",
"graph": "./sqlAgentLanggraph.ts:graph"
},
"env": ".env"
}
Copy
import fs from "node:fs/promises";
import path from "node:path";
import { SqlDatabase } from "@langchain/classic/sql_db";
import { DataSource } from "typeorm";
import { SystemMessage, createAgent, tool } from "langchain"
import * as z from "zod";
const url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db";
const localPath = path.resolve("Chinook.db");
async function resolveDbPath() {
if (await fs.exists(localPath)) {
return localPath;
}
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Failed to download DB. Status code: ${resp.status}`);
const buf = Buffer.from(await resp.arrayBuffer());
await fs.writeFile(localPath, buf);
return localPath;
}
let db: SqlDatabase | undefined;
async function getDb() {
if (!db) {
const dbPath = await resolveDbPath();
const datasource = new DataSource({ type: "sqlite", database: dbPath });
db = await SqlDatabase.fromDataSourceParams({ appDataSource: datasource });
}
return db;
}
async function getSchema() {
const db = await getDb();
return await db.getTableInfo();
}
const DENY_RE = /\b(INSERT|UPDATE|DELETE|ALTER|DROP|CREATE|REPLACE|TRUNCATE)\b/i;
const HAS_LIMIT_TAIL_RE = /\blimit\b\s+\d+(\s*,\s*\d+)?\s*;?\s*$/i;
function sanitizeSqlQuery(q) {
let query = String(q ?? "").trim();
// block multiple statements (allow one optional trailing ;)
const semis = [...query].filter((c) => c === ";").length;
if (semis > 1 || (query.endsWith(";") && query.slice(0, -1).includes(";"))) {
throw new Error("multiple statements are not allowed.")
}
query = query.replace(/;+\s*$/g, "").trim();
// read-only gate
if (!query.toLowerCase().startsWith("select")) {
throw new Error("Only SELECT statements are allowed")
}
if (DENY_RE.test(query)) {
throw new Error("DML/DDL detected. Only read-only queries are permitted.")
}
// append LIMIT only if not already present
if (!HAS_LIMIT_TAIL_RE.test(query)) {
query += " LIMIT 5";
}
return query;
}
const executeSql = tool(
async ({ query }) => {
const q = sanitizeSqlQuery(query);
try {
const result = await db.run(q);
return typeof result === "string" ? result : JSON.stringify(result, null, 2);
} catch (e) {
throw new Error(e?.message ?? String(e))
}
},
{
name: "execute_sql",
description: "Execute a READ-ONLY SQLite SELECT query and return results.",
schema: z.object({
query: z.string().describe("SQLite SELECT query to execute (read-only)."),
}),
}
);
const getSystemPrompt = async () => new SystemMessage(`You are a careful SQLite analyst.
Authoritative schema (do not invent columns/tables):
${await getSchema()}
Rules:
- Think step-by-step.
- When you need data, call the tool \`execute_sql\` with ONE SELECT query.
- Read-only only; no INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE.
- Limit to 5 rows unless user explicitly asks otherwise.
- If the tool returns 'Error:', revise the SQL and try again.
- Limit the number of attempts to 5.
- If you are not successful after 5 attempts, return a note to the user.
- Prefer explicit column lists; avoid SELECT *.
`);
export const agent = createAgent({
model: "gpt-5",
tools: [executeSql],
systemPrompt: getSystemPrompt,
});
下一步
要进行更深入的定制,请查看 此教程,了解如何直接使用 LangGraph 原语实现 SQL 代理。将这些文档连接 到 Claude、VSCode 等,通过 MCP 获取实时解答。

