Skip to main content

概述

在本教程中,您将学习如何构建一个能够使用 LangChain 代理 回答 SQL 数据库问题的代理。 从高层次来看,该代理将:
1

从数据库中获取可用的表和模式

2

确定哪些表与问题相关

3

获取相关表的模式

4

根据问题和模式信息生成查询

5

使用 LLM 双重检查查询中的常见错误

6

执行查询并返回结果

7

根据数据库引擎发现的错误进行修正,直到查询成功

8

根据结果制定响应

构建 SQL 数据库的问答系统需要执行模型生成的 SQL 查询。这样做存在固有风险。请确保您的数据库连接权限始终根据代理的需求尽可能严格地限定范围。这将减轻(但不能消除)构建模型驱动系统的风险。

概念

我们将涵盖以下概念:

设置

安装

npm i langchain @langchain/core typeorm sqlite3 zod

LangSmith

设置 LangSmith 以检查链或代理内部发生的情况。然后设置以下环境变量:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."

1. 选择 LLM

选择一个支持 工具调用 的模型:
👉 阅读 OpenAI 聊天模型集成文档
npm install @langchain/openai
import { initChatModel } from "langchain";

process.env.OPENAI_API_KEY = "your-api-key";

const model = await initChatModel("gpt-5.2");
下面示例中显示的输出使用了 OpenAI。

2. 配置数据库

您将为本教程创建一个 SQLite 数据库。SQLite 是一个轻量级数据库,易于设置和使用。我们将加载 chinook 数据库,这是一个代表数字媒体商店的示例数据库。 为方便起见,我们已将数据库 (Chinook.db) 托管在公共 GCS 存储桶中。
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 查询并获取结果:
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();
}

4. 执行 SQL 查询

在运行命令之前,检查 LLM 在 _safe_sql 中生成的命令:

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();

  // 阻止多个语句(允许一个可选的尾随 ;)
  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();

  // 只读门控
  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.")
  }

  // 仅当不存在时才追加 LIMIT
  if (!HAS_LIMIT_TAIL_RE.test(query)) {
    query += " LIMIT 5";
  }
  return query;
}

然后,使用 SQLDatabase 中的 run 通过 execute_sql 工具执行命令:
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: "执行只读 SQLite SELECT 查询并返回结果。",
    schema: z.object({
      query: z.string().describe("要执行的 SQLite SELECT 查询(只读)。"),
    }),
  }
);

5. 使用 createAgent

使用 createAgent 以最少的代码构建一个 ReAct 代理。代理将解释请求并生成 SQL 命令。工具将检查命令的安全性,然后尝试执行命令。如果命令有错误,错误消息将返回给模型。模型然后可以检查原始请求和新的错误消息,并生成一个新的命令。这可以继续,直到 LLM 成功生成命令或达到结束计数。这种向模型提供反馈(本例中是错误消息)的模式非常强大。 使用描述性的系统提示初始化代理以自定义其行为:
import { SystemMessage } from "langchain";

const getSystemPrompt = async () => new SystemMessage(`您是一位谨慎的 SQLite 分析师。

权威模式(不要发明列/表):
${await getSchema()}

规则:
- 逐步思考。
- 当您需要数据时,使用一个 SELECT 查询调用工具 \`execute_sql\`
- 仅限只读;不允许 INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE。
- 限制为 5 行,除非用户明确要求其他。
- 如果工具返回 'Error:',请修改 SQL 并重试。
- 将尝试次数限制为 5 次。
- 如果 5 次尝试后仍未成功,请向用户返回一条说明。
- 优先使用显式列列表;避免使用 SELECT *。
`);
现在,使用模型、工具和提示创建代理:
import { createAgent } from "langchain";

const agent = createAgent({
  model: "gpt-5",
  tools: [executeSql],
  systemPrompt: getSystemPrompt,
});

6. 运行代理

在示例查询上运行代理并观察其行为:
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)}`);
}
human: Which genre, on average, has the longest tracks?
ai:
tool: [{"Genre":"Sci Fi & Fantasy","AvgMilliseconds":2911783.0384615385}]
ai: Sci Fi & Fantasy — 平均曲目长度 ≈ 48.5 分钟(约 2,911,783 毫秒)。
代理正确地编写了查询,检查了查询,并运行它以告知其最终响应。
您可以在 LangSmith 追踪 中检查上述运行的所有方面,包括采取的步骤、调用的工具、LLM 看到的提示等。

(可选)使用 Studio

Studio 提供了一个“客户端”循环以及内存,因此您可以将其作为聊天界面运行并查询数据库。您可以询问诸如“告诉我数据库的模式”或“显示前 5 名客户的发票”之类的问题。您将看到生成的 SQL 命令和结果输出。下面介绍了如何启动它的详细信息。
除了前面提到的包之外,您还需要:
npm i -g @langchain/langgraph-cli@latest
在您将要运行的目录中,您需要一个包含以下内容的 langgraph.json 文件:
{
  "dependencies": ["."],
  "graphs": {
      "agent": "./sqlAgent.ts:agent",
      "graph": "./sqlAgentLanggraph.ts:graph"
  },
  "env": ".env"
}
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();

  // 阻止多个语句(允许一个可选的尾随 ;)
  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();

  // 只读门控
  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.")
  }

  // 仅当不存在时才追加 LIMIT
  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: "执行只读 SQLite SELECT 查询并返回结果。",
    schema: z.object({
      query: z.string().describe("要执行的 SQLite SELECT 查询(只读)。"),
    }),
  }
);

const getSystemPrompt = async () => new SystemMessage(`您是一位谨慎的 SQLite 分析师。

权威模式(不要发明列/表):
${await getSchema()}

规则:
- 逐步思考。
- 当您需要数据时,使用一个 SELECT 查询调用工具 \`execute_sql\`
- 仅限只读;不允许 INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE。
- 限制为 5 行,除非用户明确要求其他。
- 如果工具返回 'Error:',请修改 SQL 并重试。
- 将尝试次数限制为 5 次。
- 如果 5 次尝试后仍未成功,请向用户返回一条说明。
- 优先使用显式列列表;避免使用 SELECT *。
`);

export const agent = createAgent({
  model: "gpt-5",
  tools: [executeSql],
  systemPrompt: getSystemPrompt,
});

后续步骤

如需更深入的定制,请查看本教程,了解如何直接使用 LangGraph 原语实现 SQL 代理。