Agent应用开发:给SQLite插件加上AI功能

0 阅读13分钟

介绍

上一篇文章用GLM+Claude Code,开发了一款SQLite插件 ,写了我在插件中增加AI问答功能,但是仅实现了对话功能,简单添加了一些系统提示词。

新版本为AI增加了更实用的功能:工具调用能力。有了它才真正是个完善的AI产品,

比如查看表结构:

image.png

分析数据信息:

image.png

支持配置模型可调用工具,如果勾选写操作,就可以做到一行sql不写,让AI帮我完成创建表、添加数据等功能

image.png

下面一步步来讲,如何添加大模型问答及工具调用。中间会穿插一些知识点,以便对大模型整体有更好的理解,毕竟vibe coding时代下,技术没那么重要,但是作为指挥官,必须有系统性的认知和准确的判断力

插件地址在这里,可以体验下: www.u-tools.cn/plugins/det…

创建并展示会话

本身插件是纯前端写的,部分本地能力使用nodejs。

首先是模型调用,现在各大厂商都在推广产品,基本上都有一些免费token,测试是足够了。这里我使用的是硅基流动,注册加上一些活动,领了五十多元代金券完全够用。

UI展示

这里使用Ant Design X

@ant-design/x是基于 Ant Design 设计体系的 React UI 库、专为 AI 驱动界面设计,开箱即用的智能对话组件、无缝集成 API 服务,快速搭建智能应用界面。

image.png

具体使用就不详述了,文章重点在如何使用LangChain

模型调用

市面上有那么多模型,比如GPT、deepseek、GLM等,如果我们按GPT的文档对接,那再换个模型怎么办,再写一套对接逻辑?

OpenAI Compatible

虽然 OpenAI 并没有像 W3C 那样发布正式的国际标准文档,但凭借其先发优势和巨大的市场份额,OpenAI 的 API 规范已经成为了大模型行业事实上的“工业标准”。

这就好比:虽然 USB 接口不是某家公司强制规定的,但现在谁做充电器都得做成 USB 的形状,否则用户就不买账。

这就是所谓的 "OpenAI Compatible" (OpenAI 兼容协议) 。以下是目前业界公认的几个核心“标准”:


接口标准:URL 路径与请求方法

这是最底层的兼容,只要你照着这个路径发请求,就能被识别。

核心端点:

  • 聊天补全(最常用):POST /v1/chat/completions
  • 文本嵌入(向量):POST /v1/embeddings
  • 模型列表:GET /v1/models

鉴权方式:

  • 统一使用 HTTP Header:Authorization: Bearer sk-......
  • Base URL 习惯:
  • 通常以 /v1 结尾,方便拼接(如 https://api.openai.com/v1)。

应用场景: 几乎所有国产模型(Qwen、Kimi、DeepSeek、智谱等)的“兼容模式”,以及开源推理框架(Ollama、vLLM、LocalAI),都严格遵守这个 URL 结构。

使用langchain调用大模型

我们可以直接使用openai的npm包,但是更推荐使用langchain,对于后续做模型切换、Function Calling更友好

import { ChatOpenAI } from '@langchain/openai';

const model = new ChatOpenAI({
  modelName: 'Pro/zai-org/GLM-4.7',
  apiKey: 'xxx',
  configuration: {
    baseURL: 'https://api.siliconflow.cn/v1'
  },
  temperature: 0,
});
ChatOpenAI

除了使用ChatOpenAI,LangChain还导出了其他模型,如:ChatAnthropicChatDeepSeek

LangChain 采用了分层架构设计,其他模型必须实现BaseChatModel基类

// 最底层:@langchain/core(抽象层)
import { BaseChatModel } from "@langchain/core/language_models/chat_models";

// 实现层:各模型提供商
import { ChatOpenAI } from "@langchain/openai";      // OpenAI 实现
import { ChatAnthropic } from "@langchain/anthropic"; // Claude 实现

OpenAI Compatible 模型技术上都能用 ChatOpenAI 调用,这是 LangChain 生态的灵活性优势。但如果需要解锁模型全部能力(如思考链、特殊工具格式),或追求更好的类型安全,专用包仍是更佳选择。两者可以无缝互换,因为都继承自 BaseChatModel

temperature

temperature 是控制大模型输出随机性(创造性)与确定性(准确性) 的核心参数,它直接作用于模型生成文本时的概率分布采样过程。

通俗来讲,温度系数就是模型的"性格旋钮",0 是刻板严谨的工程师,1.0 是富有想象力的艺术家

因为我们要求模型严格按照要求来执行,所以设置为0。

获取模型输出

接下来就可以拿到上述model实例来调用模型,获取输出了。

方式1:标准调用(阻塞等待完整响应)
const response = await model.invoke([
  { role: "user", content: "写一段SQLite建表示例代码" }
]);

console.log(response.content); // 一次性输出完整内容
方式2:流式调用(实时增量接收)
const stream = await model.stream([
  { role: "user", content: "写一段SQLite建表示例代码" }
]);

let fullText = "";
// 使用 for await...of 遍历异步生成器
for await (const chunk of stream) {
  const text = chunk.content; // 每次可能收到 1-N 个 token
  fullText += text;
  console.log(text); // 实时输出到控制台(打字机效果)
  
  // 可随时中断(如用户点击"停止生成")
  if (fullText.includes("ERROR")) break;
}

这里使用方式2,流式输出有比较好的体验。

提示词模板(Prompt Templates)

提示词模板(Prompt Templates)是 LangChain 中解耦提示逻辑与业务数据的核心抽象。它解决了"硬编码字符串"带来的维护灾难,提供了类型安全、变量注入、少样本示例管理等工程化能力。

// 定义提示模板
const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    `你是一个专业的 SQLite 数据库助手。你的职责是:
**你可以帮助用户:**
- 创建、修改、删除数据库表
- 插入、更新、删除数据
- 编写和优化 SQL 查询语句
- 解释 SQLite 的语法和特性
- 数据库设计和索引优化
- 解决 SQLite 使用中的问题
**重要规则:**
- 只回答与 SQLite 数据库相关的问题
- 对于与 SQLite 无关的问题(如编程语言、其他数据库、生活问题等),请礼貌拒绝并提示:
"抱歉,我是 SQLite 数据库专属助手,只能回答与 SQLite 相关的问题。如果您有 SQLite 方面的疑问,我很乐意帮助您!"
请用简洁、准确的语言回答问题。`
  ],
  new MessagesPlaceholder("history"), // 历史消息将插入这里
  ["human", "{input}"],
]);
  1. 设定系统提示词,规定模型职责、回答范围等
  2. MessagesPlaceholder组装历史消息,方便后面上下文管理
指令遵循(Instruction Following)

如果你发现模型并没有严格按照你的提示词去回答,比如系统提示词里规定了“只回答与 SQLite 数据库相关的问题”,但是还是回答了无关问题。

指令遵循 失效:模型对系统提示词的"注意力"不够,或者你的用户输入(User Input)强度压过了系统提示(System Prompt)。

1. 策略 A:COT(Chain-of-Thought)强制自检

让模型在回答前必须先进行合规检查,增加认知负担

你是一个SQLite技术专家。
**执行规则(必须严格遵守):**
1. 首先分析用户问题是否与SQLite数据库的架构、SQL语法、性能优化、故障排查相关
2. 如果相关:正常回答
3. 如果无关:必须回复"我仅能回答SQLite相关问题",并停止输出

**自检步骤(回答前必须在内心完成):**
- 这个问题涉及数据库吗?→ 不涉及 → 拒绝
- 涉及的是SQLite吗?→ 不是 → 拒绝
- 符合以上条件吗?→ 不符合 → 拒绝

现在开始处理用户请求。
2. 策略 B:Few-shot 示例(少样本教育)

用具体例子告诉模型什么是"越界",但是会作为历史消息在后续问答携带。缺点是可能造成上下文锚定(Context Anchoring)少样本过拟合(Few-shot Overfitting),导致模型过度匹配示例中的表面特征(关键词、句式),而非理解真实用户的深层语义。

const fewShotPrompt = ChatPromptTemplate.fromMessages([
  ["system", "你是SQLite专家,严格拒绝回答非SQLite问题。"],
  // 正面示例
  ["human", "如何优化SQLite的查询速度?"],
  ["ai", "SQLite优化可以从索引、VACUUM、WAL模式等方面入手..."],
  // 负面示例(关键)
  ["human", "MySQL怎么配置主从复制?"],
  ["ai", "我仅能回答SQLite相关问题。MySQL属于其他数据库系统,不在我的专业范围内。"],
  ["human", "今天天气怎么样?"],
  ["ai", "我仅能回答SQLite相关问题。"],
  // 当前输入
  ["human", "{input}"]
]);
3. 策略 C:角色强化 + 负面约束

使用更强烈的措辞和格式突出限制

const strictSystem = `<role>SQLite技术专家</role>
<constraint>绝对禁止回答与SQLite无关的问题</constraint>
<boundary>
允许主题:SQLite SQL语法、索引优化、事务管理、WAL模式、 FTS全文检索、R*Tree索引、C API接入等
禁止主题:其他数据库(MySQL/PostgreSQL等)、通用编程、生活常识、政治、娱乐等
</boundary>
<protocol>
若检测到越界请求,严格返回:<deny>我仅能回答SQLite相关问题</deny>
且拒绝生成任何其他解释性文字
</protocol>`;

这些伪 XML/HTML 标签是 Prompt Engineering 中的结构化分隔符(Structured Delimiters) 。它们利用 LLM 的注意力机制训练数据模式,人为创造语义边界。

但是实测下来,模型影响也很大!比如使用4B的模型Qwen3.5-4B,就会经常不按系统提示词回答,而使用GLM-4.7这类模型基本没出现过此类问题。

所以说一开始尽量使用主流大模型来测试。

上下文管理(Context Management)

上下文管理指的是在多轮对话或复杂工作流中,如何让 LLM"记住"之前的信息。

最简单的就是,每次对话都将对话内容保存下来,下次对话再带上,这样模型就拥有了“记忆”能力。

但是随着对话增多,携带的信息量爆炸,每次对话都会消耗大量token。像我们平常用Claude Code,大任务执行过程中可以执行/compact来压缩上下文,或者新任务直接发起一个新的对话。

对于大部分应用来说,可以使用:

  1. 滑动窗口(ConversationBufferWindowMemory),只保留最近n条消息,其余舍弃掉
  2. 摘要压缩(ConversationSummaryMemory),当上下文到达阈值后,使用小模型做总结,保留关键信息
  3. 语义检索(VectorStoreRetrieverMemory),将历史存入向量数据库,根据当前问题检索相关片段

函数调用(Function Calling)

什么是Function Calling?

OpenAI 在发布 gpt-3.5-turbo-0613gpt-4-0613 模型更新时,正式推出了 Function Calling(当时也称为 Chat Completions API 的 function calling 功能)。

Function Calling 是由 OpenAI 在 2023 年 6 月正式发起并命名的功能。

它最初是 OpenAI API 的一项特性,因为极具实用价值,迅速被全行业采纳,成为了目前大模型应用开发领域通用的开发范式

它能干什么?

Function Calling(函数调用) 是大语言模型(LLM)的一种高级能力,它允许模型在与用户对话的过程中,根据用户的意图,生成结构化的 JSON 数据,而不是仅仅生成普通的文本。

这些结构化的数据可以被开发者的代码解析,并用来调用外部的函数或 API(如查询数据库、获取天气、发送邮件等),最后将结果返回给模型,让模型生成最终的自然语言回复。

简单来说,Function Calling 就是连接大模型“大脑”与现实世界“工具”的桥梁

如何使用
1. 创建tool

这里贴一个sql查询的tool示例

  • 主要是继承StructuredTool类,实现_call方法,注意它返回的是字符串
  • description很重要,它告诉模型这是个什么工具、何时调用、限制等
  • 注意考虑好边界条件,和数据安全问题
import { StructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { getSQLiteHelper } from "../utils/sqliteContext";

/**
 * 执行 SQL 查询语句
 * 只支持 SELECT 查询,不支持 INSERT/UPDATE/DELETE
 */
export class ExecuteQueryTool extends StructuredTool {
  name = "execute_query";

  description = `执行 SQL 查询语句并返回结果。
仅支持 SELECT 查询语句。
结果限制:最多返回 100 行数据。

使用场景:
- 用户询问数据查询问题
- AI 需要验证数据
- 统计分析类查询

注意事项:
- 只能执行 SELECT 查询
- 不支持 INSERT、UPDATE、DELETE 等修改操作
- 复杂查询建议先解释给用户确认`;

  schema = z.object({
    sql: z.string()
      .min(1)
      .describe("要执行的 SQL 查询语句"),
    limit: z.number()
      .max(1000)
      .optional()
      .default(100)
      .describe("返回结果的最大行数")
  });

  async _call(input: z.infer<typeof this.schema>) {
    const helper = getSQLiteHelper();

    try {
      // 安全检查:只允许 SELECT 语句
      const sqlTrimmed = input.sql.trim().toUpperCase();
      if (!sqlTrimmed.startsWith('SELECT')) {
        return JSON.stringify({
          success: false,
          error: "此工具仅支持 SELECT 查询语句,不支持数据修改操作"
        });
      }

      // 执行查询
      let results = helper.executeQuery(input.sql);

      // 限制返回行数
      const limited = results.length > input.limit;
      if (limited) {
        results = results.slice(0, input.limit);
      }

      return JSON.stringify({
        success: true,
        data: results,
        rowCount: results.length,
        limited,
        message: limited
          ? `查询成功,返回前 ${input.limit} 行数据`
          : `查询成功,共 ${results.length} 行数据`
      });
    } catch (error: any) {
      return JSON.stringify({
        success: false,
        error: error.message || "执行查询失败"
      });
    }
  }
}
使用tool
  1. 绑定工具到模型上
const model = new ChatOpenAI(...);
const tools = [new ExecuteQueryTool()];
model.bindTools(tools);
  1. 然后结合以下实际例子,直观的看下,流式输出中工具调用、输出、请求都是什么关系,具体调用代码就不贴了,主要是涉及一个循环的工具调用过程。

image.png

可以看到,当我们问“帮我创建一个学科表”,最终调用了3次大模型的接口。根据内容输出,可以看到调了3次工具。

第一次请求头: image.png 除了常规的消息构造,就是多了一个tools字段。没错,它就是我们绑定的工具。

image.png 接下来便是调用工具前的输出,直到返回了tool_calls关键词,调用get_table_list

image.png 第二个请求头,这个请求是在get_table_list返回了查询结果后调用的,并且在messagesrole=tool的消息中,将查询结果放入。

后面就是循环调用,如果遇到了tool_calls关键词,就调用对应工具,直到调用结束。也就是说,最后一个请求返回的内容才是正文输出。其他的信息你可以选择展示出来,也可以不展示。

这中间其实有个问题,我们每次是在流式输出过程中,遇到tool_calls关键词才认定有工具调用,那之前的输出就不能作为正文显示。这就导致了我们是不知道何时的输出才是正文,直到整个流式输出结束都没有tool_calls

反应在UI展示上就是,你需要将当前的输出一直当做正文展示,直到遇到了tool_calls,再将它转换为工具调用的描述部分。可以看看扣子等在线工具,它们也是有这样的处理,就是界面展示会“跳一下”。

先作为正文展示: image.png 放到工具调用中: image.png 如果你有比较好的解决办法,可以在评论区告诉我。

总结

LangChain已经封装的很好了,结合AI编程能很快的创建一个问答应用。虽然不用特意去记怎么写,但是上述流程,处理策略还是要有一定了解,最好的方法就是结合UI展示和network的接口调用,再去看代码结构就一目了然了。

后面会结合RAG,再从零到一做一个更复杂的应用并上架,感兴趣的可以关注下。